前置知识
1、对进程的内存空间布局有一定了解,知道栈是从高地址往低地址增长。
2、对于x86架构处理器,当函数的局部变量和参数较少时,它们会保存在寄存器而不是内存的栈中,因为访问寄存器会更快。但是当寄存器不足以存放本地数据或对局部变量使用地址运算符&(内存引用)时,系统会在函数调用栈中为函数分配一个栈帧,栈帧中存储了函数的局部变量、参数和返回地址。
3、在x86(32位)中,寄存器%ESP是栈顶指针,指向栈顶元素。寄存器%EBP是栈底指针,指向当前函数的栈帧的栈底。所以对于一个函数的栈帧,它的范围就是栈底指针到栈顶指针之间的空间,如下图
具体例子
以下面程序作为例子,main是调用函数caller,sum是被调用函数callee。按该程序来用图表示函数调用和返回过程中,栈空间的内存变化
1、main函数栈帧初始状态:此时%ESP和%EBP都指向main函数栈底,此时栈中的值是调用main函数的函数的栈底地址(看不懂没关系,后面会理解)
2、先后将局部变量和要传入sum的参数压栈
3、main函数调用sum,会执行汇编的call指令。该指令会先把当前函数的返回地址压栈,以便让程序能在sum函数执行完返回到main函数继续执行。然后程序计数器就跳转到sum函数那里去执行代码
4、执行sum函数。sum函数会先将当前的%EBP的值压栈(也就是main函数的%EBP值)。这一步是用来保存main函数的栈底指针,以便sum函数结束返回到main函数的栈帧状态
5、将%EBP移动到%ESP,此时栈底指针%EBP就成了sum函数的栈底指针。所以从Caller's EBP开始到下面的内容就是sum函数的栈帧
6、将sum的ret变量和temp临时变量压入栈中
7、如果sum函数还要调用其他函数,就会像main一样先后压入返回地址和当前EBP。但现在sum函数结束了,将栈顶指针%ESP返回到栈底指针%EBP,ret和temp变量被废弃掉。(返回值ret的值会通过寄存器来传给main函数)
8、sum函数执行出栈操作,将当前栈顶指针的值即Caller's EBP赋值给%EBP指针,这一步就会让栈底指针指回main函数的栈底
9、此时ESP又进行出栈将返回地址赋值给程序计数器,那么程序就会回到返回地址继续执行main函数下面的语句。此时栈帧又恢复成main的栈帧,调用栈的变化过程结束。(可以看到此时栈内存中还留着之前函数的内容,这解释了未初始化变量的值是随机的)
注:上面的过程省去了一些细节,但不影响整体的理解
总得来说,函数调用栈的变化过程就是
1、将函数参数和返回地址压入栈中
2、将调用函数(main)的栈底指针压入栈中,然后就把被调用函数的变量压入栈中。
3、被调用函数执行完后,栈顶指针%ESP回到栈底指针%EBP处。然后将栈中的值(调用函数的栈底地址)弹出赋值给%EBP。栈底指针恢复为main函数栈底,然后继续将栈中的值(返回地址)弹出赋值给程序计数器,然后程序就回到main原来的地址那里继续执行main函数代码。
最后
补充
函数参数传递方式:在X86的32位系统中,参数都使用栈来传递。在X86的64位系统中,前6个参数使用寄存器来传递,剩下来的用栈传递。而在ARM32中(32位系统),前四个整数(包括指针)会用r0~r3寄存器传递,剩下的通过栈来传递,如果是浮点数或者结构体就直接通过栈来传递。ARM64同理,前8个整数(包括指针)会用x0~x7寄存器传递。
参考
如果想对照着汇编语言来看整个过程,可以看这个视频:065.回顾c语言函数调用的栈帧变化过程_哔哩哔哩_bilibili 。这个视频是根据汇编来讲整个过程,这篇文章其实是对这个视频的简化版,注重于理解过程。