ChatGPT解决这个技术问题 Extra ChatGPT

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

使用来自维基百科的 this example,其中 DrawSquare() 调用 DrawLine(),

https://upload.wikimedia.org/wikipedia/commons/thumb/d/d3/Call_stack_layout.svg/342px-Call_stack_layout.svg.png

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

谁能解释一下在这种情况下 ebpesp 是什么?

从我所看到的,我会说堆栈指针总是指向堆栈的顶部,而基指针指向当前函数的开头?或者是什么?

编辑:我的意思是在 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 之前发生了什么)。

需要注意的一件重要事情是堆栈在内存中“向下”增长。这意味着要向上移动堆栈指针,您会减小它的值。
区分 EBP/ESP 和 EIP 正在做什么的一个提示:EBP 和 ESP 处理数据,而 EIP 处理代码。
在您的图表中,ebp(通常)是“帧指针”,尤其是“堆栈指针”。这允许通过 [ebp-x] 访问本地变量并通过 [ebp+x] 一致地访问堆栈参数,独立于堆栈指针(在函数中经常更改)。寻址可以通过 ESP 完成,释放 EBP 用于其他操作 - 但这样,调试器无法分辨调用堆栈或本地值。
@本。不吝啬。一些编译器将堆栈帧放入堆中。堆栈向下增长的概念就是这样,一个易于理解的概念。堆栈的实现可以是任何东西(使用堆的随机块使得覆盖堆栈部分的黑客攻击更加困难,因为它们不是确定性的)。
简而言之:堆栈指针允许推送/弹出操作工作(因此推送和弹出知道在哪里放置/获取数据)。基指针允许代码独立引用之前已压入堆栈的数据。

ネロク

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

为当地人分配空间


感谢您的解释!但我现在有点困惑。假设我调用了一个函数,并且我在它的序言的第一行,仍然没有从它执行任何一行。那时,ebp 的价值是多少?除了推送的参数之外,堆栈是否还有其他内容?谢谢!
EBP 并没有神奇地改变,所以在你为你的函数建立一个新的 EBP 之前,你仍然拥有 callers 值。除了参数之外,堆栈还将保存旧的 EIP(返回地址)
不错的答案。尽管如果不提及结语中的内容就无法完成:“离开”和“撤消”指令。
我认为这张图片将有助于澄清关于流程的一些事情。还要记住堆栈向下增长。 ocw.cs.pub.ro/courses/_media/so/laboratoare/call_stack.png
是我,还是上面的代码片段中缺少所有减号?
A
Anis LOUNIS aka AnixPasBesoin

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

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

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

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

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


到目前为止,我读到的关于 esp 和 ebp 的最佳解释。
对于 asm 新手来说,可能会产生误导的一件事是 EBP 中的 BP 代表 Base Pointer,但事实并非如此,因为 EBP 指向堆栈帧的中心,而不是底部。 Frame Pointer 感觉像是该寄存器的更好名称。
有些 CPU 实际上有一个称为“帧指针”的寄存器,例如 Digital Alpha CPU。
我认为ESP不会改变并且在整个程序生命周期中保持不变,因为它只是表示堆栈的开始(顶部)?
@EpicSpeedy不,ESP指向您压入堆栈的最后一项(或顶部的可用空间,不记得是哪个)。无论哪种方式,它都会随着您推送/弹出寄存器或调用函数/从它们返回而改变。
R
Robert Cartaino

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

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

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


@Robert:当您在调用函数之前说“上一个”堆栈顶部时,您忽略了两个参数,这些参数在调用函数和调用者 EIP 之前被压入堆栈。这可能会使读者感到困惑。假设在标准堆栈帧中,EBP 指向的位置与刚进入函数后 ESP 指向的位置相同。
当你把东西压入堆栈时,堆栈指针将递减,因为堆栈的底部有最高地址。
w
wigy

编辑:要获得更好的描述,请参阅 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,您将很难进行堆栈遍历。


我不明白 EIP 是如何存储在堆栈中的。不应该是寄存器吗?寄存器如何在堆栈上?谢谢!
调用者 EIP 被 CALL 指令本身压入堆栈。 RET 指令只是获取栈顶并将其放入 EIP。如果你有缓冲区溢出,这个事实可能被用来从特权线程跳转到用户代码。
@devouredelysium EIP 寄存器的内容(或值)放在(或复制到)堆栈上,而不是寄存器本身。
@BarbaraKwarc 感谢您提供有价值的输入。我看不出我的答案中缺少什么 OP。实际上,寄存器保持原样,只有它们的值从 CPU 发送到 RAM。在 amd64 模式下,这会变得有点复杂,但将其留给另一个问题。
那amd64呢?我很好奇。
j
jmucchiello

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


不,ESP 不指向堆栈的底部。内存寻址方案与它无关。堆栈是否增长到较低或较高的地址并不重要。堆栈的“顶部”始终是下一个值将被推送的位置(放在堆栈的顶部),或者,在其他体系结构中,最后一个推送的值被放置在哪里以及它当前所在的位置。因此,ESP 总是指向栈顶。
另一方面,堆栈的底部或底部是放置第一个(或最旧的)值的位置,然后被更新的值覆盖。这就是 EBP 的名称“基指针”的来源:它应该指向子程序当前本地堆栈的基(或底部)。
Barbara,在 Intel x86 中,堆栈是颠倒的。堆栈的顶部包含推入堆栈的第一个项目,之后的每个项目都被推到顶部项目之下。堆栈的底部是放置新项目的地方。程序从 1k 开始放置在内存中,然后增长到无穷大。堆栈从无穷大开始,实际上是最大内存减去 ROM,然后向 0 增长。ESP 指向一个地址,其值小于第一个推送的地址。
W
Wim ten Brink

自从我完成汇编编程以来已经很久了,但是 this link 可能很有用......

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

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

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

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


段寄存器在 x86 上是独立的——它们是 gs、cs、ss,除非你正在编写内存管理软件,否则你永远不会接触它们。
ds 也是一个段寄存器,在 MS-DOS 和 16 位代码时代,您肯定需要偶尔更改这些段寄存器,因为它们永远不会指向超过 64 KB 的 RAM。然而 DOS 可以访问高达 1 MB 的内存,因为它使用 20 位地址指针。后来我们得到了 32 位系统,一些有 36 位地址寄存器,现在有 64 位寄存器。所以现在你真的不需要再改变这些段寄存器了。
没有现代操作系统使用 386 段
@保罗:错了!错误的!错误的! 16 位段被 32 位段替换。在保护模式下,这允许内存虚拟化,基本上允许处理器将物理地址映射到逻辑地址。但是,在您的应用程序中,事情似乎仍然是平坦的,因为操作系统已经为您虚拟化了内存。内核在保护模式下运行,允许应用程序在平面内存模型中运行。另请参阅en.wikipedia.org/wiki/Protected_mode
@Workshop ALex:这是一个技术问题。所有现代操作系统都将所有段设置为 [0, FFFFFFFF]。那还真不算。如果您阅读链接页面,您会发现所有花哨的东西都是用页面完成的,这些页面比分段更细粒度。