这些实际体验,都进一步验证了[20讲](https://time.geekbang.org/column/article/145472)中,IR的作用:我们能基于IR对接不同语言的前端,也能对接不同的硬件架构,还能做很多的优化。
既然IR有这些作用,那你可能会问,**IR都是什么样子的呢?有什么特点?如何生成IR呢?**
本节课,我就带你了解IR的特点,认识常见的三地址代码,学会如何把高级语言的代码翻译成IR。然后,我还会特别介绍LLVM的IR,以便后面使用LLVM这个工具。
首先,来看看IR的特征。
## 介于中间的语言
IR的意思是中间表达方式,它在高级语言和汇编语言的中间,这意味着,它的特征也是处于二者之间的。
与高级语言相比,IR丢弃了大部分高级语言的语法特征和语义特征,比如循环语句、if语句、作用域、面向对象等等,它更像高层次的汇编语言;而相比真正的汇编语言,它又不会有那么多琐碎的、与具体硬件相关的细节。
相信你在学习汇编语言的时候,会发现汇编语言的细节特别多。比如,你要知道很多指令的名字和用法,还要记住很多不同的寄存器。[在22讲](https://time.geekbang.org/column/article/147854),我提到,如果你想完整地掌握x86-64架构,还需要接触很多指令集,以及调用约定的细节、内存使用的细节等等([参见Intel的手册](https://software.intel.com/en-us/download/intel-64-and-ia-32-architectures-sdm-combined-volumes-1-2a-2b-2c-2d-3a-3b-3c-3d-and-4))。
仅仅拿指令的数量来说,据有人统计,Intel指令的助记符有981个之多!都记住怎么可能啊。**所以说,汇编语言并不难,而是麻烦。**
IR不会像x86-64汇编语言那么繁琐,但它却包含了足够的细节信息,能方便我们实现优化算法,以及生成针对目标机器的汇编代码。
另外,我在20讲提到,IR有很多种类(AST也是一种IR),每种IR都有不同的特点和用途,有的编译器,甚至要用到几种不同的IR。
我们在后端部分所讲的IR,目的是方便执行各种优化算法,并有利于生成汇编。**这种IR,可以看做是一种高层次的汇编语言,主要体现在:**
- 它可以使用寄存器,但寄存器的数量没有限制;
- 控制结构也跟汇编语言比较像,比如有跳转语句,分成多个程序块,用标签来标识程序块等;
- 使用相当于汇编指令的操作码。这些操作码可以一对一地翻译成汇编代码,但有时一个操作码会对应多个汇编指令。
下面来看看一个典型IR:三地址代码,简称TAC。
## 认识典型的IR:三地址代码(TAC)
下面是一种常见的IR的格式,它叫做三地址代码(Three Address Code, TAC),它的优点是很简洁,所以适合用来讨论算法:
```
x := y op z //二元操作
x := op y //一元操作
```
每条三地址代码最多有三个地址,其中两个是源地址(比如第一行代码的y和z),一个是目的地址(也就是x),每条代码最多有一个操作(op)。
我来举几个例子,带你熟悉一下三地址代码,**这样,你能掌握三地址代码的特点,从高级语言的代码转换生成三地址代码。**
**1.基本的算术运算:**
```
int a, b, c, d;
a = b + c * d;
```
TAC:
```
t1 := c * d
a := b + t1
```
t1是新产生的临时变量。当源代码的表达式中包含一个以上的操作符时,就需要引入临时变量,并把原来的一条代码拆成多条代码。
**2.布尔值的计算:**
```
int a, b;
bool x, y;
x = a * 2 < b;
y = a + 3 == b;
```
TAC:
```
t1 := a * 2;
x := t1 < b;
t2 := a + 3;
y := t2 == b;
```
布尔值实际上是用整数表示的,0代表false,非0值代表true。
**3.条件语句:**
```
int a, b c;
if (a < b )
c = b;
else
c = a;
c = c * 2;
```
TAC:
```
t1 := a < b;
IfZ t1 Goto L1;
c := a;
Goto L2;
L1:
c := b;
L2:
c := c * 2;
```
IfZ是检查后面的操作数是否是0,“Z”就是“Zero”的意思。这里使用了标签和Goto语句来进行指令的跳转(Goto相当于x86-64的汇编指令jmp)。
**4.循环语句:**
```
int a, b;
while (a < b){
a = a + 1;
}
a = a + b;
```
TAC:
```
L1:
t1 := a < b;
IfZ t1 Goto L2;
a := a + 1;
Goto L1;
L2:
a := a + b;
```
三地址代码的规则相当简单,我们可以通过比较简单的转换规则,就能从AST生成TAC。
在课程中,三地址代码主要用来描述优化算法,因为它比较简洁易读,操作(指令)的类型很少,书写方式也符合我们的日常习惯。**不过,我并不用它来生成汇编代码,因为它含有的细节信息还是比较少,**比如,整数是16位的、32位的还是64位的?目标机器的架构和操作系统是什么?生成二进制文件的布局是怎样的等等?
**我会用LLVM的IR来承担生成汇编的任务,**因为它有能力描述与目标机器(CPU、操作系统)相关的更加具体的信息,准确地生成目标代码,从而真正能够用于生产环境。
**在讲这个问题之前,我想先延伸一下,讲讲另外几种IR的格式,**主要想帮你开拓思维,如果你的项目需求,恰好能用这种IR实现,到时不妨拿来用一下:
首先是四元式。它是与三地址代码等价的另一种表达方式,格式是:(OP,arg1,arg2,result)所以,“a := b + c” 就等价于(+,b,c,a)。
另一种常用的格式是逆波兰表达式。它把操作符放到后面,所以也叫做后缀表达式。“b + c”对应的逆波兰表达式是“b c +”;而“a = b + c”对应的逆波兰表达式是“a b c + =”。