基本块和轨迹
在tree语言中含有一些与机器语言不一致的情况,但我们又必须将tree语言转换成汇编语言或者机器语言,例如
- CJUMP指令能够跳转到两个标号中的任意一个,但真正的机器指令在条件为false时是跳转到下一个指令;
- 在表达式中使用ESEQ会很不方便,因为它会使得子树的不同计算顺序产生不同的结果;
- CALL指令也可能会有问题;
因此我们必须要对任意一棵树进行转换,以避免上述情况:
- 将一棵树重写成不含SEQ和ESEQ的规范树;
- 将这一列树组合成其内不含标号和转移的基本块组合;
- 对基本快并形成一组trace;
规范树
什么是规范树:
- 无SEQ或者ESEQ
- 每一个CALL的父亲不是EXP,就是MOVE
ESEQ的转换
消除ESEQ的方法是,在树中一级一级地将它们提升,直至它们可以变成SEQ节点。
下面,介绍四种情况,其中三四两种情况的不同在于s1与e能否交换:
一般,常数和空语句可以与任何语句交换,其它假定不能交换。
一般重写规则
一般而言,对于每一种tree语言或者表达式,都会有等价的子表达式。
例如对于[e1, e2, ESEQ(s, e3)],需要将s抽取出来放到e2和e1的左边。如果可以交换,则得到(s; [e1,e2,e3]),如果不能交换,则是上图中第三种情况。
利用函数reorder接收一个表达式,并返回由(语句,表达式表)组成的一个偶对。其中语句必须包含所有在表达式表之前执行的操作。传入reorder的是一个链表,它会抽出所有的ESEQ。reorder对参数的链表中的每一个表达式都调用de_exp,而do_exp的实现又只是对除了ESEQ之外的任何类型的表达式建立其子表达式的地址表并调用reorder。
将call移到顶层
由于在tree语言允许call节点作为子表达式,但实际中call的实现是,每一个函数将它的结果返回到同一个规定的返回值寄存器中,那么后面的调用就可能把前面的调用给覆盖掉了。
因此,我们可以重写:
CALL(func, args)--->ESEQ(MOVE(TEMP t, CALL(func, args)), TEMP t)
在这种情况中,do_stm不会调用reorder去处理CALL节点,而是把f和args看做MOVE节点的儿子
线性语句表
一旦整个函数体都已经用do_stm处理完毕之后,所有的SEQ节点都集中在树的顶部。函数linearize重复施加规则,使得SEQ(SEQ(a,b),c) ==> SEQ(a, SEQ(b,c))
但SEQ节点不提供任何结构性信息,只是简单列表。
处理条件分支
tree语言与多数机器指令集不一样的是,它具有两路分支CJUMP指令。为了便于将tree转换为机器指令,我们需要重新安排CJUMP,使得CJUMP(cond, lt, lf)后面跟随的LABEL(lf)。
我们分两步实现这一目的:取一颗规范树,并由它们形成基本块;对这些基本块进行排序,形成trace轨迹。
基本块
在分析程序的控制流时,任何非分支指令的程序语句对分析都没有意义,因此可以将非分支指令的序列集中到基本块中。
基本块是语句组成的一个序列,只能从序列的开始进入并从结尾处退出:
- 第一个语句是一个LABEL;
- 最后一个语句是JUMP或者CJUMP;
- 没有其他的LABEL, JUMP或者CJUMP;
划分基本块的算法:
从头到尾扫描语句序列,遇到一个LABEL就开始一个新的基本块并结束前一个基本块,遇到一个JUMP或者CJUMP就结束一个基本块;若过程遗留的基本块中,有任何基本块不是以LABEL开始的,则新生成一个标号插入到开始;若有不是以JUMP结束的,则在这个基本块的末尾假如一个转移到下一个基本块标号的指令。
轨迹
由于我们以任意顺序安排基本块,程序结果都是一致的,因此我们可以合理地安排基本块,以满足每个CJUMP之后都跟随它的false标号的基本块。
另外,也可以安排基本块使得JUMP之后跟随的是它们的目标标号,然后删除这些转移。
还有,需要安排轨迹使得重叠的轨迹越来越少。
完善
主要针对的是CJUMP指令:
- 所有后面直接跟随的false标号的CJUMP维持不变;
- 对任何直接跟随true标号的CJUMP,交换true标号和false标号,并将条件改为相反;
- 如果既不是true也不是false标号直接跟随着,则新生成一个标号lf',重写CJUMP;
1 | CJUMP(cond, a, b, lt, lf') |