编译原理五

目标程序运行时存储空间的组织

静态存储分配

  • 如果编译时就能够确定一个程序在运行时所需的存储空间大小,则在编译时就能够安排好目标程序运行时的全部数据空间。
  • 静态存储分配条件:
    1. 数组的上下界必须是常数(维数不能变)
    2. 过程调用不允许递归
    3. 不允许动态数组结构(即在程序运行过程中申请和释放的数据结构)比如FORRTRAN,BASIC
    临时变量
    数组
    简单变量
    形式单元(参数的传递)
    隐参数(寄存器保护区,返回地址)

简单的栈式存储分配(C语言除去可变数组)

  • 活动记录:程序活动时相关信息保留的程序数据区,调用一个过程,栈顶建立活动记录。变异可以静态确定活动记录体积。
  • 内情向量是描述数组内部情况的数据结构(维数,类型,开始地址)。
  • 先考虑没分程序结构(子程序(没返回值),不是函数),过程定义不允许嵌套,允许过程的递归调用,允许过程含有可变数组。
  • 912833853.jpg
    261488133.jpg

    具体有老SP,返回地址,参数个数,形式单元(参数),<简单变量,内情向量,临时工作单元>
    TOP和SP永远指向当前正在执行的。老TOP=SP-1
    绝对地址=SP+相对地址

  • 过程执行情况如下(P181):
  1. 过程调用
    遇到par T1,就要为其构建活动记录。(TOP+1是老SP)
    过程进入:SP=TOP+1,1[SP]=返回地址(PC),TOP=TOP+L;
    之后数据分配(数组内情向量),运行时数组起始地址回填。
  2. 过程返回:
    返回值有一个寄存器,只能有一个。
    销毁活动记录:TOP=SP-1,SP=0[SP](变址寻址,里面存着老SP),X=2[TOP] UJ0[x]

嵌套过程语言的栈式实现

用嵌套层次显示(DISPLAY表)嵌套层次简称层数。主程序是0层,之后不断+1。DISPLAY表就是把从直接外层到第0层所有活动记录基址组建成一个表,这个表叫DISPLAY表。第i层display表中有i+1个单元。(当前到第0层),把父层一抄+自己(存SP现行值)。

找变量:不断往外层找。
位置:放到活动记录。活动记录存放DISPLAY表(直接外层的)在形式单元上边。全局DISPLAY表也是连接数据。返回地址上有全局DISPLAY表地址。
1[TOP]=SP
3[TOP]=SP+d
......
之后JSR P(准备P的入口地址放PC)
过程进入:SP=TOP+1;
1【SP】=返回地址(还没放,可保护)
TOP=TOP+L
之后按照第三项连接数据抄k个单元内容,最后填新SP形成新SP。

过程返回:与第二节相同。

  • 存取链(静态链)
    加地址(直接外层活动记录的基址),返回地址上加存取链。

例题

  • 有动态变量,堆式(空闲表,空闲链)FIRSTfit bestfit worstfit

目标代码的生成

  • 绝对地址(放在指定位置.COM .EXE)通常放固定存储区可直接执行。
  • 相对地址(.OBJ)
  • 汇编语言(查表翻译到目标代码)

简单代码生成器(1、寄存器呆时间长 2、寄存器优于内存)

  • 待用信息与活跃信息:
    一个基础块,翻译四元式A=BopC,看后面还有那些四元式用A=BopC,有人用ABC,说ABC代用,不用不活跃,临时变量不用生成信息。
    四元式i对A的定值,四元式j引用A,但之间无A定值,只有i里的A影响j。A在四元式j中待用。活跃。A在后边多出被待用,生成待用信息链。(知道是否待用从后往前扫描)
  • 构建待用信息链和活跃信息链:
    1. 初值:(从基本块最后开始讨论)符号表中所有变量初值构建为非待用,临时变量不活跃,简单变量活跃
    2. 出口到入口,由后向前,每个四元式逐一处理。比如A=BopC。
    3. A待用活跃信息标注四元式,之后置非待用非活跃(定值之前认为未被引用)。BC也标注四元式i,待用信息置i,活跃信息置为活跃。
    4. 非待用非活跃F,活跃L,待用用四元式编号。
    待用可以放寄存器,活跃不待用直接放内存。。。。。。

例子:
待用活跃.jpg
  • 代码生成算法(生成目标代码):
    1. 寄存器给多个变量用说明它们有同样的值。
    2. 寄存器描述数组RVALUE:寄存器名称作为下标,存放某某某的值。变量地址描述数组:记录变量存储位置AVALUE,Ri表示在寄存器Ri放,A表示在内存里放。

  • 代码生成算法步骤(A=BopC):
    1. 调用GETREG,为了处理四元式给A获取一个寄存器,
    2. 通过地址描述数组获取B,C的存放位置,放寄存器,寄存器就叫B',C'。
    3. 给A选择的寄存器R不是B',则给B'内容传到R里边去,之后拿R和C'执行op操作,否则不用做转移(R是B')。如果B'或C'为R,删除AVALUE[B],AVALUE[C]中的R。
    4. 令A的描述数组=R,R的寄存器数组=A(R只为A服务)
    5. B和C不待用不活跃,寄存器描述数组的BC删掉。地址描述数组的
    Rk也去掉。

  • 申请寄存器的算法:
    1. 如果B的值在Ri中放着了,且Ri中只包含B的值。给A选Ri。或者B和A是同一个标识符(A=AopC),或B在该四元式之后不再被引用了(B单独用一个寄存器是大前提)。
    2. 有没有空闲寄存器
    3. 每空闲的从已分配的寄存器中选一个Ri。原则:占用Ri的值最好同时也在内存中,或该值在基本块最远的位置会被引用到。这是对寄存器Ri调整:对于AVALUE【Ri】每个变量M:M是A,A不用做任何操作。M不是A,如果D的地址描述数组不包含D(只在寄存器不再内存),做如下操作:1、MOVE D,Ri(存到内存里边去 ) 2、D的数组里边写上M把Ri去掉。3、寄存器描述数组D删除掉。4.接着处理其他。最后空着给A用。

    (注意:寄存器里有B咋整?地址描述数组得有B,同时又有Ri。之后就可去B,去不去无所谓)
    目标代码生成.jpg

    注意:A=B只改描述数组不mov,出基本块后不活跃不用存内存,否则Mov D,Ri
  • 寄存器的分配:
    指令执行代价=每条指令访问内存次数+1
    比如A=BopC,A要保护,访存1次,BC若放内存还要访存两次。

  • 代价算法:1、变量在基本块定值,分配寄存器,定值之前分配,每用一次省一次(不一定定值,只要定值之前即可,最终定不定值不管,a=a-f定值之前引用了a)。 2、基本块中定值,出基本块之后活跃,省的代价为2.。


    算式.jpg
  • 源程序到目标代码:1、生成中间代码 2、划分基本块 3、翻译

练习(重要):

符号表与错误处理

符号表

  • 符号表是一个统称,又叫名字表:标识符+跟名字相关信息
  • 符号表的组织:
    • 标识符命名为固定长度可以直接组织(BASIC),直接方式名字信息摆在那,有的不合适比如255。还有间接方式:字符串数组存放所有标识符,符号表名字表有指针指标识符(数组里的起始位置),还有长度;还有按标识符种属分类。
    • 信息栏组织分两类:固定信息内容(按种属分,特征相同,信息可一一记录信息栏,好管理),仅记录信息存放地址(不分种属,专门开辟空间存信息,数组,函数亦可采用词法)。
  • 分程序结构语言符号表的建立(带嵌套定义pascal):符号表分层建立。1、扫描声明部分,查找与本层相关的符号表,没有则加入。冲突(声明类型不同,重名)错误处理(建符号表)2、语句中(使用部分)扫描到标识符,查符号表,查不到往外层查。都找不到错误处理。
  • 组织如下:
    • 同一层标识符放在一起,分层处理。
    • 分程序表:OUTERN外层分程序编号,COUNT符号表登记项个数,POINTER指向分程序符号表起始位置。
  • 从外层往内扫描,先构建内层,先来后出用堆栈(先构建,后转移到正式符号表中)
  • 扫描开始建分程序表,扫描标识符查表,无构建,COUNT+1,最后一道正式符号表中。退出分程序,返回外层继续处理外层。
  • 一次扫描扫描完符号表删除,栈式(往后没用不用转移了)。
  • 符号表结构:线性(建简单用麻烦)(10000个平均查5000回),有序符号表(建表有开销)(新加一个往后移),二叉排序树(构建快,查找比有序差4倍,1+4log2N),散列符号表

错误处理

查错,改错。(语法错误,语义错误)

  • 语法错误校正。1、修补 2、跳过(错误的局部化) 动态编译时的错误发现不了。

推荐阅读更多精彩内容