栈与函数调用 gaunthan Posted on Jan 5 2017 ? The Implement of Program ? ## 栈 在计算机系统中,栈是一个**后进先出**的**动态**内存区域。程序可以将数据**压入**(push)栈中,也可以将数据从栈**弹出**(pop)。压栈操作使得栈增大,而弹出操作使栈减小。在经典的操作系统里,栈总是**向下增长**的。在 i386 里,栈顶由称为esp的寄存器进行定位。压栈的操作使得栈顶的地址减小,弹出的操作使得栈顶地址增大。 下图是栈的一个实例,需要注意的是地址自上而下递减:  这里栈底的地址是 0xbfffffff,而 esp 寄存器标明了栈顶,地址为 0xbffffff4。在栈上压入数据会导致 esp 减小,弹出数据使得 esp 增大。相反,直接减小 esp 的值也等效于在栈上开辟空间,直接增大 esp 的值等效于在栈上回收空间。 ## 栈与函数调用 栈保存了一个函数调用所需要的维护信息,这常常被称为**堆栈帧**(Stack Frame)或**活动记录**(Activate Record)。堆栈帧一般包括如下几方面内容: * 函数的返回地址和参数 * 临时变量:包括函数的非静态局部变量以及编译器生成的暂存变量 * 保存的上下文:包括在函数调用前后需要保持不变的寄存器 在 i386 中,一个函数的活动记录用ebp和esp这两个寄存器划定范围。esp 寄存器始终指向栈的顶部,同时也就指向当前函数的活动记录的顶部。而相对的,ebp寄存器指向了函数活动记录的一个固定位置,ebp寄存器又被称为**帧指针**(Frame Pointer)。 在参数之后的数据(包括参数)即是当前函数的活动记录,ebp 的位置是固定的,不随这个函数的执行而变化。相反地,esp 始终指向栈顶,随着函数的执行会不断变化。固定不变的 ebp 可以用来定位函数活动记录中的各个数据。ebp 直接指向的数据是调用该函数前 ebp 的值,这样在函数返回的时候,ebp 可以通过读取这个值恢复到调用前的值。之所以函数的活动记录会形成这种结构,是因为函数调用本身是如此书写的。 一个常见的活动记录示意图如下:  一个 i386 下的函数总是这样调用的: 1. 把所有或一部分参数压入栈中,如果有其他参数没有入栈,那么使用某些特定的寄存器传递 2. 把当前指令的下一条指令的地址压入堆栈(返回地址) 3. 跳转到函数体执行 ## 函数调用惯例 函数的调用方和被调用方对于函数如何调用有一个明确的约定,这样的约定称为**调用惯例**(Calling Convention)。一个调用惯例一般会规定如下几个方面的内容: * 函数参数的传递顺序和方式 函数参数的传递有很多种方式,最常见的一种是通过栈传递。函数的调用方将参数压入栈中,函数自己再从栈中将参数取出。对于有多个参数的函数,调用惯例要规定函数调用方将参数压栈的顺序:是从左至右,还是从右至左。有些调用惯例还允许使用寄存器传递参数,以提高性能。 * 栈的维护方式 在函数将参数压栈之后,函数体会被调用,此后需要将被压入栈中的参数全部弹出,以使得栈在函数调用前后保持一致。这个弹出的工作可以由函数的调用方来完成,也可以由函数本身来完成。 * 名字修饰(Name-managling)策略 为了链接的时候对调用惯例进行区分,调用惯例要对函数本身的名字进行修饰。不同的调用惯例有不同的名字修饰策略。C 语言默认的调用惯例是**cdecl**。 一些常见的调用惯例见下表: |调用惯例|出栈方|参数传递|名字修饰| |--| |cdecl|函数调用方|从右至左的顺序压参数入栈|下划线 + 函数名| |stdcall|函数本身|从右至左的顺序压参数入栈|下划线 + 函数名 + @ + 参数的字节数| |fastcall|函数本身|头两个 DWORD(4 字节)类型或者占更少字节的参数被放入寄存器,其他剩下的参数按从右至左的顺序压入栈|@ + 函数名 + @ 参数的字节数| |pascal|函数本身|从左至右的顺序压删除入栈| ## References - RandalE.Bryant, DavidR.O’Hallaron, 布赖恩特,等. 深入理解计算机系统[M]. 机械工业出版社, 2011. - 俞甲子, 石凡, 潘爱民. 程序员的自我修养[M]. 电子工业出版社, 2009. 赏 Wechat Pay Alipay Vim/gedit 中文乱码问题解决 C语言 测试代码块运行时间