这里的一切都适用于jmp,也适用于绝对地址,并且指定目标的语法是相同的。该问题涉及 JITing,但我还包括了 NASM 和 AT&T 语法以扩大范围。
另请参阅 Handling calls to far away intrinsic functions in a JIT 了解分配“附近”内存的方法,以便您可以使用 rel32 从您的 JIT 代码中调用提前编译的函数。
x86 没有将普通(接近)call 或 jmp 编码到指令中编码的绝对地址 没有绝对直接调用/jmp 编码,@ 除外987654337@ 你不想要。见Intel's insn set ref manual entry for call。 (有关文档和指南的其他链接,另请参阅 x86 tag wiki。)大多数计算机架构 use relative encodings for normal jumps 像 x86,顺便说一句。
最好的选择(如果你可以让位置依赖代码知道它自己的地址)是使用普通的call rel32,E8 rel32直接靠近调用编码,其中rel32字段为target - end_of_call_insn(2的补码二进制整数)。
参见How does $ work in NASM, exactly? 手动编码call 指令的示例;在 JITing 时执行此操作应该同样简单。
在 AT&T 语法中:call 0x1234567
在 NASM 语法中:call 0x1234567
也适用于具有绝对地址的命名符号(例如,使用 equ 或 .set 创建)。 MASM 没有等价物,它显然只接受标签作为目标,因此人们有时会使用低效的解决方法来解决工具链(和/或目标文件格式重定位类型)的限制。
这些在位置相关代码(不是共享库或 PIE 可执行文件)中可以很好地组装和链接。但不是在 x86-64 OS X 中,文本部分映射到 4GiB 以上,因此它无法到达带有rel32 的低地址。
在您要调用的绝对地址范围内分配您的 JIT 缓冲区。 例如在 Linux 上使用 mmap(MAP_32BIT) 分配低 2GB 的内存,其中 +-2GB 可以到达该区域中的任何其他地址,或者在跳转目标附近的某处提供非 NULL 提示地址。 (但不要使用MAP_FIXED;如果您的提示与任何现有映射重叠,最好让内核选择一个不同的地址。)
(Linux 非 PIE 可执行文件映射在低 2GB 的虚拟地址空间中,因此它们可以使用带有符号扩展的 32 位绝对地址的 [disp32 + reg] 数组索引,或者将静态地址放入带有 mov eax, imm32 的寄存器中零扩展绝对值。因此低 2GB,而不是低 4GB。But PIE executables are becoming the norm,所以不要假设主可执行文件中的静态地址在低 32,除非您确保使用-no-pie -fno-pie 构建+链接。和其他操作系统就像 OS X 总是把可执行文件放在 4GB 以上。)
如果你不能让call rel32 可用
但是如果你需要制作不知道自己的绝对地址的位置无关代码,或者如果你需要调用的地址比较多距离调用者 +-2GiB 远(可能是 64 位,但最好将代码放置得足够近),您应该使用寄存器间接 call
; use any register you like as a scratch
mov eax, 0xdeadbeef ; 5 byte mov r32, imm32
; or mov rax, 0x7fffdeadbeef ; for addresses that don't fit in 32 bits
call rax ; 2 byte FF D0
或 AT&T 语法
mov $0xdeadbeef, %eax
# movabs $0x7fffdeadbeef, %rax # mov r64, imm64
call *%rax
显然,您可以使用任何寄存器,例如 r10 或 r11,它们在 x86-64 System V 中被调用但不用于参数传递。AL = 可变参数函数的 XMM 参数数,所以你在调用 x86-64 System V 调用约定中的可变参数函数之前,需要 AL=0 中的固定值。
如果你真的需要避免修改任何寄存器,可以将绝对地址作为一个常量保存在内存中,并使用带有 RIP 相对寻址模式的内存间接call,例如
NASM call [rel function_pointer] ;如果你不能破坏任何 reg
美国电话电报公司call *function_pointer(%rip)
请注意,间接调用/跳转会使您的代码可能容易受到 Spectre 攻击,尤其是当您将 JIT 作为同一进程中不受信任代码的沙箱的一部分时。 (在这种情况下,仅靠内核补丁无法保护您)。
您可能需要 "retpoline" 而不是普通的间接分支来减轻 Spectre 的影响,但会以性能为代价。
间接跳转的分支预测错误惩罚也比直接跳转 (call rel32) 稍差。。正常直接call insn 的目的地在它被解码后就被知道,一旦它检测到那里有一个分支就在管道中更早。
间接分支通常可以很好地预测现代 x86 硬件,并且通常用于调用动态库/DLL。这并不可怕,但call rel32 肯定更好。
即使是直接call 也需要一些分支预测来完全避免管道泡沫。 (在解码之前需要进行预测,例如,假设我们刚刚获取了这个块,那么获取阶段应该下一个获取哪个块。jmp next_instructionslows down when you run out of branch-predictor entries 的序列)。 mov + 间接 call reg 即使在完美的分支预测下也更糟,因为它的代码量更大且微指令更多,但这是一个非常小的影响。如果额外的mov 是个问题,如果可能的话,内联代码而不是调用它是个好主意。
有趣的事实:call 0xdeadbeef 将在 Linux 上汇编但不会链接到 64 位静态可执行文件,除非您使用链接器脚本将 .text 部分/文本段更靠近那个地址。 .text 部分通常在静态可执行文件(或 non-PIE dynamic executable)中从 0x400080 开始,即在低 2GiB 的虚拟地址空间中,所有静态代码/数据都位于默认代码模型中。但是0xdeadbeef在低32位的高半部分(即在低4G而不是低2G),所以它可以表示为一个零扩展的32位整数,而不是符号扩展的32位。并且0x00000000deadbeef - 0x0000000000400080 不适合正确扩展到 64 位的有符号 32 位整数。 (从低地址环绕的负数rel32 可以到达的地址空间部分是 64 位地址空间的顶部 2GiB;通常地址空间的上半部分保留供内核使用。)
它确实与yasm -felf64 -gdwarf2 foo.asm 组装好,objdump -drwC -Mintel 显示:
foo.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <.text>:
0: e8 00 00 00 00 call 0x5 1: R_X86_64_PC32 *ABS*+0xdeadbeeb
但是当ld 尝试将其实际链接到一个静态可执行文件中,其中 .text 以0000000000400080 开头,ld -o foo foo.o 表示foo.o:/tmp//foo.asm:1:(.text+0x1): relocation truncated to fit: R_X86_64_PC32 against '*ABS*'。
在 32 位代码中,call 0xdeadbeef 可以很好地组合和链接,因为 rel32 可以从任何地方到达任何地方。相对位移不必符号扩展为 64 位,它只是 32 位二进制加法,可以环绕或不环绕。
direct far call 编码(慢,不要用)
您可能会在call 和jmp 的手册条目中注意到,有绝对目标地址编码到指令中的编码。但那些只存在于“远”call/jmp 也将CS 设置为新的代码段选择器,这很慢(see Agner Fog's guides)。
CALL ptr16:32 ("Call far, absolute, address given in operand") 有一个 6 字节的段:偏移量直接编码到指令中,而不是将其作为数据从给定位置加载通过正常寻址模式。所以直接调用绝对地址。
Far call 还推送 CS:EIP 作为返回地址,而不仅仅是 EIP,因此它甚至与仅推送 EIP 的普通(近)call 不兼容。对于jmp ptr16:32 来说,这不是问题,只是速度慢和弄清楚要为段部分放什么。
更改 CS 通常仅对从 32 位模式更改为 64 位模式有用,反之亦然。通常只有内核会这样做,尽管您可以在大多数普通操作系统下的用户空间中这样做,这些操作系统在 GDT 中保留 32 位和 64 位段描述符。不过,这更像是一个愚蠢的计算机技巧,而不是有用的东西。 (64 位内核使用 iret 或可能使用 sysexit 返回到 32 位用户空间。大多数操作系统只会在启动期间使用一次 far jmp 以在内核模式下切换到 64 位代码段。)
主流操作系统使用平面内存模型,您永远不需要更改 cs,并且没有标准化 cs 值将用于用户空间进程。即使您想使用远 jmp,您也必须弄清楚在段选择器部分中放入什么值。 (在 JITing 时很容易:只需读取当前的 cs 和 mov eax, cs。但很难移植以进行提前编译。)
call ptr16:64 不存在,远直接编码仅适用于 16 位和 32 位代码。在 64 位模式下,您只能使用 10 字节的 m16:64 内存操作数来远-call,例如 call far [rdi]。或者将 segment:offset 推入堆栈并使用retf。