过程调用的实现 gaunthan Posted on May 14 2016 ? The Implement of Program ? ## 概述 一个过程调用包括将数据(以过程参数和返回值的形式)和控制从代码的一部分传递到另一部分。另外,它还必须在进入时为过程的局部变量分配空间,并在退出时释放这些空间。大多数机器,包括 IA32(Intel Architecture x32),只提供转移控制到过程 (call) 和从过程中转移出控制 (ret) 这种简单的指令。数据传递、局部变量的分配和释放通过操纵程序栈来实现。 ## 栈帧结构 IA32程序用运行时栈来支持过程调用。机器用栈来传递过程参数、存储返回信息、保存寄存器用于以后恢复,以及本地存储。为单个过程分配的那部分栈称为**栈帧**(stack frame)。栈帧的最顶端以两个指针界定,寄存器ebp为**帧指针**,而寄存器esp为栈指针。当程序执行时,栈指针可以移动,因此大多数信息的访问都是相对于帧指针的。 下图描绘了栈帧的通用结构:  假设过程P(调用者)调用过程Q(被调用者),则Q的参数放在P的栈帧中。另外,当P调用Q时,P中的返回地址被压入栈中,形成P的栈帧的末尾。返回地址就是当程序从Q返回时应该继续执行的地方。Q的栈帧从保存的帧指针的值(例如ebp)开始,后面是保存的其他寄存器的值。 过程Q也用栈来保存其他不能存放在寄存器中的局部变量。这样做的原因如下: * 没有足够多的寄存器存放所有的局部变量。 * 有些局部变量是数组或结构,因此必须通过数组或结构引用来访问。 * 要对一个局部变量使用地址操作符`&`,就必须能够为它生成一个地址。 另外,Q会用栈帧来存放它调用的其他过程的参数。在Q被调用的过程中,第一个参数是放在相对于%ebp偏移量为+8的位置处(%ebp放旧ebp,4(%ebp)放返回地址),剩下的参数存储在后续的4字节块中,所以参数i就在相对于ebp偏移量为`4 + 4i`的地方(32位平台)。较大的参数(比如结构和较大的数字格式)需要栈上更大的区域。 ## 转移控制指令 下表是支持过程调用和返回的指令:  call指令有一个目标,即指明被调用过程起始的指令地址。同跳转一样,调用可以是直接的,也可以是间接的。在汇编代码中,直接调用的目标是一个标号,而间接调用的目标是 * 后面跟一个操作数指示符。 call指令的效果是将返回地址入栈,并跳转到被调用过程的起始处。返回地址是在程序中紧跟在call后面的那条指令的地址,这样当被调用过程返回时,执行会从此处继续。ret指令从栈中弹出地址,并跳转到这个位置。正确使用这条指令,可以使栈做好准备,栈指针要指向前面call指令存储返回地址的位置。 * `call Label`等价于下面的代码序列: ```c movl %eip, %esp # 返回地址放入栈顶 jmp Label # 跳转到对应处 ``` * `leave`等价于下面的代码序列: ```c movl %ebp, %esp # 使栈指针指向栈帧首 popl %ebp # 恢复旧%ebp并使栈指针指向返回地址 ``` * `ret`等价于下面的代码序列: ```c popl %eip # 恢复指令指针 ``` ## 寄存器使用惯例 程序寄存器组是唯一能被所有过程共享的资源。虽然在给定时刻只能有一个过程是活动的,但是我们必须保证一个过程(调用者)调用另一个过程(被调用者)时,被调用者不会覆盖某个调用者稍后会使用的寄存器的值。为此,IA32采用了一组统一的寄存器使用惯例,所有的过程都必须遵守,包括程序库中的过程。 根据惯例,寄存器被分为两类: |类别|寄存器|说明| |--| |调用者保存的寄存器|eax,edx,ecx|当过程P调用Q时,Q可以覆盖这些寄存器,而不会破坏任何P所需要的数据| |被调用者保存的寄存器|ebx,esi,edi|被调用者Q在覆盖这些寄存器的值之前,需要先把它们保存到栈中,并在返回前恢复它们| 此外,根据惯例,必须保存寄存器ebp和esp。 ## References - RandalE.Bryant, DavidR.O’Hallaron, 布赖恩特,等. 深入理解计算机系统[M]. 机械工业出版社, 2011. 赏 Wechat Pay Alipay Linux 用户管理 数组分配和访问