254

使用来自维基百科的这个例子,其中 DrawSquare() 调用 DrawLine(),

替代文字

(请注意,此图底部的高地址和顶部的低地址。)

谁能解释我在这种情况下是什么ebp和是什么?esp

从我所看到的,我会说堆栈指针总是指向堆栈的顶部,而基指针指向当前函数的开头?要不然是啥?


编辑:我的意思是在 Windows 程序的上下文中

编辑2:以及如何eip工作呢?

edit3:我有以下来自 MSVC++ 的代码:

var_C= dword ptr -0Ch
var_8= dword ptr -8
var_4= dword ptr -4
hInstance= dword ptr  8
hPrevInstance= dword ptr  0Ch
lpCmdLine= dword ptr  10h
nShowCmd= dword ptr  14h

它们似乎都是双字,因此每个占用 4 个字节。所以我可以看到从 hInstance 到 var_4 的差距为 4 个字节。这些是什么?我假设它是返回地址,如维基百科的图片所示?


(编者注:从迈克尔的回答中删除了一个不属于问题的长引用,但编辑了一个后续问题):

这是因为函数调用的流程是:

* Push parameters (hInstance, etc.)
* Call function, which pushes return address
* Push ebp
* Allocate space for locals

我的问题(最后,我希望!)现在是,从我弹出要调用的函数的参数到序言结尾的那一刻到底发生了什么?我想知道 ebp,esp 在那些时刻是如何演变的(我已经了解了 prolog 的工作原理,我只想知道在我将参数推送到堆栈之后和 prolog 之前发生了什么)。

4

7 回答 7

258

esp就像你说的那样,堆栈的顶部。

ebp通常esp在函数开始时设置为。函数参数和局部变量分别通过添加和减去常量偏移量来访问ebp。所有 x86 调用约定都定义ebp为跨函数调用保留。 ebp它本身实际上指向前一帧的基指针,这使得在调试器中进行堆栈遍历并查看其他帧的局部变量来工作。

大多数函数序言看起来像:

push ebp      ; Preserve current frame pointer
mov ebp, esp  ; Create new frame pointer pointing to current stack top
sub esp, 20   ; allocate 20 bytes worth of locals on stack.

然后在函数的后面你可能会有类似的代码(假设两个局部变量都是 4 个字节)

mov [ebp-4], eax    ; Store eax in first local
mov ebx, [ebp - 8]  ; Load ebx from second local

您可以启用的FPO 或帧指针省略优化实际上将消除这一点并ebp用作另一个寄存器并直接访问本地esp变量,但这使得调试更加困难,因为调试器不再能够直接访问早期函数调用的堆栈帧。

编辑:

对于您更新的问题,堆栈中缺少的两个条目是:

var_C = dword ptr -0Ch
var_8 = dword ptr -8
var_4 = dword ptr -4
*savedFramePointer = dword ptr 0*
*return address = dword ptr 4*
hInstance = dword ptr  8h
PrevInstance = dword ptr  0C
hlpCmdLine = dword ptr  10h
nShowCmd = dword ptr  14h

这是因为函数调用的流程是:

  • 推送参数(hInstance等)
  • 调用函数,推送返回地址
  • ebp
  • 为当地人分配空间
于 2009-09-08T18:46:30.933 回答
124

ESP是当前的堆栈指针,每当一个字或地址被压入或弹出堆栈时,它都会改变。EBP是编译器跟踪函数参数和局部变量的一种比ESP直接使用更方便的方法。

通常(这可能因编译器而异),被调用函数的所有参数都由调用函数压入堆栈(通常与在函数原型中声明它们的顺序相反,但这会有所不同) . 然后调用该函数,将返回地址( EIP) 压入堆栈。

进入函数后,旧EBP值被压入堆栈并EBP设置为 的值ESP。然后ESP递减(因为堆栈在内存中向下增长)为函数的局部变量和临时变量分配空间。从那时起,在函数执行期间,函数的参数位于堆栈上的偏移处EBP(因为它们在函数调用之前被推送),而局部变量位于偏移处EBP(因为它们在函数入口后被分配到堆栈上)。这就是为什么EBP被称为帧指针,因为它指向的中心函数调用框架

退出时,函数所要做的就是将ESP其值设置为EBP(从堆栈中释放局部变量,并暴露EBP堆栈顶部的条目),然后EBP从堆栈中弹出旧值,然后函数返回(将返回地址弹出到EIP)。

返回到调用函数后,它可以递增ESP,以便在调用另一个函数之前删除它压入堆栈的函数参数。此时,堆栈又回到了调用被调用函数之前的状态。

于 2009-09-08T19:44:52.343 回答
17

你说得对。堆栈指针指向堆栈顶部的项目,而基指针指向函数调用之前堆栈的“上一个”顶部。

调用函数时,任何局部变量都将存储在堆栈中,堆栈指针将递增。当您从函数返回时,堆栈上的所有局部变量都会超出范围。为此,您可以将堆栈指针设置回基指针(这是函数调用之前的“前一个”顶部)。

以这种方式进行内存分配非常非常快速和高效。

于 2009-09-08T18:48:39.727 回答
7

编辑:要获得更好的描述,请参阅有关 x86 程序集的 WikiBook 中的x86 反汇编/函数和堆栈帧。我尝试添加一些您可能对使用 Visual Studio 感兴趣的信息。

将调用者 EBP 存储为第一个局部变量称为标准堆栈帧,这可用于 Windows 上的几乎所有调用约定。调用者或被调用者是否释放传递的参数以及哪些参数在寄存器中传递存在差异,但这些与标准堆栈帧问题正交。

谈到 Windows 程序,您可能会使用 Visual Studio 来编译您的 C++ 代码。请注意,Microsoft 使用称为帧指针省略的优化,这使得在不使用 dbghlp 库和可执行文件的 PDB 文件的情况下几乎不可能遍历堆栈。

这种帧指针省略意味着编译器不会将旧的 EBP 存储在标准位置,而是将 EBP 寄存器用于其他用途,因此您很难在不知道给定函数的局部变量需要多少空间的情况下找到调用者 EIP。当然,Microsoft 提供了一个 API,即使在这种情况下,您也可以进行堆栈遍历,但是对于某些用例而言,在 PDB 文件中查找符号表数据库需要很长时间。

为了避免在您的编译单元中使用 FPO,您需要避免使用 /O2 或需要将 /Oy- 显式添加到项目中的 C++ 编译标志中。您可能会链接到在发布配置中使用 FPO 的 C 或 C++ 运行时,因此如果没有 dbghlp.dll,您将很难进行堆栈遍历。

于 2009-09-08T19:20:51.037 回答
6

首先,堆栈指针指向堆栈的底部,因为 x86 堆栈从高地址值构建到低地址值。堆栈指针是下一次调用 push(或调用)将放置下一个值的点。它的操作相当于 C/C++ 语句:

 // push eax
 --*esp = eax
 // pop eax
 eax = *esp++;

 // a function call, in this case, the caller must clean up the function parameters
 move eax,some value
 push eax
 call some address  // this pushes the next value of the instruction pointer onto the
                    // stack and changes the instruction pointer to "some address"
 add esp,4 // remove eax from the stack

 // a function
 push ebp // save the old stack frame
 move ebp, esp
 ... // do stuff
 pop ebp  // restore the old stack frame
 ret

基指针位于当前帧的顶部。ebp 通常指向您的返回地址。ebp+4 指向函数的第一个参数(或类方法的 this 值)。ebp-4 指向函数的第一个局部变量,通常是 ebp 的旧值,因此您可以恢复先前的帧指针。

于 2009-09-08T18:59:05.900 回答
1

自从我完成汇编编程以来已经很久了,但是这个链接可能很有用......

处理器具有一组用于存储数据的寄存器。其中一些是直接值,而另一些则指向 RAM 中的一个区域。寄存器确实倾向于用于某些特定的操作,并且汇编中的每个操作数都需要特定寄存器中的一定数量的数据。

堆栈指针主要在调用其他过程时使用。使用现代编译器,一堆数据将首先转储到堆栈上,然后是返回地址,这样系统一旦被告知要返回,就会知道在哪里返回。堆栈指针将指向可以将新数据推送到堆栈的下一个位置,它将一直停留在那里直到再次弹出。

基址寄存器或段寄存器只是指向大量数据的地址空间。结合第二个寄存器,基指针将把内存分成大块,而第二个寄存器将指向这个块中的一个项目。因此,基指针指向数据块的基。

请记住,Assembly 是非常特定于 CPU 的。我链接到的页面提供了有关不同类型 CPU 的信息。

于 2009-09-08T18:50:18.290 回答
-8

esp 代表“Extended Stack Pointer”.....ebp 代表“Something Base Pointer”......而 eip 代表“Something Instruction Pointer”......堆栈指针指向堆栈段的偏移地址. 基指针指向额外段的偏移地址。指令指针指向代码段的偏移地址。现在,关于段……它们是处理器内存区域的 64KB 小分区……这个过程称为内存分段。我希望这篇文章对您有所帮助。

于 2010-08-01T12:06:41.173 回答