这里的所有内容也适用于jmp
绝对地址,并且指定目标的语法是相同的。该问题询问有关 JITing 的问题,但我还包括了 NASM 和 AT&T 语法以扩大范围。
另请参阅在 JIT 中处理对远处内部函数的调用,了解分配“附近”内存的方法,以便您可以使用JITrel32
代码调用提前编译的函数。
x86 没有正常(近)call
或jmp
指令中编码的绝对地址的编码 没有绝对的直接调用/jmp 编码,除非jmp far
您不想要。请参阅英特尔的 insn set ref 手册条目以获取call
. (有关文档和指南的其他链接,另请参阅x86 标签 wiki 。)大多数计算机体系结构使用相对编码进行正常跳转,例如 x86、BTW。
最好的选择(如果您可以制作知道自己地址的位置相关call rel32
代码)是使用正常的E8 rel32
直接近调用编码,其中rel32
字段是target - end_of_call_insn
(2 的补码二进制整数)。
请参阅$ 如何在 NASM 中工作,究竟是什么?call
手动编码指令的示例;在 JITing 时这样做应该同样容易。
在 AT&T 语法中: call 0x1234567
在 NASM 语法中:call 0x1234567
也适用于具有绝对地址的命名符号(例如,使用equ
or创建.set
)。MASM 没有等价物,它显然只接受标签作为目标,因此人们有时会使用低效的解决方法来解决该工具链(和/或目标文件格式重定位类型)的限制。
这些在位置相关的代码(不是共享库或 PIE 可执行文件)中可以很好地组装和链接。但不是在 x86-64 OS X 中,文本部分映射到 4GiB 以上,因此它无法使用rel32
.
在要调用的绝对地址范围内分配 JIT 缓冲区。 例如,mmap(MAP_32BIT)
在 Linux 上分配低 2GB 内存,其中 +-2GB 可以到达该区域中的任何其他地址,或者在您的跳转目标附近的某处提供非 NULL 提示地址。(但不要使用MAP_FIXED
;如果您的提示与任何现有映射重叠,最好让内核选择一个不同的地址。)
(Linux 非 PIE 可执行文件映射在低 2GB 的虚拟地址空间中,因此它们可以使用[disp32 + reg]
带有符号扩展的 32 位绝对地址的数组索引,或者将静态地址放在具有mov eax, imm32
零扩展绝对地址的寄存器中。因此低 2GB,不低 4GB。 但是 PIE 可执行文件正在成为常态,所以不要假设主可执行文件中的静态地址低于 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 参数数,因此您需要在 AL=0 之前有一个固定值x86-64 System V 调用约定中对可变参数函数的调用。
如果您确实需要避免修改任何寄存器,则可以将绝对地址保留为内存中的常量,并使用call
带有 RIP 相对寻址模式的内存间接寻址,例如
NASM call [rel function_pointer]
; 如果你不能破坏任何注册
AT&Tcall *function_pointer(%rip)
请注意,间接调用/跳转使您的代码可能容易受到 Spectre 攻击,尤其是当您将 JIT 作为同一进程中不受信任代码的沙箱的一部分时。(在这种情况下,单独的内核补丁不会保护你)。
您可能需要“retpoline”而不是普通的间接分支来减轻 Spectre 的影响,但会以性能为代价。
call rel32
与直接 ( )相比,间接跳转的分支错误预测惩罚也会稍差。正常直接call
insn 的目的地在它被解码后就被知道,一旦它检测到那里有一个分支,就在管道中更早地知道它。
间接分支通常可以很好地预测现代 x86 硬件,并且通常用于调用动态库/DLL。这并不可怕,但call rel32
肯定更好。
不过,即使是直接call
也需要一些分支预测来完全避免管道泡沫。(在解码之前需要进行预测,例如,假设我们刚刚提取了这个块,那么提取阶段接下来应该提取哪个块。jmp next_instruction
当您用完分支预测器条目时,一系列减慢速度)。 mov
+ 间接call reg
即使使用完美的分支预测也更糟,因为它的代码大小更大且微指令更多,但这是一个非常小的影响。如果有额外mov
的问题,如果可能的话,内联代码而不是调用它是一个好主意。
有趣的事实:call 0xdeadbeef
在 Linux 上将汇编但不会链接到 64 位静态可执行文件,除非您使用链接器脚本将.text
节/文本段放置在更靠近该地址的位置。该.text
部分通常从0x400080
静态可执行文件(或非 PIE 动态可执行文件)开始,即在低 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
汇编和链接就好了,因为 arel32
可以从任何地方到达任何地方。相对位移不必符号扩展为 64 位,它只是 32 位二进制加法,可以环绕或不环绕。
直接远call
编码(慢,不要使用)
您可能会在手册条目中注意到,call
并且jmp
有绝对目标地址编码到指令中的编码。但是那些只存在于“远” call
/jmp
也设置CS
为新的代码段选择器,这很慢(参见 Agner Fog 的指南)。
CALL ptr16:32
(“Call far, absolute, address given in operand”)有一个 6 字节的段:偏移量编码到指令中,而不是将其作为数据从正常寻址模式给定的位置加载。所以这是对绝对地址的直接调用。
Farcall
还推送 CS:EIP 作为返回地址而不仅仅是 EIP,因此它甚至与call
只推送 EIP 的普通(近)不兼容。这不是问题jmp ptr16:32
,只是缓慢并弄清楚要为段部分放什么。
更改 CS 通常仅对从 32 位模式更改为 64 位模式有用,反之亦然。通常只有内核会这样做,尽管您可以在大多数普通操作系统下的用户空间中这样做,这些操作系统在 GDT 中保留 32 位和 64 位段描述符。不过,这更像是一个愚蠢的计算机技巧,而不是有用的东西。(64 位内核使用iret
或可能使用. 返回到 32 位用户空间sysexit
。大多数操作系统只会在启动期间使用一次 far jmp 以在内核模式下切换到 64 位代码段。)
主流操作系统使用平面内存模型,您永远不需要更改cs
,并且没有标准化cs
将用于用户空间进程的值。即使您想使用 far jmp
,您也必须弄清楚在段选择器部分中放入什么值。(在 JITing 时很容易:只需使用 . 读取当前内容cs
。mov eax, cs
但对于提前编译来说很难移植。)
call ptr16:64
不存在,远直接编码仅存在于 16 位和 32 位代码。在 64 位模式下,您只能call
使用 10 字节的m16:64
内存操作数 far- ,例如call far [rdi]
. 或将 segment:offset 推入堆栈并使用retf
.