如何使用64位绝对地址执行call指令?

6
我正在尝试从机器代码中调用一个函数,该函数在编译和链接时应具有绝对地址。我正在创建一个指向所需函数的函数指针,并尝试将其传递给call指令,但我注意到call指令最多只能接受16或32位地址。是否有一种方法可以调用绝对的64位地址?
我正在部署x86-64架构,并使用NASM生成机器代码。
如果我可以确保可执行文件肯定映射到内存底部4GB,那么我可以使用32位地址,但我不确定在哪里可以找到这些信息。
编辑:我无法使用callf指令,因为这要求我禁用64位模式。
第二次编辑:我也不想将地址存储在寄存器中并调用寄存器,因为这对性能至关重要,我不能承受间接函数调用的开销和性能损失。
最后一次编辑:我通过确保我的机器代码映射到内存的前2GB来使用rel32 call指令。这是通过mmap和MAP_32BIT标志实现的(我正在使用linux):

放入一个64位寄存器并调用它?应该很容易验证。 - Rudy Velthuis
2
为什么这被标记为C++? - Brian Bi
1
他可能正在从C++中调用汇编器,使用C++函数指针,但这与x86-64汇编器的问题确实无关。 - Rudy Velthuis
@Brian,我把C++标签删除了。 - Alexander Bolinsky
@RudyVelthuis 请查看我对问题的第二次编辑。 - Alexander Bolinsky
显示剩余2条评论
2个回答

6

相关: 处理从JIT代码调用(可能)远处的预编译函数,了解有关JIT的更多信息,特别是在代码想要调用的位置附近分配JIT缓冲区,以便您可以使用高效的call rel32。或者如果不行怎么办。

此外,在x86机器码中调用绝对指针 是关于calljmp到绝对地址的良好规范问答。


TL:DR: 要通过名称调用函数,只需像正常人一样使用call func,让汇编器+链接器来处理。由于你说你正在使用NASM,我猜你实际上是用汇编器生成机器代码。这听起来像一个更复杂的问题,但我认为你只是想问正常方式是否安全。


间接call r/m64 (FF /2) 在64位模式下使用64位寄存器或内存操作数。

所以你可以这样做

func equ  0x123456789ab
; or if func is a regular label

mov   rax, func          ; mov r64, imm64,  or mov r32, imm32 if it fits
call  rax

通常情况下,您可以使用lea rax, [rel func]将标签地址放入寄存器中,但如果该方法可编码,则可以直接使用call rel32
或者,如果您知道机器代码将存储在哪个地址,则可以在计算从目标到call指令结尾的地址差之后,使用正常的直接call rel32编码。
如果您不想使用间接调用,则rel32编码是您唯一的选择。确保您的机器代码进入低2GiB,以便可以到达低4GiB中的任何地址。
对于Linux、Windows和OS X,这是默认的代码模型。AMD64调用/跳转指令和RIP相对寻址只使用rel32编码,因此所有系统都默认为“小型”代码模型,其中代码和静态数据位于低2GiB中,因此可以保证链接器只需填写一个rel32即可到达向前2G或向后2G。
x86-64 System V ABI讨论了大型/巨大型代码模型,但我不知道是否有人会使用它,因为寻址数据和进行调用的效率很低。
关于效率:是的,mov/call rax更低效。如果分支预测未命中并且无法从BTB提供目标预测,则我认为它会显着变慢。然而,即使是call rel32jmp rel32仍需要BTB才能实现全面的性能。请参见Slow jmp-instruction,其中包含相对jmp next_insn在巨大循环中过多时减速的实验结果。
使用热门分支预测器,间接版本只是额外的代码大小和一个额外的uop(mov)。它可能会消耗更多的预测资源,但甚至可能不是这样。
另请参见What branch misprediction does the Branch Target Buffer detect?

谢谢你提供有关默认代码模型的有用信息。你有相关的参考资料吗? - Alexander Bolinsky
1
@AlexanderBolinsky:是的,我回答中最后一段中的URL是官方ABI标准的链接。另请参阅x86标签wiki以获取其他ABI的链接。 - Peter Cordes
@PeterCordes:call rel32call r64之间的性能差异是什么? - Rudy Velthuis
我明白他们在做什么。这两者之间的性能差异是什么? - Rudy Velthuis
@RudyVelthuis:添加了一些关于直接跳转性能的内容,这仍然需要预测资源。哎呀,我看到我在阅读您之前的评论时错过了一个单词。在这个和另一个答案线程之间进行太多的心理上下文切换。 - Peter Cordes
实际上,只有两个单词。 <g> - Rudy Velthuis

2
在新的APX extension中,Intel添加了一个新的JMPABS指令,它接收一个64位立即数作为绝对跳转目标。
不幸的是,没有CALLABS,所以你需要像这样解决它。
nearby_trampoline:
    jmpabs target64
...
call nearby_trampoline

我不知道它是否比传统的“mov reg, target64; call reg”序列更快。然而,APX还增加了16个寄存器和3操作数整数指令(即非破坏性目标),因此寄存器和I/O压力可能不再存在,你可以为绝对地址保留一个寄存器,并直接使用“call reg”。

我认为mov reg,imm64 / call reg仍然比jmp/call更快。(前几天我在Call an absolute pointer in x86 machine code的回答中添加了一个APX脚注)。很好的观点是,即使所有16个旧寄存器都被占用,已经存在的代码适应APX也可以使用其中一个新寄存器作为跳转目标,代价是增加三个额外字节的代码大小(REX2替代REX用于mov r16, imm64,以及在call r16上使用REX2,而不是mov rax, imm16 / call rax)。 - Peter Cordes
2
(另外,我建议不要在示例标签名称中使用“far”一词。这是一个很长的距离,但跳到新的cs并不是一个“far”的跳跃。也许可以用“nearby_abs_jmp”或“nearby_trampoline”,因为重点是call目标在rel32范围内,可能非常接近调用点)。嗯,如果我们将其他内容放在调用点附近,也许只需将绝对地址作为数据,并使用call qword [RIP+rel32],就像gcc -fno-plt生成代码一样。但这样你就依赖于d-cache的命中以避免停顿。 - Peter Cordes
@PeterCordes 我觉得你提到的 mov rax, imm16 不正确。 - ecm
@ecm:糟糕,我是指rax, imm64 / call rax。当然了。我打错了16,因为我想着我选择的寄存器编号。 - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接