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.
我很晚才来参加聚会&我相信在这个问题被问到的过去7年里,你会对事情有更清晰的理解,当然,如果你选择进一步追问这个问题的话。然而,我想我还是会给予一下**,特别是序言和后记的为什么部分**。 此外,公认的答案优雅而简单地解释了后记和序言的“如何”,并有很好的参考资料。我只打算用 * 为什么 (至少是逻辑上的为什么)部分来补充这个答案。 我将引用下面从接受的答案和尝试扩展它的解释。 在IA-32(x86)cdecl中,语言使用ebp寄存器来跟踪函数的堆栈帧。处理器使用esp寄存器来指向堆栈上最近添加的值(顶部值)。 call指令做两件事:它首先将返回地址压入堆栈,然后跳转到被调用的函数。在调用之后,esp立即指向堆栈上的返回地址。 上面引用的最后一行是immediately after the call, esp points to the return address on the stack. 为什么? 所以让我们假设我们的代码正在执行,有下面的情况,如下面的( 真的画得很糟糕 *)图所示
5条答案
按热度按时间gzszwxb41#
有很多资源可以解释这一点:
仅举几例。
基本上,正如你所描述的,“堆栈”在程序执行中有几个目的:
1.在调用函数时跟踪返回到何处
1.在函数调用的上下文中存储局部变量
1.从调用函数向被调用方传递参数。
prolouge是函数开始时发生的事情。它的职责是建立被调用函数的堆栈框架。结尾部分正好相反:它是函数中最后发生的事情,其目的是恢复调用(父)函数的堆栈帧。
在IA-32(x86)cdecl中,语言使用
ebp
寄存器来跟踪函数的堆栈帧。处理器使用esp
寄存器来指向堆栈上最近添加的值(顶部值)。(在优化代码中,使用ebp
作为帧指针是可选的;也可以使用其他方法来展开异常堆栈,因此实际上不需要花费指令来设置它。)call
指令做两件事:它首先将返回地址压入堆栈,然后跳转到被调用的函数。紧接着call
之后,esp
指向堆栈上的返回地址。(因此,在函数入口处,设置了ret
可以执行以将返回地址弹出回EIP。序言把ESP指向了其他地方,这也是为什么我们需要一个结语的部分原因。)然后执行序言:
在这一点上,我们有:
后记:
wbgh16ku2#
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
之前)所做的。我们已经回答了“如何”,我们也回答了“为什么”。**只是一个结束注解:**为了简洁起见,我没有考虑堆栈地址可能以相反的方式增长的事实。
3vpjnl9f4#
一张图片胜过千言万语,所以这里有一些图表,展示了堆栈在函数调用过程中如何变化-请记住,在这些图表中,内存地址增长,堆栈增长:)
调用者将参数和返回地址压入堆栈。
然后被调用方将其调用方的堆栈帧基指针(ebp)压入堆栈,将ebp设置为当前堆栈指针(esp)值,然后在运行函数之前为局部变量添加空间。
然后,被调用方将堆栈顶部的值(返回地址)弹出到指令指针寄存器(eip)中,因此执行的下一条指令将返回调用方。
函数现在已经返回,调用者可以继续执行,期望堆栈看起来像这样:
smdncfj35#
每个函数都有一个相同的序言(函数代码的开始)和尾声(函数的结束)。
Prologue:Prologue的结构如下:push ebp mov esp,ebp
结语:Prologue的结构是这样的:离开网
更多详情:what is Prologue and Epilogue