700 likes | 841 Vues
编译原理. 主讲 : 肖秀春 E-Mail: xiaoxxc @ sohu.com. 第六章 运行时的存储空间. 6.1 运行时的存储空间结构. 6.2 运行时的存储空间分配. 6.3 运行时的过程活动记录与栈区的组织结构. 6.4 运行时的变量访问. 6.5 非正常出口和形式过程语句. 6.6 分程序记录和动态数组空间. 本章主要内容 (Chapter 6). 6.1 运行时的存储空间结构. 1 ) 概述. 程序设计语言已经经历了四代,正向第五代迈进。. 程序设计语言的发展要求使用更先进的运行时的存储技术。.
E N D
编译原理 主讲:肖秀春 E-Mail: xiaoxxc@sohu.com
6.1 运行时的存储空间结构 6.2 运行时的存储空间分配 6.3 运行时的过程活动记录与栈区的组织结构 6.4 运行时的变量访问 6.5 非正常出口和形式过程语句 6.6 分程序记录和动态数组空间 本章主要内容(Chapter 6)
6.1 运行时的存储空间结构 1) 概述 • 程序设计语言已经经历了四代,正向第五代迈进。 • 程序设计语言的发展要求使用更先进的运行时的存储技术。 • 早期的程序设计语言存储分配都是静态的,后来发展的语言引入了栈和堆的机制,允许变量的存储单元地址是动态分配的。现代计算机程序设计语言几乎都允许上述先进的存储分配机制。 • 存储空间静态分配是指所有变量的存储单元地址在程序运行期间都是固定的。 • 采用栈式和堆式存储分配机制允许变量的存储单元地址是变量的,即变量的存储空间是动态的。
6.1 运行时的存储空间结构 1) 概述(续) • 前述提到的栈和堆是不同的概念,栈是一种先进后出的存储结构;而堆则允许对其存储空间进行动态申请与回收。 • 栈主要应用于保存过程或函数调用与返回时的环境;堆则应用于需要动态申请和释放存储空间的场合。 • 先进的存储分配机制与操作系统的发展是密切相关的。
←最大地址 堆区空间 ↓ ↑ 栈区空间 ←最小地址 库代码空间 静态区空间 目标代码空间 6.1 运行时的存储空间结构 2) 运行时的存储空间结构 • 目标代码运行时,存储器里一般包含如下内容:目标代码;程序库代码;控制信息;有用数据表;常数值和变量值。 • 程序运行时的一种存储空间结构可能为:
6.2 运行时的存储空间分配 1)静态区的存储分配 • 静态存储分配是最简单的一种分配方法,其缺点在于:造成存储空间的浪费;无法满足某些程序设计理念。 • 早期的程序设计语言中,所有的存储分配都是静态的,即数据对象在存储单元地址在程序运行期间是固定的。 • 适宜静态存储分配的对象有:全程变量;其值不改变的对象;某些信息表。 • 静态存储分配的特点是:访问其地址可以采用绝对地址。
6.2 运行时的存储空间分配 1)静态区的存储分配(续) • 程序编译时,会构造一些信息表,这些表在程序运行时可能还要用到,因此把这些信息表保存到程序的静态区中。 • 在程序编译时,编译程序会为上述信息表分配存储空间,但由于此时无法确定各个信息表的长度,因此,信息表之间可能有很多的空白和无用的表。 • 尽管变量的访问地址可以采用绝对地址,但这并不意味在编译阶段,一开始就用绝对地址。因为这样做不利于删除信息表之间的空白和无用的表所占用的存储空间。
编译程序 源程序 目标程序 信息表L1 信息表L2 信息表Ln 编译时的形式 6.2 运行时的存储空间分配 1)静态区的存储分配(续) • 为了方便删除信息表之间的空白和无用的表,一般采用块地址法来表示各存储对象的地址(dataArea,off)。
编译程序 目标代码 信息表L1 源程序 静态数据区 信息表L2 目标程序 ...... 信息表L1 信息表Ln 信息表L2 信息表Ln 编译后的形式 编译时的形式 6.2 运行时的存储空间分配 1)静态区的存储分配(续) • 如果采用块地址法表示存储对象的地址,则通过下面的方法很容易实现将块地址转化为绝对地址: • 假设静态区的首地址为add1,表Lk的长度为aK,则表Lk编译后的首地址为: 表Lk编译后的首地址=表Lk-1编译后的首地址+ak-1(2≤k≤n)
6.2 运行时的存储空间分配 2)栈区的存储分配 • 有递归调用的过程或函数中变量的存储分配面临的问题: 如果把有递归调用的过程或函数中的变量按静态区分配,则程序将无法运行得到正确的结果。 可以举例说明这一点: function f (a:integer):integer; var x:integer; begin x:=a; if x=0 then f:=1 else f:=f(x-1)*x end 结论:上述例子表明,对于有递归调用的过程或函数中的变量来说,有可能需要保存每次递归调用时变量的值。 这说明,存在递归调用时,过程中每个变量需要多个存储单元。
6.2 运行时的存储空间分配 2)栈区的存储分配(续) • 栈式存储分配法可以有效解决一个变量分配多个存储单元的问题 • 栈式存储分配法具体解决原理是: 过程每一次调用,就给过程中声明的所有变量分配一次新单元,而当过程返回时,释放该过程最后一次分配的存储单元。
6.2 运行时的存储空间分配 2)栈区的存储分配(续) • 栈式存储分配法存在一个问题:如何确定被调用过程所需的空间长度?这是一个比较复杂的问题,原因在于: 1. 在过程中可能有可变长类型变量,这时只能在代码运行阶段计算出所需长度; 2. 在过程所需空间里包括临时变量,而临时变量需要在目标代码生成阶段才能计算出来; • 栈区的主要分配对象:1. 过程的形参和局部变量;2. 临时变量
6.2 运行时的存储空间分配 2)栈区的存储分配(续) • 过程的形参和局部变量在栈区分配的空间长度可在语义分析时确定;而临时变量分配的空间长度在目标代码生成阶段可以计算出来 • 过程的形参和局部变量在栈区中地址可以采用形式地址表示,即(Level,Off),由形式地址可以很方便地生成汇编代码Off[sp],其中sp寄存器中为当前过程空间块的开始地址。这样CPU将采用变址方式访问sp指向的过程空间的第Off单元。
Offsetx X=0 f(0)调用分配的栈区空间 Offsetx X=1 f(1)调用分配的栈区空间 Offsetx X=2 f(2)调用分配的栈区空间 6.2 运行时的存储空间分配 2)栈区的存储分配(续) • 以前面的例子来说明栈区存储分配策略: 要计算f(2),显然f(2) f(1)*2 f(0)*1*2 1*1*2 2 即要调用3次f函数,因此要为f函数中的各存储对象分配三次空间,如下图所示:
6.2 运行时的存储空间分配 2)栈区的存储分配(续) • 栈式分配法解决了过程的递归调用问题,同时解决了空间共享问题,因为当过程返回时立即释放该过程最后一次分配的存储单元。因此,栈区空间可供多个过程所共享。 • 栈式分配法也存在一些无法解决的问题,主要有下列情形: 1. 过程中变量的寿命大于过程本身时; 2. 过程中需要动态申请存储空间的变量; 3. 过程中可被多个过程共享的存储空间; • 上述情形说明栈式分配还不能完全满足现代程序设计语言对存储分配的要求。
6.2 运行时的存储空间分配 3)堆区的存储分配 • 栈式分配法存在一些缺陷: 1.对过程中所有变量统一分配;有些局部变量即使是早已无用,也要占用到过程结束为止。 2.对过程中所有变量统一释放;有些局部变量存储空间不能随着过程的结束而释放,但却释放了。 • 堆式分配法可以解决上述问题,其特点是: 1. 随时分配存储空间; 2. 随时释放存储空间。 因此堆式分配法是适应范围最大的分配方法;也是时间代价最高的存储分配方法。
6.2 运行时的存储空间分配 3)堆区的存储分配(续) • 堆区空间主要用于存放动态申请空间变量的值,即凡是动态申请的空间都分配到堆区中。 • 对于Pascal语言来说,可以用new语句来动态申请存储空间,用dispose来释放动态申请的存储空间(注:静态空间不能释放)。 例:var p:↑ tp; ... new(p); ... dispose(p); ... 上述new语句实际执行操作:在变量p的单元中送入所申请空间的首地址,申请空间的长度由tp来确定。
6.2 运行时的存储空间分配 3)堆区的存储分配(续) • new(p)不防理解成:p:=new(n),其中new语句在堆区中寻找长度为n的空闲存储块,并将其首地址赋给变量p。 • 堆区的分配尽管消耗时间,但易于实现,但其回收则不简单,而且要付出巨大时间代价。经过反复多次分配和释放,堆区空间将呈现下面形式:(其中着色部分为占用块,空白部分为空闲块)
6.2 运行时的存储空间分配 3)堆区的存储分配(续) • 堆区空间的释放,可有显式释放、隐式释放两种 • 显式释放 显式释放方法实现起来比较简单,许多语言里有释放堆区空间的语句,比如Pascal语言里的dipose(p),表示要释放p所指向的堆片,由于p中有被回收区的开始地址,而且其长度可收p的定义类型静态确定。 • 隐式释放 隐式释放方法实现起来比较复杂,它需要系统和目标代码合作自动寻找没有用的堆片。如果某堆片没有指向它的指针,则该堆片是可以释放的。 隐式释放方法的关健是:对于每个堆片,怎么有效判断有无指向指向它。
6.2 运行时的存储空间分配 3)堆区的存储分配(续) • 隐式释放 单指针堆片释放法 适应范围:如果堆片至多有一个指针指向它 实现原理:如果p是一个指针变量,则当p的指针值改变时,回收对应p的堆片。要做到判别指针值是否改变,可以由目标代码来确定。 如何确定指针变量p是单指针变量?--指针变量的指针值如果没有出现在赋值语句的左部,则该指针变量为单指针变量。
6.2 运行时的存储空间分配 3)堆区的存储分配(续) • 隐式释放 计数堆片释放法 适应范围:适应多指针变量 实现原理:对第一个堆片,引入一个引用计数: 1. 当执行new(p)时,令对应p的堆片的引用计数为1; 2. 当执行R:=Q时,令对应R的堆片的引用计数减1,而对应Q的堆片加1; 3. 当堆片的引用计数为0时,释放该堆片;
6.2 运行时的存储空间分配 3)堆区的存储分配(续) • 隐式释放 标记堆片释放法 适应范围:适应多指针变量 实现原理:用标记来指示是否为无用堆片; 1. 找出所有指向堆片的指针(这些指针称为活指针); 2. 把所有堆片标记为无用堆片; 3. 凡是活指针可到达的堆片标记为有用堆片; 4. 把所有无用堆片收集起来,即释放这些存储空间;
6.2 运行时的存储空间分配 4)堆区的空间管理 • 堆管理器主要负责堆区空间的分配和释放,其分配和释放可以有不同的算法 • 在分配时,如果有多个空闲堆片适合于新的堆片的分配,则有如下几种方案供选择: 1. 找到第一个这样的空闲片,分配后出现最小的剩余片; 2. 从头开始找到第一个这样的空闲片,大小够用即可; 3. 从上次分配余下地方开始找大小够用的空闲片; • 上述方案各有优缺点: 1. 可产生小的碎片,但其代价高,效率低; 2. 容易造成小碎片堆积在前面,代价也较高; 3. 可克服1,2中的缺陷;
AddrP P 6.2 运行时的存储空间分配 4)堆区的空间管理(续) • 堆片的一种结构 1. 输入指针表表示本堆片的指针变量地址表 2. 输出指针表表示本堆片所指向的所有堆片的地址表
6.2 运行时的存储空间分配 4)堆区的空间管理(续) • 对堆区的管理可以用目标代码和堆区管理系统联合完成 1. 当执行new(p)的目标代码时: a. 按|p|的长度申请一新堆片HBi; b. 设置堆片长度:HBi.Length:=|p|; c. 设置堆片输入表:HBi.Inlist:=[addr(p)]; d. 设置堆片输出表:HBi.Outlist:=[ ]; 2. 当执行Q:=R的目标代码时: a. 把Q单元地址addr(Q)加到R堆片输入指针表中: HBR.Inlist:=Insert(HBR.Inlist,addr(Q)); b. 从Q堆片的输入指针表中去掉addr(Q)地址: HBQ.Inlist:=Delete(HBQ.Inlist,addr(Q));
6.2 运行时的存储空间分配 4)堆区的空间管理(续) • 对堆区的管理可以用目标代码和堆区管理系统联合完成 3. 当结束一个过程时: 调用堆区管理程序,它将逐个检查HBi.Inlist,如果其中的一个地址d大于arp地址,则从HBi.Inlist删除地址d,重复此过程,直到收敛。此时输入表为空的堆片将回收。其中的arp为过程的首地址。
6.2 运行时的存储空间分配 4)堆区的空间管理(续) • 对堆区的管理还需要在收集堆片的同时对堆片进行压缩 1. 堆片需要移动的压缩可以获得更大的存储空间 2. 而边界融合方法是一种有效的方法
6.3 运行时的过程活动记录与栈区的组织结构 1)过程活动记录(Activation Record) • 前面介绍了静态区、栈区、堆区的分配和释放问题,这些工作并不需要程序设计者来做,全都交由编译器完成。但程序设计者需要各种分配方法的原理及其利弊,以便编程时选用何种分配方法。 • 栈区存储分配方法能够有效地解决过程递归调用问题,事实上,我们把过程调用所需的存储空间(如保存机器状态、保存过程中的局部变量等)都分配到栈区中(不管过程是否递归)。 • 栈区是由过程的活动记录组成的。 • 那么 a. 栈区中过程活动记录的结构是什么? b. 栈区中过程活动记录的内容是什么? c. 栈区中过程活动记录与过程调用是什么关系? d. 栈区中过程活动记录是如何连接起来的?
6.3 运行时的过程活动记录与栈区的组织结构 1)过程活动记录(续) • 过程的每个活动记录也就是过程的一个现场记录。 • 一个活动记录中存放应该包含以下几部分: a. 过程控制信息:返回地址、先行活动记录动态链指针、层数、长度等。 b. 机器状态信息:包括寄存器状态等过程中断时的机器状态。 c. 全局变量信息:包括有关访问非局部变量的信息 d. 局部变量信息:包括形式参数、局部变量、临时变量信息
临时变量区 本层变量 和返回值 局部变量区 形参变量区 返回值 全局变量环境 全局变量信息 机器状态 机器状态信息 过程层数 控制状态信息 返回地址 动态链指针 sp 6.3 运行时的过程活动记录与栈区的组织结构 1)过程活动记录(续) • 下面是活动记录AR的一种可能的结构(其中sp中为当前AR的始地址):
6.3 运行时的过程活动记录与栈区的组织结构 1)过程活动记录(续) • 当调用过程时,过程的目标代码会先在栈区中建立自己的活动记录,然后在其中填写控制信息(包括返回地址、动态链指针等)、机器状态、全局变量、实参、局部变量、临时变量等信息。 • 当过程返回时,将释放当前AR在栈区的存储空间,AR中的动态链指针将赋值给sp,用来指示过程返回后新的当前过程存储区的始地址。 • 图中过程的层数指相应过程的层数,它也是一个重要的数据,主要用来非正常出口时情形。 • 图中过程的长度M指相应过程在栈区需要的存储单元数,也很重要,它用于计算为过程分配栈区空间后,栈区的栈顶(top=sp+M),即栈区的第一自由单元地址;也用来判断栈区与堆区是否越界。
6.3 运行时的过程活动记录与栈区的组织结构 1)过程活动记录(续) • 当调用过程时,过程的目标代码将完成下列工作: a. 实参单元送入NewAR的形参单元中; b. 将控制信息(如返回地址/层数/AR长度/动态链地址等)填入NewAR的相应单元中; c. 将机器状态信息(如寄存器信息)填入NewAR的相应单元中; d. 将全局变量信息填入NewAR相应单元中; e. 将sp:=sp+CurrentAR.size; f.CPU过程的其它代码;
top Q的活动记录 sp P的活动记录 ... 静态区 6.3 运行时的过程活动记录与栈区的组织结构 2)动态链(Dynamic Chain) • 当过程Q返回时,需要释放Q的AR,并把寄存器sp的内容修改回调用Q之前的P的AR。
Q的活动记录存储区 P的AR始地址 sp P的活动记录存储区 R的AR始地址 R的活动记录存储区 主程序始地址 R的活动记录存储区 nil 6.3 运行时的过程活动记录与栈区的组织结构 2)动态链(Dynamic Chain) • 为了将sp修改回过程Q调用之前P的AR的始地址,需要保存P的AR始地址到Q的活动记录中。即存在如下图形式的动态链:
AR0 AR1 AR2 AR3 AR4 sp 6.3 运行时的过程活动记录与栈区的组织结构 2)动态链(Dynamic Chain) • 为了定义动态链的概念,不防先定义调用链: 调用链是过程名序列,第一个过程名为主程序M(把主程序看作过程),之后为过程/函数名: (M)是调用链,若(M,...,R)是调用链,并且R调用了S,则(M,...,R,S)也是调用链。 我们把(M,...,R,S)记为CallChain(S)。 假设有调用链CallChain(S)=(M,...,R,S),则意味着正在执行S过程体,而M,...R是执行S前被中断了的过程。此时,栈区中的内容是: [AR(M),...,AR(R),AR(S)] 我们称[AR(M),...,AR(R),AR(S)]为CallChain(S)的调 用链
6.3 运行时的过程活动记录与栈区的组织结构 2)动态链(Dynamic Chain) • 每当一个过程被调用或返回时,动态链将发生变化,而每当动态链发生变化时,必须调整指针sp的内容,以反映栈区空间的现状。 • 下面是过程Q调用时所完成的工作: • NewAR.DynaChainPointer:=sp; • NewAR.Return:=ReturnAddr; • NewAR.Level:=Level(Q); • NewAR.Size:=Size(Q); • NewAR.Machine:=(R1,R2,...,Rn); • ...... • sp:=sp+CurrentAR.Size; • 转向Q子程序
6.3 运行时的过程活动记录与栈区的组织结构 2)动态链(Dynamic Chain) • 下面是过程Q返回时所完成的工作(非特殊情况): • (R1,R2,...,Rn):=CurrentAR.Machine; • Value:=CurrentAR.ReturnValue; • ...... • sp:=CurrentAR.DynaChainPointer; • 按CurrentAR.return返回
6.3 运行时的过程活动记录与栈区的组织结构 3)活跃活动记录 • 假设当前调用链是(M,P1,P2,Q1,R1,R2,R3),则当前动态AR链为: [AR(M),AR(P1),AR(P2),AR(Q1),AR(R1),AR(R2),AR(R3)] 其中Pi表示P的第i次调用 则我们定义过程S的活跃活动记录为:过程S的最新活动记录。 即: LiveAR(M)=AR(M) LiveAR(P)=AR(P2) LiveAR(Q)=AR(Q1) LiveAR(R)=AR(R3)
主程序 主程序 过程P 过程Q 6.4 运行时的变量访问 1)变量的访问环境 • 当前过程的局部变量区是栈区中的当前AR区,但当前过程可能访问过程外的变量,过程如何访问过程外的变量区(即过程的非局部区)? • 非局部区肯定在动态AR链的活跃AR区中,但要找到过程中每个非局部变量在哪个AR中,这不是容易的事情。
6.4 运行时的变量访问 1)变量的访问环境(续) • 过程声明链(DeclaChain):每个DeclaChain是过程名序列,主程序(M)是过程声明链,若(M,...,P)是过程声明链,且P中有过程Q的声明,则(M,...,P,Q)是过程声明链。 显然,对于任意一个Q,DeclaChain(Q),是唯一的;并且,Q中出现的变量声明一定在DeclaChain(Q)中。从而,Q的变量存储区是: [LiveAR(M),...,LiveAR(P),LiveAR(Q)] 这就是过程Q的变量访问环境。
6.4 运行时的变量访问 1)变量的访问环境(续) • 假设对于Q有VarVisitEnv(LiveAR(Q))=[LiveAR(M),...,LiveAR(P),LiveAR(Q)],Q中访问了变量XQ,YM,ZP,X,Y,Z分别在过程Q,M,P中声明,则这些变量的存储单元地址为: addr(XQ)=<LiveAR(Q)>+OffsetX addr(YM)=<LiveAR(M)>+OffsetY addr(ZP)=<LiveAR(P)>+OffsetZ 上述事实表明,变量的存储单元地址与VarVisitEnv(LiveAR(Q))密切相关。
6.4 运行时的变量访问 1)变量的访问环境(续) • 过程Q可访问的变量一定在VarVisitEnv(LiveAR(Q))环境中,但并不是说这一环境中的任意变量过程Q都可以访问。 • 过程Q可访问的变量无疑在:嵌套了过程Q的所有外层过程的活跃活动记录中。--这意味着我们需要掌握两方面的信息,才能求出过程Q的外层过程的活跃活动记录,即: 1. 过程Q的声明链 2. 过程Q的声明链中每个过程的活跃活动记录 注:过程Q可访问的变量的AR总数为:过程Q的层数+1 • 事实上,上述两方面的信息都可以从VarVisitEnv(LiveAR(Q))中获取。
6.4 运行时的变量访问 1)变量的访问环境(续) • 那么如何由VarVisitEnv(LiveAR(Q))获取前述两个方面的信息? • 前述第2个问题很容易解决。下面介绍如何解决第1个问题。 • 第1个问题,即:如何获取声明链? • 重要结论:若(M,...,P,Q)是Q的调用链,且Q的层数为N,则有: DeclaChain(Q)=DeclaChain(P)N⊙Q 上述结论可以通过验证P207图6.4.1.1
6.4 运行时的变量访问 1)变量的访问环境(续) • 类似结论:若[AR(M),...,AR(Pi),AR(Qj)]是Q的动态AR链,且Q的层数为m,则有: VarVisitEnv(AR(Qj))=VarVisitEnv (AR(Pi))m⊙AR(Qj) 例:P208
6.4 运行时的变量访问 1)变量的访问环境(续) • 主程序的变量的处理方法有两种:1. 将它们放到静态区中; 2. 将它们放到栈区中。 • 为统一起见,我们采用第2种方法。
6.4 运行时的变量访问 1)变量的访问环境(续) • 变量访问环境的实现方法有多种,常见到的有: 1. 局部Display表方法 2. 全局Display表方法 3. 静态链方法 4. 寄存器方法 其中,Display表方法是用表结构来表示变量访问环境,静态链方法是链表来表示变量访问环境,而寄存器方法则是用寄存器来表示变量访问环境。 具体采用哪种方法,取决于机器条件:如果寄存器较多,则用4最好;如果寄存器极少,用1,2是合适的;如机器能提供较好的间接操作,则用3是可选的。
6.4 运行时的变量访问 1)变量的访问环境(续) • 从技术角度来讲,寄存器方法和全局Display表方法属于同一类,而静态表方法和局部Display表方法属于同一类。 • 提高目标代码运行速度的关健之一是提高变量的访问速度,因此很多编译器都想方设法提高非局部量的访问速度。
...... Display表 ...... 动态链指针 sp 6.4 运行时的变量访问 2)局部Display表方法 • 局部Display表方法中“局部”的意思是:每个AR都有自己的变量访问环境;“Display表”的意思是:变量访问环境是以地址表的形式保存在AR中的。
...... AddrARQ1 Q的 AddrARP2 Display 表 Q1,第2层 AddrARM1 ...... 主程序M(调用P) 动态链指针(AddrARP2) 主程序 AddrARQ1 过程P(调用P,Q) ...... 过程Q Display表 P2,第1层 ...... 动态链指针(AddrARP1) AddrARP2 ...... Display表 P1,第1层 ...... 动态链指针(AddrARM1) AddrARP1 ...... Display表 M1,第0层 ...... nil AddrARM1 6.4 运行时的变量访问 2)局部Display表方法(续) • 当前过程P的Display表的长度(即地址个数)为:P的层数+1。 某次运行调用过程链为:(M1,P1,P2,Q1)