您的位置:首页 > 其它

C编译器剖析_2.5 UCC编译器的符号表管理

2015-01-28 20:26 99 查看
这一节,我们准备初步讨论一下UCC编译器的符号表管理,与符号表管理相关的代码主要在ucl\symbol.h和ucl\symbol.c中。UCC编译器内部需要对用到的所有符号进行分类,并建立相应的数据结构来记录与符号相关的信息。图2.5.1第25行的结构体struct symbol用于记录与符号相关的信息,这些信息如图第7至23行所示。第8行的kind用于区别不同类别的符号,其取值范围如图第1至5行所示;第9行的name用于记录符号的名称;第11行的ty用于记录符号的类型,第23行的pcoord用于记录符号所在的文件位置,在调试时方便定位。其余各行的作用我们以后会结合上下文进行讨论。



图2.5.1 struct symbol
下面,我们结合一个具体的C程序来熟悉一下UCC编译器内部对符号所做的分类,如图2.5.2所示。



图2.5.2 各种不同类型的符号
在图2.5.2的第1至18行,是一个简单的C程序,而第20至30行是由UCC编译器产生的中间代码。结合图2.5.1和图2.5.2,我们能看到以下几种不同的符号:

(1) 结构体名Data和枚举名Color,对应的类别为SK_Tag;
(2) 由关键字typedef给已有类型int取的别名INT32,对应的类别为SK_TypedefName;
(3) 枚举常量RED、GREEN和BLUE,对应的类别为SK_EnumConstant;
(4) 常量3,对应的类别为SK_Constant;
(5) 全局变量名dt、a、b、c、d和局部变量hi,对应的类别为SK_Variable;
(6) 临时变量t0、t1和t2,对应的类别为SK_Temp;
(7) 用于访问结构体成员的dt.num2,经UCC编译器处理后,我们更关注域成员num2
在结构体Data中的偏移,因为结合dt的首地址和偏移4,我们就能定位dt.num2的具体位置。在UCC编译器内部,为dt.num2取的名字为dt[4],对应的类别为SK_Offset;
(8) 第21行的str0,是UCC编译器内部为第11行的字符串”Hello World”所取的名字,
对应的类别为SK_String;
(9) 标号名Begin,对应的类别为SK_Label;
(10) 函数名main,对应的类别为SK_Function;

UCC编译器把汇编代码中出现的形如”%eax”这样的寄存器名也当作符号来管理,对应的类别为SK_Register。这11种符号正是图2.5.1第3和第4行所列出的各种不同类别的符号。实际上,当寄存器eax中存放的是地址,而非一般数值时,我们是把寄存器当指针变量来使用,在汇编代码中表示为”(%eax)”,在UCC内部会对应一个特殊的类别SK_IRegister,是IndirectRegister的缩写,代表通过寄存器进行间接寻址。所以,UCC编译器内部要管理的符号共有12种。这12种符号中,有些符号的相关信息用struct
symbol对象就可以记录;而有些符号需要记录更多的信息,比如SK_Variable、SK_Temp和SK_Offset类别的符号,就需要用到图2.5.3第73行所示的struct variableSymbol。



图2.5.3 variableSymbol
在图2.5.3的第75行,我们再次看SYMBOL_COMMON,这说明我们可以把structvariableSymbol当成是struct symbol的子类。第79行的offset用于记录SK_Offset类别的符号的偏移值,例如上述的dt[4]中的4。第76行记录用于变量初始化的值。这里,我们重点关注一下第77行的def和第78行的uses的作用。为表述方便,从图2.5.2中抽取出部分代码,如图2.5.4所示。



图2.5.4 公共子表达式
在图2.5.4中,我们可以发现第2行的”c = a+b;”中的子表达式”a+b”确实进行了计算,其结果存到第7行的临时变量t1中,但是第3行的”a+b”则没有必要进行再次计算,因为此时a和b都没有发生变化,我们可以延用临时变量t1中的保存的值。但是第4行对a进行了修改,导致在t1中保存的”a+b”失效,此时第5行的”a+b”就需要重新进行计算。UCC编译器期望能其产生的汇编代码能减少不必要的计算,所以引入了图2.5.3第57行的struct
valueDef,是”valuedefinition”的缩写,当我们进行a+b的计算后,就产生了一个新的value,我们用valueDef结构体中的src1来记录操作数a,用src2来记录操作数b,用op来记录运算符’+’,而用dst来记录运算结果t1,图2.5.3第63行的ownBB则用来记录这个value是在哪个基本块中产生的。如果要在不同基本块之间重用已经计算出来的形如”a+b”的值,则需要进行较复杂的数据流分析。UCC编译器为了简单起见,限于人力物力,仅在同一个基本块内,对形如”a+b”这样的公共子表达式进行优化。下面,我们以图2.5.4的第7行为例,来说明由valueDef和valueUse所形成的数据结构,如图2.5.5所示。



图2.5.5 valueDef和valueUse
从图2.5.5,我们可以看到,变量a对应的uses域实际上是个由若干个structvalueUse对象构成的一个链表。每个structUse对象的def域记录了变量a在哪个子表达式中被使用。当图2.5.4第4行给a重新赋值为3后,则我们可以使a的uses链上的每个struct valueUse对象记录的valueDef失效。这正是ucl\gen.c中的函数TrackValueChange()所要完成的功能,如图2.5.6所示。图2.5.6第55行的代码使valueDef对象中的dst域为NULL,这就使得公共子表达式”a+b”不再有效。当然不论变量a的内容如何变化,在变量a的生命周期内,其地址是不会变化的,所以子表达式”&a”会一直有效,这正是图2.5.6第54行的if条件所要检测的情况,即当valueDef对象中的op域不是取地址运算时,才使valueDef对象失效。



图2.5.6 TrackValueChange()
从图2.5.5中可以发现,到目前为止,我们还没有解决structsymbol对象a、b和t1如何存放的问题,及如何快速找到valueDef对象的问题。这需要我们引入图2.5.7的两个结构体。



图2.5.7 struct table
图2.5.7的第82行定义了struct functionSymbol用来描述SK_Function类别的符号的相关信息,这实际上是个函数名,所以在第85行的params记录了函数形参构成的单向链表,而第86行的locals用于记录局部变量构成的单向链表,第88行的nbblock代表函数体包含的基本块的个娄和,第89行的entryBB是入口的基本块,而exitBB是出口的基本块。而第92行的valNumTable[16]则是一个哈希表,用于快速查找形如”a+b”的公共子表达式,如图2.5.8所示。图中标为struct
symbol的对象可能是struct symbol对象,也可能是其“子类”的对象,例如struct variableSymbol对象。



图2.5.8 valueNumTable哈希表
为了查找哈希表,我们还需要定义相应的哈希函数。UCC源代码中的ucl\gen.c的TryAddValue()函数给出了相关代码,如图2.5.9所示。图中第4行就是一个简单的哈希函数,因为哈希表valueNumTable中只定义了16个链表,所以整数h的值要落在区间[0,15]之间。如果在代码中访问全局变量a的地址,例如使用过”ptr = &a;”,则即可以通过”*pt=3;”也可以通过”a
= 3;”来改变a的值,此时要判断变量a的内容是否发生变化就需要做更复杂的分析。UCC编译器采取的策略很简单,此时不再去使用”a+b”这样的已计算过的值,而是重新进行计算。图中第9行的if语句就是针对这种情况进行判断。从这里,我们也能再次看到UCC编译器在优化上还有待加强。



图2.5.9 TryAddValue()
图中第12至19行用于在哈希表中查找是否存在已经在同一个基本块中计算过的相同的子表达式。在第18行我们看到了对def->dst是否为NULL的判断,而在图2.5.6的TrackValueChange()函数中,我们正是通过”use->def->dst= NULL;”来使子表达式”a+b”无效。如果能找到,则从图2.5.9第19行返回,否则我们需要重新计算子表达式,并把结果保存到第22行创建的新的临时变量中,之后还要在第24至26行把新生成的valueDef对象加入到哈希表中。
以上我们解决了如何快速查找形如”a+b”的公共子表达式的问题。另一个问题就是大量的structsymbol对象要如何存放的问题。图2.5.7中的第94行的struct table正是用来做这个事情的。这也是我们本节标题中所说的符号表。查看ucl\symbol.c的函数AddSymbol(),我们不难发现,符号表仍然是采用“哈希表”的数据结构,如图2.5.10所示。



图2.5.10 AddSymbol()
所采用的哈希函数在第120行,而宏SYM_HASH_MASK的值为127,由第126行我们可知,符号表struct table共有128个哈希桶(bucket)。第124至130行在哈希表为空时,从堆空间分配128个哈希桶。之后在第132至134行,用于往符号表里添加一个新的符号。下面,我们再结合一个简单的C程序,来讨论一下图2.5.7第98行outer成员的作用。



图2.5.11 C语言的作用域
如图2.5.11所示,在第2行定义了一个全局变量a,在第5行定义了一个同名的局部变量a,而在第8行又定义了一个局部变量a。在C语言中,一对大括号一般代表一块新的作用域。当然,第6行的大括号是用于初始化数组,并不代表一个作用域。由标准C的文法,我们可知一对大括号包括的语句实际上对应的是复合语句compound-statement,以左大括号开始,之后跟着若干个声明,再跟上由若干条statement,最后是右大括号。在C语言中,函数体实际上就是一个复合语句,如图2.5.11的第3至第10行所示。当然,复合语句内部还可以包含新的复合语句,如上图的第7至第10行所示。

compound-statement:
{ declaration-listopt statement-listopt }

每一个复合语句对应一个新的作用域,在UCC编译器内部,每当进入一个新的作用域时,我们就会创建一张新的符号表,用来记录在该作用域中声明的符号。图2.5.12给出了UCC编译器为图2.5.11所创建的符号表。



图2.5.12 多个作用域的符号表
由图2.5.12,我们可知,上述程序中”int a = 10;”对应符号a是存放在level为0的符号表中,而在level为1的符号表中,我们并没有定义新的局部变量,但在level为2的符号表中,我们又定义了”int a = 20;”,在level为3的符号表中,定义了”int a = 30;”。当程序执行到图2.5.11的第13行时,当前符号表是level为1的符号表,第13行”a
= 60;”需要访问符号a,我们要检查一下当前符号表中是否符号a,如果没有,就通过outer指针查找外层的符号表,此时会在level为0的符号表中查找到全局变量a。通过图2.5.12的多级符号表,我们实际上实现了C语言中“作用域scope”的概念。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: