assembly C语言中的函数序言与结语

lmvvr0a8  于 2023-05-07  发布在  其他
关注(0)|答案(5)|浏览(185)

我知道嵌套函数调用中的数据会进入堆栈。堆栈本身实现了一个逐步的方法,用于在函数被调用或返回时存储和检索堆栈中的数据。这些方法的名称最为人所知的是Prologue和Epilogue。
我试图搜索关于这个主题的材料,但没有成功。你们知道任何关于函数序言和尾声在C中一般如何工作的资源(网站,视频,文章)吗?如果你能解释一下就更好了。
P.S:我只想了解一些大概的情况,不要太详细。

gzszwxb4

gzszwxb41#

有很多资源可以解释这一点:

仅举几例。
基本上,正如你所描述的,“堆栈”在程序执行中有几个目的:
1.在调用函数时跟踪返回到何处
1.在函数调用的上下文中存储局部变量
1.从调用函数向被调用方传递参数。
prolouge是函数开始时发生的事情。它的职责是建立被调用函数的堆栈框架。结尾部分正好相反:它是函数中最后发生的事情,其目的是恢复调用(父)函数的堆栈帧。
在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.
wbgh16ku

wbgh16ku2#

  1. C Function Call Conventions and the Stack很好地解释了调用堆栈的概念
  2. Function prologue简要解释了汇编代码以及如何和为什么。
  3. The gen on function perilogues
5gfr0r5j

5gfr0r5j3#

我很晚才来参加聚会&我相信在这个问题被问到的过去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是接下来应该执行的。让我们在正在生成的call的上下文中将该地址2称为return address

问题一

因此,在转移实际发生之前,返回地址2需要临时存储在某个地方。
可以有许多选择将其存储在任何可用寄存器或某个存储器位置等中。但由于(我相信有充分的理由),决定将返回地址存储在堆栈上。
因此,现在需要做的是递增ESP(堆栈指针),以便堆栈顶部现在指向堆栈上的下一个地址。因此,指向地址(比如292)的TOS’(递增之前的TOS)现在被递增并开始指向地址293。这就是我们放return address 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(在函数call之前)和epilog(在函数ret之前)所做的。我们已经回答了“如何”,我们也回答了“为什么”。

**只是一个结束注解:**为了简洁起见,我没有考虑堆栈地址可能以相反的方式增长的事实。

3vpjnl9f

3vpjnl9f4#

一张图片胜过千言万语,所以这里有一些图表,展示了堆栈在函数调用过程中如何变化-请记住,在这些图表中,内存地址增长,堆栈增长:)

调用者将参数和返回地址压入堆栈。

    • 被调用方**期望在堆栈上找到参数(反向)和返回地址:
|      ...       | <- End of caller's stack frame
+----------------+
|   Argument n   | <- Start of callee's stack frame
+- - - - - - - - +
+- - - - - - - - +
|   Argument 2   |
+----------------+
|   Argument 1   |
+----------------+
| Return address | <- "Top" of stack (esp)
+----------------+

然后被调用方将其调用方的堆栈帧基指针(ebp)压入堆栈,将ebp设置为当前堆栈指针(esp)值,然后在运行函数之前为局部变量添加空间。

|      ...       |
+----------------+
|   Argument n   |
+- - - - - - - - +
+- - - - - - - - +
|   Argument 2   |
+----------------+
|   Argument 1   |
+----------------+
| Return address | <- Before: previous top of stack
+----------------+
| Previous $ebp  |
+----------------+
|  Local var 1   |
+----------------+
|  Local var 2   |
+- - - - - - - - +
+- - - - - - - - +
|  Local var n   | <- After: new top of stack (esp)
+----------------+
    • 被调用方**在运行函数体后,清空局部变量的堆栈空间,并将堆栈顶部(之前的ebp值)弹出到ebp中,为调用方的帧重置它。
|      ...       |
+----------------+
|   Argument n   |
+- - - - - - - - +
+- - - - - - - - +
|   Argument 2   |
+----------------+
|   Argument 1   |
+----------------+
| Return address | <- Top of stack (esp)
+----------------+

然后,被调用方将堆栈顶部的值(返回地址)弹出到指令指针寄存器(eip)中,因此执行的下一条指令将返回调用方。
函数现在已经返回,调用者可以继续执行,期望堆栈看起来像这样:

|      ...       |
+----------------+
|   Argument n   |
+- - - - - - - - +
+- - - - - - - - +
|   Argument 2   |
+----------------+
|   Argument 1   | <- Top of stack (esp)
+----------------+
smdncfj3

smdncfj35#

每个函数都有一个相同的序言(函数代码的开始)和尾声(函数的结束)。
Prologue:Prologue的结构如下:push ebp mov esp,ebp
结语:Prologue的结构是这样的:离开网
更多详情:what is Prologue and Epilogue

相关问题