c语言函数调用栈

​ 函数调用栈是一个非常重要的过程,也是整个程序运行的关键,下面简单地说一下这个调用过程。

寄存器

​ 寄存器是程序运行的重要载体,可以用来存放数据和指令,函数调用过程与其密切相关。首先是几个特殊点的寄存器:%eax ,%ebx ,%ecx ,%edx, 各自都能作为独立的16位寄存器,低16位还能再分成8位的寄存器,这应该是为了向下兼容。在汇编语言中,这些通用寄存器通常以%e(AT&T语法)或直接以e(Intel语法)开头来引用,如,mov $5, %eax,或者mov eax, 5 ,都表示将立即数赋值给寄存器%eax 。

​ 而x86架构中,EIP是一个比较特殊的寄存器,它被称为指令寄存器,永远指向处理器下一条等待执行的指令地址(有时也用偏移量表示),每次执行完EIP的值就会增加,由于它的特殊性,不能像访问普通寄存器那样访问它,找不到可用来寻址EIP并对其进行读写的操作码(Opcode),EIP可被jmp、call 、ret等指令隐藏式地改变。

​ 另外要注意的是,寄存器是唯一能被所有函数共享的资源,需要保证A函数调用B函数时,B函数不会修改或覆盖A函数稍后会用到的寄存器值,为此,有这么一些规定:寄存器%eax,%ebx,%ecx,为主调函数保存寄存器(caller-saved registers),函数调用时,如果主调函数想要保存这些寄存器的值,必须在调用前显示地将其保存在栈中,然后被调函数就可以覆盖这些寄存器,这样就不会破坏主调函数所需的数据。寄存器%ebx,%esi,%edi 为被调函数保存寄存器(callee-saved registers),即被调函数在覆盖这些寄存器的值时,必须先把寄存器的原值压入栈中保存起来,并在函数返回前恢复其值,因为主调函数也可能会用到这些寄存器。除此之外,被调函数还必须保持寄存器%ebp,%esp,并在函数返回后将其恢复到调用前的值,即必须恢复主调函数的栈帧。

栈帧

​ 刚刚说到了栈帧(stack frame),栈帧是什么呢?栈帧本质上就是一种栈,只是这种栈专门用于保存函数调用过程中的各种信息。同样有栈顶和栈底,栈底地址最高,x86-32bit架构中,用%ebp指向栈底,即基址指针,%esp指向栈顶,即栈顶指针。函数调用栈时的内存布局:

调用栈时的内存布局

​ 注意这个图基于两个假设:第一,函数返回值不是结构体或联合体,否则的话第一个参数将位于12(%ebp)处;第二,每个参数都是4字节大小。

​ 从这个图也能看出,函数调用时的入栈顺序为:实参(从右到左)->主调函数的返回地址->主调函数的帧基指针EBP ->被调函数局部变量1~N。具体来看,主调函数首先将参数按照调用约定依次入栈,然后将指令指针EIP入栈以保存主调函数的返回地址。进入被调函数时,被调函数将主调函数的帧基指针EBP入栈,并将主调函数的栈顶指针ESP值赋给被调函数的EBP(作为被调函数的栈底),接着改变ESP值来为函数局部变量预留空间。此时被调函数帧基指针指向被调函数的栈底。以此地址为基准,向(上)栈底方向可获得主调函数的返回地址和参数值,向(下)栈顶方向可获得被调函数的局部变量值,而该地址又放着上一层主调函数的帧基指针值。本级调用结束后,EBP指针值赋给ESP,使其再次指向被调函数栈底以释放局部变量;再将以压栈的主调函数帧基指针弹出到EBP,并弹出返回地址到EIP。ESP继续上移越过参数,回到调用前的状态,恢复原来主调函数的栈帧,递归便形成了函数调用栈。