因为普通内存(如new/malloc/栈分配)默认不可执行,jit代码需prot_read|prot_write|prot_exec权限,必须用mmap(linux)或virtualalloc(windows)显式申请可执行内存,并同步指令缓存。

为什么不能直接用普通指针存 JIT 生成的机器码地址
因为 JIT 生成的代码必须放在可执行(PROT_EXEC)且可写(PROT_WRITE)的内存页中,而 new、malloc 或栈分配的内存默认不可执行。直接把机器码 memcpy 进去再用函数指针调用,会在大多数现代系统(Linux/macOS/Windows)上触发段错误或访问违规。
关键不是“指针怎么声明”,而是“内存怎么申请”——你得先拿到一块既可写又可执行的内存区域,然后把机器码写进去,最后用函数指针转型跳转过去。
Linux 下用 mmap 分配可执行内存并写入机器码
这是最常见也最可控的方式。你需要显式调用 mmap,禁用 ASLR(可选但推荐调试时加),并确保权限组合正确:
-
mmap的prot参数必须同时含PROT_READ | PROT_WRITE | PROT_EXEC -
flags推荐用MAP_PRIVATE | MAP_ANONYMOUS,避免文件映射干扰 - 分配大小要对齐到页边界(通常 4096 字节),且预留足够空间(比如 128 字节够放一段简单 x86-64 stub)
- 写完机器码后,建议调用
__builtin___clear_cache(GCC/Clang)或__builtin_ia32_clflushopt(x86)来同步指令缓存,否则 CPU 可能执行旧缓存内容
示例(x86-64 Linux,返回一个无参无返回值的函数指针):
#include <sys>
#include <cstring><p>typedef void (*jit_func_t)();</p>
<p>jit_func_t emit_add_one() {
const size_t code_size = 16;
uint8_t<em> mem = (uint8_t</em>)mmap(nullptr, code_size,
PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (mem == MAP_FAILED) return nullptr;</p>
<pre class="brush:php;toolbar:false;">// x86-64: mov eax, 1; ret
mem[0] = 0xb8; mem[1] = 0x01; mem[2] = 0x00; mem[3] = 0x00; mem[4] = 0x00;
mem[5] = 0xc3;
__builtin___clear_cache(mem, mem + code_size);
return reinterpret_cast<jit_func_t>(mem);
}
Windows 上用 VirtualAlloc 替代 mmap
Windows 没有 mmap,对应的是 VirtualAlloc。核心差异在于权限标志和初始状态:
-
flAllocationType用MEM_COMMIT | MEM_RESERVE -
flProtect必须是PAGE_EXECUTE_READWRITE(不能分两步设为PAGE_READWRITE再改,会失败) - 同样需要在写完后调用
FlushInstructionCache同步 CPU 指令缓存 - 不能用
VirtualProtect动态升权:从PAGE_READWRITE改成PAGE_EXECUTE_READWRITE在多数 Windows 版本上被禁止(DEP 保护)
简略示意:
#include <windows.h><p>jit_func_t emit_add_one_win() {
uint8_t<em> mem = (uint8_t</em>)VirtualAlloc(nullptr, 16,
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (!mem) return nullptr;</p>
<pre class="brush:php;toolbar:false;">mem[0] = 0xb8; mem[1] = 0x01; mem[2] = 0x00; mem[3] = 0x00; mem[4] = 0x00;
mem[5] = 0xc3;
FlushInstructionCache(GetCurrentProcess(), mem, 16);
return reinterpret_cast<jit_func_t>(mem);
}
指针转型和调用时的常见陷阱
即使内存和代码都对了,C++ 类型系统仍可能悄悄破坏你的调用约定或 ABI:
- 不要用
void*中转后再转函数指针——C++ 标准禁止void*→ 函数指针的直接 reinterpret_cast(UB);必须用reinterpret_cast<jit_func_t>(mem)</jit_func_t> - 确保机器码的调用约定匹配目标平台(x86-64 System V ABI 默认用
%rdi,%rsi传参;Windows x64 用%rcx,%rdx) - 如果生成的函数带参数或返回值,函数指针类型必须精确声明(如
int (*)(int, const char*)),否则栈/寄存器使用错位 - 生成的代码不能引用外部符号(如
printf),除非你手动解析并 patch 地址——JIT 本身不提供链接器功能
真正难的从来不是“怎么让指针指向某地址”,而是保证那个地址上的字节序列对 CPU 来说是合法、自洽、且上下文干净的机器码——寄存器状态、栈对齐、RIP-relative 引用、CFI 元数据,这些全得你手写或靠轻量级库(如 asmjit、zydis + custom allocator)兜底。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











