使用这个来自维基百科的例子,其中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之前发生了什么)。
编辑:有关更好的描述,请参阅维基书中关于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,您将很难执行堆栈遍走。
就像你说的,超能力是最重要的。
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堆栈是从高地址值构建到低地址值。堆栈指针是下一个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汇编的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,您将很难执行堆栈遍走。