全文基于_cdecl函数调用约定

#include <stdio.h>

int sum(int a, int b) {
int temp = 0;
temp = a + b;
return temp;
}

int main() {
int a = 10;
int b = 20;

int ret = 0;
ret = sum(a, b);
printf("ret = %d\n", ret);
return 0;
}

我们编译代码查看汇编指令,所有函数第一条语句前面都会有这么一段代码,我们在讲sum函数的时候讲

C函数栈帧开辟以及回退过程_寄存器

局部变量都是通过栈底指针ebp偏移访问,不生成符号,不属于数据,属于指令

C函数栈帧开辟以及回退过程_寄存器_02


根据上面三条汇编指令,我们可以画出如下栈空间示意图,ebp是栈底指针,保存的是一个地址

C函数栈帧开辟以及回退过程_调用函数_03


接下来就是调用sum函数,首先需要实参压栈,C/C++是从右向左压栈,而有些语言,比如PASCAL是从左向右压实参的

由于C/C++是需要支持可变长参数,实参的个数不固定,形参用​​...​​来接收实参,所以是从右向左压栈,这样压栈的时候就能知道用户到底传入了多少个实参,而从左向右压栈无法确定用户传入了多少个实参

C函数栈帧开辟以及回退过程_调用函数_04


将实参压栈后,我们可以得到以下栈空间图

C函数栈帧开辟以及回退过程_C_05

形参空间是在调用函数栈帧(main)中开辟的,而不是在被调用函数栈帧(sum)中开辟,每压栈一个实参,都会开辟一个形参的空间,栈顶指针esp都会减4字节(栈顶指针esp为低地址,栈底指针ebp为高地址)

实参压栈完成后,需要调用call指令,去执行sum函数,执行完sum函数后需要回到调用指令(call)的下一条指令继续执行

所以call指令需要做以下两件事:

  • 把call指令下一行指令的地址(0x0040109A)入栈
  • jmp跳转

C函数栈帧开辟以及回退过程_压栈_06


执行call指令后,程序跳转到了这么一个地方,而不是sum函数的指令部分

C函数栈帧开辟以及回退过程_压栈_07


符号重定向:编译阶段是不分配符号地址的,因为我们当前文件可能引用外部的符号,而编译阶段是独立编译的,我们链接的时候才会进行符号解析、合并符号表等操作,之后再给符号分配内存地址

也就是说,编译阶段给符号分配的地址都是不合法的,对于数据符号编译阶段分配的是0地址,函数符号则分配的是-4。链接后给函数符号分配的地址是与下一行指令地址的一个偏移量。这样当程序需要跳转到某个函数地址的时候,取出PC寄存器保存的地址与该偏移量相加就得到函数的入口地址

进入sum函数后,需要执行这么一段指令,才执行我们写的代码

C函数栈帧开辟以及回退过程_寄存器_08

C函数栈帧开辟以及回退过程_寄存器_09

C函数栈帧开辟以及回退过程_寄存器_10


总结,每次执行一个函数之前都需要执行三个操作:

  • 调用函数栈顶main成为被调用函数sum栈底(mov ebp,esp)
  • 移动栈顶指针esp,给被调用函数sum开辟栈帧(sub esp,44h)
  • 初始化新栈帧内存(rep stos dword ptr [edi])

接下来需要执行以下指令

C函数栈帧开辟以及回退过程_压栈_11

C函数栈帧开辟以及回退过程_C_12

局部变量通过栈底指针ebp负向偏移访问,形参通过ebp正向偏移访问,eax为a+b的计算结果,讲计算结果赋值给temp,return的时候将temp的值赋值给eax,给调用方返回

栈帧清退

C函数栈帧开辟以及回退过程_寄存器_13


栈帧清退的时候修改esp和ebp,用ebp给esp赋值,从栈上取出调用方栈底地址给ebp赋值

C函数栈帧开辟以及回退过程_寄存器_14


可以看到,开辟栈帧的时候我们对占内存进行了初始化,但是栈帧清退的时候仅仅就是修改了esp和ebp,没有做其他任何操作,如果我们此时通过一些手段去访问已被清退栈帧的内存,还是可以访问到的,因为数据还存在现在执行ret指令,相当于​​pop IP​​,把栈顶元素的值(调用处下一条指令的地址)赋给PC寄存器,并且移动esp

C函数栈帧开辟以及回退过程_压栈_15


sum函数执行完成后,取出PC寄存器中存放的地址继续执行,这是回到call指令的下一条指令处,这条指令的操作就是回收形参变量内存,形参内存由调用方开辟和释放而sum函数的返回值由eax寄存器带回来

C函数栈帧开辟以及回退过程_压栈_16


函数调用过程中用pop ebp恢复栈底,用​ret​指令取出在栈上保存的返回地址存到PC寄存器

sum函数栈底保存的是main的栈底地址,main函数栈底保存的是调用main的函数的栈底地址