【问题标题】:Jumps for a JIT (x86_64) [duplicate]跳转 JIT (x86_64) [重复]
【发布时间】:2015-06-30 03:05:24
【问题描述】:

我正在用 C 语言为 x86_64 linux 编写 JIT 编译器。

目前的想法是在可执行内存的缓冲区中生成一些字节码(例如,通过 mmap 调用获得)并使用函数指针跳转到它。

我希望能够将多个可执行内存块链接在一起,以便它们可以仅使用本机指令在彼此之间跳转。

理想情况下,指向可执行块的 C 级指针可以作为绝对跳转地址写入另一个块,如下所示:

unsigned char *code_1 = { 0xAB, 0xCD, ... };
void *exec_block_1 = mmap(code1, ... );
write_bytecode(code_1, code_block_1);
...
unsigned char *code_2 = { 0xAB, 0xCD, ... , exec_block_1, ... };
void *exec_block_2 = mmap(code2, ... );
write_bytecode(code_2, exec_block_2); // bytecode contains code_block_1 as a jump
                                      // address so that the code in the second block
                                      // can jump to the code in the first block

但是我发现 x86_64 的局限性在这里是一个很大的障碍。没有办法跳转到 x86_64 中的绝对 64 位地址,因为所有可用的 64 位跳转操作都是相对于指令指针的。这意味着我不能将 C 指针用作生成代码的跳转目标。

是否有解决此问题的方法可以让我按照我所描述的方式将块链接在一起?也许是我不知道的 x86_64 指令?

【问题讨论】:

  • 嗯,也许您高估了生成超过 2 GB 代码的需求。 jitter 的好处是你总是可以告诉你需要回退到一个间接跳转,比如jmp rax
  • @HansPassant 说得好。目前我的目标只是实现最简单的工作,然后再担心性能。
  • 还相关:Handling calls to far away intrinsic functions in a JIT/ re:使用带有提示地址的mmap 分配彼此靠近的块,因此您可以使用直接的calljmp rel32 编码。

标签: assembly compiler-construction x86-64 jit machine-code


【解决方案1】:

嗯,我不确定我是否清楚地理解了您的问题,以及这是否是正确的答案。实现这一目标的方式相当复杂:

    ;instr              ; opcodes [op size] (comment)
    call next           ; e8 00 00 00 00 [4] (call to get current location)
next:
    pop rax             ; 58 [1]  (next label address in rax)
    add rax, 12h        ; 48 83 c0 12 [4] (adjust rax to fall on landing label)
    push rax            ; 50 [1]  (push adjusted value)
    mov rax, code_block ; 48 b8 XX XX XX XX XX XX XX XX [10] (load target address)
    push rax            ; 50 [1] (push to ret to code_block)
    ret                 ; c3 [1] (go to code_block)
landing:    
    nop
    nop

e8 00 00 00 00 只是为了获取堆栈顶部的当前指针。然后代码调整rax 以稍后落在着陆标签上。您需要将XX(在mov rax, code_block 中)替换为code block 的虚拟地址。 ret 指令用作调用。当调用者返回时,代码应该落在landing

这是你想要达到的目标吗?

【讨论】:

  • 谢谢。做'mov rax, code_block', 'push rax', 'ret' 有我想要的效果。理想情况下,我想要一些不需要接触堆栈的东西,但现在就足够了。
  • 请注意:现代英特尔处理器会跟踪返回地址以进行分支目标预测。手动推送另一个地址,然后使用ret 跳转到它会干扰预测器,并且可能会导致性能下降。有关详细信息,请参阅优化手册。
  • @AlexJ136:与call raxjmp rax 相比,这几乎是最低效的。 x86-64 有 register-indirect 调用和 jmp,使用它们! push/ret 更糟糕,因为它通过不匹配的调用和 ret 破坏了返回地址预测器堆栈。 (call next 是一种特殊情况,在大多数 CPU blog.stuffedcow.net/2018/04/ras-microbenchmarks/#call0 中不会出现在 RAS 上,因此这使得它对于未来的返回不平衡以及迫使这个ret 错误预测。 ) 使用call/pop 查找自己的地址在x86-64 中也很疯狂;这就是我们有 RIP 相关 LEA 的原因。
【解决方案2】:

如果您在发出跳转指令时知道块的地址,您只需检查从跳转指令的地址到目标块地址的字节距离是否符合jXX 系列指令的 32 位有符号偏移量。

即使您分别mmap 每个块,也很有可能不会得到两个相隔超过 ±2GiB 的相邻(在控制流意义上)块。话虽如此,这样单独映射每个块有几个很好的理由。首先,mmap 的最小分配单位是(几乎按照定义)一个页面,大概至少 4KiB。这意味着每个块的代码之后的未使用空间被浪费了。其次,将基本块封装得更紧密会增加指令缓存的利用率和较短的跳转编码有效的机会。

也许是我不知道的 x86_64 指令?

顺便说一句,有一条指令可以将 64 位立即数加载到 rax 中。 GNU 工具链将其称为movabs

0000000000000000 <.text>:
   0:   49 b8 ff ff ff ff ff    movabs rax,0x7fffffffffffffff
   7:   ff ff 7f

所以如果你真的想,你可以简单地将指针加载到rax 并使用跳转来注册。

【讨论】:

  • iTLB 效果将是一个更相关的问题,除非您真的只是在谈论微小的基本块。 i-cache 位置在 64 字节块中(或考虑到 L2 相邻行预取,为 128 字节),而 short-jmp 是 rel8 -128..+127 字节位移。但是,是的,movabs / jmp *rax 是显而易见的选择。另见Handling calls to far away intrinsic functions in a JIT
猜你喜欢
  • 2020-03-07
  • 1970-01-01
  • 2015-06-09
  • 2016-08-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多