我正在为x86_64编写一个JT重编译器,有时发出的代码需要从已编译的二进制文件中调用一个函数.

由于ASLR,我程序的.text段被放置在某个随机地址.

我可以以这样的方式分配32 MiB的JT代码缓存,即其地址始终足够接近.text段,以便我可以安全地发出相对调用而不是绝对调用?我在Linux中.

推荐答案

接近重复:100 -我的答案建议在附近分配以启用jmp/call rel32,并建议了一些实现这一目标的方法.

实现这一目标的一个好方法是在OSS中使用数组,而不是动态分配. 由于对于您的JT区域来说,您的已知大小限制并不大(32 MiB),因此这是完美的.

#include <sys/mman.h>
#include <stdalign.h>  // for alignas instead of _Alignas, if you're not using C23

// page aligned to make sure it doesn't share a page with anything else
// And for easy use of mprotect (or Windows VirtualProtect)
alignas(4096) unsigned char JIT_buf[32ULL * 1024*1024];

void jit_init(){
   // 31 MiB of code, for example, leaving 1 MiB of data READ|WRITE without exec
   mprotect(JIT_buf, 31ULL * 1024*1024, PROT_READ|PROT_WRITE|PROT_EXEC);
}

在x86-64的标准代码模型中,可执行对象或共享对象的所有静态代码+数据彼此之间的距离在+-2GiB以内,使得可以通过rel 32地址从任何地方访问任何地方. 在非PIE代码模型中,它位于低2 GiB的虚拟地址空间中(因此mmap(MAP_32BIT)可以). 但现代PIE可执行文件和共享库被映射到低2 GiB之外.

如果您的大小不是那么小,但上界低于几个GiB,则您可能仍然在OSS末尾使用array. 也许使用__attribute__((section(".bss.jit")),如果默认的链接器与所有.bss*个名称不匹配,如果有必要,请使用自定义链接器脚本,以将其链接到OSS的末尾. 这避免了损害其他全局变量的dTSB局部性. 在从未使用超过__attribute__((section(".bss.jit")) MiB的运行中,基本上就好像数组只有__attribute__((section(".bss.jit")) MiB,并且您使用的部分与其他全局数连续.

您从未接触过的内存实际上并不需要任何费用,因为Linux确实过度提交,例如来自mmap或OSS. (在页面错误处理程序中,内核在第一次访问时会懒惰地将其归零.)


如果这是在共享库中,请将其设置为static__attribute__((hidden))(也许通过将其设置为默认值),这样它确实位于该共享库的OSS中,而不是主可执行文件中.

如果这不是共享库,另一种 Select 是用sbrk分配内存,但前提是您从不使用malloc / new,或者避免让他们使用sbrk. 这将为您提供接近(主可执行文件的)OSS结尾的内存. 不太连续:ASLR随机化中断,但在我的系统(Linux内核6.7.2)上,全局变量到sbrk分配的距离从1到31 MiB(same on Godbolt)不等,这仍然很容易将其置于+-2GiB rel 32静态代码/数据范围内.

但这可能不适合future 或可移植;future 或其他当前系统可能会随机地将这种突破远离静态代码/数据. 如果您这样做,您应该进行判断,以便您可以告诉用户报告哪些系统没有按照您预期的方式工作.

GliBC malloc使用sbrk进行小额分配. 据@zwol 100、"The implementation of 101 may assume that no code other than itself calls 102 with a nonzero argument."例如,它可能认为自己拥有从上次呼叫到新中断的所有内存,其中包括您的分配. 或者更微妙的簿记腐败. 我不确定gliBC malloc是否真的会崩溃,但相关标准(可能是BOX)并不要求它工作.

如果你tune Glibc malloc永远不使用brk / sbrk,你可能没问题. 默认情况下,它只使用mmap进行大量分配,因此它可以立即免费将它们返回到操作系统,即使同时进行了其他分配. 或者使用根本不使用sbrk的不同malloc库. 我听说gliBC的malloc并不出色,第三方malloc对于某些用例来说可能更有性能.

请注意,G++ new/delete内部使用与malloc相同的分配器.


与往常一样,在向__builtin___clear_cache(&JIT_buf[start], &JIT_buf[end])存储一些字节后,在调用使用该数组作为机器代码的函数指针之前,不要忘记调用__builtin___clear_cache(&JIT_buf[start], &JIT_buf[end]). 在x86上,它实际上不会清除缓存,只是阻止死存储消除等优化(例如:https://godbolt.org/z/5671x3MYn). 这可能不会成为问题,因为调用mprotect意味着指向缓冲区的指针已"逃逸"到优化器看不到的代码中,但在存储代码和调用代码之间做到这一点是个好主意.如果不出意外,它可能会简化移植到确实需要asm中某些内容的架构(大多数非x86). 更多详细信息,请参阅How to get c code to execute hex machine code?.


如果这些都不起作用,您可能会将一组函数指针放在JT缓冲区中的某个地方,作为"静态"数据,那里的代码可以使用RIP+ rel 32地址模式(例如call [rel32])访问. 或者,如果您向您的函数传递一个指向某个地址(call [reg+disp8]call [reg+disp32])的寄存器.

我认为(或希望)这就是丹尼尔·A.当怀特提到vtable时,他试图提出建议. 正常的C++ vtables意味着从存储在对象中的指针开始的多个间接级别. 您不想要这样,只是一个函数指针array. GOT是一个更好的类比;带有GOT条目的call [rip+rel32]是库函数调用与gcc -fno-plt一起编译的方式.

或者作为一次性的,mov rax, imm64 / call rax总是有效,但确实比直接呼叫效率低一些. 100. 根据周围的代码及其运行频率,它可能比具有必须作为数据加载的函数指针的call [rel32]更差. 如果经常使用几行函数指针,以便它们在缓存中保持热状态,那么这可能比10字节的movabs更好.

当然,Direct call rel32始终是最有效的;代码大小最小,并且它具有最早可供前端使用的地址,以防BTB在完全解码之前无法预测分支的存在和目的地. (或者对于间接分支,在执行之前.)

可能很难对因需要预测的间接分支较少而引起的差异进行微基准测试,因为这很可能只在整个大型程序中起作用. 任何微基准测试都可以完美地预测一切. 直接呼叫带来的更好的前端吞吐量可能会产生也可能不会产生可衡量的差异,具体取决于您将它们纳入哪些内容.

C++相关问答推荐

如何启用ss(另一个调查套接字的实用程序)来查看Linux主机上加入的多播组IP地址?

为什么在C中设置文件的位置并写入文件,填充空字符?

在C中使用动态内存分配找到最小的负数

手动矢量化性能差异较大

为什么可以通过指向常量int的指针间接地改变整数的值?

在C中将通用字符名称转换为UTF-8

struct 上的OpenMP缩减

自定义变参数函数的C预处置宏和警告 suppress ?

判断X宏的空性

C指针概念分段故障

GCC错误,共享内存未定义引用?

使用正则表达式获取字符串中标记的开始和结束

意外的C并集结果

我的代码可以与一个编译器一起使用,但不能与其他编译器一起使用

如何在Rust中处理C的longjmp情况?

C 和 C++ 标准如何告诉您如何处理它们未涵盖的情况?

如何找出C中分配在堆上的数组的大小?

为什么写入关闭管道会返回成功

为什么创建局部变量的指针需要过程在堆栈上分配空间?

段错误try 访问静态字符串,但仅有时取决于构建环境