使用来自维基百科的 this example,其中 DrawSquare() 调用 DrawLine(),
https://upload.wikimedia.org/wikipedia/commons/thumb/d/d3/Call_stack_layout.svg/342px-Call_stack_layout.svg.png
(请注意,此图底部的高地址和顶部的低地址。)
谁能解释一下在这种情况下 ebp
和 esp
是什么?
从我所看到的,我会说堆栈指针总是指向堆栈的顶部,而基指针指向当前函数的开头?或者是什么?
编辑:我的意思是在 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
它们似乎都是双字,因此每个占用 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 之前发生了什么)。
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
为当地人分配空间
ESP
是当前堆栈指针,每当一个字或地址被压入或弹出堆栈时,它都会改变。 EBP
是编译器跟踪函数参数和局部变量的一种比直接使用 ESP
更方便的方法。
通常(这可能因编译器而异),被调用函数的所有参数都由调用函数压入堆栈(通常与在函数原型中声明它们的顺序相反,但这会有所不同) .然后调用该函数,将返回地址 (EIP
) 压入堆栈。
进入函数后,旧的 EBP
值被压入堆栈,EBP
被设置为 ESP
的值。然后 ESP
递减(因为堆栈在内存中向下增长)为函数的局部变量和临时变量分配空间。从那时起,在函数的执行过程中,函数的参数位于堆栈上与 EBP
的 正 偏移处(因为它们在函数调用之前被推送),并且局部变量位于 EBP
的负 偏移处(因为它们是在函数进入之后在堆栈上分配的)。这就是为什么 EBP
被称为 帧指针,因为它指向 function call frame 的中心。
退出时,函数所要做的就是将 ESP
设置为 EBP
的值(从堆栈中释放局部变量,并在堆栈顶部公开条目 EBP
),然后弹出旧的EBP
值,然后函数返回(将 返回地址 弹出到 EIP
中)。
返回到调用函数后,它可以增加 ESP
以删除它在调用另一个函数之前压入堆栈的函数参数。此时,堆栈又回到了调用被调用函数之前的状态。
EBP
中的 BP
代表 Base Pointer
,但事实并非如此,因为 EBP
指向堆栈帧的中心,而不是底部。 Frame Pointer
感觉像是该寄存器的更好名称。
你说得对。堆栈指针指向堆栈顶部的项目,而基指针指向函数调用之前堆栈的“上一个”顶部。
调用函数时,任何局部变量都将存储在堆栈中,堆栈指针将递增。当您从函数返回时,堆栈上的所有局部变量都会超出范围。为此,您可以将堆栈指针设置回基指针(这是函数调用之前的“前一个”顶部)。
以这种方式进行内存分配非常非常快速和高效。
编辑:要获得更好的描述,请参阅 WikiBook 中有关 x86 程序集的 x86 Disassembly/Functions and Stack Frames。我尝试添加一些您可能对使用 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,您将很难进行堆栈遍历。
首先,堆栈指针指向堆栈的底部,因为 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 的旧值,因此您可以恢复先前的帧指针。
自从我完成汇编编程以来已经很久了,但是 this link 可能很有用......
处理器具有一组用于存储数据的寄存器。其中一些是直接值,而另一些则指向 RAM 中的一个区域。寄存器确实倾向于用于某些特定的操作,并且汇编中的每个操作数都需要特定寄存器中的一定数量的数据。
堆栈指针主要在调用其他过程时使用。使用现代编译器,一堆数据将首先转储到堆栈上,然后是返回地址,这样系统一旦被告知要返回,就会知道在哪里返回。堆栈指针将指向可以将新数据推送到堆栈的下一个位置,它将一直停留在那里直到再次弹出。
基址寄存器或段寄存器只是指向大量数据的地址空间。结合第二个寄存器,基指针将把内存分成大块,而第二个寄存器将指向这个块中的一个项目。因此,基指针指向数据块的基。
请记住,Assembly 是非常特定于 CPU 的。我链接到的页面提供了有关不同类型 CPU 的信息。
不定期副业成功案例分享