如何使用JMP模拟CALL指令?

时间:2014-01-21 02:51:41

标签: function assembly x86

像这样但没有CALL指令。我想我应该使用JMP以及可能的其他说明。

PUSH 5
PUSH 4
CALL Function

3 个答案:

答案 0 :(得分:13)

这很容易做到。将返回地址压入堆栈,然后跳转到子例程。 最终代码如下所示:

   PUSH 5
   PUSH 4
   PUSH offset label1
   jmp Function
 label1: ; returns here
   leas esp, 8[esp]

 Function: 
   ...
   ret

虽然这样可行,但你真的不想这样做。在大多数现代处理器上,保留了一个片上调用堆栈返回地址缓存,它会在调用时推送返回地址,并且pop返回RET上的地址。在处理器上,这具有极短的更新/访问时间,这意味着RET指令可以使用调用堆栈高速缓存弹出值来预测PC下一步应该去哪里,而不是等待从实际指向的内存位置读取的实际内存通过ESP。如果你做“PUSH offset label1”技巧, 此缓存不会更新,因此RET分支预测错误,处理器管道爆炸,对性能产生严重的负面影响。 (我认为IBM拥有特殊指令的专利,基本上是“PUSHRETURNADDRESS k”和“POPRETURNADDESS”,允许在他们的一些CPU上使用这个技巧。唉,不在x86上。

答案 1 :(得分:4)

这取决于具体情况。如果你的函数在返回之前做的最后一件事是调用另一个函数,你可以简单地跳转到该函数。这称为尾调用消除,是许多编译器执行的优化。例如:

foo:
    call B
    call A
    ret

尾部调用消除使用单个跳转指令替换最后两行:

foo:
    call B
    jmp  A

这是有效的,因为堆栈包含foo调用者的返回地址。因此,当函数A返回时,它将返回到调用foo的函数。

你希望执行在跳转到A之后恢复,在跳转之前将该地址压入堆栈:

foo:
    call B
    push offset bar
    jmp  A
bar:

但是,我认为没有理由为什么要这样做。

答案 2 :(得分:1)

在x86-64之前,call是唯一可以读取EIP的指令。 (我猜也是int,但它不会将结果放在您可以从用户空间读取的任何位置。)

因此,不可能在与位置无关的代码中模拟call。实际上,32位PIC代码使用 call来查找自己的地址。

但是在x86-64中,我们有相对于RIP的lea

    ... put function args in registers

    lea    rax, [rel ret_addr]    ; AT&T lea ret_addr(%rip), %rax
    push   rax
    jmp    call_target
ret_addr:

call本身在内部解码为push RIP / jmp target,其中,一条指令执行期间的RIP =该指令结束的地址=下一条指令的开始。


当然,这通常会降低性能,使返回地址预测变量堆栈失去平衡。 http://blog.stuffedcow.net/2018/04/ras-microbenchmarks/。使用普通的call,除非您想要 ret进行错误的预测,例如retpoline或specpoline。

(使用 just jmp进行尾调用,可以将一个呼叫/重发对折叠成一个jmp,但是手动推送新的返回地址始终是个问题。)