在 x86 NASM 汇编语言中离开函数时,堆栈会自动弹出吗?
Does the stack automatically get popped when leaving a function in x86 NASM assembly language?
例如让我们进入一个函数...
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 寄存器是否只是从堆栈顶部删除?
"To my eye if you pushed anything onto the stack during the function it would still be there when the pop ebp happens[…]"
不,因为就在 pop ebp
指令之前,您有:
mov esp, ebp ;Restoring the saved value of esp
记住esp
本质上就是栈"top"的地址。入栈和出栈会改变这个寄存器。因此,如果您更改此寄存器,您将更改下一个 push
或 pop
发生的位置。
所以上面的指令,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]
引用其 32 位参数(跳过推送的 ebp
和 eip
)。
在您执行函数期间,堆栈可能如下所示:
+------------+ | "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
。)
好久没写汇编了,全凭记忆。我可能在一些细节上有所偏差,但我希望你能了解总体情况。
堆栈没有分配与未分配的概念 space per sé.
这一切都归结为:约定.
由于堆栈是你们(当前例程)、OS和所有加载的库之间的共享资源,所以必须有一些"social" 防止事情失控的规则。
这些规则是:
- 你不讲堆栈规则。
- 每个人都可以随时递减堆栈指针,不需要问任何人就可以递减多少。
- 如果您将堆栈指针递减 X,那么您在一天结束时将其递增(一步或多步)恰好 X,不多不少。
所以如果这是我们的堆栈状态
Stack decrements in this direction ==>
___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___
... |___|___|___|___|___|___|___|___|___|___|___| ...
^
|
Stack pointer ---+
根据第二条规则我们知道栈指针右边的所有东西
不安全,因为在 OS 中断我们1 或者我们调用子程序,这些内存位置将被覆盖。
我们可以说SP左边是allocated,右边是unallocated.
Stack decrements in this direction ==>
.
Allocated : Unallocated
:
:
___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___
... |___|___|___|___|___|___|___|___|___|___|___| ...
^
|
Stack pointer ---+
如果我们想分配一些 space 我们可以再次诉诸第二条规则,只需减少堆栈指针。
如果我们碰巧有我们想要写在新分配的 space 上的值,我们可以优化它并使用 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 ---+
现在你肯定明白了:我们通过移动堆栈指针.[=25reserve/free堆栈上的内存=]
我们如何移动它并不重要:push
、sub
、and
、add
或 pop
reserve/free 堆栈内存的堆栈指针操作的所有示例。
您可以自由使用您喜欢的或最适合您需要的那个,但是您不能根据第三条规则忘记您分配的内容。
这就是为什么你最终得到
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 寄存器的情况一样。
理解函数序言和结尾的关键不是考虑调用期间发生的事情,而是考虑在另一个调用中的调用期间发生的事情。如果你能return,你就完全明白了。
1这不再是特权上下文切换的问题,因为 OS 有自己的私有特权堆栈,但为了清楚起见,请与我相处.
例如让我们进入一个函数...
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 寄存器是否只是从堆栈顶部删除?
"To my eye if you pushed anything onto the stack during the function it would still be there when the pop ebp happens[…]"
不,因为就在 pop ebp
指令之前,您有:
mov esp, ebp ;Restoring the saved value of esp
记住esp
本质上就是栈"top"的地址。入栈和出栈会改变这个寄存器。因此,如果您更改此寄存器,您将更改下一个 push
或 pop
发生的位置。
所以上面的指令,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]
引用其 32 位参数(跳过推送的 ebp
和 eip
)。
在您执行函数期间,堆栈可能如下所示:
+------------+ | "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
。)
好久没写汇编了,全凭记忆。我可能在一些细节上有所偏差,但我希望你能了解总体情况。
堆栈没有分配与未分配的概念 space per sé.
这一切都归结为:约定.
由于堆栈是你们(当前例程)、OS和所有加载的库之间的共享资源,所以必须有一些"social" 防止事情失控的规则。
这些规则是:
- 你不讲堆栈规则。
- 每个人都可以随时递减堆栈指针,不需要问任何人就可以递减多少。
- 如果您将堆栈指针递减 X,那么您在一天结束时将其递增(一步或多步)恰好 X,不多不少。
所以如果这是我们的堆栈状态
Stack decrements in this direction ==>
___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___
... |___|___|___|___|___|___|___|___|___|___|___| ...
^
|
Stack pointer ---+
根据第二条规则我们知道栈指针右边的所有东西
不安全,因为在 OS 中断我们1 或者我们调用子程序,这些内存位置将被覆盖。
我们可以说SP左边是allocated,右边是unallocated.
Stack decrements in this direction ==>
.
Allocated : Unallocated
:
:
___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___
... |___|___|___|___|___|___|___|___|___|___|___| ...
^
|
Stack pointer ---+
如果我们想分配一些 space 我们可以再次诉诸第二条规则,只需减少堆栈指针。
如果我们碰巧有我们想要写在新分配的 space 上的值,我们可以优化它并使用 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 ---+
现在你肯定明白了:我们通过移动堆栈指针.[=25reserve/free堆栈上的内存=]
我们如何移动它并不重要:push
、sub
、and
、add
或 pop
reserve/free 堆栈内存的堆栈指针操作的所有示例。
您可以自由使用您喜欢的或最适合您需要的那个,但是您不能根据第三条规则忘记您分配的内容。
这就是为什么你最终得到
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 寄存器的情况一样。
理解函数序言和结尾的关键不是考虑调用期间发生的事情,而是考虑在另一个调用中的调用期间发生的事情。如果你能return,你就完全明白了。
1这不再是特权上下文切换的问题,因为 OS 有自己的私有特权堆栈,但为了清楚起见,请与我相处.