前言:本篇文章在汇编语言的角度介绍了函数的调用过程,也就是函数的栈帧,如果有什么问题,欢迎指出,共同进步。
1.什么是函数栈帧
每一次函数的调用都是一个过程,在这个过程中要为函数开辟栈空间,用于本次函数调用中临时变量的保存和现场保护等等。栈是从高地址向低地址延伸的。寄存器ebp指向当前的栈帧的底部(高地址),寄存器esp指向当前的栈帧的顶部(低地址)。
2.以Add函数为例,图解栈帧
- 测试代码
#include<stdio.h>
int Add(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 10;
int b = 20;
int ret = Add(a, b);
printf("ret=%d", ret);
return 0;
}
将程序调试起来(如下图),查看【调用堆栈】,我们可以发现main函数在被调用之前是在函数__mainCRTStartup中调用,而__mainCRTStartup又是在mainCRTStartup函数中调用。
- main函数的栈帧:
3.从main函数开始分析函数栈帧
从第一句汇编分析对应如上图
a.首先是__mainCRTStartup()函数的调用,然后在调用main()函数;
b.将ebp做压栈处理,保存指向栈底的地址(方便函数返回之后的现场恢复;
c.将esp的值赋给ebp,产生新的ebp;
d.给esp减去一个16进制数0E4H(为main函数预开辟空间);
e.将ebx、esi、edi做压栈处理,对应蓝色部分;
f.lea指令,加载有效地址,初始化预开辟的空间为0xcccccccc,对应橙色部分;
g.变量a和b的创建。下边是Add函数的调用过程
a.将实参b存入寄存器eax,再将eax做压栈处理;(传参过程,从右向左传递,先传b)
b.将a存入寄存器ecx,再将ecx压栈;
c.call指令的调用,先将call指令下一条指令的地址压栈,然后跳转(jmp)到Add()函数的地方。
- 执行call指令
按f11执行call指令,如下图
- 进入Add函数
a.首先将main()函数ebp压栈处理,保存指向main()函数栈帧底部的地址(方便函数返回之后的现场恢复),此时esp指向新的栈顶位置;
b.将esp的值赋给ebp,产生新的ebp,即Add()函数栈帧的ebp;
c.给esp减去一个16进制数0E4H(为Add()函数预开辟空间);
d.将 ebx、esi、edi做压栈处理;
e.lea指令,加载有效地址;
f.初始化预开辟的空间为0xcccccccc;
g.变量z的创建;
h.获取形参的a和b再相加,将结果存储到z中;
i.将结果存储到eax寄存器,通过寄存器带回函数的返回值。
- Add函数返回
a.edi、esi、ebx依次出栈,esp向下移动;
b.将ebp赋给esp,使esp指向ebp指向的地方;
c.ebp 出栈,将出栈的内容给ebp(即main-ebp),回到main()函数的栈帧;
d.ret指令,出栈一次,并将出栈的内容当做地址,并跳转到该地址处(call指令下一条指令)。
注:函数栈帧在不同编译器上实现存在差异,偏移量不同,但思路都是一样的。
例子:下面是我代码,在VC6.0++输出结果是什么。
#include<stdio.h>
void fun()
{
int tmp = 10;
int *p = (int*)(*(&tmp + 1));
*(p - 1) = 20;
}
int main()
{
int a = 0;
fun();
printf("a=%d\n", a);
return 0;
}
这段代码看似和fun函数没有什么关系,但是输出结果却是20。它实际上是与函数栈帧有关系,感兴趣的朋友可以根据上边的栈帧图解做一做题目。