我在理解如何使用esp
和ebp
寄存器时遇到了一些麻烦。
我们为什么这样做:
pushl %ebp
movl %esp, %ebp
在每个功能的开始?ebp
第一次推送时保持什么?
我在理解如何使用esp
和ebp
寄存器时遇到了一些麻烦。
我们为什么这样做:
pushl %ebp
movl %esp, %ebp
在每个功能的开始?ebp
第一次推送时保持什么?
我们为什么这样做:
这是有历史原因的。在 16 位代码中...
16*ss
或16*ds
)。因为sp
不能用于直接访问内存(例如10(%sp)
- 这在 16 位代码中是不可能的),所以您首先必须复制sp
到另一个寄存器然后访问内存(例如复制sp
到bp
然后执行10(%bp)
)。
当然,也可以使用bx
,si
或di
代替bp
。
然而,第二个问题是段:使用其中一个寄存器将访问由ds
寄存器指定的段。要访问堆栈上的内存,我们必须这样做ss:10(%bx)
而不是10(%bx)
. Usingbp
隐式访问包含堆栈的段(与显式指定段相比,它更快并且指令短一个字节)。
在 32 位(或 64 位)代码中,所有这些都不再需要了。我刚刚用现代 C 编译器编译了一个函数。结果是:
movl 12(%esp), %eax
imull 8(%esp), %eax
addl 4(%esp), %eax
ret
如您所见,ebp
未使用该寄存器。
ebp
但是,在现代代码中仍然使用它有两个原因:
8(%ebp)
即使你的函数包含push
和pop
指令。使用esp
第一个参数的位置随每个push
orpop
操作而变化。alloca
:这个函数将以esp
一种编译器甚至可能无法预测的方式修改寄存器!所以你需要一份原始esp
登记簿的副本。使用示例alloca
:
push %ebp
mov %esp, %ebp
call GetMemorySize # This will set %eax
# ---- Start of alloca() ----
# The alloca "function" will reserve N bytes on the
# stack while the value N is calculated during
# the run-time of the program (here: by the function
# GetMemorySize)
or $3, %al
inc %eax
# This has the same effect as multiple "push"
# instructions. However, we don't know how many
# "push" instructions!
sub %eax, %esp
mov %esp, %eax
# From this moment on, we would not be able to "restore"
# the original stack any more if we didn't have a copy
# of the stack pointer!
# ---- End of alloca() ----
push %eax
mov 8(%ebp), %eax
push %eax
call ProcessSomeData
mov %ebp, %esp
pop %ebp
# Of course we need to restore the original value
# of %esp before we can do a "ret".
ret
在每个函数的开头,ebp 指向调用函数想要它的任何位置,它与当前函数无关,直到当前函数的代码选择使用它。如果您选择使用堆栈帧,ebp 只是一个堆栈帧指针。这个概念是您可以使用 ebp 为您的函数提供对堆栈的非移动引用,同时您可以自由地继续使用 esp 在堆栈上添加或删除项目。如果您不使用堆栈指针并继续使用 esp 作为对堆栈的引用,那么堆栈上的特定项目在您的函数过程中的位置相对于 esp 会有所不同。如果您在开始使用堆栈之前设置了 ebp(除了保存 ebp),那么对于您的函数关心的堆栈上的参数(例如传递的参数),您有一个固定的相对地址,
您可以完全自由地使用 eax 或 edx 或任何其他寄存器作为函数中的堆栈帧指针,ebp 可以作为通用寄存器供您用于堆栈帧,因为 x86 历来具有堆栈依赖性(返回地址和旧的调用约定是基于堆栈的)。其他具有更多寄存器的指令集可能会简单地为编译器实现选择一个寄存器作为函数指针/堆栈帧指针。如果您可以选择并选择使用堆栈框架。它会烧掉一个你可以用来做其他事情的寄存器,烧掉更多的代码和执行时间。与使用其他通用寄存器一样,根据当今使用的调用约定,ebp 是非易失性的,您需要保留它并以您找到它的方式返回它。所以它指向的是特定于函数的。
一个特定的编译器实现可以选择有栈帧并且可以选择它如何使用 ebp。如果它在启用时始终以相同的方式使用,那么使用该工具链,您可能拥有一个可以利用它的调试器或其他工具。例如,如果函数中的第一件事是将 ebp 压入堆栈,那么在任何与 ebp 相关的函数中调用函数的返回地址都是固定的(除非有一些尾部优化,否则它可能是调用者的调用者(调用者(调用者的)))。您正在为此功能烧录寄存器和堆栈空间以及代码空间,但是,就像为调试而编译一样,您可以在开发期间使用堆栈帧进行编译以使用这些功能。
从 push 开始的原因是这是使用帧指针并定义一致位置的好方法。将它推入堆栈作为您做的第一件事 1) 保留 ebp,因此您不会使调用函数崩溃 2) 在 ebp 下方定义一致的参考点地址是返回地址,并且在固定偏移量处调用参数函数的持续时间。对于这样的方案,局部变量位于 ebp 以上的固定地址。编译器以及人类完全不需要这样做,我的第一个参数可能在代码中的某个点位于 esp-20,然后我可能会在堆栈上再推 8 个字节,因为相同的参数是在 esp-28,只需将其编码。
但出于调试目的调试生成的代码,有时例如在固定偏移处查找返回地址。烧另一个寄存器,虽然 IMO 很懒,但绝对可以帮助调试并提高编译器输出的质量。更快地发现编译器输出中的错误,并帮助尝试阅读代码的人们以更少的努力更快地理解它。在正确使用堆栈帧指针的情况下,所有参数和局部变量在堆栈帧指针的设置和清理点之间的函数持续时间内都处于堆栈帧指针的固定偏移量。推送指针以保存它,将帧指针设置为堆栈指针,带或不带偏移量。到弹出的帧指针返回之前。
在函数执行期间,可以将各种对象压入堆栈。push 递减%esp
(或者%rsp
如果你使用 64 位硬件)指向堆栈上的下一个可用内存,而%ebp
(或%rbp
)保持一个不变的指向函数堆栈帧开始的指针,因此,相对于%ebp
,函数是能够找到已经存储在堆栈中的各种对象。
早期的 8 位 CPU(如 1970 年代和 1980 年代的旧 6502)没有%ebp
. 缺少%epb
,请考虑以下 C 代码:
int a = 10;
++a;
{
int b = 20;
--b;
a += b;
}
a
存储在0(%esp)
,除了当被b
压入堆栈时,a
实际上没有移动的 ,现在位于4(%esp)
。你看到问题了吗?
使用%ebp
,a
总是在-4(%ebp)
,b
范围内的时间在-8(%ebp)
。