栈与调用惯例
1.栈
栈被定义为一个特殊的容器,用户可以将数据压入栈中,也可以将栈中的数据弹出,而且要遵循先进后出(FILO)的原则。在计算机系统中,栈是具有以上属性的动态内存区域。程序可以将数据压入栈中,也可以将数据从栈顶弹出。
在经典的操作系统里,栈从高地址向低地址增长,在i386下,栈顶由esp寄存器进行定位。压栈使栈顶地址减小,弹出使栈顶地址增大。
栈在程序运行过程中具有举足轻重的地位。栈保存了一个函数调用所需要维护的信息,这常常被称为堆栈帧或活动记录。一般包括以下几个方面:
- 函数的返回地址和参数。
- 临时变量:包括函数的非静态局部变量以及编译器自动生成的其他临时变量。
- 保存的上下文:包括在函数调用前后需要保持不变的寄存器。
一个常见的活动记录如下图所示:
一个函数的活动记录由ebp和esp两个寄存器划定范围。esp寄存器始终指向栈的顶部,同时也就指向了当前函数的活动记录的顶部。ebp寄存器指向了函数活动记录的一个固定位置,被称为帧指针。固定不变的ebp可以用来定位函数活动记录中的各个数据。例如函数返回地址的地址为ebp+4,再往前是压入栈中的参数,地址分别是ebp+8,ebp+12等,视参数数量和大小而定。ebp所直接指向的值是调用该函数前ebp的值,这样在函数返回的时候,ebp可以通过读取这个值恢复到调用前的值。
在i386下函数调用的过程:
- 把所有或一部分参数压入栈,如果有其他参数没有入栈,那么使用某些特定的寄存器传递。
- 把当前指令的下一条指令的地址压入栈中。
- 跳转到函数体执行。
- push ebp。
- mov ebp,esp:esp=ebp。
- 【可选】sub esp,XXX:在 栈上分配XXX字节的临时空间。
- 【可选】push XXX:如有必要,保存名为XXX寄存器。
- 【可选】pop XXX:恢复保存过的寄存器。
- mov ebp,esp:esp=ebp,恢复ESP同时回收局部变量空间。
- pop ebp:从栈中恢复保存的ebp的值。
- ret:从栈中取得返回地址,并跳转到该位置。
2.调用惯例
函数的调用方和被调用方对于函数如何调用必须有一个明确的约定,只有双方都遵守同样的约定,函数才能被正确的调用,这样的约定就称为调用惯例。
调用惯例包含以下几个方面的内容:
- 函数参数的传递顺序和方式。顺序:从左向右或从右向左;方式:栈传递或寄存器传递。
- 栈的维护方式。是指栈中数据弹出的过程,可以有函数的调用方来完成,也可以由函数本身完成。
- 名字修饰策略。为了链接的时候对调用惯例进行区分,调用惯例要求对函数本身的名字进行修饰。
几种主要的调用惯例: