如果esp指向堆栈的顶部,ebp指向哪里?

问题描述 投票:2回答:3

我在理解如何使用espebp寄存器时遇到了一些麻烦。

我们为什么这样做:

pushl %ebp 
movl %esp, %ebp

在每个功能的开头?什么是ebp第一次推动时持有?

function assembly cpu-registers stack-pointer
3个回答
3
投票

在每个函数的开头,ebp指向调用函数所需的位置,它与当前函数无关,直到当前函数的代码选择使用它。 ebp只是一个堆栈帧指针,以防您选择具有堆栈帧。这个概念是你可以使用ebp为你的函数添加一个非移动的引用,同时你可以继续使用esp继续添加或删除堆栈中的项目。如果你不使用堆栈指针并且继续使用esp作为堆栈的引用,那么堆栈中的特定项目在你的函数的过程中相对于esp而言是不同的。如果您在开始使用堆栈之前设置了ebp(除了保存ebp),那么您对函数关注的堆栈上的参数有一个固定的相对地址,如传递的参数,局部变量等。

您可以完全自由地使用eax或edx或任何其他寄存器作为函数中的堆栈帧指针,ebp作为通用寄存器供您用于堆栈帧,因为x86历来具有堆栈依赖性(返回地址,和旧的调用约定是基于堆栈的)。具有更多寄存器的其他指令集可以简单地选择用于编译器实现的寄存器作为函数指针/堆栈帧指针。如果您有选项并选择使用堆栈框架。它会烧掉你可能用于其他东西的寄存器,烧掉更多的代码和执行时间。与使用其他通用寄存器一样,根据当前使用的调用约定,ebp是非易失性的,您需要保留它并以找到它的方式返回它。所以它指向的是功能特有的。输入函数时指向的内容特定于调用函数。

特定的编译器实现可以选择具有堆栈帧并且可以选择它如何使用ebp。如果在启用时始终以相同的方式使用它,那么使用该工具链可能会有一个调试器或其他可以利用它的工具。例如,如果函数中的第一件事是在堆栈上推送ebp,那么相对于ebp的任何函数中的调用函数的返回地址是固定的(除非有一些尾部优化,然后可能是调用者的调用者(的)调用者(来电者)))。您正在为此功能刻录寄存器和堆栈空间和代码空间,但是,与编译调试一样,您可以在开发期间使用堆栈帧进行编译以使用这些功能。

你开始推动的原因是这是使用帧指针和定义一致位置的好方法。把它推到堆栈上作为你做的第一件事1)保留ebp所以你不要使调用函数崩溃2)定义一个一致的参考点地址下面的ebp是返回地址和调用参数固定偏移量的持续时间功能。对于像这样的方案,局部变量位于ebp以上的固定地址。编译器以及人类不仅不需要这样做,我的第一个参数可能在代码中的某一点处于esp-20,然后我可能会在堆栈上再推8个字节,因为相同的参数是在esp-28,只需编码就可以了。但出于调试目的,调试生成的代码,有时例如在固定偏移量处查找返回地址。烧另一个寄存器,是IMO懒惰但是,绝对可以帮助调试并提高编译器输出的质量。更快地找到编译器输出中的错误,并帮助尝试阅读代码的人更快地理解它。正确使用堆栈帧指针所有参数和局部变量都在堆栈帧指针的固定偏移处通过堆栈帧指针设置和清理的点之间的函数持续时间。 push指针保存它将帧指针设置为堆栈指针,有或没有偏移量。在返回之前到框架指针的弹出。


4
投票

我们为什么这样做:

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

  • ... x86 CPU不允许所有寄存器用于存储器寻址。
  • ......地址是相对于“段”(例如16*ss16*ds)。

因为sp不能直接访问内存(例如10(%sp) - 这在16位代码中是不可能的),你首先必须将sp复制到另一个寄存器然后访问内存(例如将sp复制到bp然后再执行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 $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

2
投票

在执行该功能期间,可以将各种对象推入堆栈。推送减少%esp(或%rsp,如果你使用64位硬件)指向堆栈上的下一个可用内存,而%ebp(或%rbp)维护一个不变的指针指向函数的堆栈帧的开始,以便相对于%ebp,该函数能够找到已经存储在堆栈中的各种对象。

早期的8位CPU,如20世纪70年代和80年代的旧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)

© www.soinside.com 2019 - 2024. All rights reserved.