CPU 不知道什么是函数/等...该指令将从那里的跳转ret
指向的内存中获取值。esp
例如,您可以执行以下操作(以说明 CPU 对您如何组织源代码不感兴趣):
; slow alternative to "jmp continue_there_address"
push continue_there_address
ret
continue_there_address:
...
此外,您不需要从堆栈中恢复寄存器,(甚至不需要将它们恢复到原始寄存器),只要在执行esp
时指向返回地址ret
,它将被使用:
call SomeFunction
...
SomeFunction:
push eax
push ebx
push ecx
add esp,8 ; forget about last 2 push
pop ecx ; ecx = original eax
ret ; returns back after call
如果您的函数应该与代码的其他部分互操作,您可能仍希望按照您正在编程的平台的调用约定的要求存储/恢复寄存器,因此从调用者的角度来看,您不会修改某些寄存器值应该保留,等等......但这些都不会打扰 CPU 和执行指令ret
,CPU 只是从堆栈([esp]
)加载值,然后跳转到那里。
此外,当返回地址存储到堆栈时,它与其他压入堆栈的值没有任何区别,所有这些值都只是写入内存中的值,因此ret
没有机会以某种方式在堆栈中找到“返回地址”并跳过“值”,对于 CPU,内存中的值看起来相同,每个 32 位值就是 32 位值。无论它是由call
, push
,mov
还是其他东西存储的,都没有关系,信息(价值的来源)不会被存储,只有价值。
如果是这样,我们不能只使用 push 和 pop 而不是 call 和 ret 吗?
您当然可以push
首选将返回地址放入堆栈(我的第一个示例)。但你不能这样做pop eip
,没有这样的指示。实际上就是ret
这样,所以pop eip
实际上是同一件事,但是没有 x86 汇编程序员使用这样的助记符,并且操作码与其他pop
指令不同。您当然可以pop
将返回地址放入不同的寄存器中,例如eax
,然后执行jmp eax
,以进行慢速ret
替代(也可以修改eax
)。
也就是说,复杂的现代 x86 CPU 确实会跟踪一些call/ret
配对(以预测下一个ret
将返回的位置,因此它可以快速预取代码),所以如果您将使用其中一种替代的非标准方式,在某些时候CPU 将意识到它的返回地址预测系统偏离了真实状态,它必须丢弃所有这些缓存/预加载并从真实值中重新获取所有内容eip
,因此您可能会因为混淆它而付出性能损失。