首先,我编译然后反汇编了您的函数,以便您可以看到 ASM 级别的实际情况。我禁用了优化并编译为 32 位代码以保持简单:
Dump of assembler code for function add:
0x080483cb <+0>: push %ebp
0x080483cc <+1>: mov %esp,%ebp
0x080483ce <+3>: sub $0x10,%esp
0x080483d1 <+6>: mov 0x8(%ebp),%edx
0x080483d4 <+9>: mov 0xc(%ebp),%eax
0x080483d7 <+12>: add %edx,%eax
0x080483d9 <+14>: mov %eax,-0x4(%ebp)
0x080483dc <+17>: mov -0x4(%ebp),%eax
0x080483df <+20>: leave
0x080483e0 <+21>: ret
End of assembler dump.
尝试查看上面的反汇编并识别它在做什么以及它如何匹配您的 C 代码。现在回答你的问题。
现在是 Prolog 部分:旧的 EBP 地址被压入堆栈,并使 EBP 指向 ESP。最后,局部变量被压入堆栈[这些只是它们的值存储的地址吗?]
在这里,序言从0x080483cb <+0>
被0x080483ce <+3>
包含在内。首先我们push %ebp; mov %esp,%ebp
按照你说的创建一个框架,然后我们为堆栈上的局部变量分配 0x10 字节的空间sub $0x10,%esp
。该指令所做的只是将堆栈指针向下移动 0x10 个字节。它不存储任何值,它只是留下一些空间,如果我们愿意,我们可以将其用于局部变量(我们会看到编译器甚至不会使用所有这些空间!)。
接下来我们有函数的实际逻辑。首先,我们将堆栈中的两个参数 x 和 y 加载到寄存器中:
0x080483d1 <+6>: mov 0x8(%ebp),%edx
0x080483d4 <+9>: mov 0xc(%ebp),%eax
我们将它们加在一起:
0x080483d7 <+12>: add %edx,%eax
现在我们将结果存储在一个局部变量中。该局部变量实际上只是我们在序言中分配的堆栈空间。我们为局部变量分配了 0x10 个字节,这里我们只使用前 4 个字节来存储加法的结果:
0x080483d9 <+14>: mov %eax,-0x4(%ebp)
而且因为没有任何优化,我们立即将结果从局部变量加载回寄存器,以便我们可以返回它:
0x080483dc <+17>: mov -0x4(%ebp),%eax
如您所见,代码效率非常低,但至少它相当容易阅读。现在只剩下结语了,这很简单:
0x080483df <+20>: leave
0x080483e0 <+21>: ret
leave
销毁我们在序言中创建的帧,并返回ret
到调用函数的下一条指令。
Epilog 是为当前帧展开堆栈的时间。ESP 移至 EBP,因此(通常)无法访问局部变量。旧的 EBP 从堆栈中弹出,并指向其原始地址。ESP 指向保存的 EIP,这是调用 add(3,4) 之前的位置。
在我学习的解释中,最后一部分是返回指令将保存的 EIP 值弹出回 EIP 寄存器。[当然这不是函数中的 return 语句,而是机器级别的 ret 指令,对吧?]
函数中的 return 语句对应于机器级别的 ret 指令。是直接翻译。请记住,您的计算机不会直接运行 C 代码,所有 C 都首先编译为机器代码,而ret
这里的指令确实是弹出 EIP 的内容。
最后一个问题,有人能解释一下函数中的代码在执行时发生了什么,以及调用、序言和结语发生在什么时候?或者提供一个清晰解释的好链接?
您在上面看到的反汇编是计算机运行内容的粗略文本表示。EIP 包含计算机将运行的下一条指令的地址。当您的程序运行时,它存储在内存中的某个位置,而 EIP 直接指向内存中的指令。
所以计算机只会按照编写的顺序运行函数,prolog 和 epilog 是函数的一部分。
prolog 和 epilog 是一种约定,但它们只是代码。如果你愿意,你可以完全删除序言并编写一个疯狂的结语,它也可以。
我建议您使用反汇编器和调试器,以熟悉它的实际工作原理。这并不难,而且非常合乎逻辑。