像这样但没有CALL
指令。我想我应该使用JMP
以及可能的其他说明。
PUSH 5
PUSH 4
CALL Function
答案 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,但是手动推送新的返回地址始终是个问题。)