全文基于_cdecl函数调用约定
我们编译代码查看汇编指令,所有函数第一条语句前面都会有这么一段代码,我们在讲sum函数的时候讲
局部变量都是通过栈底指针ebp偏移访问,不生成符号,不属于数据,属于指令
根据上面三条汇编指令,我们可以画出如下栈空间示意图,ebp是栈底指针,保存的是一个地址
接下来就是调用sum函数,首先需要实参压栈,C/C++是从右向左压栈,而有些语言,比如PASCAL是从左向右压实参的
由于C/C++是需要支持可变长参数,实参的个数不固定,形参用...
来接收实参,所以是从右向左压栈,这样压栈的时候就能知道用户到底传入了多少个实参,而从左向右压栈无法确定用户传入了多少个实参
将实参压栈后,我们可以得到以下栈空间图
形参空间是在调用函数栈帧(main)中开辟的,而不是在被调用函数栈帧(sum)中开辟,每压栈一个实参,都会开辟一个形参的空间,栈顶指针esp都会减4字节(栈顶指针esp为低地址,栈底指针ebp为高地址)
实参压栈完成后,需要调用call指令,去执行sum函数,执行完sum函数后需要回到调用指令(call)的下一条指令继续执行
所以call指令需要做以下两件事:
- 把call指令下一行指令的地址(0x0040109A)入栈
- jmp跳转
执行call指令后,程序跳转到了这么一个地方,而不是sum函数的指令部分
符号重定向:编译阶段是不分配符号地址的,因为我们当前文件可能引用外部的符号,而编译阶段是独立编译的,我们链接的时候才会进行符号解析、合并符号表等操作,之后再给符号分配内存地址
也就是说,编译阶段给符号分配的地址都是不合法的,对于数据符号编译阶段分配的是0地址,函数符号则分配的是-4。链接后给函数符号分配的地址是与下一行指令地址的一个偏移量。这样当程序需要跳转到某个函数地址的时候,取出PC寄存器保存的地址与该偏移量相加就得到函数的入口地址
进入sum函数后,需要执行这么一段指令,才执行我们写的代码
总结,每次执行一个函数之前都需要执行三个操作:
- 调用函数栈顶main成为被调用函数sum栈底(mov ebp,esp)
- 移动栈顶指针esp,给被调用函数sum开辟栈帧(sub esp,44h)
- 初始化新栈帧内存(rep stos dword ptr [edi])
接下来需要执行以下指令
局部变量通过栈底指针ebp负向偏移访问,形参通过ebp正向偏移访问,eax为a+b的计算结果,讲计算结果赋值给temp,return的时候将temp的值赋值给eax,给调用方返回
栈帧清退
栈帧清退的时候修改esp和ebp,用ebp给esp赋值,从栈上取出调用方栈底地址给ebp赋值
可以看到,开辟栈帧的时候我们对占内存进行了初始化,但是栈帧清退的时候仅仅就是修改了esp和ebp,没有做其他任何操作,如果我们此时通过一些手段去访问已被清退栈帧的内存,还是可以访问到的,因为数据还存在现在执行ret指令,相当于pop IP
,把栈顶元素的值(调用处下一条指令的地址)赋给PC寄存器,并且移动esp
sum函数执行完成后,取出PC寄存器中存放的地址继续执行,这是回到call指令的下一条指令处,这条指令的操作就是回收形参变量内存,形参内存由调用方开辟和释放而sum函数的返回值由eax寄存器带回来
函数调用过程中用pop ebp
恢复栈底,用ret
指令取出在栈上保存的返回地址存到PC寄存器
sum函数栈底保存的是main的栈底地址,main函数栈底保存的是调用main的函数的栈底地址