在 x86 机器码中调用绝对指针 [英] Call an absolute pointer in x86 machine code

查看:17
本文介绍了在 x86 机器码中调用绝对指针的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

在 x86 机器代码中调用绝对指针的正确"方法是什么?有没有什么好方法可以在一条指令中做到这一点?

What's the "correct" way to call an absolute pointer in x86 machine code? Is there a good way to do it in a single instruction?

想要做什么:

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

I'm trying to build a kind of simplified mini-JIT (still) based on "subroutine threading". It's basically the shortest possible step up from a bytecode interpreter: each opcode is implemented as a separate function, so each basic block of bytecodes can be "JITted" into a fresh procedure of its own that looks something like this:

{prologue}
call {opcode procedure 1}
call {opcode procedure 2}
call {opcode procedure 3}
...etc
{epilogue}

所以这个想法是每个块的实际机器代码可以从模板中粘贴出来(根据需要扩展中间部分),唯一需要动态"处理的部分是复制函数指针作为每个调用指令的一部分,将每个操作码放入正确的位置.

So the idea is that the actual machine code for every block can just be pasted out of a template (extending the middle part as necessary), and the only bit that needs to be "dynamically" handled is copying the function pointers for each opcode into the right places as part of each call instruction.

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

The problem I'm having is understanding what to use for the call ... part of the template. x86 doesn't seem to be set up with this kind of usage in mind, and favours relative and indirect calls.

看起来我可以使用 FF 15 EFBEADDE2E FF 15 EFBEADDEDEADBEEF(基本上是通过将东西放入汇编器和反汇编器并查看产生有效结果的结果,不是通过了解它们的作用来发现这些的),但我不了解有关段和特权的内容,相关信息足以查看差异,或者这些信息与更常见的 call 指令的行为有何不同.Intel 架构手册还建议这些仅在 32 位模式下有效,而在 64 位模式下无效".

It looks like I can use either FF 15 EFBEADDE or 2E FF 15 EFBEADDE to call the function hypothetically at DEADBEEF (basically discovered these by putting stuff into an assembler and disassembler and seeing what produced valid results, not by understanding what they do), but I don't understand the stuff about segments and privileges and associated information well enough to see the difference, or how these will behave differently from a more frequently-seen call instruction. The Intel architecture manual also suggests that these are only valid in 32-bit mode, and "invalid" in 64-bit mode.

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

Can someone explain these opcodes and how, or if, I would use them or others for this purpose?

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

(There's also the obvious answer of using an indirect call through a register, but that seems like the "wrong" approach - assuming a direct call instruction actually exists.)

推荐答案

这里的一切都适用于 jmp 绝对地址,并且指定目标的语法是相同的.该问题询问了 JITing,但我还包括了 NASM 和 AT&T 语法以扩大范围.

Everything here applies to jmp to absolute addresses, too, and the syntax for specifying the target is the same. The question asks about JITing, but I also included NASM and AT&T syntax to broaden the scope.

另见 处理对远处内在函数的调用在 JIT 中分配附近"内存的方法,以便您可以使用 rel32 从 JIT 代码中调用提前编译的函数.

See also Handling calls to far away intrinsic functions in a JIT for ways to allocate "nearby" memory so you can use rel32 to call ahead-of-time compiled functions from your JITed code.

x86 没有对指令中编码的绝对地址的正常(接近)calljmp 的编码 有没有绝对的直接调用/jmp 编码,除了您不想要的 jmp far.请参阅 英特尔的 insn set ref 手册条目,用于 call.(另请参阅 x86 标签 wiki 以获取文档和指南的其他链接.)大多数计算机架构 使用相对编码进行正常跳转,例如x86,顺便说一句.

x86 doesn't have an encoding for a normal (near) call or jmp to an absolute address encoded in the instruction There are no absolute direct call/jmp encodings, except jmp far which you don't want. See Intel's insn set ref manual entry for call. (See also the x86 tag wiki for other links to docs and guides.) Most computer architectures use relative encodings for normal jumps like x86, BTW.

最好的选择(如果你可以制作知道自己地址的位置依赖代码)是使用普通的call rel32E8 rel32 直接近调用编码,其中 rel32 字段是 target - end_of_call_insn(2 的补码二进制整数).

The best option (if you can make position-dependent code that knows its own address) is to use the normal call rel32, the E8 rel32 direct near call encoding, where the rel32 field is target - end_of_call_insn (2's complement binary integer).

参见 $ 在 NASM 中究竟是如何工作的? 示例手动编码call指令;在 JITing 的同时进行应该同样容易.

See How does $ work in NASM, exactly? for an example of manually encoding a call instruction; doing it while JITing should be just as easy.

在 AT&T 语法中:call 0x1234567
在 NASM 语法中:call 0x1234567

也适用于具有绝对地址的命名符号(例如,使用 equ.set 创建).MASM 没有等效项,它显然只接受标签作为目标,因此人们有时会使用低效的解决方法来解决该工具链(和/或目标文件格式重定位类型)的限制.

In AT&T syntax: call 0x1234567
In NASM syntax: call 0x1234567

Also works to a named symbol with an absolute address (e.g. created with equ or .set). There's no equivalent for MASM, it apparently only accepts a label as a destination so people sometimes use inefficient workarounds to workaround that toolchain (and/or object file format relocation type) limitation.

这些在依赖位置的代码(不是共享库或 PIE 可执行文件)中可以很好地组装和链接.但不是在 x86-64 OS X 中,文本部分映射到 4GiB 以上,因此它无法使用 rel32 到达低地址.

These assemble and link just fine in position-dependent code (not a shared lib or a PIE executable). But not in x86-64 OS X where the text section is mapped above 4GiB so it couldn't reach a low address with a rel32.

在要调用的绝对地址范围内分配 JIT 缓冲区.例如在 Linux 上使用 mmap(MAP_32BIT) 在低 2GB 中分配内存,其中 +-2GB 可以到达该区域中的任何其他地址,或者在靠近您的位置的某个位置提供一个非 NULL 提示地址跳跃目标是.(不过,不要使用 MAP_FIXED;如果您的提示与任何现有映射重叠,最好让内核选择不同的地址.)

Allocate your JIT buffer in range of the absolute addresses you want to call. e.g. with mmap(MAP_32BIT) on Linux to allocate memory in the low 2GB where +-2GB can reach any other address in that region, or provide a non-NULL hint address somewhere near where your jump target is. (Don't use MAP_FIXED, though; probably best to let the kernel just pick a different address if your hint overlapped with any existing mappings.)

(Linux 非 PIE 可执行文件被映射到低 2GB 的虚拟地址空间,因此它们可以使用带有符号扩展的 32 位绝对地址的 [disp32 + reg] 数组索引,或放置mov eax, imm32 寄存器中的静态地址,用于零扩展绝对值.因此低 2GB,而不是低 4GB.但是 PIE 可执行文件正在成为常态,所以不要假设主可执行文件中的静态地址在低 32 位,除非您确保使用 -no-pie -fno-pie 构建+链接.而其他操作系统(如 OS X)总是将可执行文件放在 4GB 以上.)

(Linux non-PIE executables are mapped in the low 2GB of virtual address space, so they can use [disp32 + reg] array indexing with sign-extended 32-bit absolute addresses, or put static addresses in registers with mov eax, imm32 for zero-extended absolutes. Thus low 2GB, not low 4GB. But PIE executables are becoming the norm, so don't assume that static addresses in your main executable are in the low 32 unless you make sure to build+link with -no-pie -fno-pie. And other OSes like OS X always put executables above 4GB.)

但是如果你需要制作不知道自己绝对地址的位置无关代码,或者如果你需要调用的地址更多比 +-2GiB 远离调用者(在 64 位中可能,但最好将代码放置得足够近),您应该使用寄存器间接call

But if you need to make position-independent code that doesn't know its own absolute address, or if the address you need to call is more than +-2GiB away from the caller (possible in 64-bit, but it's better to place code close enough), you should use a register-indirect 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 语法

Or AT&T syntax

mov   $0xdeadbeef, %eax
# movabs $0x7fffdeadbeef, %rax      # mov r64, imm64
call  *%rax

显然,您可以使用任何寄存器,例如 r10r11,它们被调用破坏但不用于 x86-64 System V 中的 arg-passing.AL = numberXMM args 到可变参数函数,因此在 x86-64 System V 调用约定中调用可变参数函数之前,您需要 AL=0 中的固定值.

Obviously you can use any register, like r10 or r11 which are call-clobbered but not used for arg-passing in x86-64 System V. AL = number of XMM args to a variadic function, so you require a fixed value in AL=0 before a call to a variadic function in the x86-64 System V calling convention.

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

If you really need to avoid modifying any registers, maybe keep the absolute address as a constant in memory and use a memory-indirect call with a RIP-relative addressing mode, like

NASM call [rel function_pointer] ;如果你不能破坏任何注册
AT&T 调用 *function_pointer(%rip)

NASM call [rel function_pointer] ; If you can't clobber any reg
AT&T call *function_pointer(%rip)

请注意,间接调用/跳转使您的代码可能容易受到 Spectre 攻击,尤其是当您将 JIT 作为同一进程中不受信任代码的沙箱的一部分时.(在那种情况下,仅靠内核补丁无法保护您).

Note that indirect calls / jumps make your code potentially vulnerable to Spectre attacks, especially if you're JITing as part of a sandbox for untrusted code within the same process. (In that case kernel patches alone won't protect you).

您可能需要一个 "retpoline"而不是普通的间接分支,以牺牲性能来减轻 Spectre.

You may want a "retpoline" instead of a normal indirect branch to mitigate Spectre at the cost of performance.

间接跳转的分支错误预测惩罚也会比直接跳转(call rel32)稍差.正常的直接call insn 的目的地一经解码就知道,一旦检测到那里有分支,就会在管道的早期.

Indirect jumps will also have slightly worse branch-misprediction penalties than direct (call rel32). The destination of a normal direct call insn is known as soon as it's decoded, earlier in the pipeline as soon as its detected that there's a branch there at all.

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

Indirect branches generally predict well on modern x86 hardware, and are commonly used for calls to dynamic libraries / DLLs. It's not terrible, but call rel32 is definitely better.

即使是直接的 call 也需要一些分支预测来完全避免管道气泡.(在解码之前需要预测,例如,假设我们刚刚提取了这个块,接下来提取阶段应该提取哪个块.jmp next_instruction 当您用完分支预测器条目时会变慢).mov + 间接 call reg 即使有完美的分支预测也更糟糕,因为它的代码大小更大,uop 也更多,但这是一个非常小的影响.如果额外的 mov 是一个问题,如果可能的话,内联代码而不是调用它是个好主意.

Even direct call needs some branch prediction to avoid pipeline bubbles entirely, though. (Prediction is needed before decode, e.g. given that we just fetched this block, which block should the fetch stage fetch next. A sequence of jmp next_instruction slows down when you run out of branch-predictor entries). mov + indirect call reg is also worse even with perfect branch prediction because it's larger code-size and more uops, but that's a pretty minimal effect. If an extra mov is an issue, inlining the code instead of calling it is a good idea, if possible.

有趣的事实:call 0xdeadbeef 将汇编但不会链接到 Linux 上的 64 位静态可执行文件,除非您使用链接器脚本来放置 .text 部分/更接近该地址的文本段..text 部分通常在静态可执行文件(或 非 PIE 动态可执行文件),即在虚拟地址空间的低 2GiB 中,所有静态代码/数据都位于默认位置代码模型.但是0xdeadbeef是在低32位的高半部分(即在低4G而不是低2G),所以它可以表示为一个零扩展的32位整数而不是符号-扩展的 32 位.而 0x00000000deadbeef - 0x0000000000400080 不适合可以正确扩展到 64 位的有符号 32 位整数.(您可以使用负 rel32 从低地址环绕的地址空间部分是 64 位地址空间的顶部 2GiB;通常地址空间的上半部分保留供内核.)

Fun fact: call 0xdeadbeef will assemble but not link into a 64-bit static executable on Linux, unless you use a linker script to put the .text section / text segment closer to that address. The .text section normally starts at 0x400080 in a static executable (or a non-PIE dynamic executable), i.e. in the low 2GiB of virtual address space, where all static code / data lives in the default code model. But 0xdeadbeef is in the high half of the low 32 bits (i.e. in the low 4G but not the low 2G), so it can be represented as a zero-extended 32-bit integer but not sign-extended 32-bit. And 0x00000000deadbeef - 0x0000000000400080 doesn't fit in a signed 32-bit integer that will correctly extend to 64 bits. (The part of address space you can reach with negative rel32 that wraps around from a low address is the top 2GiB of the 64-bit address space; normally the top half of address space is reserved for use by the kernel.)

它确实可以与 yasm -felf64 -gdwarf2 foo.asm 组合,并且 objdump -drwC -Mintel 显示:

It does assemble ok with yasm -felf64 -gdwarf2 foo.asm, and objdump -drwC -Mintel shows:

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.ofoo.o:/tmp//foo.asm:1:(.text+0x1): relocation truncated to fit: R_X86_64_PC32 against '*ABS*'.

But when ld tries to actually link it into a static executable where .text starts at 0000000000400080, ld -o foo foo.o says foo.o:/tmp//foo.asm:1:(.text+0x1): relocation truncated to fit: R_X86_64_PC32 against '*ABS*'.

在 32 位代码中,call 0xdeadbeef 可以很好地组装和链接,因为 rel32 可以从任何地方到达任何地方.相对位移不必符号扩展到 64 位,它只是 32 位二进制加法,可以环绕或不环绕.

In 32-bit code call 0xdeadbeef assembles and links just fine, because a rel32 can reach anywhere from anywhere. The relative displacement doesn't have to be sign-extended to 64-bits, it's just 32-bit binary addition which can wrap around or not.

您可能会注意到 calljmp 表示有绝对目标地址的编码直接编码到指令中.但那些只存在于 "far" call/jmp 也将 CS 设置为一个新的代码段选择器,这很慢 (参见 Agner Fog 的指南).

You might notice in the manual entries for call and jmp that there are encodings with absolute target addresses encoded right into the instruction. But those only exist for "far" call/jmp that also set CS to a new code segment selector, which is slow (see Agner Fog's guides).

CALL ptr16:32 ("Call far, absolute, address given in operation") 有一个 6 字节的段:偏移量直接编码到指令中,而不是从正常寻址模式给定的位置将其作为数据加载.所以它是对绝对地址的直接调用.

CALL ptr16:32 ("Call far, absolute, address given in operand") has a 6-byte segment:offset encoded right into the instruction, rather than loading it as data from a location given by a normal addressing mode. So it's a direct call to an absolute address.

Far call 还推送 CS:EIP 作为返回地址,而不仅仅是 EIP,因此它甚至与仅推送 EIP 的普通(近)call 不兼容.对于 jmp ptr16:32 来说,这不是问题,只是速度慢并弄清楚要为段部分放置什么.

Far call also pushes CS:EIP as the return address instead of just EIP, so it's not even compatible with normal (near) call that only pushes EIP. That's not an issue for jmp ptr16:32, just the slowness and figuring out what to put for the segment part.

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

Changing CS is generally only useful for changing from 32 to 64-bit mode or vice versa. Usually only kernels would do this, although you can do this in user-space under most normal OSes that keep 32 and 64-bit segment descriptors in the GDT. That would be more of a silly computer trick than something useful, though. (64-bit kernels return to 32-bit userspace with iret or maybe with sysexit. Most OSes would only use a far jmp once during bootup to switch to a 64-bit code segment in kernel mode.)

主流操作系统使用扁平内存模型,您永远不需要更改cs,并且没有标准化cs 值将用于用户空间进程.即使您想使用远 jmp,您也必须弄清楚在段选择器部分中放入什么值.(JITing 时很容易:只需使用 mov eax, cs 读取当前的 cs.但对于提前编译来说很难移植.)

Mainstream OSes use a flat memory model where you never need to change cs, and it's not standardized what cs value will be used for user-space processes. Even if you wanted to use a far jmp, you'd have to figure out what value to put in the segment selector part. (Easy while JITing: just read the current cs with mov eax, cs. But hard to be portable for ahead-of-time compilation.)

call ptr16:64 不存在,远直接编码只存在于 16 位和 32 位代码.在 64 位模式下,您只能使用 10 字节的 m16:64 内存操作数远 call,例如 call far [rdi].或者将 segment:offset 压入堆栈并使用 retf.

call ptr16:64 doesn't exist, the far direct encodings only exist for 16 and 32-bit code. In 64-bit mode you can only far-call with a 10-byte m16:64 memory operand, like call far [rdi]. Or push segment:offset on the stack and use retf.

这篇关于在 x86 机器码中调用绝对指针的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆