在 x86 程序集的过程中调用 ret 指令的位置是不是重要
Posted
技术标签:
【中文标题】在 x86 程序集的过程中调用 ret 指令的位置是不是重要【英文标题】:Does it matter where the ret instruction is called in a procedure in x86 assembly在 x86 程序集的过程中调用 ret 指令的位置是否重要 【发布时间】:2018-03-24 16:30:00 【问题描述】:我目前正在学习 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
【问题讨论】:
tl;dr 答案摘要:否。ret
= pop eip
所以它关心 esp
指向什么。如果使用得当,它是高级功能的构建块,但它不是魔法。参见说明参考手册:felixcloutier.com/x86/RET.html
如果这是 AT&T 语法,那么涉及堆栈指针的两个移动都是错误的!如果这是 Intel 语法,请去掉 % 字符。
【参考方案1】:
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
)。 p>
也就是说,复杂的现代 x86 CPU 确实会跟踪 call/ret
配对(以预测下一个 ret
将返回的位置,因此它可以快速预取代码),所以如果您将使用其中一个替代的非标准方式,在某些时候CPU会意识到它的返回地址预测系统偏离了真实状态,它必须丢弃所有这些缓存/预加载并从真实eip
值重新获取所有内容,所以你可能会因为混淆它而付出性能损失。
【讨论】:
【参考方案2】:您必须保留找到的堆栈和非易失性寄存器。调用函数不知道您可能对它们做了什么 - 调用函数将简单地继续执行ret
之后的下一条指令。只有在你完成清理之后ret
。
ret
将始终在堆栈顶部查找其返回地址,并将pop
将其放入EIP
。如果ret
是一个“远”返回,那么它还将pop
代码段放入CS
寄存器(这也将被call
推送用于“远”调用)。因为这些是call
推送的第一件事,所以它们一定是ret
弹出的最后几件事。否则你最终会在某个未定义的地方ret
ing。
【讨论】:
【参考方案3】:在示例代码中,如果返回是在pop %ebp
之前完成的,它将尝试返回到函数开头的 ebp 中的“地址”,这将是返回的错误地址。
【讨论】:
以上是关于在 x86 程序集的过程中调用 ret 指令的位置是不是重要的主要内容,如果未能解决你的问题,请参考以下文章
Android 逆向x86 汇编 ( push / pop 入栈 / 出栈 指令 | ret / retn 函数调用返回指令 | set 设置目标值指令 )