编译原理运行时刻环境
运行时存储组织概述
编译程序是将源程序的算法描述部分和数据说明部分,分别翻译成机器目标代码和数据存储单元,最终获得目标程序。
目标程序在目标机环境中运行时,都置身于自己的一个运行时存储空间。在基于操作系统之上运行的情况下,目标程序将在自己的逻辑地址空间内运行并存储数据。编译程序在生成代码时,负责明确各类对象在逻辑地址空间是如何存放的,以及目标代码运行时,如何使用逻辑地址空间。
在编译过程中,源程序的对象地址分配往往是相对于运行存储空间的偏移量,对象访问采用“基地址+偏移量”寻址方式进行,使得可以选择内存的任意可用区域作为目标程序运行时的存储区。这样生成的目标代码称为浮动地址代码
注:
“基地址”是指运行存储空间之首址。
运行存储分配策略
编译器在工作过程中,必须为源程序中出现的一些数据对象分配运行时的存储空间
- 对于那些在编译时刻就可以确定大小的数据对象,可以在编译时刻就为它们分配存储空间,这样的分配策略称为静态存储分配
- 反之,如果不能在编译时完全确定数据对象的大小,就要采用动态存储分配的策略。即在编译时仅产生各种必要的信息,而在运行时刻,再动态地分配数据对象的存储空间
- 栈式存储分配
- 堆式存储分配
- 其中,静态和动态分别对应编译时刻和运行时刻
运行时存储组织的任务和作用
编译程序生成的代码大小通常是固定的,一般存放在专用的区域,即代码区;
目标程序运行过程中,需要创建和访问的数据对象存放在数据区。
程序运行时存储空间的布局
活动记录
使用过程(或函数、方法)作为用户自定义动作的单元的语言,其编译器通常以过程为单位分配存储空间
过程体的每次执行称为该过程的一个活动(activation)
过程每执行一次,就为它分配一块连续存储区,用来管理过程一次执行所需的信息,这块连续存储区称为活动记录(activation record)
活动记录的一般形式
活动记录一般包括以下内容
- 实参
- 返回值
- 控制链:指向调用者的活动记录
- 访问链:用来访问存放于其它活动记录中的非局部数据
- 保存的机器状态
- 局部数据
- 临时变量
栈式存储分配
有些语言使用过程、函数或方法作为用户自定义动作的单元,几乎所有针对这些语言的编译器都把它们的(至少一部分的)运行时刻存储以栈的形式进行管理,称为栈式存储分配
- 当一个过程被调用时,该过程的活动记录被压入栈;当过程结束时,该活动记录被弹出栈
- 这种安排不仅允许活跃时段不交叠的多个过程调用之间共享空间,而且允许以如下方式为一个过程编译代码:
- 非局部变量的相对地址总是固定的
- 过程调用序列无关
活动树
用来描述程序运行期间控制进入和离开各个活动的情况的树称为活动树
- 树中的每个结点对应于一个活动。根结点是启动程序执行的main过程的活动
- 在表示过程p的某个活动的结点上,其子结点对应于被p的这次活动调用的各个过程的活动。按照这些活动被调用的顺序,自左向右地显示它们。一个子结点必须在其右兄弟结点的活动开始之前结束。
控制栈
- 每个活跃的活动都有一个位于控制栈中的活动记录
- 活动树的根的活动记录位于栈底
- 程序控制所在的活动的记录(即当前活动)位于栈顶
- 栈中全部活动记录的序列对应于在活动树中到达当前控制所在的活动结点的路径
设计活动记录的一些原则
在调用者和被调用者之间传递的值一般被放在被调用者的活动记录的开始位置,这样它们可以尽可能地靠近调用者的活动记录
固定长度的项被放置在中间位置:控制连、访问链、机器状态字
在早期不知道大小的项被放置在活动记录的尾部
栈顶指针寄存器top_sp指向活动记录中局部数据开始的位置,以该位置作为基地址
调用序列和返回序列
过程调用和过程返回都需要执行一些代码来管理活动记录栈,保存或恢复机器状态等
- 调用序列:实现过程调用的代码段。为一个活动记录在栈中分配空间,并在此记录的字段中填写信息
- 返回序列:恢复机器状态,使得调用过程能够在调用结束之后继续执行
- 一个调用代码序列中的代码通常被分割到调用过程(调用者)和被调用过程(被调用者)中。返回序列也是如此
调用序列
- 调用者计算实际参数的值
- 调用者将 返回地址(程序计数器的值)放到被调用者的机器状态字段中。将原来的top-sp值放到被调用者的控制链中。然后,增加top-sp的值,使其指向被调用者局部数据开始的位置
- 被调用者保存寄存器值和其它状态信息
- 被调用者初始化其局部数据并开始执行
返回序列
- 被调用者将返回值放到与参数相邻的位置
- 使用机器状态字段中的信息,被调用者将恢复top-sp和其它寄存器,然后跳转到由调用者放在机器状态字段中的返回地址
- 尽管top-sp已经被减小(已恢复),但调用者仍然知道返回值相对于当前top-sp值的位置(位于下一个活动记录中,虽然此时已经弹出,但数据仍然有效)。因此,调用者可以使用那个返回值
变长数据的存储分配
在现代程序设计语言中,在编译时刻不能确定大小的对象将被分配在堆区。但是,如果它们是过程的局部对象,也可以将它们分配在运行时刻栈中。尽量将对象放置在栈区的原因:可以避免对它们的空间进行垃圾回收,也就减少了相应的开销
只有一个数据对象局部于某个过程,且当此过程结束时它变得不可访问,才可以使用栈为这个对象分配空间
非局部数据的访问
一个过程除了可以使用过程自身定义的局部数据以外,还可以使用过程外定义的非局部数据
语言可以分为两种类型
- 支持过程嵌套声明的语言
- 可以在一个过程中声明另一个过程
- 一个过程除自身定义的局部数据和全局定义的数据以外,还可以使用外围过程中声明的对象
例: Pascal
- 不支持过程嵌套声明的语言
- 不可以在一个过程中声明另一个过程
- 过程中使用的数据要么是自身定义的局部数据,要么是在所有过程之外定义的全局数据
例:C
无过程嵌套声明时的数据访问
变量的存储分配和访问
- 全局变量被分配在静态区,使用静态确定的地址访问它们
- 其它变量一定是栈顶活动的局部变量。可以通过运行时刻栈的top_sp指针访问它们
有过程嵌套声明时的数据访问
嵌套深度
- 过程的嵌套深度
- 不内嵌在任何其它过程中的过程,设其嵌套深度为1
- 如果一个过程p在一个嵌套深度为i的过程中定义,则设定p的嵌套深度为i +1
- 变量的嵌套深度
- 将变量声明所在过程的嵌套深度作为该变量的嵌套深度
- 将变量声明所在过程的嵌套深度作为该变量的嵌套深度
访问链(Access Links)
静态作用域规则:只要过程b的声明嵌套在过程a的声明中,过程b就可以访问过程a中声明的对象
可以在相互嵌套的过程的活动记录之间建立一种称为访问链(Access link)的指针,使得内嵌的过程可以访问外层过程中声明的对象
- 如果过程b在源代码中直接嵌套在过程a中(b的嵌套深度比a的嵌套深度多1),那么b的任何活动中的访问链都指向最近的a的活动
访问链的建立
建立访问链的代码属于调用序列的一部分
假设嵌套深度为nx的过程x调用嵌套深度ny为的过程y(x->y)
- nx < ny的情况(外层调用内层)
- y一定是直接在x中定义的(例如:s->q, q->p),因此ny=nx+1,
- 在调用代码序列中增加一个步骤:在的y访问链中放置一个指向x的活动记录的指针
- nx = ny的情况(本层调用本层)
- 递归调用(例如:q->p)
- 被调用者的活动记录的访问链与调用者的活动记录的访问链是相同的,可以直接复制
- nx > ny的情况(内层调用外层,如:p->e )
- 过程x必定嵌套在某个过程z中,而z中直接定义了过程y
- 从x的活动记录开始,沿着访问链经过nx-ny+1步就可以找到离栈顶最近的z的活动记录。y的访问链必须指向z的这个活动记录
堆式存储分配
堆式存储分配是把连续存储区域分成块,当活动记录或其它对象需要时就分配
块的释放可以按任意次序进行,所以经过一段时间后,对可能包含交错的正在使用和已经释放的区域
申请
设当前自由块总长为M,欲申请长度为n
- 如果存在若干个长度大于或等于n的存储块,可按以下策略之一进行存储分配
- 取长度m满足需求的第1个自由块,将长度为m-n的剩余部分仍放在自由链中
- 取长度m满足需求的最小的自由块
- 取长度m满足需求的最大的自由块
- 如果不存在长度大于或等于n的存储块
- 如果M>=n,将自由块在堆中进行移位和重组(对各有关部分都需作相应的修改,是一件十分复杂和困难的工作)
- 如果M<n,则应采用更复杂的策略来解决堆的管理问题
释放
只需将被释放的存储块作为新的自由块插入自由链中,并删除已占块记录表中相应的记录即可
小结
为实现堆式存储管理,须完成大量的辅助操作。如排序、查表、填表、插入、删除、…。其空间和时间的开销较大
参考资料
《编译原理》 第二版