编译原理之运行环境
之前我们讨论的内容主要是编译程序的前端,研究分析了编译程序对原语言静态分析的各个阶段的原理,方法和技术。对源程序进行词法分析,生成属性字流,接着进行语法分析,最后进行语义分析生成中间目标代码,而生成的目标程序能否正常运行与支持目标程序的运行时环境密切相关。本次我们就讨论运行环境。
1.程序运行时的存储组织与分配
1.1存储组织
我们在程序执行时要解决程序存储组织的问题,搞清楚运行中的程序信息是如何进行存储和访问的。
我们知道,在程序执行过程中,程序中数据的存取是通过对应的存储单元来进行的。
我们现有的高级编程语言拥有基于高级语言的编译程序,这样我们在编写程序的时候不必直接和内存地址打交道,在程序中使用存储单元通过标识符来表示,而标识符对应的存储地址是由编译程序在编译时或者生成目标程序运行时进行分配的。
什么是数据空间的分配?实际上就是将源程序中的名字与相应的存储位置关联起来,这种关联具有两种属性(环境【表示名字到存储位置的映射函数】和状态【表示存储位置到值的映射】)
一般而言,编译器从OS申请到一块存储区,需要设计的保存对象包括:
- 生成的目标代码
- 数据对象(用户定义的各种类型数据对象,中间结果,临时参数等)
- 记录过程活动的控制栈
一般产生的目标代码所占的空间在编译时可以确定,所以编译器会把目标代码放在静态确定的区域(一般是内存低地址区)
作为运行时存储分配的一个原则就是:尽可能对数据对象进行静态分配,这样可以把数据对象地址直接编译到目标代码中。
1.2 活动记录(activation record)
作为存储分配的基本单元实际上就是一块连续的存储区,存于存放过程或者函数的一次调用执行所需要的信息。
由于需要保存函数的一些信息,因此,一个活动记录只是需要包含以下信息
1.3 存储分配策略
基本所有的程序设计语言都是采用一下三种分配策略之一或混合
- 静态存储分配策略:编译时进行存储分配。适用于不允许递归调用,不允许可变体积的数据结构语言。
- 栈式存储分配策略:属于动态存储分配。实质是活动记录的分配与释放,不断进出栈。
- 堆式存储分配策略:属于动态存储分配。如果存在用户可以自由申请释放存储空间;活动结束,局部变量仍需要保存;被调用过程生存期比调用过程生存期长三种之一的就使用堆式存储分配策略。
2. 静态运行时环境与存储分配
编译时就可以将程序中的名字关联到存储单元,确定其存储位置。
根据名字的类型,编译器可以确定该名字所需要的存储空间。
使用静态存储分配语言是有限制的。
- 数据对象的长度和它在内存中的位置的限制必须在编译时知道
- 不允许递归过程。
- 数据结构不能动态建立,因为没有运行式的存储分配机制。
3.基于栈的运行时环境的动态存储分配
当调用一个过程时,需要的数据空间就分配在栈顶,过程结束时就释放这部分空间。
4.基于堆的运行时环境的动态存储分配
如果程序设计语言允许用户动态的申请和释放存储空间,且申请和释放之间不是遵循“后申请先释放,先申请后释放”的原则。可以在任何时间以内以任何顺序来进行,则栈式动态存储分配策略显然不适用,通常采用堆式动态分配策略。
基本思想是:假设程序有一个大的空闲存储区,称为堆。每当程序提出申请时,就按某种分配原则在堆的可使用区中,寻找一块能满足需求的存储空间分配给它。而对于释放操作,则是程序将不再占用的存储空间归还给堆,使之变成空闲区。
最后在进行堆式存储分配的时候要留意悬空引用。