如果esp指向栈顶,那么ebp指向哪里呢?

If esp points to the top of the stack, where does ebp point to?

我无法理解 espebp 寄存器的使用方式。

我们为什么这样做:

pushl %ebp 
movl %esp, %ebp

在每个函数的开头?第一次推的时候ebp拿着什么?

在函数执行期间,可以将各种对象压入堆栈。推送递减 %esp(或 %rsp,如果您使用 64 位硬件)指向堆栈中的下一个可用内存,而 %ebp(或 %rbp)保持指向函数堆栈帧开始的不变指针,因此相对于 %ebp,函数能够找到已存储在堆栈中的各种对象。

早期,像 1970 年代和 1980 年代的旧 6502 这样的 8 位 CPU 没有 %ebp。缺少 %epb,请考虑以下 C 代码:

int a = 10;
++a;
{
    int b = 20;
    --b;
    a += b;
}

a存储在0(%esp)处,只是在将b压入栈时,未实际移动的a是现在 4(%esp)。你看到问题了吗?

使用 %ebpa 始终位于 -4(%ebp),而范围内的 b 位于 -8(%ebp).

Why do we do:

这是有历史原因的。在 16 位代码中 ...

  • ...x86 CPU 不允许所有寄存器都用于内存寻址。
  • ...地址是相对于 "segments" 的(例如 16*ss16*ds)。

因为 sp 不能用于直接访问内存(例如 10(%sp) - 这在 16 位代码中是不可能的),你首先必须将 sp 复制到另一个注册然后访问内存(例如复制spbp然后做10(%bp))。

当然也可以使用bxsidi代替bp

然而,第二个问题是段:使用这些寄存器之一将访问由 ds 寄存器指定的段。要访问堆栈上的内存,我们必须执行 ss:10(%bx) 而不是 10(%bx)。使用 bp 隐式访问包含堆栈的段(与显式指定段相比,速度更快,指令短一个字节)。

在 32 位(或 64 位)代码中,所有这些都不再需要了。我刚刚用现代 C 编译器编译了一个函数。结果是:

movl    12(%esp), %eax
imull   8(%esp), %eax
addl    4(%esp), %eax
ret

如您所见,未使用ebp寄存器。

但是,ebp在现代代码中仍然使用的原因有两个:

  • 创建函数更容易。您知道第一个参数始终位于 8(%ebp),即使您的函数包含 pushpop 指令也是如此。使用 esp 第一个参数的位置随着每个 pushpop 操作而改变。
  • 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 , %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 历来具有堆栈依赖性(return 地址,旧的调用约定是基于堆栈的)。其他具有更多寄存器的指令集可能只是为编译器实现选择一个寄存器作为函数 pointer/stack 帧指针。如果您有选择并选择使用堆栈框架。它会烧掉一个你可以用来做其他事情的寄存器,烧掉更多的代码和执行时间。与使用其他通用寄存器一样,根据当今使用的调用约定,ebp 是非易失性的,您需要保存它并 return 按照您找到它的方式保存它。所以它指向的是特定于函数的。输入函数时它指向的内容特定于调用函数。

特定的编译器实现可能会选择堆栈帧并可能会选择它如何使用 ebp。如果它在启用时始终以相同的方式使用,那么使用该工具链,您可能拥有可以利用它的调试器或其他工具。例如,如果函数中的第一件事是将 ebp 压入堆栈,那么相对于 ebp 的任何函数中调用函数的 return 地址都是固定的(好吧,除非有一些尾部优化,那么它可能是调用者呼叫者(呼叫者的(呼叫者的)))。您正在为此功能烧录寄存器和堆栈 space 以及代码 space,但是,就像为调试而编译一样,您可以在开发期间使用堆栈帧进行编译以使用这些功能。

之所以从推送开始,是因为这是使用帧指针和定义一致位置的好方法。将它推入堆栈作为你做的第一件事 1) 保留 ebp 所以你不会崩溃调用函数 2) 在 ebp 下面定义一个一致的参考点地址是 return 地址和调用参数函数持续时间内的固定偏移量。对于这样的方案,局部变量位于 ebp 之上的固定地址。编译器和人类完全不需要这样做,我的第一个参数可能在代码中的某个位置是 esp-20,然后我可能会在堆栈上多压 8 个字节,因为相同的参数是在 esp-28,就这样编码。

但出于调试目的调试生成的代码,有时例如在固定偏移处找到 return 地址。燃烧另一个寄存器,是 IMO 懒惰但是,绝对可以帮助调试和提高编译器输出的质量。更快地在编译器的输出中找到错误,并帮助那些试图阅读代码的人更快地理解它,更省力。在正确使用堆栈帧指针的情况下,在设置和清理堆栈帧指针的点之间的函数持续时间内,所有参数和局部变量都处于堆栈帧指针的固定偏移量。推送指针以将其设置为具有或不具有偏移量的堆栈指针的帧指针。弹出return.

前的帧指针