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

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

问题描述

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

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?

想要要做的事情:

我正在尝试基于子例程线程"构建一种简化的mini-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 EFBEADDE假设地在DEADBEEF处调用该函数一样(基本上是通过将内容放入汇编器和反汇编器中并查看产生的有效值来发现它们的)结果,不是是通过了解它们的作用来实现的),但是我对有关段和特权以及相关信息的理解不够好,无法看到差异,或者它们与更频繁地表现出不同的方式-见call指令.英特尔架构手册还建议这些仅在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从JITed代码中调用提前编译的函数.

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 far您不想要的.请参阅 call 的Intel insn set ref手动输入. (有关指向文档和指南的其他链接,另请参见 x86标签Wiki .)大多数计算机体系结构

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 rel32 ,而E8 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指令的过程;在准时进行操作应该同样容易.

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.

这些汇编和链接恰好在位置相关的代码中(而不是共享的lib或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的虚拟地址空间中较低,因此它们可以将[disp32 + reg]数组索引与带符号扩展的32位绝对地址一起使用,或将静态地址放入具有mov eax, imm32的寄存器中,以用于零扩展的绝对值.因此低2GB,而不是低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 =可变参数函数的XMM 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.

如果您真的需要避免修改任何寄存器,则可以将绝对地址保持为内存中的常数,并使用具有RIP相对寻址模式的内存间接call,例如

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 call *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的调用.这并不可怕,但是call 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

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部分通常从静态可执行文件(或0x400080开始-x86-64-linux>非PIE动态可执行文件),即在虚拟地址空间的2GiB较低的位置,其中所有静态代码/数据都位于默认代码模型中.但是0xdeadbeef在低32位的高半部分(即在低4G而不是低2G中),因此它可以表示为零扩展的32位整数而不是符号扩展的32位.并且0x00000000deadbeef - 0x0000000000400080不适合有符号的32位整数,该整数可以正确扩展到64位. (您可以使用负数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.

您可能会在 call call/jmp中,它们也将CS设置为新的代码段选择器,这很慢

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(调用远,绝对,操作数中给出的地址")具有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位模式,反之亦然.通常,只有内核才能执行此操作,尽管您可以 在大多数普通的OS(在GDT中保留32位和64位段描述符)的用户空间中执行此操作.但是,那将是更多愚蠢的计算机技巧,而不是有用的东西. (64位内核使用iretsysexit返回到32位用户空间.大多数操作系统在启动过程中仅使用一次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 far [rdi])对far- call进行操作.或将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天全站免登陆