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

Leu*_*nko 7 x86 jit machine-code

什么是callx86机器代码中绝对指针的"正确"方法?有没有一种方法可以在一条指令中完成它?

想做什么:

我正在尝试基于"子程序线程"构建一种简化的迷你JIT(仍然).它基本上是字节码解释器中最短的步骤:每个操作码都是作为一个单独的函数实现的,因此每个基本的字节码块都可以"JIT"到它自己的新程序中,如下所示:

{prologue}
call {opcode procedure 1}
call {opcode procedure 2}
call {opcode procedure 3}
...etc
{epilogue}
Run Code Online (Sandbox Code Playgroud)

因此,我们的想法是每个块的实际机器代码只能从模板中粘贴(根据需要扩展中间部分),并且需要"动态"处理的唯一位是将每个操作码的函数指针复制到正确的位置作为每个调用指令的一部分.

我遇到的问题是了解call ...模板部分的用途.x86似乎没有考虑到这种用法,并且有利于相对和间接调用.

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

有人可以解释这些操作码以及我是如何或者是否会为此目的使用它们或其他人?

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

Pet*_*des 8

这里的所有内容也适用于jmp绝对地址,指定目标的语法也是相同的.问题是关于JITing,但我还包括NASM和AT&T语法以扩大范围.


x86没有对指令中编码的普通(近)rel32call绝对地址进行编码(即直接调用/ jmp). 请参阅英特尔的insn set参考手册jmp.(另请参阅x86标签wiki以获取文档和指南的其他链接.)大多数计算机体系结构使用相对编码进行正常跳转,如x86,BTW.

最好的选择(如果你可以使位置相关的代码知道它自己的地址)是使用普通jmp farcall近直接调用编码,其中call rel32字段是E8 rel32(2的补码二进制整数).

看看$如何在NASM中工作?作为手动编码rel32指令的示例; 在JITing期间这样做应该同样容易.

在AT&T语法中: target - end_of_call_insn
在NASM语法中:call

也适用于具有绝对地址的命名符号(例如,使用call 0x1234567或创建call 0x1234567)

这些在位置相关的代码(不是共享库或PIE可执行文件)中组装和链接得很好.但不是在x86-64 OS X中,文本部分映射到4GiB以上,因此无法通过a到达低地址equ.选择您的绝对地址,使其位于您呼叫的范围内.


但是如果你需要使位置无关的代码不知道它自己的绝对地址,或者你需要调用的地址距离调用者的距离大于+ -2GiB(可能是64位,但最好放置代码足够接近),你应该使用寄存器间接.set

; 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
Run Code Online (Sandbox Code Playgroud)

或AT&T语法

mov   $0xdeadbeef, %eax
# movabs $0x7fffdeadbeef, %rax      # mov r64, imm64
call  *%rax
Run Code Online (Sandbox Code Playgroud)

如果你真的需要避免修改任何寄存器,可以将绝对地址保存为内存中的常量,并使用内存间接rel32和RIP相对寻址模式,如

NASM mmap(MAP_32BIT) ; 如果你不能破坏任何reg
AT&TMAP_FIXED


请注意,间接调用/跳转会使您的代码容易受到Specter攻击,特别是如果您在同一进程中将JIT作为不受信任代码的沙箱的一部分进行JIT操作.(在这种情况下,单独的内核补丁不会保护你).

间接跳跃也会比直接跳跃([disp32 + reg])稍差一些的错误预测惩罚.普通直接mov eax, imm32insn 的目的地一旦被解码就知道了,一旦它检测到那里有一个分支就在管道的早期.

间接分支通常在现代x86硬件上很好地预测,并且通常用于调用动态库/ DLL.这并不可怕,但-no-pie -fno-pie肯定更好.

但是,直接call rel32需要一些分支预测来完全避免管道泡沫.(在解码之前需要进行预测,例如,假设我们刚刚获取了这个块,那么取出阶段应该取下一个块.call 当你用完分支预测器条目时,一系列的速度变慢). 即使有完美的分支预测,r10+间接r11也会更糟,因为它的代码大小更大,uop更多,但这是一个非常小的效果.如果额外call是一个问题,如果可能的话,内联代码而不是调用代码是个好主意.


有趣的事实:call [rel function_pointer]将在Linux上组装但不链接到64位静态可执行文件,除非您使用链接描述文件将call *function_pointer(%rip)部分/文本段放置得更靠近该地址.该call rel32部分通常从call静态可执行文件(或非PIE动态可执行文件)开始,即在虚拟地址空间的低2GiB中,其中所有静态代码/数据都存在于默认代码模型中.但是call rel32处于低32位的高半部分(即低4G而不是低2G),因此它可以表示为零扩展的32位整数但不是符号扩展的32位.并且call不适合签名的32位整数,它将正确地扩展到64位.(jmp next_instruction从低地址回绕的负数可以到达的地址空间部分是64位地址空间的前2GiB;通常地址空间的上半部分保留供内核使用.)

它组合好了mov,并call reg显示:

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
Run Code Online (Sandbox Code Playgroud)

但是当mov试图将它实际链接到.text开头的静态可执行文件时call 0xdeadbeef,.text.text.

在32位代码0x400080组装和链接就好了,因为a 0xdeadbeef可以从任何地方到达任何地方.相对位移不必符号扩展到64位,它只是32位二进制加法,它可以包围或不包含.


直接远程0x00000000deadbeef - 0x0000000000400080编码(慢,不要使用)

您可能会注意到手册中的条目,rel32并且yasm -felf64 -gdwarf2 foo.asm编码的绝对目标地址编码在指令中.但那些只存在于"远" objdump -drwC -Mintel/ ld也设置0000000000400080为新的代码段选择器,这很慢(参见Agner Fog的指南).

ld -o foo foo.o("调用far,绝对,操作数中给出的地址")有一个6字节的段:偏移编码到指令中,而不是从正常寻址模式给出的位置加载它作为数据.所以这是对绝对地址的直接调用.

Far foo.o:/tmp//foo.asm:1:(.text+0x1): relocation truncated to fit: R_X86_64_PC32 against '*ABS*'还将CS:EIP作为返回地址而不仅仅是EIP,因此它甚至不能与call 0xdeadbeef只推动EIP的普通(近)相兼容.这不是一个问题rel32,只是缓慢并找出应该为细分部分提供什么.

更改CS通常仅对从32位模式更改为64位模式有用,反之亦然.通常只有内核会这样做,尽管你可以在大多数普通操作系统的用户空间中执行此操作,这些操作系统在GDT中保留32位和64位段描述符.不过,这将是一个愚蠢的计算机技巧而不是有用的东西.(64位内核用call或可能用于返回32位用户空间call.大多数操作系统在启动期间只使用一次远jmp,在内核模式下切换到64位代码段.)

主流操作系统使用平面内存模型,您永远不需要更改jmp,并且不标准call将用于用户空间进程的值.即使你想使用远jmp,你也必须弄清楚要在段选择器部分中放入什么值.(易而JIT编译:刚读当前CSCALL ptr16:32,但很难成为便携式的名列前茅的时间编译.)


call如果不存在,则远程编码仅存在于16位和32位代码中.在64位模式下,您只能call使用10字节的jmp ptr16:32内存操作数,例如iret.或推段:堆栈上的偏移量并使用sysexit.


Bjö*_*ist 1

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

0000000002347490: 48b83412000000000000  mov rax, 0x1234
000000000234749a: 48ffd0                call rax
Run Code Online (Sandbox Code Playgroud)

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