技术文章:编译器的中间代码生成

编译器的中间代码,是一种类似汇编三地址码

中间代码生成,就是把抽象语法树转化成中间代码的序列(线性表)。

计算机的内存就是一个巨大的一维数组,当源代码通过抽象语法树这个过渡结构,转化成中间代码的线性表之后,就与内存的结构一样了。

然后把它翻译成机器码之后,就可以直接加载到内存里运行,就像我们最后看到的可执行程序一样。

通俗点说,中间代码生成,就是把if else / while / for等复杂的分支循环结构,转化为if goto这种简单结构。

通过比较跳转来表达程序的复杂逻辑(与汇编一样)。

中间代码生成

上图分别是一段简单代码的源码、抽象语法树、和中间代码。

中间代码和汇编码的差别是非常小的,无非是汇编必须用寄存器,而中间代码可以用变量名

在指令的设计上,二者几乎相同。

中间代码的序列是一维的,如果没有跳转指令,代码就只能按照先后顺序依次运行。

源代码里的复杂逻辑结构,是在这个基础上通过跳转指令来实现的。

什么情况下跳转,需要根据比较运算的结果来确定。

在汇编里的比较运算,一般有2条指令:cmp和test。

cmp指令:它是把两个操作数做减法,然后根据结果大于0、小于0、等于0来作为条件。

test指令:它是把两个操作数做(按位)与运算,结果只有2个:等于0、不等于0

跳转指令,则分为绝对跳转(jmp)条件跳转(jcc)

中间代码的指令与汇编是类似的,也是这么设计的。

要把抽象语法树转化为中间代码,只需要把它遍历一遍就可以。

抽象语法树的遍历,必须根据每个节点的类型做不同的处理,这就是程序的语义

1,if / else语句的中间代码生成:

先处理条件表达式,然后处理if主体块,最后是else部分(有时没有)。

if (i < 10) i++;

else i--;

它的中间代码生成,过程如下:

首先处理条件表达式,获得中间代码:

cmp i, 10

这个比较运算有2个结果i < 10或i >= 10,前者进入if分支,后者进入else分支,所以这里要添加一行条件跳转

jge great,当i >= 10时,即条件不成立时,跳转到else分支。

然后处理if主体块,即i++,它的中间代码很简单:

inc i

在if主体块的末尾需要添加一个绝对跳转跳过接下来的else分支,到达整个if语句块之后的其他代码的开头:

jmp end,因为这里就几行代码,if分支处理完之后跳转到程序末尾end。

else分支的代码紧接在这里,因为是从条件表达式的后面跳转过来的,它带有1个标号:

great: dec i,

这个great标号,在生成机器码的时候要计算成具体的内存偏移量

在中间代码生成阶段,这个偏移量是没法确定的,暂时用标号表示。

最后是程序的末尾:

end.

2,while语句的中间代码生成

while (i < 10) i++;

while语句的中间代码生成,与if语句的区别是,它有一个往回跳的跳转指令。

在while主体块的末尾跳转回while的开头,再次进行条件表达式的检测,从而形成循环。

while的中间代码

条件表达式:

start:

cmp i, 10,

如果条件不成立,则直接跳过整个while语句块:

jge end,

while的主体部分:

inc i,

跳转回while的开头:

jmp start,因为要从这里跳转回开头,所以开头要加标号start

结尾:

end.

while流程图

while是一个循环语句,转化成中间代码之后的流程图如上。

一般情况下,在转化成中间代码时会把while语句写成if do while语句,以保证循环有唯一的入口和出口

while (i < 10) i++;

等价于:

if (i < 10) {

// 循环入口

do {

i++;

} while (i < 10);

// 循环出口

}

这么一改写之后,除非while循环里有goto语句,否则它的入口和出口必然在if语句块与do while语句块之间。

很多编程教材上都不建议使用goto,就是因为goto不但会打乱源代码的可读性,而且还会给编译器的循环分析制造麻烦。

3,for循环的中间代码生成

先处理初始化表达式,再处理条件表达式,然后是循环体,最后是更新表达式。

例如:

int sum = 0;

for (int i = 0; i < 10; i++) sum += i;

mov 0, sum,

for循环开始,初始化表达式:

mov 0, i,

条件表达式

cond:

cmp i, 10

jge end,// 如果条件不成立,跳转到末尾

循环体:

add sum, sum, i,// 循环体的三地址码

更新表达式:

inc i,

跳转回去,再次比较条件表达式:

jmp cond,

结束:

end.

for循环的初始化表达式并不会被多次运行,严格来说它并不是循环的一部分。

为了让for循环和while循环一样具有唯一的入口和出口,它也可以改写成if do while的形式:

int sum = 0;

int i = 0; // 初始化表达式

if (i < 10) { //条件表达式

// 循环入口

do {

sum += i; //循环体

i++; //更新表达式

} while (i < 10); // 条件表达式

// 循环出口

}

在循环体里只有break或continue语句,而没有goto语句的情况下,无论是while还是for循环的入口和出口都是唯一的

4,顺序表达式语句

这类语句的中间代码生成就比较简单了,就是根据运算符的优先级来的。

例如,a = b + c * d:

mul t0, c, d,

add t1, b, t0

mov t1, a,

其中t0和t1是编译器生成的临时变量,用于存储中间计算结果

在抽象语法树上,运算符的优先级越高,越接近叶子节点

我在scf编译器框架里,直接拿运算符的AST节点当作了临时变量[捂脸]懒得去额外记录临时变量了。

这只是一个大概的简介,具体的实现细节还是很多的。

scf的中间代码生成,在文件scf_operator_handler_3ac.c里,它在scf/core目录

中间代码也叫三地址码,3 address code,简写为3ac[呲牙]

发表评论
留言与评论(共有 0 条评论) “”
   
验证码:

相关文章

推荐文章