CSAPP-2-3:程序的机器级运行过程
程序的机器级表示
过程
过程提供了一种封装代码的方式,用一组指定的参数和一个可选的返回值实现了某种功能,然后可以在程序不同的地方调用这个函数。 过程的形式多样:函数(function),方法(method),子例程(subroutine),处理函数(handler)等等。
要提供对过程的机器级支持,必须要处理许多不同的属性。假设过程P调用过程Q,Q执行后返回到P,这些动作包括下面一个或多个机制:
- 传递控制:在进入过程Q时,程序计数器必须被设置为Q的代码的起始地址。在返回时,要把程序计数器设置为P中调用Q后的指令的地址。
- 传递数据:P必须能够向Q提供一个或多个参数,Q必须能够向P返回一个值。
- 分配和释放内存:在开始时,Q可能需要为局部变量分配空间,而在返回前,又必须释放这些空间。
过程时栈
栈提供了后进先出的内存管理原则。在过程P调用Q的例子中,当Q在执行时,P以及所有向上追溯到P的调用链中的过程都是暂时被挂起的。当Q运行时,它只需要为局部变量分配新的存储空间或者设置到另一个过程的调用。另一方面,当Q返回时,任何它所分配的局部存储空间都可以被释放。因此,程序可以用栈来管理它的过程所需要的存储空间。

x86-64的栈向低地址方向增长,%rsp指向栈顶元素,将栈指针减小一个适当的量可以为没有指定初始值的数据在栈上分配空间,类似的可以通过增加栈指针来释放空间。
当x86-64过程需要的存储空间超过寄存器能存放的大小时,就会在栈上分配空间,这个部分称为过程的栈帧(stack frame)。上图中,当P调用Q时,会把返回地址压入栈中,指明当Q返回时要从程序的哪个位置继续执行,我们把这个返回地址当做P的栈帧的一部分,因为这是和P相关的状态。
Q的代码会扩展当前栈的边界,分配它的栈帧所需的空间,在这个空间中可以保存寄存器的值,分配局部变量空间,为它调用的过程设置参数。大多数栈帧都是定长的,但有些过程需要变长的栈帧(后面会讲),通过寄存器,过程P最多可以传递6个人整数值(指针和整数),如果需要更多参数,P可以在调用Q之前在自己的栈帧中存储好这些参数。
转移控制
将控制从P转移到Q只需要简单地把程序计数器(PC)设置为Q的代码起始位置。不过,当稍后从Q返回时,处理器必须记录好它需要继续P的执行的代码的位置。这个信息是用指令call Q调用过程Q来记录的。该指令会把地址A压入栈中,并将PC设置为Q的起始地址。压入的地址A称为返回地址,是紧跟call后面的那条指令的地址。对应的指令ret会从栈中弹出A并把PC设置为A。

下面是一个例子:


数据传送
x86-64中可以通过寄存器最多传递6个整型参数。寄存器使用是有特殊顺序的并且寄存器使用的名字取决于要传递的数据类型的大小。
如果一个函数有大于6个整型参数,超过的部分需要用栈来传递。假设P调用Q有n个整型参数且n大于6,则P的栈帧必须要容纳7~n号参数的存储空间并且参数7位于栈顶。 通过栈传递参数时,所有的数据大小都向8的倍数对齐。参数到位之后就可以调用call指令了。
下面是参数传递的例子,可以看到最后两个参数位于相对于栈指针距离为8和16的位置。注意:第6行movl从内存读入4字节,而后面的addb指令只是用其中的低位1字节。(因为它重置了存储a2的寄存器),另外这里也没有做扩展,因为它只是读取,不涉及再做转换,与2.1节的练习不同。

要时刻注意MOV指令的使用。
栈上的局部存储
有些时候局部数据必须存放在内存中:
- 寄存器不足够存放所有数据
- 对一个局部变量使用地址运算符
- 某些局部变量是数组或结构
下面是两个例子:



注意3-6行,通过栈传参时数据大小按8的倍数对齐,分配局部变量内存时不用。再注意17-20行在读取内存的同时还会将其扩展到适当的大小。
寄存器中的局部存储空间
寄存器组是唯一被所有过程共享的资源,虽然在给定时刻只有一个过程是活动的,我们仍要保证一个过程调用另一个过程时,被调用者不会覆盖调用者稍后要使用的寄存器值。因此,根据惯例,寄存器%rbx,%rbp,$r12~%r15被划分为被调用者保存寄存器,当过程P调用过程Q时,Q必须保存这些寄存器的值,也就是说如果要用到它们,就要用pushq将原始值压入栈中,并在返回前从栈中弹出旧值。
所有其他的寄存器除了%rsp都分类为调用者保存寄存器,这意味着任何函数都能修改它们,所以在调用之前首先保存好这个数据是调用者的责任。
下面是一个例子:

递归过程
递归调用一个函数本身与调用其他函数是一样的,栈提供了这种机制,每次函数调用都有他自己私有的状态信息。栈分配和释放规则自然的就与函数调用和返回顺序匹配。
