我们都知道,在 C语言 中,有个东西叫做函数,任何的高手编写C语言的程序,基本都会用到函数,用函数去实现一个基本的功能,举个例子,实现一个求和的函数,如下:
int fun_xuanph(int a , intb){
return a+b;
}
int main(){
int a = 1;
int b = 2;
printf ("sum=%dn",fun_xuanph(a,b));
}
上面的程序是那么的简单,虽然简单,但是对于讲解 函数调用 背后的逻辑,足以了,可能有的人看到这个程序第一眼,会觉得这太简单了,不就是一个调用一个函数然后返回一个求和的值吗?我想说,你说得对,就是这么个玩意,但是,你可知道调用一个函数之前,都需要做什么吗?这就涉及到函数入栈的问题,这里要表达一个知识点,那就是任何函数都有自己的栈顶和栈底,那么为什么要有栈呢?
一个程序运行中,之所以要有栈,主要有以下的原因:
1) 保存上下文的环境
我们知道一个函数调用完后,要回到原来的地方去执行,那么就需要保留之前的数据,比如,返回地址, 寄存器 等,这些值会被存到栈中。
2) 局部变量 的值也要保存到栈空间中。
一个函数内部使用的局部变量,也要存到栈中。
现在我们知道,一个函数想要执行,一定要开辟一段内存空间,来存放上下文以及函数内部的局部变量,这块空间就是栈。
我们刚才收到一个函数有自己的栈顶和栈底,那么用什么来表示栈顶和栈底呢?
其实是用两个寄存器来表示,栈顶是esp寄存器,栈底是ebp寄存器,出栈和入栈都会操作esp寄存器,将上面的程序进行 反汇编 ,得到下面的汇编代码,下面就基于汇编程序讲讲函数入栈和出栈的过程:
000000000040052d <fun_xuanph>:
40052d: 55 push %rbp
40052e: 48 89 e5 mov %rsp,%rbp
400531: 89 7d fc mov %edi,-0x4(%rbp)
400534: 89 75 f8 mov %esi,-0x8(%rbp)
400537: 8b 45 f8 mov -0x8(%rbp),%eax
40053a: 8b 55 fc mov -0x4(%rbp),%edx
40053d: 01 d0 add %edx,%eax
40053f: 5d pop %rbp
400540: c3 retq
0000000000400541 <main>:
400541: 55 push %rbp
400542: 48 89 e5 mov %rsp,%rbp
400545: 48 83 ec 10 sub $0x10,%rsp
400549: c7 45 f8 01 00 00 00 movl $0x1,-0x8(%rbp)
400550: c7 45 fc 02 00 00 00 movl $0x2,-0x4(%rbp)
400557: 8b 55 fc mov -0x4(%rbp),%edx
40055a: 8b 45 f8 mov -0x8(%rbp),%eax
40055d: 89 d6 mov %edx,%esi
40055f: 89 c7 mov %eax,%edi
400561: e8 c7 ff ff ff callq 40052d <fun_xuanph>
400566: 89 c6 mov %eax,%esi
400568: bf 04 06 40 00 mov $0x400604,%edi
40056d: b8 00 00 00 00 mov $0x0,%eax
400572: e8 99 fe ff ff callq 400410 <printf@plt>
400577: b8 00 00 00 00 mov $0x0,%eax
40057c: c9 leaveq
40057d: c3 retq
可以看到第15行,将esp向下移动了16个字节,实际上这就是为 main函数 开辟的栈空间,这16个字节,存放的是局部变量a,局部变量b,以及调用fun_xuanph函数时,下一条指令的地址,如下图所示:
通过上图我们知道,rsp指向的是下一条指令的下面,这是由call指令产生的,在调用call指令时,会被call指令的下一条指令的地址进行入栈,因此rsp往下走8个字节,存下一条指令的地址,接下来,我们再看fun_xuanph函数的栈空间:
可以看到,汇编语言的第二行,调用了push rbp ,将rbp寄存器压入了栈中,进行保存,紧接着又调用了mov rsp,rbp 将rsp赋值给了rbp寄存器,此时rbp就是函数fun_xuanph的栈底,rsp就是fun_xuanph函数的栈顶。