在x86机器代码中调用绝对指针

call在x86机器代码中指向绝对指针的“正确”方法是什么?有一个好的方法可以在一条指令中完成吗?


我想做什么:


我正在尝试基于“子例程线程”构建一种简化的mini-JIT(仍然)。从根本上讲,这是从字节码解释器开始的最短步骤:每个操作码都是作为单独的函数实现的,因此可以将每个基本字节码块“ JITted”到它自己的新过程中,如下所示:


{prologue}

call {opcode procedure 1}

call {opcode procedure 2}

call {opcode procedure 3}

...etc

{epilogue}

因此,我们的想法是每个块的实际机器代码都可以从模板中粘贴(根据需要扩展中间部分),唯一需要“动态”处理的位是将每个操作码的功能指针复制到正确的地方,作为每个呼叫说明的一部分。


我遇到的问题是了解call ...模板部分要使用什么。x86似乎没有考虑到这种用法,而是支持相对和间接调用。


它看起来像我可以使用FF 15 EFBEADDE或2E FF 15 EFBEADDE在假设调用函数DEADBEEF(通过把东西变成一个汇编和反汇编,看到什么产生有效的结果,基本上发现了这些未通过了解他们在做什么),但我不理解的东东细分和特权以及相关信息,足以看出差异,或者它们与更常见的call指令之间的行为会有何不同。英特尔架构手册还建议这些仅在32位模式下有效,而在64位模式下“无效”。


有人可以解释这些操作码,以及为此目的如何或是否将其使用?


(通过寄存器使用间接调用也有明显的答案,但这似乎是“错误的”方法-假设实际存在直接调用指令。)


慕容森
浏览 769回答 2
2回答

LEATH

这里的所有内容也适用于jmp绝对地址,并且用于指定目标的语法相同。该问题询问有关JITing的问题,但我还添加了NASM和AT&T语法以扩大范围。另请参阅在JIT中处理对遥远的内在函数的调用,以获取分配“附近”内存的方法,以便您可以用来rel32从JITed代码中调用提前编译的函数。x86没有对指令中的普通(近)call或jmp绝对地址进行编码的编码。 没有绝对的直接调用/ jmp编码,除非jmp far您不需要。请参阅英特尔的insn set ref手册条目call。(有关文档和指南的其他链接,另请参见x86标签wiki。)大多数计算机体系结构都使用相对编码来进行正常跳转,例如x86,BTW。最好的选择(如果可以使位置依赖的代码知道其自身的地址)是使用normalcall rel32,E8 rel32直接近距离调用编码,该rel32字段为target - end_of_call_insn(2的补码二进制整数)。请参阅$在NASM中如何工作?以手动编码call指令为例;在JITing期间执行此操作应该同样容易。在AT&T语法中: call 0x1234567在NASM语法中:call 0x1234567也适用于具有绝对地址的命名符号(例如使用equ或创建.set)。MASM没有等效功能,它显然只接受标签作为目的地,因此人们有时会使用低效的解决方法来解决工具链(和/或目标文件格式重定位类型)的限制。这些汇编和链接恰好在位置相关的代码中(而不是共享的lib或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 scratchmov&nbsp; &nbsp;eax, 0xdeadbeef&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; 5 byte&nbsp; mov r32, imm32&nbsp; &nbsp; &nbsp;; or mov rax, 0x7fffdeadbeef&nbsp; &nbsp;; for addresses that don't fit in 32 bitscall&nbsp; rax&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; 2 byte&nbsp; FF D0或AT&T语法mov&nbsp; &nbsp;$0xdeadbeef, %eax# movabs $0x7fffdeadbeef, %rax&nbsp; &nbsp; &nbsp; # mov r64, imm64call&nbsp; *%rax很明显,你可以使用任何寄存器,比如r10或r11这是呼叫重挫,但不用于ARG-传递的x86-64系统V. AL = XMM参数的个数数的可变参数函数,所以你需要在AL = 0之前的固定值x86-64 System V调用约定中对可变参数函数的调用。如果确实需要避免修改任何寄存器,则可以将绝对地址保持为内存中的常数,并使用call具有RIP相对寻址模式的间接内存,例如NASM call [rel function_pointer] ; 如果您无法破坏AT&T的任何法规call *function_pointer(%rip)请注意,间接调用/跳转会使您的代码容易受到Spectre攻击,尤其是在同一流程中将JIT作为不信任代码的沙箱的一部分时。(在那种情况下,仅内核补丁将无法保护您)。您可能希望使用“ retpoline”而不是普通的间接分支来减轻Spectre的性能。间接跳转的分支错误预测惩罚也比直接(call rel32)稍差。普通直接callinsn 的目的地一经解码就被知道,一旦它检测到根本没有分支,就在管道中更早地知道。间接分支通常可以在现代x86硬件上很好地预测,并且通常用于对动态库/ DLL的调用。这并不可怕,但是call rel32绝对更好。但是,即使直接也call需要一些分支预测来完全避免管道气泡。(在解码之前需要进行预测,例如,假设我们刚刚获取了该块,则提取阶段接下来应获取该块。jmp next_instruction 当用完分支预测器条目时,速度会变慢)。 即使具有完美的分支预测,mov间接+ call reg也更糟糕,因为它具有更大的代码大小和更多的微指令,但是效果很小。如果有其他mov问题,如果可能的话,内联代码而不是调用它是一个好主意。有趣的事实:call 0xdeadbeef它将在Linux上汇编但不会链接到64位静态可执行文件中,除非您使用链接程序脚本将.textsection / text segment 放在靠近该地址的位置。该.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:&nbsp; &nbsp; &nbsp;file format elf64-x86-64Disassembly of section .text:0000000000000000 <.text>:&nbsp; &nbsp; 0:&nbsp; &nbsp;e8 00 00 00 00&nbsp; &nbsp; &nbsp; &nbsp;call&nbsp; &nbsp;0x5&nbsp; &nbsp;1: R_X86_64_PC32&nbsp; &nbsp; &nbsp; &nbsp; *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汇编和链接很好,因为a rel32可以从任何地方到达任何地方。相对位移不必将符号扩展为64位,而只需32位二进制加法即可。直接远call编码(慢,不使用)您可能会在的手册条目中注意到,call并且jmp其中的编码带有绝对目标地址,直接编码在指令中。但那些只存在于“远” call/ jmp也设置CS一个新的代码段选择,这是缓慢的(见昂纳雾指南)。CALL ptr16:32(“在操作数中给出的远,绝对地址调用”)具有6个字节的段:将偏移量直接编码到指令中,而不是将其作为数据从普通寻址模式下的位置加载。因此,这是对绝对地址的直接调用。Far call还将Push CS:EIP作为返回地址,而不仅仅是EIP,因此它甚至与call仅推送EIP的普通(附近)兼容。这不是问题jmp ptr16:32,只是缓慢和弄清楚段部分的内容。更改CS通常仅对从32位模式更改为64位模式有效,反之亦然。通常,只有内核才能执行此操作,尽管您可以在大多数普通的OS(在GDT中保留32位和64位段描述符)下的用户空间中执行此操作。但是,那将是更多愚蠢的计算机技巧,而不是有用的东西。(带有iret或带有的64位内核将返回到32位用户空间sysexit。大多数操作系统在引导过程中仅使用远jmp一次即可切换到内核模式下的64位代码段。)主流操作系统使用的平面内存模型不需要更改cs,并且cs对于用户空间进程将使用什么值还没有标准化。即使您想使用far jmp,也必须找出要在细分选择器部分中输入的值。(易而JIT编译:刚读当前cs有mov eax, cs,但很难提前-的即时编译可移植的。)call ptr16:64不存在,远距离直接编码仅适用于16位和32位代码。在64位模式下,您只能call使用10字节的m16:64内存操作数,例如call far [rdi]。或将segment:offset推入堆栈并使用retf。

小唯快跑啊

您仅凭一条指令就无法做到。一个不错的方法是使用MOV + CALL:0000000002347490: 48b83412000000000000&nbsp; mov rax, 0x1234000000000234749a: 48ffd0&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; call rax如果要调用的过程的地址发生更改,请更改从偏移2开始的八个字节。如果调用0x1234的代码的地址发生更改,则无需执行任何操作,因为该寻址是绝对的。
打开App,查看更多内容
随时随地看视频慕课网APP