说说恶龙禁区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 小时前
别再用错了!深入扒一扒Python里列表和元组那点事
后端·python
编程乐趣3 小时前
基于.Net开发的数据库导入导出的开源项目
后端
赵星星5203 小时前
别再搞混了!深入浅出理解Java线程中start()和run()的本质区别
java·后端
Ray663 小时前
FST
后端
白露与泡影3 小时前
SpringBoot 自研运行时 SQL 调用树,3 分钟定位慢 SQL!
spring boot·后端·sql
花花无缺4 小时前
接口(interface)中的常量和 类(class)中的常量的区别
java·后端
舒一笑4 小时前
利用Mybatis自定义排序规则实现复杂排序
后端·排序算法·mybatis
毕设源码-郭学长4 小时前
【开题答辩全过程】以 基于vue+springboot的校园疫情管理系统的设计与实现为例,包含答辩的问题和答案
java·spring boot·后端
中国lanwp4 小时前
Tomcat 中部署 Web 应用
java·前端·tomcat