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

(请注意,这张图底部是高地址,顶部是低地址。)

谁能解释一下ebp和esp在这里是什么意思?

从我所看到的,我想说堆栈指针总是指向堆栈的顶部,而基指针指向当前函数的开始?还是别的什么?


edit:我指的是在windows程序的上下文中

edit2: eip又是如何工作的呢?

编辑3:我有以下代码从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

它们似乎都是dwords,因此每个都占用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之前发生了什么)。


当前回答

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

当你调用一个函数时,任何局部变量都将被存储在堆栈中,并且堆栈指针将被递增。当从函数返回时,堆栈上的所有局部变量都超出作用域。为此,可以将堆栈指针设置回基指针(即函数调用之前的“上一个”顶部)。

这样做内存分配是非常快速和有效的。

其他回答

就像你说的,超能力是最重要的。

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 为本地人分配空间

编辑:有关更好的描述,请参阅维基书中关于x86汇编的x86反汇编/函数和堆栈框架。我试着添加一些你可能对使用Visual Studio感兴趣的信息。

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

说到Windows程序,你可能会使用Visual Studio来编译你的c++代码。请注意,微软使用了一种称为帧指针省略的优化,这使得不使用dbghlp库和可执行文件的PDB文件几乎不可能遍历堆栈。

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

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

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

当你调用一个函数时,任何局部变量都将被存储在堆栈中,并且堆栈指针将被递增。当从函数返回时,堆栈上的所有局部变量都超出作用域。为此,可以将堆栈指针设置回基指针(即函数调用之前的“上一个”顶部)。

这样做内存分配是非常快速和有效的。

我很久没有做汇编编程了,但是这个链接可能有用…

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

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

基寄存器或段寄存器只是指向大量数据的地址空间。与第二个寄存器结合使用,Base指针将内存划分为巨大的块,而第二个寄存器将指向该块中的一个项。基指针指向数据块的基。

请记住,程序集是特定于CPU的。我所链接到的页面提供了关于不同类型CPU的信息。

首先,堆栈指针指向堆栈的底部,因为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的旧值,这样你就可以恢复之前的帧指针。