1.活动记录

​ 活动记录(activity record)为函数、过程在运行时,栈上为了维护运行时基址、局部变量等信息所保存的数据结构。

1.1.活动记录组成与安排

​ 活动记录的组成:

  1. 返回值
  2. 实在参数
  3. 可选的访问链:即静态链,如果有过程嵌套,那么访问链指向定义本过程的父过程。
  4. 返回地址
  5. 动态链:即控制链,该单元存放着调用者bp,并且本单元的地址为本活动记录的bp,即函数调用开头将bp入栈后所在单元。
  6. 可选机器状态
  7. 局部数据
  8. 临时区

1.2.栈式分配下过程调用、返回

1.2.1.过程调用

​ 过程调用指的是分配被调过程的AR并填入相关信息,然后程序控制转移到被调过程入口的操作

​ 如果过程A调用B,那么A准备工作包括:

  1. 如果B有返回值,分配空间给返回值,即减小esp,分配对应大小空间。如果没有省略这一步。
  2. 计算实参,并放入B的AR(push)。
  3. 将返回地址放入B的AR,并跳转到B过程的入口,开始执行B(call完成)。

​ 开始执行B后,B的入口代码完成如下操作:

  1. 在B自己的AR中保存A的活动记录的基址(即当前bp,对应操作push bp)并且将这个位置作为B自己AR的基址(对应操作mov sp,bp,即 bp<-sp)。
  2. 保存可选的机器状态,例如esi,edi寄存器。
  3. 为局部变量分配空间,sp <- sp- local_size,可能要进一步对齐,提高访问速度。
  4. B的AR建好,开始执行。

1.2.2.过程返回

​ B的出口代码完成如下操作:

  1. 回收局部变量区域,这一步有很多方法完成,例如lea指令,或者增加sp。
  2. 恢复保存的机器状态,例如使用pop恢复esi、edi。
  3. 完成1、2后,sp在B的bp位置;接下来恢复A的AR基址,方法为:mov bp,sp; pop bp; ret ,等价于leave; ret

​ 回到A执行后,A的调用善后代码完成如下操作:

  1. A取回返回值以及引用型参数(如果有的话)
  2. A调整栈顶sp,回收在准备阶段分配给返回值、实际参数的空间,主要根据参数个数以及可能有的访问链和可能有的返回值以及B执行时的对齐方式来调整。

2.C语言过程调用、返回注意事项

2.1.调用者准备阶段代码

​ 在调用下一个函数时,由调用者减小esp从而分配空间给返回值用(如果函数有返回值,没有无需分配空间给返回值),然后通过push将参数放入栈内;参数倒序入栈。另外,C程序不存在访问链(静态链),因此在call指令将返回地址入栈前,没有将可选的访问链压栈这一步。

​ 实例:例如对于函数调用int f(x,y),准备阶段代码为:

subl 4, %esp ;分配空间给返回值
pushl y ;这里真正代码为 pushl offest(%ebp)
pushl x
call f

​ 由于活动记录里ebp所指向位置往高地址依次为返回地址、访问链、参数,因此访问第一个参数一般而言偏移为8

2.2.被调用者入口代码

​ 入口代码要保存ebp、调整ebp、分配空间并按16字节对齐。一般而言代码如下。

push %ebp
movl %esp, %ebp
subl space, %esp ;分配空间
andl $-16, %ebp ;16字节对齐

2.3.被调用者出口代码

​ 出口处代码首先要设置返回值,也可以将返回值放在eax。如果有进入函数时保存的机器状态内容,要将esp回到那里(通过ebp-offset),然后出栈。例如将esi和edi压栈,最后返回前需要使用如下代码恢复机器状态,并设置返回值(假设为0,并且被调用函数没有参数)。

leal -8(%ebp), %esp ;这里是因为保存了edi和esi在栈上
popl %edi
popl %esi
movl $0, 12(%ebp) ;没有参数,因此偏移为12,因为返回值往低地址为访问链、返回地址、ebp...
popl %ebp
ret

; or
movl %ebp, %esp
popl %ebp
ret ;
;
leave
ret

2.4.调用者善后代码

​ 返回后,由调用者把之前分配给返回值、参数的空间回收(addl $number %esp),回收的时候要注意可能函数进行了16字节对齐,因此回收的空间是16的倍数。例如对于2.1中调用int f(x,y),善后阶段代码为:

addl $16, %esp

​ 虽然只分配了12字节空间,但是被调用的函数进行了16字节对齐,因此回收16字节。

2.5.连续数据结构的存放、传参

2.5.1.数组

​ 数组存放为首地址在低处,然后元素向高地址排列。

​ 对于初始化,数组使用预先分配好的段来初始化。

2.5.2.结构体

​ 结构体内如果出现字符串数组,那么会按四字节对齐。并且字符串仍然从低地址开始,也就是说padding位于高地址。

​ 结构体存放顺序与数组类似为首地址在低地址,成员变量从前往后向高地址排列。入栈时逆序入栈,因此先将排在最后(地址最高的)成员入栈。也就是说,如果栈低地址画在下面,那么作为局部变量的结构体成员变量从下往上排列,作为传值参数的结构体成员变量从上往下排列,但是不管是哪种结构体,其中数组、对齐的字符串的内容排列不变(低地址向高地址)。

​ 结构体按值传递时,如果内容较少通过push,内容较多通过串传递指令连续传递。

2.6.其他

  1. 对C语言,默认为小端(低位数字在地址低端(栈顶)),例如栈内从高地址到低地址存放了四字节数据,分别为1,2,3,4,那么按16进制打印的结果为1234。(如果这四字节数组是按四字节对齐的)

过程嵌套

过程嵌套中使用静态访问链访问定义了当前过程的父过程的过程内的变量。

1.嵌套深度

读过过程名,嵌套深度加1,即过程定义语句算在上一层。也因此,过程的参数和局部变量深度是相同的(都位于procedure语句之后)。

定义点深度即包围被定义过程的最内层过程深度,静态链指向父过程的活动记录。引用点深度定义类似。