基指针和堆栈指针到底是什么?他们指的是什么?

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

alt text

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

有人能解释一下在这个上下文中ebpesp是什么吗?

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


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

edit2: 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

它们似乎都是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之前发生了什么)。

238160 次浏览

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

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

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

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

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

处理器有一个用于存储数据的寄存器集合。其中一些是直接值,而另一些则指向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的旧值,这样你就可以恢复之前的帧指针。

编辑:有关更好的描述,请参见关于x86程序集的WikiBook中的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,您将很难执行堆栈遍走。

ESP是当前堆栈指针,当一个单词或地址被推入或弹出堆栈时,该指针将发生变化。对于编译器来说,EBP是一种比直接使用ESP更方便的方法来跟踪函数的形参和局部变量。

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

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

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

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