x86 NASM汇编语言中,离开函数时堆栈是否会自动弹出?

4
例如,让我们进入一个函数...
push ebp ;Saving ebp
mov ebp, esp ;Saving esp into ebp
sub esp, 4 ;Saving four bytes onto the stack

并退出函数...

mov esp, ebp ;Restoring the saved value of esp
pop ebp ;Restoring the value of ebp from the stack

我的问题是当恢复ESP时,栈上的四个字节变量是否会被弹出或者以某种魔法方式消失?我不明白POP EBP为什么不会弹出栈中保留(并且很可能已被使用)的四个字节。在我看来,如果你在函数中将任何东西推送到栈中,当POP EBP发生时,它仍然会在那里,并且因此POP EBP不会产生保存的EBP,而是栈顶的内容。改变ESP寄存器的值时,是否只是在其值恢复时从栈顶砍掉了一部分?


为了更加准确,你展示的代码示例涉及设置和撤销堆栈帧,与进入和离开函数无关(通常使用callret指令)。 - stakx - no longer contributing
2个回答

6

在我的眼中,如果你在函数中把任何东西压入栈中,在执行pop ebp指令时,它仍会存在[...]

不会,因为在pop ebp指令之前,你会看到这个:

mov esp, ebp ;Restoring the saved value of esp
请记住,esp 实质上是栈的“顶部”地址。将数据压入和从栈中弹出会更改该寄存器的值。因此,如果您更改了此寄存器,则会更改下一次 pushpop 操作发生的位置。
因此,上述指令 mov esp, ebp 实质上将堆栈指针重置为初始 push ebp 操作后的位置。(该位置由 mov ebp, esp 指令将其保存到 ebp 寄存器中。)
这就是为什么 pop ebp 可以弹出正确的东西。
但是,请注意,这假设您的函数未更改 ebp 寄存器。
更新:
我在这里假设一个特定的调用约定,我们来举个例子。假设我们有一个函数,它接受一个 32 位参数,并通过调用堆栈传递给该函数。
为了调用我们的函数,我们执行以下操作:
push eax      ; push argument on stack
call fn       ; call our function; this pushes `eip` onto the stack

fn 的第一件事是设置自己的堆栈帧(并确保可以在结束时恢复先前的堆栈帧):

push ebp      ; so we can later restore the previous stack frame
mov ebp, esp  ; initialize our own function's stack frame
sub esp, 8    ; make room for 8 bytes (for local variables)
sub esp, 8 的作用类似于将8个字节压入堆栈,只是不会写入内存位置。因此,我们最终得到了8个未初始化的字节;这就是函数可以用于本地变量的内存区域。它可以通过例如 [ebp-4][ebp-8] 引用这些本地变量,并且可以通过 [ebp+8](跳过推送的 ebpeip)引用其32位参数。
在你的函数期间,堆栈可能如下所示:
+------------+                         | "push" decreases "esp"
|  <arg>     |                         |
+------------+  <-- ebp+8              |
| <prev eip> |                         v
+------------+  <-- ebp+4
| <prev ebp> |
+------------+  <-- ebp
|  <locals>  |
+------------+  <-- ebp-4
|  <locals>  |                         ^
+------------+  <-- ebp-8              |
|  ...       |                         |
+------------+  <-- esp                | "pop" increases "esp"

在函数结束时,将发生以下情况:
mov esp, ebp  ; "pop" over local variables and everything else that was pushed
pop ebp       ; restore previous stack frame

最后:
ret           ; essentially this does a "pop eip" so program execution gets
              ; transferred back to instruction after the "call fn"

(PS: 调用代码将不得不弹出传递给函数的参数,例如在 call fn 之后立即执行 add esp, 4。)
我很久没有做汇编语言了,所以这些都是从记忆中恢复的。我可能在一些细节上有所偏差,但希望您能理解大致情况。

是的,但我的问题是即使esp指向它所在的位置,我们使用的那四个字节(我们保留了它,但没有使用它,但假设我们使用它并加载一个值到它中)是否仍然在栈顶(从esp减去4)?还是说在esp的负数部分会消失?我之所以这样说是因为如果您想要在函数后将一个值推入堆栈,它会在esp的-8处,先前的值(在esp的-4处)仍然存在吗?或者通过重置esp会删除[esp-4]处的变量? - Sam
1
mov esp, ebp ;“弹出”本地变量和所有被推入的东西 - Sam
很高兴能帮忙,尽管答案可能会对该主题进行过多的阐述。 - stakx - no longer contributing
详细说明是受欢迎的!所以每次设置esp时,它会弹出栈顶上的内容? - Sam
@Sam: add esp, ... 就像一次或多次的 pop,不同之处在于它除了 esp 本身外不改变任何其他寄存器或内存位置,即像将值从堆栈中弹出到虚空中。sub esp, ... 就像 push,但是堆栈内存没有被写入,所以最终你会得到未初始化数据在堆栈的“顶部”。mov esp, X 可以是一个“push”或“pop”,具体取决于 X 是大于还是小于 esp - stakx - no longer contributing
显示剩余2条评论

4
栈本身没有已分配和未分配空间的概念,这完全取决于惯例。由于栈是您(当前例程)、操作系统和所有加载库之间共享的资源,因此必须有一些“社会”规则来避免事情失控。这些规则是:
  1. 不要谈论堆栈规则。
  2. 每个人都可以随意减少堆栈指针,无需询问任何人。
  3. 如果将堆栈指针减小X,则在结束时必须将其逐步增加X,且只能增加X。不能多,不能少。
所以如果这是我们的栈状态:
     Stack decrements in this direction ==>
     ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___  
... |___|___|___|___|___|___|___|___|___|___|___| ...
                  ^
                  |
 Stack pointer ---+

根据第二条规则,我们知道堆栈指针右侧的所有内容都是不安全的。因为在操作系统中断或调用子程序时,这些内存位置将被覆盖。
我们可以说SP左侧的内容是已分配的,而右侧的内容是未分配的。

     Stack decrements in this direction ==>
                    .
     Allocated      :  Unallocated
                    :
                    :
     ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___  
... |___|___|___|___|___|___|___|___|___|___|___| ...
                  ^
                  |
 Stack pointer ---+

如果我们想要分配一些空间,我们可以再次使用第二条规则并仅减少堆栈指针。
如果我们恰好有要写入新分配空间的值,我们可以优化这个过程并使用push
这就是您提供的代码正在发生的事情。
push ebp ;Saving ebp
mov ebp, esp ;Saving esp into ebp
sub esp, 4 ;Saving four bytes onto the stack

提供堆栈状态的函数

     Stack decrements in this direction ==>
                            .
             Allocated      :  Unallocated
                            :
   4 bytes reserved --+     :
     ___ ___ ___ ___ _V_ ___ ___ ___ ___ ___ ___  
... |___|___|___|EBP|___|___|___|___|___|___|___| ...
                  ^       ^
   Frame pointer -+       |
         Stack pointer ---+

现在你肯定明白了:我们通过移动栈指针在栈上分配/释放内存。 如何移动它并不重要:使用pushsubandaddpop都是操作栈指针以在栈上分配/释放内存的示例。
您可以自由选择其中任何一个,或者选择最适合您需要的,但是不能忘记已经通过第三条规则分配了什么。
这就是为什么最终会有未回收的内存。
mov esp, ebp ;Restoring the saved value of esp
pop ebp ;Restoring the value of ebp from the stack

这段代码的作用是将堆栈指针恢复到EBP位置。这本可以使用add esp, 04h命令,但事情有时会变得复杂,最好使用第一种方法。
因为我们不再关心指针下面(右边)存储的值,所以我们可以使用add命令,而不是一系列pop命令将其弹出到未使用的寄存器中,只需将堆栈指针放回原位即可释放内存。如果要从堆栈中获取值,就必须使用pop命令,就像必须恢复EBP寄存器的值一样。
理解函数前导和后继需要考虑的关键是,不是发生了什么,而是在一个调用内部另一个调用时会发生什么。如果你能从两者返回,那么你就完全掌握了它。

1随着操作系统拥有自己的私有特权堆栈,这不再是特权上下文切换的问题,但出于清晰起见,请与我保持一致。


太棒了!一些真正好的、易于理解的解释。 :-) 这让我希望我也能写出这样的答案! - stakx - no longer contributing

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接