我们都知道,在C语言中,有个东西叫做函数,任何的高手编写C语言的程序,基本都会用到函数,用函数去实现一个基本的功能,举个例子,实现一个求和的函数,如下:
int fun_xuanph(int a , intb){ return a+b; }int main(){ int a = 1; int b = 2; printf('sum=%d\n',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 retq0000000000400541 <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函数时,下一条指令的地址,如下图所示:
main函数栈空间
通过上图我们知道,rsp指向的是下一条指令的下面,这是由call指令产生的,在调用call指令时,会被call指令的下一条指令的地址进行入栈,因此rsp往下走8个字节,存下一条指令的地址,接下来,我们再看fun_xuanph函数的栈空间:
fun_xuanph函数的栈空间
可以看到,汇编语言的第二行,调用了push rbp ,将rbp寄存器压入了栈中,进行保存,紧接着又调用了mov rsp,rbp 将rsp赋值给了rbp寄存器,此时rbp就是函数fun_xuanph的栈底,rsp就是fun_xuanph函数的栈顶。
联系客服