目录
过程
过程中是软件中一种很重要的抽象,提供了一种封装代码的方式,用一组指定的参数和一个可选的返回值实现了某种功能。然后,可以在程序的不同的地方使用这个功能。设计良好的软件用过程作为抽象机制,隐藏某个行为的具体实现,同时又提供清晰简洁的接口定义,说明要计算的是哪些值,过程会对程序状态产生什么样的影响。不同的编程语言中,过程的形式多样:函数、方法、子例程、处理函数等,但是它们有一些共有的特性。
假设过程P调用过程Q,Q执行后返回到P。这些动作包括下面一个或多个机制:
- 传递控制,在进入过程Q的时候,程序计数器必须被设置为Q的代码的起始地址,然后在返回时,要把程序计数器设置为P中调用Q后面那条指令的地址。
- 传递数据,P必须能够向Q提供一个或多个参数,Q必须能够向P返回一个值。
- 分配和释放内存,在开始时,Q可能需要为局部变量分配空间,而在返回前,又必须释放这些空间。
1 运行时栈
C语言过程调用机制的一个关键特性在于使用了栈数据结构提供的后进先出的内存管理原则。程序可以使用栈来管理过程所需要的存储空间,栈和程序寄存器存放着传递控制和数据、分配内存所需要的信息,当P调用Q时,控制和数据信息添加到栈顶,当Q返回时,这些信息会释放掉。
当x86-64过程需要的存储空间超出寄存器能够存放的大小时,就会在栈上分配空间。这个部分称为过程的栈帧。当函数满足两个条件时不需要分配栈帧:(1)该函数不会调用其他函数,(2)所有局部变量都可以保存在寄存器中。
当前正在执行的过程的栈帧总是在栈顶。当过程P调用过程Q时,会把返回地址压入栈中,指明当Q返回时,要从P程序的哪个位置继续执行。返回地址作为P的栈帧的一部分,存放的是与P相关的状态。Q的代码会拓展当前栈的边界,分配栈帧所需的空间。在这个空间中可以保存寄存器的值,分配局部变量空间,为调用过程设置参数。通过寄存器,过程P最多可以传递6个整数值(也就是指针和整数),但是如果Q需要更多的参数,P可以在调用Q之前在自己的栈帧中存储这些参数。
2 转移控制
将控制从函数P转移到函数Q只需要简单地把程序计数器设置为Q的代码的起始位置。从Q返回时,处理器必须记录需要继续P的执行的代码位置。
在x86-64机器中,指令call Q
调用过程Q,记录起始位置。该指令将地址A(返回地址)压入栈中,并将PC设置为Q的起始位置。ret
指令会从栈中弹出地址A,并将PC设置为A。
图为两个函数top和leaf的反汇编代码,以及在main函数中调用top处的代码。main中的callq
指令将栈指针%rsp
的值设置为该指针后的mov
指令地址 0x400560,程序计数器的值设置为top的起始指令地址 0x400545。
类似的,top中的callq
指令将栈指针%rsp
的值设置为该指针后的add
指令地址 0x40054e,程序计数器%rip
的值设置为leaf函数的起始指令地址0x400540。而leaf函数中的retq
指令弹出add
指令地址(调用leaf函数的返回地址)0x40054e,栈指针%rsp
的值变为栈中弹出指令的上一条指令地址,即mov
指令地址(调用top函数的返回地址) 0x400560 ,程序计数器%rip
的值设置add
指令地址 0x40054e 。
3 数据传送
在x86-64中,大部分过程间的数据传送是通过寄存器实现的。当过程P调用过程Q时,P的代码必须首先把参数复制到适当的寄存器中,参数在寄存器%rdi
、%rsi
和其他寄存器中传递。当从Q返回到P时,P的代码可以访问寄存器%rax
中的返回值。
x86-64中,可以通过寄存器最多传递6个整型(如整数和指针)参数,根据参数在参数列表中的顺序分配寄存器。寄存器使用的名字取决于要传递的数据类型的大小,可以通过64位寄存器适当的部分访问小于64位的参数。例如,如果第一个参数是32位的,那么可以用%edi来访问。
如果一个函数有大于6个整型参数,超出6个的部分就要通过栈来传递。假设调用过程Q,有 n 个整型参数,且 n > 6,那么P的代码分配的栈帧必须要能容纳7到 n 号参数的存储空间。也就是把参数7~ n 放到栈(P的栈帧)上,参数7位于栈顶。参数准备完成后,程序才执行call指令将控制转移到过程Q。
4 栈上局部存储
在部分情况下,局部数据必须放在内存中,常见的情况包括:
- 寄存器不足够存放所有的本地数据
- 对一个局部变量使用地址运算符&,因此必须能够产生一个地址
- 某些局部变量是数组或结构,因此必须能够通过数组或结构引用或被访问到
一般来说,过程通过减小栈指针在栈上分配空间,分配的结果作为栈帧的一部分,标号为局部变量。
图3-32是一个必须在栈上分配局部变量存储空间的函数示例。
call_proc
的汇编代码从第2行到第15行都是为调用proc
做准备,其中包括为局部变量(第3行到第6行)和函数参数(第7行到第15行)建立栈帧,将函数参数加载至寄存器(第10行到第15行)。x1、x2、x3、x4
为局部变量,首先在栈帧中为四个局部变量分配空间,然后将前六个函数参数(x1、&x1、x2、&x2、x3、&x3
)加载到寄存器,后两个函数参数x4、&x4
存放在栈空间。最后,调用过程proc。