660 likes | 1.18k Views
编译器设计与实现. ——Lcc 原理剖析. 华中科技大学计算机学院 张 德. 一、概述. 源程序. 1 、编译器各阶段. 词法分析器. 语法分析器. 语义分析器. 符号表管理器. 错误处理器. 中间代码生成器. 代码优化器. 代码生成器. 目标程序. 2 、编译器各阶段的分组. 前端 :依赖于语言并很大程度上独立于目标机器。一般包括 语法分析、词法分析、符号表的建立、语义分析、中间代码生成 以及相关错误处理。
E N D
编译器设计与实现 ——Lcc原理剖析 华中科技大学计算机学院 张 德
一、概述 源程序 1、编译器各阶段 词法分析器 语法分析器 语义分析器 符号表管理器 错误处理器 中间代码生成器 代码优化器 代码生成器 目标程序
2、编译器各阶段的分组 • 前端:依赖于语言并很大程度上独立于目标机器。一般包括语法分析、词法分析、符号表的建立、语义分析、中间代码生成以及相关错误处理。 • 后端:依赖于目标机器的阶段或某些阶段的某些部分。一般来说,后端完成的任务不依赖于源语言而只依赖于中间语言。主要包括代码优化、代码生成以及相关的错误处理和符号表操作。
二、符号表 • 符号表是编译器保存信息的中心库,编译器的各部分通过符号表进行交互,并访问符号表中的数据——符号。 • 符号表把各种名字映射到符号集合。常量、标识符和标号都是名字,不同名字有不同的属性。 • 符号管理不仅要处理符号本身,还管理符号的作用域。
1、符号的表示 struct symbol { char *name; //名称 int scope; //作用域 Coordinate src; //在源程序中位置 Symbol up; //连接符号表中上一个符号 List uses; //可保存一个Coordinate列表,表示使用情况 int sclass; //扩展存储类型 <symbol flag> //符号标记 Type type; //如变量、函数、常量、结构或联合等信息 float ref; //被引用的粗略次数 union { //联合u为标号、结构、联合、枚举、常量、全局 <appendent info> //和静态变量提供附加信息 } u; // Xsymbol x; //由后端处理,如为变量分配寄存器 <debugger extension>// 为调试器产生数据信息 }
1、符号的表示 • scope域: enum { CONSTANTS=1, LABELS, GLOBAL, PARAM, LOCAL }; 第k层中声明的局部变量其scope域等于LOCAL+k。 • src域: typedef struct coord { char *file; unsigned x, y; } Coordinate; file指名包含该符号定义文件名,y和x表示出现的行号及行中位置。 • sclass域:符号扩展类型 可以是AUTO、REGISTER、STATIC或EXTERN等 • 首字母大写的类型表示全小写类型的指针,如Symbol。
2、符号表的表示 extern Table constants; extern Table externals; extern Table globals; extern Table identifiers; extern Table labels; extern Table types; structtable { int level; //同symbol中scope域 Tableprevious; //符号表链表,指向level-1的表 structentry { struct symbol sym; struct entry *link; } *buckets[256]; //这是一个哈希链数组,方便插入、查找 Symbol all; //指向当前及其外层所有符号列表的表头 };
3、符号表举例 int x, y; f(int x, int a){ int b; y = x + a*b; if (y < 5){ int a; y = x + a*b; } }
3 0 4 5 6 0 0 0 0 0 a b x y
4、符号表的相关操作 • 查找和建立标识符 Symbol install(const char * name, Table * tpp, int level, int arena); Symbol lookup(const char *name, Table tp); • 标号:与标识符相似,但不涉及作用域 • 常量:这些符号保存在constants表中 • 产生变量:用于产生静态变量保存字符串等
三、代码生成接口 • 这一章内容定义了与目标机器无关的前端和与目标机器相关的后端之间的接口。 • Lcc接口包括一些共享数据结构、18个函数和包括36个操作符的语言。该语言用于将可执行代码从源程序生成dag(有向无环图)。 • 共享数据结构可供前后端共享,但某些域为一端私有。symbol就是一个共享数据结构。
1、类型度量 typedef struct metrics { unsigned char size, align, outofline; } Metrics; size:类型的大小; align:对齐字节数; outofline:控制相关类型的常量的放置。为1时,不出现在dag中,存于静态变量中。 Metricscharmetric; Metricsshortmetric; Metricsintmetric; Metricsfloatmetric; Metricsdoublemetric; Metricsstructmetric;
2、接口记录 typedef struct interface { <metrics> <interface flags> <interface functions> Xinterface x; } Interface; lcc为每一种目标机器形成一个独有的接口实例。x域是对interface的扩展,后段使用它存放与目标及其相关的接口数据和函数,对后端私有。
3、dag操作 • 可执行代码用dag来描述。函数体是用dag组成的序列或森林。每个dag都可以同过gen函数传给后端。 • dag节点 struct node { short op; short count; Symbol syms[3]; Node kids[2]; Node link; Xnode x; };
3、dag操作 op域存放dag操作符。 dag操作符后缀表示操作数类型: enum { F=FLOAT, I=INT, U=UNSIGNED, P=POINTER, V=VOID, B=STRUCT }; 如CNST,有变体CNSTI、CNSTU、CNSTP等。 CNST=1<<4; CNSTC=CNST+F; CNSTI=CNST+I; … …
3、dag操作 举例: int i, *p; f() { i = *p++;} 2 INDIRP 5 ASGNP 8 ASGNI 1 ADDRGP p 6 ADDRGP i 4 ADDP 7 INDIRI 3 CNSTI 4
4、接口标志 unsigned little_endian:1; 目标机器存储是低位优先还是高位优先 unsigned mulops_calls:1; 有硬件实现乘、除和求余,mulopes_calls应等于0 unsigned wants_callb:1; 通知前端产生CALLB节点以调用返回结构的函数 unsigned wants_argb:1; 通知前端节点产生ARGB节点以产生结构参数 unsigned left_to_right:1; 告诉前端按照从左到右的顺序计算和提交参数给后端 unsigned wants_dag:1; 告诉前端传递dag给后端
5、函数 • 前端将函数编译为私有数据结构。将函数的任意部分传递给后端之前,前端必须先对每个函数进行完整的分析。 • 函数的处理:function函数包括前端过程gencode遍历前端的私有数据结构,将dag的每个森林传给后端过程gen。gen选择代码,在dag上添加注释并将返回一个dag指针。gencode还可以调用local宣告新的局不变量。前端过程emitcode再次遍历,将gen返回的指针传递给emit函数发送代码。
6、上行调用 前段调用后端以执行代码生成和发送。后端调用前端完成输出、分配存储空间、查询类型等功能。上行调用即后端调用前端。 allocate分配空间,保证对齐方式符合机器多数类型 newnode分配新的dag节点 newconst符号表中创建新的常量 newtemp符号表中创建新的变量 … …
四、词法分析 • 词法分析器读入源程序,产生语言的基本词法单元。 • 例:*prt = 56;
1、输入 \n cp limit 当limit-cp小于某一个固定值时,调用fillbuf函数填充buffer buffer+MAXLINE buffer buffer+ MAXLINE+MAXSIZE
2、单词识别 • 部分文法: token: keyword identifier constant operator punctuator punctuator: one of [ ] ( ) { } * , : = ; … 定义: ID 标识符 FCON 浮点常量 ICON 整型常量 SCON … INCR DECR DEREF ……
emun{ #define xx(a,b,c,d,e,f,g) a=b, #define yy(a,b,c,d,e,f,g) #include “token.h” LAST } token.h文件: yy(0, 0, 0, 0, 0, 0, 0) xx(FLOAT, 1, 0, 0, 0, CHAR, "float") xx(DOUBLE, 2, 0, 0, 0, CHAR, "double") xx(CHAR, 3, 0, 0, 0, CHAR, "char") xx(SHORT, 4, 0, 0, 0, CHAR, "short") xx(INT, 5, 0, 0, 0, CHAR, "int") xx(UNSIGNED, 6, 0, 0, 0, CHAR, "unsigned") xx(POINTER, 7, 0, 0, 0, 0, "pointer") xx(VOID, 8, 0, 0, 0, CHAR, "void") xx(STRUCT, 9, 0, 0, 0, CHAR, "struct") … …
3、关键字的识别 可以通过查表完成,也可以通过硬编码方式识别。 例如,当起始小写字母为i时由gettok函数中switch语句的case ‘i’处理。 case 'i': if (rcp[0] == 'f'&& !(map[rcp[1]] &(DIGIT|LETTER))){ cp = rcp + 1; return IF; } if (rcp[0] == 'n'&& rcp[1] == 't' && !(map[rcp[2]]&(DIGIT|LETTER))) { cp = rcp + 2; tsym = inttype->u.sym; return INT; } goto id;
4、标识符识别 case 'h': case 'j': case 'k': case 'm': case 'n': case 'o': case 'p': case 'q': case 'x': case 'y': case 'z': case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': case 'G': case 'H': case 'I': case 'J': case 'K': case 'M': case 'N': case 'O': case 'P': case 'Q': case 'R': case 'S': case 'T': case 'U': case 'V': case 'W': case 'X': case 'Y': case 'Z': id: if (limit - rcp < MAXLINE) { cp = rcp - 1; fillbuf(); rcp = ++cp; } assert(cp == rcp); token = (char *)rcp - 1; while (map[*rcp]&(DIGIT|LETTER)) rcp++; token = stringn(token, (char *)rcp - token); tsym = lookup(token, identifiers); cp = rcp; return ID; 检查是否需要填充buffer
5、其他 • 另外还有: • 数字识别 • 字符常量和字符串识别 都是有gettok函数实现,实现方法相似。 词法分析器可以有象LEX这样的工具实现。Lcc手工实现了词法分析器,体积更小。
五、语法分析 • 根据语言的文法分析,以确认输入是否符合语言规则,并建立输入源程序的内部表示。 • Lcc采用递归下降的语法分析。 • 语法分析以形式语言理论为基础,采取语法制导翻译方法,处理程序中的错误。
1、表达式 • 表达式的表示: (a+b)+b*(a+b) ADD+I MUL+I ADD+I ADD+I INDIR+I INDIR+I INDIR+I INDIR+I INDIR+I ADDRG+P a ADDRG+P b ADDRG+P b ADDRG+P a ADDRG+P b
表达式的分析: c语言的小部分表达式语法: expr: term{+term} term: factor {*factor} factor: ID| ‘(’ expr ‘)’ T(expr) T(term{+term}) T(term)T({+term}) term();T({+term}) term();while(t == ‘+’) {T(+term)} term();while(t == ‘+’) {T(+)T(term)} term();while(t == ‘+’) {t = gettok();T(term)} term();while(t == ‘+’) {t = gettok(); term()} 同理得分析函数term是:factor();while(t == ‘*’) {t = gettok(); factor()} void factor(){ if(t==ID) t=gettok(); else if (t == ‘(’){ t=gettok(); expr(); expect(‘)’) ; } }
c语言表达式分析 • 赋值表达式: assignment-expression: conditional-expression unary-expression assign-operator assignment-expression Tree expr1(int tok) { static char stop[] = { IF, ID, 0 }; Tree p = expr2(); if (t == '=‘ || (prec[t] >= 6 && prec[t] <= 8) || (prec[t] >= 11 && prec[t] <= 13)) { int op = t; t = gettok(); if (oper[op] == ASGN) p = asgntree(ASGN, p, value(expr1(0))); else <augmented assignment> return p }
条件表达式: conditonal-expression: binary-expression[? expression : conditional-expression] static Tree expr2(void) { Tree p = expr3(4); if (t == '?') { Tree l, r; Coordinate pts[2]; if (Aflag > 1 && isfunc(p->type)) warning("%s used in a conditional expression\n", funcname(p)); p = pointer(p); t = gettok(); pts[0] = src; l = pointer(expr(':')); pts[1] = src; r = pointer(expr2()); } <other > return p; }
另有二元表达式、一元表达式、后缀表达式和基本表达式。另有二元表达式、一元表达式、后缀表达式和基本表达式。 • 表达式分析多是用递归和大量switch语句实现。 • 在编译领域用一个分析函数代替n个函数处理n级优先是非常流行的。 • 关于表达式的分析还包括表达式语义的分析,如类型检查转换、函数调用分析等各种操作。
2、语句分析 • 代码的表示:表达式首先被编译为分析树然后转化为dag。每个函数的dag在代码表中被串起来,代码表表示了函数的代码。 code结构: struct code { enum { Blockbeg, Blockend, Local, Address, Defpoint, Label, Start, Gen, Jump, Switch } kind; Code prev, next; union { <unions> }u; }
语句的识别: void statement(int loop, Swtch swp, int lev) { float ref = refinc; if (Aflag >= 2 && lev == 15) warning("more than 15 levels of nested statements\n"); switch (t) { case IF: ifstmt(genlabel(2), loop, swp, lev + 1); break; case WHILE: whilestmt(genlabel(3), swp, lev + 1); break; case DO: dostmt(genlabel(3), swp, lev + 1); expect(';'); break; … … } <check > refinc = ref; } expect(‘;’) break;
if语句的识别: if expression == 0 goto L statement1 goto L+1 L: statement2 L+1: static void ifstmt(int lab, int loop, Swtch swp, int lev) { t = gettok(); expect(‘(’); //判断if后的( definept(NULL); walk(conditional(‘)’), 0, lab); //包含listnode函数生成dag并加入 refinc /= 2.0; //森林,把入口加入代码表.同时根 statement(loop, swp, lev); //据接过设置flab,tlab if (t == ELSE) { branch(lab + 1); t = gettok(); definelab(lab); statement(loop, swp, lev); if (findlabel(lab + 1)->ref) definelab(lab + 1); } else definelab(lab); }
3、小结 • 在循环、switch、goto语句中都用到了标号 和跳转,标号使通过definelab函数定义的, 而跳转通过branch函数生成。 • 除语句识别外,还有声明的识别。声明的识别非常复杂,c语言中声明的形式很多,处理时大量的相互递归调用。 • 经过前端的分析后,将源程序转化为dag,并添加进代码表。
六、中间代码生成 • 编译器的后端通过function接口函数调用gencode和emitcode来遍历代码表。 • walk和listnodes函数操作处理dag森林。 • newnode函数为节点分配内存并用它的参数只来初始化节点的域。 • listnode还负责删除公共子表达式。
1、构建节点 Node listnodes(Tree tp, int tlab, int flab) { Node p = NULL, l, r; int op; if (tp == NULL) return NULL; if (tp->node) //node标识listnode访问过的树 return tp->node; if (isarray(tp->type)) op = tp->op + sizeop(voidptype->size); else op = tp->op + sizeop(tp->type->size); switch (generic(tp->op)) { <listnodes cases> } tp->node = p; return p; }
2、控制流 • 最简单的一元和二元操作加入结点表,但是并不会出现在根中。赋值等操作可以用这种情况解决。 要改变控制流需要跳转。 case JUMP: { l = listnodes(tp->kids[0], 0, 0); list(newnode(JUMP+V, l, NULL, NULL)); reset(); } break;
static void list(Node p) { if (p && p->link == NULL) { if (forest) { p->link = forest->link; forest->link = p; } else p->link = p; forest = p; } } forest是一个循环链表,不为空则指向链表最后一个节点,为空则将其初始化,link域可以表示根结点。
case LT: { //LT代表大于转移,是接口dag标识符 l = listnodes(tp->kids[0], 0, 0); r = listnodes(tp->kids[1], 0, 0); if (tlab) list(newnode(generic(tp->op) + opkind(l->op), l, r, findlabel(tlab))); else if (flab) { switch (generic(tp->op)) { case EQ: op = NE; break;case NE: op = EQ; break; case GT: op = LE; break; case LT: op = GE; break; case GE: op = LT; break; case LE: op = GT; break; default: assert(0); } list(newnode(op + opkind(l->op), l, r, findlabel(flab))); } if (forest && forest->syms[0]) forest->syms[0]->ref++; } break;
a[i]&&a[i]+b[i]>0&&a[i]+b[i]<10的森林 EQI2 LEI2 GEI2 LABELV2 INDIRI CNSTI 0 CNSTI 10 ADDI ADDP INDIRI LSHI ADDRGP a ADDP INDIRI CNSTI 2 ADDRGP b ADDRGP i
七、代码生成器 • 代码生成器:为编译前端提供接口函数,接口函数用与目标机器相关的指令来实现无关的中间代码。接口函数也为临时变量指派寄存器、固定的函数单元或栈空间。 • Lcc将大部分与机器无关的函数重组到一个大的与机器无关的程序中。
1、选择和发送指令 • Lcc的指令选择器时由程序lburg根据紧缩规范自动生成的,lburg是代码生成器的生成器。 • lburg接收紧缩规范并产生一个用c语言编写的树分析程序,该程序为目标机器选择指令。树分析程序接受中间代码的主题树,并将它分割成与目标机器相对应的程序块,成为数覆盖。
模式:ADDI(reg,con)表示如果ADDI的第一个子节点能递归的匹配reg,第二个子节点匹配con,那么该模式就在ADDI除匹配一棵树。模式:ADDI(reg,con)表示如果ADDI的第一个子节点能递归的匹配reg,第二个子节点匹配con,那么该模式就在ADDI除匹配一棵树。 规则:addr:ADDI(reg,con)规定了非终结符addr与上述模式相匹配 规则:stmt: ASGNI(addr,reg)规定了ASGNI节点的每子节点递归的与addr和reg匹配非终结符stmt就与该ASGNI匹配。 例: ASGNI(ADDP(INDIRP(ADDRLP(p)),CNSTI(4)),CNSTI(5))的覆盖 ASGNI stmt:ASGNI(addr,reg) reg:con con:CNSTI addr:ADDP(addr,con) ADDP CNSTI 5 reg:INDIRP(addr) INDIRP CNSTI 4 con:CNSTI ADDRLP p addr:ADDRLP