在理解如何使用esp
和ebp
寄存器时遇到一些麻烦。
我们为什么这样做:
pushl %ebp
movl %esp, %ebp
在每个功能开始时? ebp
首次按下时是什么?
答案 0 :(得分:4)
我们为什么这样做:
这有历史原因。用16位代码...
16*ss
或16*ds
)。由于无法使用sp
直接访问内存(例如10(%sp)
-在16位代码中无法实现),因此您首先必须将sp
复制到另一个寄存器,然后然后访问内存(例如,将sp
复制到bp
,然后执行10(%bp)
)。
当然也可以使用bx
,si
或di
代替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)
和push
指令,第一个参数也始终位于pop
处。使用esp
,每个自变量push
或pop
的操作都会改变第一个参数的位置。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
答案 1 :(得分:4)
ebp指向调用函数所需的任何位置,它与当前函数无关,直到当前函数的代码选择使用它为止。 ebp只是一个堆栈帧指针,以防您选择使用堆栈帧。这个想法是,您可以使用ebp对功能的堆栈进行固定移动引用,而您可以继续使用esp继续添加或删除堆栈中的项目。如果您不使用堆栈指针,而是继续使用esp作为对堆栈的引用,则函数过程中堆栈上的特定项相对于esp而言会有所不同。如果您在开始使用堆栈之前已设置ebp(而不是保存ebp),则您的函数所关心的堆栈上的参数(例如传递的参数,局部变量等)具有固定的相对地址。
您可以完全自由地将eax或edx或任何其他寄存器用作函数中的堆栈框架指针,因为x86历来具有堆栈依赖性(返回),因此ebp可以作为通用寄存器用于堆栈框架地址和旧的调用约定基于堆栈)。具有更多寄存器的其他指令集可能只是选择用于编译器实现的寄存器作为函数指针/堆栈帧指针。如果可以选择并选择使用堆栈框架。它会刻录您可能用于其他用途的寄存器,并刻录更多代码和执行时间。像使用其他通用寄存器一样,按照今天使用的调用约定,ebp也是非易失性的,您需要保留它并以找到它的方式返回它。因此,它所指向的是特定于该功能的。当您输入函数时,它指向的是特定于调用函数的。
特定的编译器实现可能选择具有堆栈框架,并可能选择如何使用ebp。而且,如果启用时始终使用相同的方式,那么使用该工具链,您可能会拥有可以利用该工具的调试器或其他工具。例如,如果函数中的第一件事是将ebp推送到堆栈上,则相对于ebp的任何函数中的调用函数的返回地址都是固定的(好吧,除非进行一些尾部优化,否则可能是调用方的调用方( (呼叫者)。)。您正在为此功能刻录寄存器,堆栈空间和代码空间,但是,像进行调试编译一样,可以在开发过程中使用堆栈框架进行编译以使用这些功能。
为什么要从推入开始,这是使用框架指针并定义一致位置的好方法。作为第一件事将其压入堆栈1)保留ebp,以免使调用函数崩溃2)定义ebp以下的一致参考点地址是返回地址,并且在以下时间段内以固定偏移量调用参数功能。对于这样的方案,局部变量位于ebp之上的固定地址处。编译器以及人类都不需要这样做,我的第一个参数可能在代码中的某个点位于esp-20处,然后我可以在栈上再推8个字节,因为相同的参数是在esp-28,只需将其编码为这样。但是出于调试目的,调试产生的代码,有时还查找固定偏移量的返回地址。刻录另一个寄存器是IMO懒惰的,但是绝对可以帮助调试并提高编译器输出的质量。更快地发现编译器输出中的错误,并帮助尝试阅读代码的人们以更少的精力更快地理解它。在正确使用堆栈框架指针的情况下,通过函数在设置和清除堆栈框架指针的点之间的持续时间,所有参数和局部变量都相对于堆栈框架指针处于固定偏移量。推送指针以保存它,将帧指针设置为带有或不带有偏移量的堆栈指针。返回前的帧指针弹出。
答案 2 :(得分:2)
在执行功能期间,可以将各种对象压入堆栈。推减%esp
(如果使用的是64位硬件,则递减%rsp
)指向堆栈上的下一个可用内存,而%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)
。看到问题了吗?
使用%ebp
,a
始终位于-4(%ebp)
,而b
的范围位于-8(%ebp)
。