基本指针和堆栈指针到底是什么?他们指出了什么?


225

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

替代文字

(请注意,此图在底部具有高地址,在顶部具有低地址。)

任何人都可以解释我什么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是如何演变的(我已经了解了序言的工作原理,我只是想知道在将参数推入堆栈之后和序言之前发生了什么)。


23
需要注意的重要一件事是,堆栈在内存中“向下”增长。这意味着向上移动堆栈指针会减小其值。
BS

4
区分EBP / ESP和EIP在做什么的一个提示:EBP和ESP处理数据,而EIP处理代码。
mmmmmmmm

2
在您的图形中,ebp(通常)是“帧指针”,特别是“堆栈指针”。这允许一致地通过[ebp-x]访问局部变量,并通过[ebp + x]一致地访问堆栈参数,而与堆栈指针无关(在函数中经常更改)。可以通过ESP完成地址分配,将EBP释放给其他操作-但是那样,调试器无法告知调用堆栈或本地变量的值。
peterchen

4
@本 并非整洁。一些编译器将堆栈帧放入堆中。堆栈向下生长的概念就是这样,这个概念很容易理解。堆栈的实现可以是任何东西(使用堆的随机块会使黑客改写堆栈的某些部分变得更加困难,因为它们不是确定性的)。
马丁·约克2009年

1
用两个词来说:堆栈指针允许push / pop操作工作(因此push和pop知道在哪里放置/获取数据)。基本指针允许代码独立引用先前已推入堆栈的数据。
tigrou

Answers:


228

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

1
感谢您的解释!但是我现在有点困惑。假设我调用了一个函数,并且我处于函数序言的第一行,但还没有执行任何一行代码。那时,ebp的价值是多少?除了压入的参数之外,堆栈中是否还有其他内容?谢谢!
吞噬了极乐世界09年

3
EBP并没有发生神奇的变化,因此,在为您的功能建立新的EBP之前,您仍然会拥有调用者的价值。除参数外,堆栈还将保留旧的EIP(返回地址)
MSalters 2009年

3
好答案。尽管没有提到结尾处的内容是不可能完成的:“离开”和“退出”指令。
Calmarius

2
我认为这张图片将有助于弄清流程是什么。另外请记住,堆栈向下生长。 ocw.cs.pub.ro/courses/_media/so/laboratoare/call_stack.png
Andrei-Niculae Petre

是我还是上面的代码片段中缺少所有减号?
BarbaraKwarc

96

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

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

进入函数后,旧的EBP值将被压入堆栈,并将EBP设置为ESP的值。然后,ESP递减(因为堆栈在内存中向下增长),以便为函数的局部变量和临时变量分配空间。从那时起,在函数执行期间,函数的参数位于堆栈上,与EBP的偏移量为正值(因为它们在函数调用之前被压入),而局部变量位于与EBP的偏移量为负值的位置(因为它们是在函数输入之后分配在堆栈上的)。这就是EBP之所以称为框架指针的原因,因为它指向函数调用frame的中心。

退出时,所有功能所需要做的就是将ESP设置为EBP的值(这会从堆栈中取消分配局部变量,并将条目EBP暴露在堆栈的顶部),然后从堆栈中弹出旧的EBP值,然后函数返回(将返回地址弹出到EIP中)。

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


15

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

调用函数时,任何局部变量都将存储在堆栈上,并且堆栈指针将递增。从函数返回时,堆栈上的所有局部变量都超出范围。您可以通过将堆栈指针设置回基本指针(这是函数调用之前的“上一个”顶部)来做到这一点。

这样的内存分配这种方式是非常非常快速,高效。


14
@Robert:当您在调用函数之前在栈顶说“上一个”时,您将忽略两个参数,这两个参数在调用函数和调用者EIP之前就被压入了栈。这可能会使读者感到困惑。让我们说,在标准堆栈框架中,EBP指向进入函数 ESP指向的位置。
wigy

7

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

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

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

此帧指针遗漏意味着编译器不会在标准位置存储旧的EBP,而将EBP寄存器用于其他内容,因此,您很难找到调用方EIP,而又不知道给定功能局部变量需要多少空间。当然,Microsoft提供了一个API,即使在这种情况下,您也可以执行堆栈遍历,但是在某些用例中,在PDB文件中查找符号表数据库会花费很长时间。

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


我不知道EIP如何存储在堆栈中。不应该是寄存器吗?寄存器如何在堆栈中?谢谢!
吞噬了极乐世界09年

通过CALL指令本身将调用者EIP压入堆栈。RET指令只是获取堆栈的顶部并将其放入EIP中。如果您有缓冲区溢出,则可以使用此事实从特权线程中跳入用户代码。
wigy

@devouredelysium EIP寄存器的内容(或value)放置(或复制到)堆栈上,而不是寄存器本身。
BarbaraKwarc

@BarbaraKwarc感谢您提供输入。我看不到我的答案缺少什么操作。实际上,寄存器保持原样,只有它们的值从CPU发送到RAM。在amd64模式下,这会变得更加复杂,但是还有另一个问题。
wigy

那amd64呢?我很好奇。
BarbaraKwarc

6

首先,由于x86堆栈从高地址值构建到低地址值,所以堆栈指针指向堆栈的底部。堆栈指针是下一个要推送(或调用)的调用将放置下一个值的点。它的操作等效于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的旧值,因此您可以恢复先前的帧指针。


2
不,ESP不会指向堆栈的底部。内存寻址方案与此无关。堆栈增长到更高还是更低的地址都没有关系。堆栈的“顶部”始终是下一个值将被推入的位置(放在堆栈的顶部),或者在其他体系结构上,最后一个推入的值将被放置在当前位置。因此,ESP始终指向堆栈的顶部
BarbaraKwarc

1
另一方面,堆栈的底部底部是放置第一个(或最旧的)值的位置,然后被更新的值覆盖。这就是EBP的名称“基础指针”的来源:它应该指向子例程当前本地堆栈的基础(或底部)。
BarbaraKwarc

芭芭拉(Barbara),在Intel x86中,堆栈朝下。堆栈顶部包含第一个被压入堆栈的项目,之后的每个项目都被压在顶部项目的下方。堆栈的底部是放置新项目的位置。程序从1k开始放入内存,直到无穷大。堆栈从无穷大开始,实际上是最大内存减去ROM,然后向0增长。ESP指向其值小于所推入的第一个地址的地址。
jmucchiello

1

自完成汇编编程以来已经很长时间了,但是此链接可能很有用...

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

调用其他过程时,通常使用堆栈指针。对于现代编译器,将首先将一堆数据转储到堆栈中,然后是返回地址,因此一旦被告知要返回,系统将知道要返回的位置。堆栈指针将指向下一个可以将新数据推入堆栈的位置,该位置将一直保持到再次弹出该数据为止。

基址寄存器或段寄存器仅指向大量数据的地址空间。与第二个稳压器结合使用时,基本指针将把内存划分成巨大的块,而第二个寄存器将指向该块内的一个项目。因此,基本指针指向数据块的基础。

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


段寄存器在x86上是分开的-它们分别是gs,cs,ss,除非您正在编写内存管理软件,否则请不要触摸它们。
Michael

ds也是一个段寄存器,在MS-DOS和16位代码时代,您肯定需要偶尔更改这些段寄存器,因为它们永远不能指向超过64 KB的RAM。但是DOS最多可以访问1 MB的内存,因为它使用20位地址指针。后来,我们得到了32位系统,其中一些具有36位地址寄存器,现在具有64位寄存器。因此,如今,您实际上不再需要更改这些段寄存器。
Wim 10 Brink

没有现代的操作系统使用386段
Ana Betts

@Paul:错误!错误!错误!16位段由32位段代替。在保护模式下,这允许内存虚拟化,基本上允许处理器将物理地址映射到逻辑地址。但是,由于操作系统已为您虚拟化了内存,因此在您的应用程序中,情况似乎仍然很平坦。内核以保护模式运行,允许应用程序在平面内存模型中运行。另请参见en.wikipedia.org/wiki/Protected_mode
Wim十Brink

@Workshop ALex:这是技术性。所有现代OS均将所有段设置为[0,FFFFFFFF]。那真的不算数。而且,如果您阅读链接的页面,您会发现所有花哨的内容都是由页面完成的,这些页面的细化程度远高于细分。
MSalters

-4

编辑是的,这主要是错误的。它描述了完全不同的情况,以防有人感兴趣:)

是的,堆栈指针指向堆栈的顶部(无论是第一个空堆栈位置还是我不确定的最后一个完整堆栈位置)。基本指针指向正在执行的指令的内存位置。这是在操作码的级别上-您可以在计算机上获得的最基本的指令。每个操作码及其参数都存储在存储位置中。一条C或C ++或C#行可以转换为一个操作码,也可以转换为两个或两个以上的序列,具体取决于它的复杂程度。将它们依次写入程序存储器并执行。通常情况下,基址指针递增一条指令。对于程序控制(GOTO,IF等),可以将其递增多次,也可以将其替换为下一个存储器地址。

在这种情况下,功能存储在程序存储器中的某个地址处。调用该函数时,某些信息被压入堆栈,使程序可以找到其返回到调用该函数的位置以及该函数的参数,然后将该函数在程序存储器中的地址压入堆栈中。基本指针。在下一个时钟周期,计算机将从该内存地址开始执行指令。然后在某个时候,它将在调用该函数的指令之后返回到存储位置,并从那里继续。


我在理解ebp是什么时遇到了麻烦。如果我们有10行MASM代码,这意味着当我们停止运行这些行时,ebp会一直增加吗?
吞噬了极乐世界09年

1
@Devoured-不。那是不正确的。eip将会增加。
迈克尔

您的意思是我说的是正确的,但不是对EBP,而是对IEP,对吗?
吞噬了极乐世界09年

2
是。EIP是指令指针,在执行每条指令后都会隐式修改。
迈克尔

2
哦,我的坏。我在想一个不同的指针。我想我会洗脑。
斯蒂芬·弗里德里希斯

-8

esp代表“扩展堆栈指针” ..... ebp代表“某些基本指针” ..和eip代表“某些指令指针” ...堆栈指针指向堆栈段的偏移地址。基本指针指向额外段的偏移地址。指令指针指向代码段的偏移地址。现在,关于段...它们是处理器内存区域的64KB小分区.....此过程称为“内存分段”。希望这篇文章对您有所帮助。


3
这是一个老问题,但是,sp代表堆栈指针,bp代表基址指针,ip代表指令指针。每个人开头的e只是说它是32位指针。
海顿2015年

1
细分与此处无关。
BarbaraKwarc
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.