1、mcy,1,课程内容 第一章 概论 第二章 词法分析 第三章上下文无关文法及分析 第四章自上而下的语法分析 第五章自下而上的语法分析 第六章语义分析 第七章运行时环境 第八章代码生成,mcy,2,第7章 运行时环境(存储空间),当源程序的目标代码被运行时,在内存中不仅有目标代码,而且还要保存各种信息,如必须为源程序中所出现的一些量(常量、变量及某些数组等)分配运行时的存储空间。即编译器要从操作系统得到一块存储区,用于被编译过的目标程序的运行。,mcy,3,存储分配是在运行阶段进行的,但编译程序在编译阶段要为其设计好存储组织形式,并将这种组织形式通过生成的目标代码体现出来。(举例说明:函数调用分
2、析.txt) 目标代码运行时,存储空间的组织称为目标代码的运行时环境。,mcy,4,运行时环境有三个类型:完全静态环境(fully static environment)、基于栈的环境(stack-based environment),以及完全动态环境(fully dynamic environment)。这3种类型的混合形式也是可能的。,mcy,5,7.1 程序执行时的存储器组织,7.2 完全静态的运行时环境,7.3 基于栈的运行时环境,7.4 动态存储器,7.5 参数存储机制,mcy,6,7.1程序执行时的存储器组织,目标代码运行时,操作系统为目标代码的运行分配的存储空间按用途可划分为下面
3、几个部分:,mcy,7,代码区域:目标代码的存储区域,由于代码区在执行之前是固定的,所以在编译时所有目标代码的地址都是可计算的; 全程/静态区域:静态数据区用来存放那些具有绝对地址的数据和变量(如静态变量和全程变量);编译器可以确定其所占用存储空间的大小; 栈区:在运行时分配存储空间的数据就分配在栈区;编译器知道存在栈中的具体数据大小和存活时间; 堆区:供用户动态申请存储空间,编译器不需要知道究竟得从heap中分配多少空间,也不需要知道从heap上分配的空间究竟需要存在多久。 由于栈区和堆区的长度会随着目标代码的运行而变化,因此把它们分配在数据区的两端。一般情况下,栈向下长,堆向上长,可以使栈
4、和堆共用一空白存储空间。,mcy,8,在PASCAL,C语言中,通常采用以过程为单位的动态存储分配方案: 当一过程(函数)被调用时,就在栈顶为该过程分配所需的数据空间(过程活动记录),当一个过程工作完毕返回时,它在栈顶的数据空间(过程活动记录)也即释放。 过程的活动记录(activation record,AR)是一段连续的存储区,用于存放过程的一次执行所需要的信息,当调用或激活过程或函数时,必须为被调用过程的活动记录分配空间。 活动记录存放的信息至少应包括以下几个部分:,mcy,9,存放主调过程为被调过程提供的实参信息;,存放目标程序临时变 量的值;,存放本次执行中的局部数据,用于指向主调过
5、程的活动记 录的控制链和返回地址;,mcy,10,b:2 a:1,该函数调用结束时的返回地址(00401353),主调函数的控制链,C语言所调用函数的活动记录示例(函数调用分析中的举例),c:3,栈底,栈顶,mcy,11,第7章 运行时环境,7.1 程序执行时的存储器组织,7.2 完全静态的运行时环境,7.3 基于栈的运行时环境,7.4 动态存储器,7.5 参数存储机制,mcy,12,7.2 完全静态的运行时环境,在完全静态环境中,不仅全局变量,所有的变量都是静态分配,即整个程序所需数据空间的总量在编译时是完全确定的,从而每个数据名的地址就可静态地进行分配,适于静态分配的语言,要求满足的条件是
6、: 每个数据名所需的存储空间的大小都是常量 不允许采用动态的数据结构,即在程序运行过程中申请或释放的数据结构 过程不可递归调用,mcy,13,整个程序存储器如下所示:,mcy,14,程序所需的数据空间在程序运行前就可确定,称为_管理技术。 静态存储 动态存储 栈式存储 堆式存储,mcy,15,静态存储分配允许程序出现_。 递归过程 可变体积的数据项目 静态变量 待定性质的名字,mcy,16,第7章 运行时环境,7.1 程序执行时的存储器组织,7.2 完全静态的运行时环境,7.3 基于栈的运行时环境,7.4 动态存储器,7.5 参数存储机制,mcy,17,7.3 基于栈的运行时环境,7.3.1没
7、有局部过程的基于栈的环境,在一个所有过程都是全局的、过程定义不允许嵌套,但允许过程的递归调用的程序设计语言(例如C语言)中,基于栈的动态运行时环境有两个指针: sp:栈顶部(top of stack)指针;对于x86系统来说,它采用sp或esp寄存器存储栈顶部的地址;,mcy,18,fp(frame point)指向当前活动记录的控制链的指针,对于x86系统,它采用bp或ebp寄存器存储当前活动记录的控制链的地址,其作用如下: 1.通过该指针可以访问当前执行函数的局部变量; 2.通过该指针可以访问主调程序的活动记录; 3.允许在当前的被调函数执行完毕时,用它来恢复主调函数的活动记录。,mcy,
8、19,b:2 a:1,该函数调用结束时的返回地址(cs:eip) 00401353,主调函数的控制链(main.fp),C语言当前执行函数的活动记录示例(函数调用分析中的举例),c:3,sp,fp,栈底,栈顶,mcy,20,当一个过程被调用时,在栈顶为该过程分配所需的数据空间(过程活动记录)如下:,将实参的值压入在该函数对应的新活动记录中。 将被调函数执行完毕后的返回地址压入在新的活动记录中。 完成到被调用的过程的代码一个转移。 将主调函数的fp作为控制链压入到新的活动记录中。 改变fp以使其指向新的活动记录(将sp复制到该fp中) 将该函数的局部变量和局部临时变量压入到新的活动记录中。,mc
9、y,21,b:2 a:1,C语言当前执行函数的活动记录示例:,mcy,22,当被调函数执行完毕返回时,其对应的活动记录从栈中弹出的过程: 将fp复制到sp中。 将控制链装载到fp中。 完成到返回地址主调函数的一个转移。 改变sp以弹出实参。,mcy,23,例:计算两个非负整数的最大公约数的c代码如下:,#include int x,y; int gcd(int u,int v) if(v= =0) return u;else return gcd(v,u%v) main() scanf(“%d%d”, ,mcy,24,mcy,25,例:考虑下列程序清单的c代码。 int x=2; void g
10、(int); void f(int n) static int x=1;g(n);x- -; void g(int m) int y=m-1; if (y0)f(y);x- -;g(y); ,main() g(x);return 0; ,画出第二次对g调用时,程序的运行时环境:,mcy,26,mcy,27,目标代码的生成必须支持变量和临时变量的实际定位,并增加支持运行时环境所必需的代码。 对名字的访问 如何处理可变长度的问题 局部临时变量 嵌套声明,mcy,28,对名字的访问:,在没有局部过程的基于栈的运行时环境中,所有的非局部的名字都是全局的,因此也就是静态的,都具有一个固定的静态地址,可以
11、被直接访问。 对函数参数和局部变量而言,在大多数的语言中,每个局部声明的偏移量仍是可有编译程序静态地计算出来,因为过程的声明在编译时是固定的,而且为每个声明分配的存储器大小也根据其数据类型而固定。,mcy,29,m:2 返回地址 控制链y:1,fp,mOffset,yOffset,mOffset=+8,yOffset=-4,高端地址,低端地址,mcy,30,例:考虑下面的C过程 Viod f(int x,char c) int a10;double y; 对f调用的活动记录为:,c x返回地址 控制链 a9 a1 a0 y,fp,xOffset,aOffset,cOffset,yOffset,
12、xOffset=+8,cOffset=+12,aOffset=-40,yOffset=-48,现在对ai访问,要求计算地址: (-40+4*i)(fp),mcy,31,目标代码的生成必须支持变量和临时变量的实际定位,并增加支持运行时环境所必需的代码。 对名字的访问 如何处理可变长度的问题 局部临时变量 嵌套声明,mcy,32,处理可变长度的数据,有时编译程序必须处理数据变化的可能性,表现在数据对象的数量和每个对象的大小上。发生在支持基于栈的环境的语言中的两种情况如下: 调用中的自变量的数量可根据调用的不同而不同。 数组参数或局部数组变量的大小可根据调用的不同而不同。,printf(“%d%s%
13、c“, n, prompt, ch);,printf(“Hello, worldn“);,通常,C编译程序一般通过把调用的自变量按相反顺序(in reverse order)压入到运行时栈来处理这一点。,mcy,33,目标代码的生成必须支持变量和临时变量的实际定位,并增加支持运行时环境所必需的代码。 对名字的访问 如何处理可变长度的问题 局部临时变量 嵌套声明,mcy,34,局部临时变量:,考虑C表达式:xi=(i+j)*(i/k+f(j)在这个表达式从左到右的求值计算中,在对f的调用过程中需要保存中间结果: xi的地址、i+j的和、i/k的商。 这些中间值可计算到寄存器中,根据寄存器进行保存
14、和恢复;或者可将它们作为临时变量存储在对f调用之前的运行时栈中。如下所示:,mcy,35, 返回地址 控制链 xi的地址 i+j的结果 i/j的结果,fp,(栈的其余部分),sp,临时栈,调用f(将要创建的)时的 新活动记录,包含表达式过程的活动记录,自由空间,mcy,36,目标代码的生成必须支持变量和临时变量的实际定位,并增加支持运行时环境所必需的代码。 对名字的访问 如何处理可变长度的问题 局部临时变量 嵌套声明,mcy,37,嵌套声明:,嵌套声明也出现了与局部临时变量同样的问题。考虑以下的C代码: Void p(int x,double y) char a;int i;A:double
15、x;int j;B:char *a;int k; ,mcy,38,一个简单的处理办法是按照与临时表达式相类似的办法 在嵌套的块中处理声明,并在进入块时在栈中分配它们。,x: y:返回地址控制链 a: i: x: j:,fp,(栈的其余部分),sp,块A的分配区,调用p时的活动记录,自由空间,当进入块A后,运行时栈如下所示:,mcy,39,x: y:返回地址 控制链 a: i: a: k:,fp,(栈的其余部分),sp,块B的分配区,调用p时的活动记录,自由空间,当进入块B后,运行时栈如下所示:,mcy,40,第7章 运行时环境,7.1 程序执行时的存储器组织,7.2 完全静态的运行时环境,7.
16、3 基于栈的运行时环境,7.4 动态存储器,7.5 参数存储机制,mcy,41,7.4.1对象类型,在大多数面向对象的语言中,对象都有构造函数和 析构函数,它们分别在创建对象和释放对象时被调 用。如果这就是全部内容的话,那么对象的实现将 非常简单。假定我们有一对象类A,它有方法m1,m2 以及字段a1和a2。那么类A对象的运行时表示有包含 字段a1和a2记录组成:,a1,a2,m1_A,m2_A,另外,编译程序维护着一张类A的编译时方法表:,7.4 动态存储器,mcy,42,在上述简单的模块中,字段选择可以像记录字段选择一样 实现,方法选择由编译程序内的识别过程来实现。即通过 一个指向对象的指
17、针,方法可以像函数一样实现。因此方 法m2_A可以翻译为c语言中的函数。void m2_A(class_A *this,int i)方法m2_A的程序体,通过this-x访问任一对象字段x 方法的调用a .m2(3)可以翻译成m2_A(&a,3),mcy,43,继承特性,现在假定类B通过增添方法m3和字段b1来扩展类A, 那么类B的运行时表示如下:,a1,a2,m1_A,m2_A,b1,另外类B的方法编译时表如下:,m3_B,mcy,44,方法重载,假定上例中类B重新定义了方法m2,那么A中方法m2的定义既是它唯一的声明也是它的第一次定义,而它在类B中的定义为重定义。为使名字既可以反映声明它的
18、类,也可以反映定义它的类。方法的名字可有三部分组成:方法名、声明方法的类名、定义方法类名。各部分之间用下划线()分开。因此在类A中声明在类B中定义的方法m2其名字记为m2_A_B。方法重载影响方法的编译时表,现在类A的方法的编译时表如下:,m1_A_A,m2_A_A,m1_A_A,m2_A_B,m3_B_B,类B的方法的编译时表如右:,mcy,45,现在假定a是类A的一个对象,而b是类B的一个对象。方法调用a.m2()将翻译成对m2_A_A的调用,而方法调用b.m2()将翻译成对m2_A_B的调用。m2_A_A在类A中声明和定义。m2_A_B在类A中生明,在类B中定义。如果继承是语言中唯一的面
19、向对象的特征,那么m2_A_A翻译形式为: void m2_A_A(Class_A *this,int i); 而m2_A_B的翻译形式为: void m2_A_B(Class_B *this,int i);,mcy,46,多态性,当类B继承类A并且该语言允许“类B的指针”类型的指针 能够赋给一个“类A的指针”类型的变量时,那么该语言 支持多态型。例如: Class B *b=; Class A *a=b; 则第二行被翻译成: class A *a=convert_ptr_to_B_to_ptr_to_A(b); 现在,过程convert_ptr_to_B_to_ptr_to_A()为一编译
20、时类型的操作,它将指向子类B的一个对象指针转换为指 向其父类A对象的指针。因为类B的对象也是从类A的字 段开始,因而指针的值并不需要改变,唯一影响的是改 变了指针的类型:,mcy,47,a1,a2,b1,指向B的指针,指向B中A的指针,但要注意,现在同一指针指向了不同类的对象,mcy,48,动态绑定:,因为类型class A*的指针p可能实际上引用了类B的一 个对象,动态绑定认为如果实际上为类B的对象,那就 应该调用m2_A_B,如果实际为A的对象,那么就应该调 用m2_A_A.对于方法调用P-m2(3),p是一指向类A对象 指针,可以被翻译成如下形式: switch(dynamic_type
21、_of(p)case Dynamic_class_A:m2_A_A(p,3);break;case Dynamic_class_B:m2_A_B(convert_ptr_to_A_to_ptr_to_B(p),3);break; 当p为一指向类B对象指针时,可以立即将p-m2(3)调用翻译为:m2_A_B(p,3);,mcy,49,动态绑定对于方法P-m2(3)的更好翻译方法为:,如果p是一指向类A对象指针,则进行如下处理有: (dynamic_type_of(p)= =Dynamic_Class_A ? m2_A_A:m2_A_B)(p,3);如果指针p实际上引用了类B的一个对象,则执行步骤
22、2中的方法。 void m2_A_B(Class_A *this_A,int i) Class_B *this=convert_ptr_to_A_to_ptr_to_B(this_A); 方法m2_A_B的程序体,通过this-x访问任一对象字段x 如果p是一指向类B对象指针,则进行如下处理有: m2_A_B(convert_ptr_to_B_to_ptr_to_A(p),3); 执行的方法同上述的步骤2;,mcy,50,对于上述的翻译方法,每一个对象的类型信息实现为一个 指向分派表的指针,如下图所示(分派表是存储方法地址 的记录,下图分派表存储的是方法m1_A_A, m2_A_B和 m1_B
23、_B的地址),a1,a2,b1,指向B的指针,指向B中A的指针,m1_A_A,m2_A_B,m3_B_B,类B对象的表示,分派表,B-object,B-class,mcy,51,7.4.2 堆管理,对于允许程序为变量在运行时动态申请和释放存储空间的语言,采用堆式分配是最有效的解决方案. 堆式分配的基本思想是,为运行的程序划出适当大的空间(称为堆Heap),每当程序申请空间时,就从堆的空闲区找出一块空间分配给程序,每当释放时则回收之.,mcy,52,在C中处理链表等结构时,常常随机地插入或删除一些结点,利用指针变量和结构类型,可动态地生成新结点(使用malloc()函数), 或删除之(使用fre
24、e()函数). 例如 struct node char data; struct node *next;定义了链表的结点,下面函数可在表的尾部添加新结点:void Append(struct node *head,char ch) struct node *p=head;while(*p) p=p-next; p-next=malloc(sizeof(struct node);p-next-data=ch; p-next-next=NULL; 还可用下面的函数在表头删除一结点: void Delete(struct node *head) struct node *p=head; if(*p)
25、 head =head-next; free(p); ,堆分配的必要性,mcy,53,将存储空间划分为若干存储块;用户可随机地申请或释放一个或多个块; 在存储空间中建立两个队列:空闲队列和忙队列,空闲队列拉成链,链首用FREE指针指明,忙队列用一(已占块)记录表记录各占用块的首址及大小信息(也可用链进行记录). 申请时可按需要找到合适的块分配之(分配策略有最佳分配或随机分配等); 释放时将该块插入到空闲队列(能合并时可合并),并从占用记录表中删除相应的项.,堆存储管理的实现,mcy,54,第7章 运行时环境,7.1 程序执行时的存储器组织,7.2 完全静态的运行时环境,7.3 基于栈的运行时环
26、境,7.4 动态存储器,7.5 参数存储机制,mcy,55,7.5 参数传递机制,值传递,值传递的处理方法是:进入过程时,送入形参对应的形式单元的是相应实参的值;过程体中对形参的任何赋值都按对形式单元的直接访问来产生代码。 因此,一旦把实参之值送入对应形式单元之后,在执行过程体期间,除了以实参值作为形参的初值进行运算之外,将不再与实参发生任何联系。由此可见,过程执行的结果决不会改变实参之值。,void inc2( int x) /* incorrect! */ +x;+x; ,void inc2( int* x) /* now ok */ +(*x);+(*x); ,mcy,56,引用传递,控
27、制转入被调过程后,由被调过程将实参的地址写入相应的形式单元。过程体中对形式参数的任何引用或赋值,都按对相应形式单元间接访问的寻址方式为其产生代码。显然,执行过程时,型参就变成了实参的别名,对形参的赋值将会影响相应实参之值。,在C+中,是通过在参数说明中使用特殊字符 ,mcy,57,引用传递不要求复制被传递的值,这与值传递不同。当要复制的值是一个较大的结构时,如果禁止自变量的值有任何变化,在这种情况下,引用传递能够做到值传递而无需覆盖值的拷贝。在C+中可将调用写作: void f(const MuchData &x) 其中的MuchData是带有大型结构的数据类型。这仍是引用传递,但是编译程序还
28、必须执行一个静态检查:x从不出现在一个赋值的左边或是被改变。,mcy,58,已知C程序: #include int i, b4; void func( int x, int y) i=2; x +=2 ; b i =15; y+=3; b i+1=20; void main( ) for( i=0; i4; i+) bi=i;i=1;func(bi, bi+1);for(i=0;i4;i+) printf(“ %d ”, bi); printf(“ %dn”, i ); 1给出该程序的输出结果; 2若C语言采用的是引用调用的形实结合方式, 输出结果又如何?,mcy,59,值结果传递,在过程中复
29、制和使用实参的值,然后当过程退出时,再将形参的最终值复制回实参变量的地址。因此,这个方法有时也被称为复制进,复制出,或复制存储。,在调用p之后,若使用了引用传递,则a的值为3;若使用了值结果传递,则a的值为2。,例如,在以下的代码中( C语法):,void p(int x, int y) +x; + + y ; ,main( ) int a=1;p(a,a);return 0; ,mcy,60,名字传递,实参的名称或是它在调用点上的结构表示取代了它对应的形式参数的名字。,main( ) i = 1; a1=1; a2=2; p(ai); return 0; ,int i; int a10; v
30、oid p(int x) +i;+x ; ,对p的调用的结果是将a2设置为3并保持a1不变。,mcy,61,作业,什么是活动记录?请给出C语言活动记录的内容。 设有c语言程序: main() printf(“%dn”,f(3); f(int x) if(x=1)return 1;elsereturn x*f(x-1); 试给出运行该程序在返回主函数之前运行栈的活动 记录示意图。,mcy,62,设有c语言程序: int x = 10,y =20; main() static int m = 55,n = 66;int k,p = (1)写出该程序的输出结果。 (2)从运行时存储空间分配的角度分析产生上述结果的原因。,