说说恶龙禁区Unsafe——绕过静态类型安全检查&直接操作内存的外挂

概述

今天要给 JYM 介绍的是 Unsafe 的概念以及各个语言中的现有实现。

Unsafe(字面意思"不安全")通常指一组绕过语言或运行时常规安全机制的底层 API

它们允许开发者直接操作内存、对象布局、线程调度等,打破了编程语言设计时的"安全边界"。

正常情况下,语言会保护你不去做危险操作,比如:

  • Java 不让你随便分配/释放堆外内存。
  • Python/JavaScript 不让你直接操作对象在内存里的地址。
  • C# 不让你轻易越过 CLR 的类型检查。

Unsafe 就是提供一个"后门":

  • 直接访问内存:读写任意地址的数据。
  • 绕过构造函数创建对象
  • 实现原子操作(CAS)
  • 操作线程/类加载器的底层细节

为什么要有 Unsafe?

  1. 性能优化

    • 比如 Java 的并发库、Netty、Kafka,都依赖 Unsafe 实现高性能内存和锁操作。
  2. 实现底层功能

    • 高层语言本身没有提供的能力(比如直接分配堆外内存)。
  3. 框架需要"突破口"

    • JVM/CLR/解释器很多 API 不开放,框架只能通过 Unsafe 来实现。

但是 Unsafe 也会有风险,比如:

  • 可能导致 JVM/语言运行时崩溃
  • 可能破坏 类型安全
  • 可能导致 内存泄漏 / 越界访问
    换句话说:它把本来 C/C++ 才有的风险带进了"安全语言"。

Java的Unsafe类

先来说说 Java 的 Unsafe 类,JDK 通常有这两个类:sun.misc.Unsafejdk.internal.misc.Unsafe

这两个类其实指向的是同一套"底层工具类" ,但它们出现在不同 JDK 版本中,有一些历史和封装上的区别:

sun.misc.Unsafe

  • 最早出现在 JDK 1.4 ~ JDK 8。
  • 属于 Sun 私有 API,但广泛被框架(如 Netty、Hadoop、Kafka 等)使用,用来做堆外内存、CAS 操作、对象实例化等"黑科技"。
  • 包路径是 sun.misc,并不是 Java SE 标准 API。

jdk.internal.misc.Unsafe

  • JDK 9 引入,随着 模块化系统(Jigsaw) 的推出。
  • JDK 开发团队想逐渐替换掉 sun.misc.Unsafe,把它迁移到 jdk.internal.misc,因为 jdk.internal 模块是 JDK 内部专用模块
  • 目的是更好地管理 API 暴露,避免第三方随意依赖。

示例代码:

  • 获取 Unsafe 实例

Unsafe 的构造方法和 getUnsafe() 都是受限的,一般要通过反射来获取。

java 复制代码
package org.codeart;

import sun.misc.Unsafe;

import java.lang.reflect.Field;

public class Main {

    public static void main(String[] args) throws Exception {
        // 通过反射获取 Unsafe 实例
        Field field = Unsafe.class.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        Unsafe unsafe = (Unsafe) field.get(null);

        System.out.println("Unsafe 实例: " + unsafe);
    }
}
  • 直接分配和释放内存:类似 C 的 malloc / free
java 复制代码
long size = 8; // 8字节
long address = unsafe.allocateMemory(size);  // 分配堆外内存
unsafe.setMemory(address, size, (byte) 1);   // 填充为 1

byte value = unsafe.getByte(address);
System.out.println("读取到的值: " + value);

unsafe.freeMemory(address); // 释放内存
  • 绕过构造函数创建对象:
java 复制代码
class A {
    private A() {
        System.out.println("构造函数被调用");
    }
}

A obj = (A) unsafe.allocateInstance(A.class);
System.out.println("创建对象: " + obj);
  • CAS(Compare and Swap)
java 复制代码
class Counter {
    volatile int value = 0;
}

Counter counter = new Counter();
long valueOffset = unsafe.objectFieldOffset(Counter.class.getDeclaredField("value"));

boolean success = unsafe.compareAndSwapInt(counter, valueOffset, 0, 42);
System.out.println("CAS 成功了吗? " + success);
System.out.println("新值: " + counter.value);
  • 获取对象字段的偏移量并直接修改
java 复制代码
class Person {
    private int age = 20;
}

Person p = new Person();
long ageOffset = unsafe.objectFieldOffset(Person.class.getDeclaredField("age"));
unsafe.putInt(p, ageOffset, 30);
System.out.println("新的 age: " + unsafe.getInt(p, ageOffset));

这些操作能绕过 JVM 的安全检查,写错可能导致 JVM 崩溃。

JDK 9 以后 Unsafe 已被标记为内部 API,建议使用 VarHandle、java.util.concurrent 里的原子类,除非你真的要写类似 Netty/Kafka 这种框架。

Go的unsafe包

unsafe 包是 Go 标准库的一个特殊的包。它提供了一些不安全的底层操作能力,可以绕过 Go 的类型系统和内存安全机制。

主要用于:指针运算、内存布局操作、类型转换。

示例代码:

  • unsafe.Pointer

一个通用指针类型,可以和任意 *T 相互转换。

go 复制代码
import (
    "fmt"
    "unsafe"
)

func main() {
    var x int = 10
    p := unsafe.Pointer(&x)         // *int -> unsafe.Pointer
    px := (*int)(p)                 // unsafe.Pointer -> *int
    fmt.Println(*px)                // 10
}
  • uintptr 和指针运算

uintptr 是整数,可以保存指针地址,可以做地址偏移,相当于 C 里的指针算术。

go 复制代码
package main

import (
    "fmt"
    "unsafe"
)

func main() {
    arr := [4]int{1, 2, 3, 4}
    p := unsafe.Pointer(&arr[0])
    p2 := (*int)(unsafe.Pointer(uintptr(p) + unsafe.Sizeof(arr[0])))
    fmt.Println(*p2) // 2
}
  • Sizeof / Alignof / Offsetof

用来获取结构体的内存布局信息。

go 复制代码
package main

import (
    "fmt"
    "unsafe"
)

func main() {
    type S struct {
       A int8
       B int32
    }
    fmt.Println(unsafe.Sizeof(S{}))     // 8
    fmt.Println(unsafe.Alignof(S{}))    // 4
    fmt.Println(unsafe.Offsetof(S{}.B)) // 4
}

unsafe 不是"非法",但会破坏 Go 的内存安全保证。一旦用错,可能导致:程序崩溃、数据错乱、GC 无法正确回收。

因此 Go 官方文档明确说:只有在性能或底层库开发时才用它,普通业务代码应避免使用。

JavaScript的Unsafe替代品

严格来说,JavaScript 并没有类似 Java Unsafe 的官方 API,因为 JS 是运行在 沙箱环境(浏览器 / Node.js)里的,默认是被限制的,不允许直接操作裸内存。

但实际上,JS 里还是有一些"比较底层"的 API,可以起到 半个 Unsafe 的作用:

  • ArrayBuffer + TypedArray + DataView

这是最接近"直接内存访问"的 JS API。允许你分配一段原始二进制缓冲区,然后以不同的数据类型去读写:

js 复制代码
// 分配 16 字节内存
const buf = new ArrayBuffer(16);
const view = new DataView(buf);

// 写入 long (64-bit)
view.setBigInt64(0, 123456789n, true);

// 读取 long
console.log(view.getBigInt64(0, true));

这块内存还是受 JS 引擎管理,不能越界访问。

  • WebAssembly Memory

如果你用 WebAssembly (Wasm) ,可以分配并直接访问线性内存:

js 复制代码
const memory = new WebAssembly.Memory({ initial: 1 });
const bytes = new Uint8Array(memory.buffer);
bytes[0] = 42;
console.log(bytes[0]); // 42

这就更接近"裸内存指针",能随意读写。

  • Node.js 的 Buffer

在 Node.js 里,有一个 Buffer 类,可以直接操作内存:

js 复制代码
const buf = Buffer.alloc(8);
buf.writeBigInt64LE(123456789n, 0);
console.log(buf.readBigInt64LE(0));

如果用 Buffer.allocUnsafe(size),甚至能获得 未清零的内存块(可能包含旧数据)。这非常接近 Java 的 Unsafe.allocateMemory

Python的ctypes&cffi

严格来说,Python 并没有类似 Java Unsafe 的官方类或 API,因为 Python 的设计理念就是"安全、高层抽象",屏蔽了底层内存操作。

但如果你想要 突破 Python 的安全边界,去做类似 Java Unsafe 的事情,其实有几种方式:

  • ctypes 模块

Python 内置库,可以直接操作内存、调用 C 函数。功能类似 Unsafe.allocateMemory / Unsafe.putLong 等:

py 复制代码
import ctypes

# 分配一块内存
buf = ctypes.create_string_buffer(16)
ctypes.memset(buf, 0x41, 16)   # 把内存填充为 'A'

# 读写内存地址
addr = ctypes.addressof(buf)
print("内存地址:", hex(addr))
print("内容:", buf.raw)

通过 ctypes 你可以直接越过 Python 对象的边界,甚至能修改 CPython 内部结构,风险和能力都类似 Unsafe

  • cffi 库

这是一个更现代的 C 接口,可以写 C 代码并在 Python 里调用。

py 复制代码
from cffi import FFI

ffi = FFI()
p = ffi.new("int[4]", [1, 2, 3, 4])
print(p[0], p[1])

也能直接操作内存指针,甚至执行内联 C。

总结

为什么上文没有提到 C/C++ 呢?

哈哈哈哈,因为 C/C++ 任何指针操作都是直接操作内存的,本身就是不安全的!

C 和 C++ 是 系统级语言,设计目标就是"给开发者完全的控制权",所以:

  • 所有内存操作(malloc/free、new/delete、指针算术、数组下标访问等)本质上都是"不安全"的
  • 编译器不会帮你做边界检查,也没有 GC,能直接操作裸内存。
相关推荐
在下村刘湘3 分钟前
maven pom文件中<dependencyManagement><dependencies><dependency> 三者的区别
java·maven
不务专业的程序员--阿飞1 小时前
JVM无法分配内存
java·jvm·spring boot
你的人类朋友1 小时前
JWT的组成
后端
李昊哲小课1 小时前
Maven 完整教程
java·maven
Lin_Aries_04211 小时前
容器化简单的 Java 应用程序
java·linux·运维·开发语言·docker·容器·rpc
脑花儿1 小时前
ABAP SMW0下载Excel模板并填充&&剪切板方式粘贴
java·前端·数据库
北风朝向2 小时前
Spring Boot参数校验8大坑与生产级避坑指南
java·spring boot·后端·spring
闭着眼睛学算法2 小时前
【华为OD机考正在更新】2025年双机位A卷真题【完全原创题解 | 详细考点分类 | 不断更新题目 | 六种主流语言Py+Java+Cpp+C+Js+Go】
java·c语言·javascript·c++·python·算法·华为od
山海不说话2 小时前
Java后端面经(八股——Redis)
java·开发语言·redis
哈哈很哈哈2 小时前
Flink SlotSharingGroup 机制详解
java·大数据·flink