C语言中的功能序言与结语

时间:2013-02-08 03:53:06

标签: c++ c

我知道嵌套函数调用中的数据会转到堆栈。堆栈本身实现了一个逐步的方法,用于在函数被调用或返回时从堆栈中存储和检索数据。这些方法的名称是众所周知的作为序幕和结语。

我尝试搜索此主题的材料没有成功。你们知道关于函数序言和结语如何在C中起作用的任何资源(网站,视频,文章)吗?或者,如果你能解释会更好。

P.S:我只是想要一些普遍看法,不太详细。

4 个答案:

答案 0 :(得分:16)

有很多资源可以解释这个:

仅举几例。

基本上,正如您所描述的那样,“堆栈”在执行程序时有多种用途:

  1. 在调用函数时跟踪返回的位置
  2. 在函数调用的上下文中存储局部变量
  3. 将参数从调用函数传递给被调用者。
  4. 延长是在函数开始时发生的事情。它的职责是设置被调用函数的堆栈帧。 epilog恰恰相反:它是函数中最后发生的事情,其目的是恢复调用(父)函数的堆栈帧。

    在IA-32(x86)cdecl中,语言使用ebp寄存器来跟踪函数的堆栈帧。处理器使用esp寄存器指向堆栈上的最新添加(最高值)。

    call指令执行两项操作:首先,它将返回地址压入堆栈,然后跳转到被调用的函数。在call之后,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.
    

答案 1 :(得分:4)

  1. C Function Call Conventions and the Stack很好地解释了调用堆栈的概念

  2. Function prologue简要介绍了汇编代码以及方法和原因。

  3. The gen on function perilogues

答案 2 :(得分:1)

我参加聚会已经很晚了,我可以肯定,自从提出问题以来的最近7年中,您对事情有了更清晰的了解,当然,如果您选择继续研究该问题,进一步。但是,我认为我仍然会尝试特别是序言和结语的为什么部分。

此外,被接受的答案优雅而简洁地解释了Epilog和序言的 how ,并提供了很好的参考。我只打算用为什么(至少是逻辑上的原因)部分来补充该答案。

我将从引用的答案中引用以下内容,并尝试扩展其解释。

在IA-32(x86)cdecl中,语言使用ebp寄存器来保持 函数堆栈框架的轨迹。 esp寄存器由 处理器指向最近的加法(最高值) 堆栈。

调用指令有两件事:首先,它推动返回 地址到堆栈上,然后跳转到被调用的函数。 通话结束后,esp会立即指向 堆栈。

上面引号的最后一行是immediately after the call, esp points to the return address on the stack.

那是为什么?

因此,假设我们当前正在执行的代码具有以下情况,如下图(绘制得很差)所示

enter image description here

因此,我们要执行的下一条指令位于地址2。这就是EIP指向的位置。当前指令具有一个函数调用(它将在内部转换为汇编call指令)。

现在,理想情况下,因为EIP指向下一条指令,所以确实是要执行的下一条指令。但是,由于当前执行流程存在某种转移,(现在由于call而有所预期),EIP的值将发生变化。为什么?因为现在可能需要执行另一条指令,该指令可能位于其他地方,例如地址1234(或其他位置)。但是,为了按照程序员的意图完成程序的执行流程,在完成转移活动之后,控件必须返回到地址2,因为如果没有发生转移,则下一步应该执行该地址。让我们在正在创建的return address的上下文中将此地址2称为call

问题1

因此,在实际发生转移之前,必须将返回地址2临时存储在某个地方。

可能有很多选择将其存储在任何可用的寄存器中,或在某些内存位置等。但是出于(我相信有充分的理由),决定将返回地址存储在堆栈中。

因此,现在需要做的是增加ESP(堆栈指针),以使堆栈顶部现在指向堆栈上的下一个地址。因此,指向地址(例如292)的TOS'(递增前的TOS)现在递增并开始指向地址293。这就是我们放置return address 2的位置。像这样:

enter image description here

所以看起来现在我们已经实现了将返回地址临时存储在某个地方的目标。现在,我们应该进行转移call。我们可以。但是有一个小问题。在执行被调用函数期间,可以多次操作堆栈指针以及其他寄存器值。

问题2

因此,尽管我们的返回地址仍存储在堆栈中的位置293处,在被调用函数完成执行之后,执行流程如何知道现在应该转到293并在其中找到寄信人地址?

所以(我再次出于充分的理由相信)解决上述问题的一种方法可能是将堆栈地址293(返回地址在其中)存储在称为EBP的(指定的)寄存器中。但是,EBP的内容又如何呢?那不会被覆盖吗?当然,这是有道理的。因此,让我们将EBP的当前内容存储到堆栈上,然后将此堆栈地址存储到EBP中。像这样:

enter image description here

堆栈指针递增。 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之前)。已经回答了方法,我们也回答了为什么

仅是结尾提示:为简洁起见,我没有考虑堆栈地址可能会反过来增长这一事实。

答案 3 :(得分:-2)

每个函数都有相同的序言(函数代码的开头)和结尾(函数的结尾)。

序言:Prologue的结构如下: 推ebp mov esp,ebp

结语:Prologue的结构如下: 离开 保留

更详细:what is Prologue and Epilogue