为了账号安全,请及时绑定邮箱和手机立即绑定

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

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

慕容森 2020-02-02 15:33:42
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位模式下“无效”。有人可以解释这些操作码,以及为此目的如何或是否将其使用?(通过寄存器使用间接调用也有明显的答案,但这似乎是“错误的”方法-假设实际存在直接调用指令。)
查看完整描述

2 回答

?
LEATH

TA贡献1936条经验 获得超6个赞

这里的所有内容也适用于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 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这是呼叫重挫,但不用于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:     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汇编和链接很好,因为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。


查看完整回答
反对 回复 2020-02-02
?
小唯快跑啊

TA贡献1863条经验 获得超2个赞

您仅凭一条指令就无法做到。一个不错的方法是使用MOV + CALL:


0000000002347490: 48b83412000000000000  mov rax, 0x1234

000000000234749a: 48ffd0                call rax

如果要调用的过程的地址发生更改,请更改从偏移2开始的八个字节。如果调用0x1234的代码的地址发生更改,则无需执行任何操作,因为该寻址是绝对的。


查看完整回答
反对 回复 2020-02-02
  • 2 回答
  • 0 关注
  • 766 浏览
慕课专栏
更多

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信