我目前正在学习x86程序集。但是当我使用堆栈进行函数调用时,我仍然不清楚某些事情。我知道调用指令将涉及在堆栈上推送返回地址,然后加载程序计数器和要调用的函数的地址。 ret指令将该地址加载回程序计数器。
我的困惑是,在过程/函数中调用ret指令时是否重要?它是否总能找到存储在堆栈中的正确返回地址,或者堆栈指针当前是否必须指向存储返回地址的位置?如果是这样的话,我们不能只使用push和pop而不是call和ret吗?
例如,下面的代码可能是第一个进入函数的代码,如果我们在堆栈上推送不同的寄存器,只有在反向命令弹出寄存器后才能调用ret指令,以便在弹出%ebp之后指令,堆栈指针将指向返回地址所在的堆栈上的正确位置,或者无论它在何处被调用,它仍会找到它?提前致谢
push %ebp
mov %ebp, %esp
//push other registers
...
//pop other registers
mov %esp, %ebp
(could ret instruction go here for example and still pop the correct return address?)
pop %ebp
ret
答案 0 :(得分:5)
您必须在找到它们时保留堆栈和非易失性寄存器。调用函数不知道你可能用它们做了什么 - 否则调用函数将继续执行ret
之后的下一条指令。清理完成后只有ret
。
ret
将始终在堆栈顶部查找其返回地址,并pop
将其转换为EIP
。如果ret
是一个“远”返回,那么它还会pop
将代码段CS
放入call
寄存器(也可能由call
推送到“远” “打电话”。由于这些是ret
推送的第一件事,因此它们必须是由ret
弹出的最后一件事。否则你最终会SELECT DISTINCT street
FROM table
WHERE NOT(age>3)
某处未定义。
答案 1 :(得分:5)
CPU不知道什么是函数/ etc ... 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会意识到它的返回地址的预测系统已经脱离真实状态,它将不得不丢弃所有这些缓存/预加载并从真实的{{1值,因此您可能会因为混淆而支付性能损失。
答案 2 :(得分:1)
在示例代码中,如果返回是在pop %ebp
之前完成的,它将尝试返回"地址"在函数开头的ebp中,这将是返回的错误地址。