我知道嵌套函数调用中的数据会进入堆栈。堆栈本身实现了一个分步方法,用于在函数被调用或返回时从堆栈中存储和检索数据。这些方法的名称最广为人知的是 Prologue 和结语。
我尝试搜索有关此主题的材料但没有成功。你们知道关于函数序言和结尾如何在 C 中通常工作的任何资源(站点、视频、文章)吗?或者如果你能解释会更好。
PS:我只是想要一些一般的看法,而不是太详细。
我知道嵌套函数调用中的数据会进入堆栈。堆栈本身实现了一个分步方法,用于在函数被调用或返回时从堆栈中存储和检索数据。这些方法的名称最广为人知的是 Prologue 和结语。
我尝试搜索有关此主题的材料但没有成功。你们知道关于函数序言和结尾如何在 C 中通常工作的任何资源(站点、视频、文章)吗?或者如果你能解释会更好。
PS:我只是想要一些一般的看法,而不是太详细。
有很多资源可以解释这一点:
仅举几例。
基本上,正如您所描述的,“堆栈”在执行程序时有多种用途:
序言是函数开始时发生的事情。它的职责是设置被调用函数的栈帧。结语正好相反:它是函数中最后发生的事情,其目的是恢复调用(父)函数的堆栈帧。
在 IA-32 (x86) cdecl 中,ebp
语言使用寄存器来跟踪函数的堆栈帧。处理器使用该esp
寄存器来指向堆栈上最近添加的值(顶部值)。(在优化的代码中,ebp
用作帧指针是可选的;其他展开堆栈的方法是可能的,因此实际上不需要花费指令来设置它。)
该call
指令做了两件事:首先它将返回地址压入堆栈,然后跳转到被调用的函数。紧跟在call
, 之后esp
指向堆栈上的返回地址。(所以在函数入口处,设置了一些东西,以便ret
可以执行将返回地址弹出回 EIP。序言将 ESP 指向其他地方,这也是我们需要尾声的部分原因。)
然后执行序言:
push ebp ; Save the stack-frame base pointer (of the calling function).
mov ebp, esp ; Set the stack-frame base pointer to be the current
; location on the stack.
sub esp, N ; Grow the stack by N bytes to reserve space for local variables
此时,我们有:
...
ebp + 4: Return address
ebp + 0: Calling function's old ebp value
ebp - 4: (local variables)
...
结语:
mov esp, ebp ; Put the stack pointer back where it was when this function
; was called.
pop ebp ; Restore the calling function's stack frame.
ret ; Return to the calling function.
C 函数调用约定和堆栈很好地解释了调用堆栈的概念
函数序言简要解释了汇编代码及其方法和原因。
我参加聚会已经很晚了,我相信在提出问题后的过去 7 年中,您会对事情有更清晰的理解,当然,如果您选择进一步追问这个问题。但是,我想我仍然会试一试,尤其是序言和结语的部分原因。
此外,接受的答案优雅而简单地解释了结语和序言的方式,并提供了很好的参考。我只打算用为什么(至少是逻辑上的为什么)部分来补充这个答案。
我将从接受的答案中引用以下内容并尝试扩展它的解释。
在 IA-32 (x86) cdecl 中,语言使用 ebp 寄存器来跟踪函数的堆栈帧。处理器使用 esp 寄存器指向堆栈中最近添加的值(顶部值)。
call 指令做了两件事:首先它将返回地址压入堆栈,然后跳转到被调用的函数。调用后, esp 立即指向堆栈上的返回地址。
上面引用的最后一行说immediately after the call, esp points to the return address on the stack.
因此,假设我们当前正在执行的代码有以下情况,如下图(画得非常糟糕)所示
所以我们要执行的下一条指令是,比如地址 2。这就是 EIP 指向的地方。当前指令有一个函数调用(将在内部转换为汇编call
指令)。
现在理想情况下,因为 EIP 指向下一条指令,那确实是下一条要执行的指令。但是由于当前执行流程路径存在某种转移,(由于 ,现在可以预期call
)EIP 的值会发生变化。为什么?因为现在可能需要执行另一条指令,可能在其他地方,比如地址 1234(或其他)。但是为了按照程序员的意图完成程序的执行流程,在转移活动完成后,控制必须返回到地址2,因为如果转移没有发生,那应该是接下来应该执行的。让我们将此地址 2 称为return address
正在生成的上下文中的call
。
问题 1
因此,在转移实际发生之前,返回地址 2 需要临时存储在某个地方。
可以有很多选择将它存储在任何可用的寄存器或某个内存位置等中。但是(我相信有充分的理由)决定将返回地址存储到堆栈中。
所以现在需要做的是递增 ESP(堆栈指针),使得堆栈的顶部现在指向堆栈上的下一个地址。因此,指向地址(例如 292)的 TOS'(增量前的 TOS)现在递增并开始指向地址 293。这就是我们放置return address 2
. 所以是这样的:
所以看起来现在我们已经实现了将返回地址临时存储在某个地方的目标。我们现在应该开始转移注意力call
。我们可以。但是有一个小问题。在被调用函数的执行过程中,堆栈指针以及其他寄存器值可以被多次操作。
问题 2
因此,尽管我们的返回地址仍然存储在堆栈中的位置 293,但在被调用函数完成执行后,执行流程如何知道它现在应该转到 293 并且它会找到返回地址?
所以(我再次相信有充分的理由)解决上述问题的方法之一可能是将堆栈地址 293(返回地址所在的位置)存储在称为 EBP 的(指定)寄存器中。那么EBP的内容呢?那不会被覆盖吗?当然,这是一个有效的观点。因此,让我们将 EBP 的当前内容存储到堆栈中,然后将此堆栈地址存储到 EBP 中。像这样的东西:
堆栈指针递增。EBP 的当前值(表示为 EBP'),即 xxx,存储在栈顶,即地址 294。现在我们已经对 EBP 的当前内容进行了备份,我们可以放心地将EBP 上的任何其他值。所以我们把当前栈顶地址,也就是地址294,放到EBP中。
有了上述策略,我们就解决了上面讨论的问题 2。如何?所以现在当执行流程想知道它应该从哪里获取返回地址时,它会:
首先从 EBP 中获取值并将 ESP 指向该值。在我们的例子中,这将使 TOS(栈顶)指向地址 294(因为这是存储在 EBP 中的内容)。
然后它将恢复 EBP 的先前值。为此,它只需取 294(TOS)的值,即 xxx(实际上是 EBP 的旧值),并将其放回 EBP。
然后它将递减堆栈指针以转到堆栈中的下一个较低地址,在我们的例子中是 293。因此最终达到 293(请参阅我们的问题 2)。在那里它会找到返回地址,即 2。
它最终会将这 2 弹出到 EIP 中,这就是如果没有发生转移,理想情况下应该执行的指令,记住。
而我们刚刚看到的步骤,以及所有杂耍,临时存储返回地址然后检索它正是函数 prolog(在 function 之前call
)和 epilog(在 function 之前)所做的事情ret
。如何已经回答了,我们也只是回答了为什么。
只是一个结束说明:为了简洁起见,我没有注意到堆栈地址可能会反过来增长的事实。
每个函数都有相同的序言(函数代码的开头)和结尾(函数的结尾)。
Prologue:Prologue 的结构是这样的: push ebp mov esp,ebp
Epilogue:Prologue 的结构是这样的:leave ret