编译原理(十一)——抽象地址、符号表

    科技2022-08-29  143

    一个标识符的语义信息含有三部分内容,种类 类型 抽象地址 种类可以分为:常量标识符、类型标识符、变量标识符、域名标识符、过函标识符 类型可以有:基本数据类型、子界、数组、枚举、结构体、集合、文件、指针等等

    语义分析:

    静态分析——静态语义动态分析——动态语义

    核心:

    程序中是否有语义错误在语义分析阶段要构建一个符号表为整个后序过程提供支持

    一、抽象地址

    1.1 地址分配

    在进行编译的时候为每一个变量标识符都分配一个相对地址,当目标程序在真正运行的时候,一旦首地址确定下来,那么所有的变量对应的地址就可以确定下来了,这是一种比较简单直观的方法,例如fortune语言就是用这样的方式,但是也有其局限性。比如有动态申请地址空间的时候,这种方法不适用。

    两个比较典型的例子:

    指针变量,他是在程序的运行过程中可以动态的申请存储空间,申请空间的次数和占有多大空间,这个在程序没有执行的时候是没有办法确定的,如果存在于程序的某一个分支或者循环中,程序没运行则没有办法确定。既然没有办法确定,就没有办法预先分配存储空间。程序中的递归函数,这是一个比较有特点的调用形式。每次调用一个函数都要给它的形参和内部的变量开辟对应的存储空间,在函数被递归调用(最外面那个函数被调用)的时候,一个局部变量比方说x的时候他对应的就不是一个空间,而是一串的存储空间,这一个变量占用多少空间取决于一个递归程序执行的次数,静态的时候没有办法确定次数,因为影响次数的因素非常多,比方说跟输入有关、跟什么条件有关,所以没有办法静态的确定到底占多大的空间,这样的情况也不能采用静态的方式来分配。

    只能用动态的方式,给变量在程序中分配的不是一个(程序中变量分配的地址)不是一个具体的地址,而是一个抽象地址。一个变量对应的存储单元现在是不确定的只是一个抽象的地址,在真正的目标程序运行的时候,再把这个地址映射到一个实际的物理地址上,这样的话,需要多少,就可以动态的分配多少,通过抽象地址进行映射,得到所需要的存储空间。现在大多数语言,因为有动态的数据结构,有递归,所以都采用的是动态的存储空间形式,动态的管理模式。

    这是两种最常用的地址分配方式。现在主要采用动态的方式

    1.2 抽象地址结构

    采用的都是动态的管理模式,一个变量分配的都是一个抽象地址。抽象地址通常来说具有两部分组成,第一部分为层数,第二部分为偏移。程序设计语言从函数定义结构上可以分成两大类,一类属于并列式语言,一类属于嵌套式语言。并列就是c fortune,函数之间都是并列的,一个函数内部不可以定义其他的函数。而另外一类语言就是属于嵌套式语言,比如pascal语言,属于嵌套结构,在一个函数中可以定义另外一个函数,还可以继续定义。这种嵌套的方式,可以直接使函数的使用范围受到约束,避免了不应该调用这个函数的调用出现,避免程序中出现不应该有的问题。这是两类常见的程序设计语言,由于编译是针对一般程序设计语言来进行考虑的,课程是针对一般性的程序设计语言来介绍编译过程的原理,所以可能对不同类型的语言都要给出介绍

    1.3 层数定义

    对于并列式语言这个层数是不是没有意义呢? 其实也不是,只是这个层数相对更为简单。比如把全局的变量定义成0层,函数中的变量定义成1层。

    1.4 过程活动记录

    偏移: 每次函数被调用的时候,都会给函数分配一片空间,包括一些管理信息,形参、局部变量、临时变量要占据存储单元,偏移量是针对于这个起始地址而言,他的偏移是多少,以后只需要知道起始位置在哪,就可以找到对应变量所在的单元。起始位置+偏移量就是对应的一个物理地址,可以通过这样的形式把它表示出来

    过程活动记录:一个函数或者一个过程在他真正调用的时候会占用一片存储空间,我们把这片存储空间称作过程活动记录,表示一个函数被真正调用时,它所占用的存储空间,这个存储空间中包括的信息主要有: 管理信息(返回地址、寄存器现场的信息等等的管理信息,这些信息占用的空间大小是固定的,对于一个具体的目标机而言,这个是固定的)

    形参区,在真正函数调用的时候也要有具体的存储单元,所以也要给他们分配具体的存储单元,比方说整型的形参分多少、数组的分多少。 局部变量区是指在函数内部声明的变量所占用的空间在这一块区域分配。

    临时变量区,在生成目标代码之前,可能会产生很多的中间计算结果,中间的结果全都存在寄存器中恐怕是做不到的,因为有的时候结果比较多,寄存器的个数是有限的,所以不能全都存在寄存器里,有一部分要存在内存中,既然是存在内存里,就要给分配一块存储空间,这里就是存的这种临时变量。

    这就是一个函数在调用的过程中所要占用的空间,这里的存储顺序为什么是按照这个顺序排列的呢?这个是按照处理的顺序分配的,比方说管理区是固定的,是函数调用的时候大小固定, 然后先处理的是形参,从函数头开始处理,所以给形参分区,然后是局部变量,然后是临时变量。这是过程活动记录的大概的结构.

    1.5 分配原则

    分配原则,一个简单变量、数组变量、记录变量,分配的大小肯定是不一样的。简单的我们知道是确定的,其他的就不一定,所以一定要给出一个确定的分配原则。因为不同的目标机分配的单元个数可能是不一样的,有的机器是16位,有的是8位,这样对于一个变量来说占用的空间大小可能就不太一样,这里为了简单起见,假定了基本变量类型占几个,这个都是假定的,考试的时候如果题目中说假定占两个也没问题,至于说具体多少就要根据实际情况来确定临时变量分配1个存储单元,临时变量一般是一个计算结果,结果都是一个简单值,所以就假定占用一个。局部变量分配的单元要按照类型的长度分,每一种类型都要确定它的大小,目的是什么呢?就是为了分配存储单元的时候有一个依据。所以局部变量大小是根据类型来决定的,这也是类型的一个作用。形参有两类——值引用的形参(要把实参的值传给形参,不管实参是什么类型的 值都是传过来的,所以值引用型形参也是按照类型的大小来分空间)、地址引用型的形参(接受的是实参的地址信息,因此统一的给它分配一个存储单元,前提是地址用一个单元能保存出来,不管实参是简单类型也好,结构类型也好,传递过来的时候,假如一个数组,不是把这100个元素都传过来,而是传递的这个实参的地址,然后通过间接寻址的方式 对它进行操作,所以形参给的是一个存储单元)对于过程函数形参,一个函数作为形参的时候,通常要分配两个存储单元,第一个存储单元意义非常明确,以后要传的实参函数的入口地址,第二个是要构造运行环境的地址。

    二、符号表

    2.1 符号表结构和总体组织

    下面就要看一下具体的符号表的建设和管理情况。从语法分析之后得到的是一个token序列,token序列中的标识符部分都变成了:标识符->符号表符号表的结构,符号表存的就是标识符的语义信息。符号表的信息就是由两个,标识符 语义字(种类 类型 抽象地址)这样的话一个符号表就是由这样一串标识符的语义信息组成的,类型信息可以和类型信息表关联。

    2.2 New token与符号表的关系

    2.3 符号表查表技术

    一般使用顺序查表法。

    2.4 标识符的作用域

    函数,从函数名后面开始就进入了局部化区,函数结束退出这个局部化区。 另外一个是域名是一种特殊的局部化区,记录变量在进入记录之前,也是有一个局部化区,特殊的情况就是域名和变量名可以相同,但是由于有域名,就跟一般的变量进行了区分。 不同的语言在约定上不太相同,c语言中的分程序结构用{}的部分也是局部化区,在{}可以再声明一些变量和标识符,出了这个花括号就没了,

    2.5 局部化区的语义错误检查

    在一个局部化区的时候,要考虑一些语义上的问题,主要考虑的是两类语义错误的问题,第一大类的语义错误就是变量的重复声明,第二大类就是变量没有声明的使用。 第一类问题:变量的重复声明。 在一个程序的局部化区里,同一个标识符不能被声明两次 第二类问题:标识符的使用没有声明,现在的程序设计语言都属于强类型语言,也就是说所有的标识符都要给出对应的声明,没有声明出现的标识符认为就是有错的。比方说(),在某些弱类型的语言中也可以不用这么声明,比方说qbasac。

    2.6 标识符处理原则

    作废方法:

    直接删除,直接将退出的局部化区的全部内容从符号表中删除,但是这对于后续维护等操作来说会带来困难在退出的位置开辟一个新的空间存储一个指向当前局部化区最开始位置的一个指针,后面的变量在查找的时候从下往上找会略过这个区域,但是注意,返回的是局部化区的开始而不是完全跳出,因为这样还可以在当前位置再次跳入该局部化区。

    2.7 实例分析

    例1:

    符号表(忽略m定义) NameLevelOffP1NULLx2Off0a2Off0+2i2Off0+1002P12NULLa13Off0a23Off0+1x3Off0+1001a3Off0+1002P23NULLn4Off0x4Off0+1 根据函数中的使用性出现,从下往上进行使用性变量定义的寻找

    例2:

    答案在下一节笔记上。

    嵌套式语言并列式语言的比较

    Processed: 0.009, SQL: 9