1
1
# 中间代码生成 - LLVM
2
2
3
- 对于大部分同学来说,可能是第一次接触 ** LLVM** ,虽然看上去上手比较困难,但了解了基本语法后还是比较容易的。本教程将分模块为大家讲解 LLVM 主要特点和语法结构,实验文法、C 语言函数与 LLVM IR 之间的对应转换关系,以及如何使用 LLVM 进行代码生成。建议学习过程中可以结合** 语法分析** 和** 中间代码生成** 。
3
+ 对于大部分同学来说,可能是第一次接触 ** LLVM** ,虽然看上去上手比较困难,但了解了基本语法后还是比较容易的。本教程将分模块为大家讲解 LLVM IR 主要特点和语法结构,实验文法、C 语言函数与 LLVM IR 之间的对应转换关系,以及如何使用 LLVM IR 进行代码生成。建议学习过程中可以结合** 语法分析** 和** 中间代码生成** 。
4
4
5
5
## 一、基本概念
6
6
@@ -151,7 +151,7 @@ int main() {
151
151
}
152
152
```
153
153
154
- 使用命令 `clang -S -emit-llvm main.c -o main.ll` 后,会在同目录下生成一个 `main.ll` 的文件。在 LLVM 中,注释以 `;` 打头。
154
+ 使用命令 `clang -S -emit-llvm main.c -o main.ll` 后,会在同目录下生成一个 `main.ll` 的文件。在 LLVM IR 中,注释以 `;` 打头。
155
155
156
156
```llvm
157
157
; ModuleID = 'main.c'
@@ -236,7 +236,7 @@ LLVM IR 使用的是**三地址码**。下面对上述代码进行简要注释
236
236
- 大括号中间的是函数体,是由一系列 ` BasicBlock ` 组成的。每个 ` BasicBlock ` 都有一个 label,label 使得该 ` BasicBlock ` 有一个符号表的入口点。` BasicBlock ` 以 terminator instruction(` ret ` 、` br ` 等)结尾。每个 ` BasicBlock ` 由一系列 ` Instruction ` 组成,` Instruction ` 是 LLVM IR 的基本指令。
237
237
- ` %7 = add i32 %5, %6 ` :随便拿上面一条指令来说,` %7 ` 是一个临时寄存器,是 ` Instruction ` 的实例,它的操作数里面有两个值,一个是 ` %5 ` ,一个是 ` %6 ` 。` %5 ` 和 ` %6 ` 也是临时寄存器,即前两条 ` Instruction ` 的实例。
238
238
239
- ### (3)LLVM 指令概览
239
+ ### (3)LLVM IR 指令概览
240
240
241
241
对于一些常用的 Instructions,下面给出示例。对于一些没有给出的,可以参考 [ LLVM IR 指令集] ( https://llvm.org/docs/LangRef.html#instruction-reference ) 。
242
242
@@ -250,28 +250,28 @@ LLVM IR 使用的是**三地址码**。下面对上述代码进行简要注释
250
250
| ` icmp ` | ` <result> = icmp <cond> <ty> <op1>, <op2> ` | 比较指令 |
251
251
| ` and ` | ` <result> = and <ty> <op1>, <op2> ` | 按位与 |
252
252
| ` or ` | ` <result> = or <ty> <op1>, <op2> ` | 按位或 |
253
- | ` call ` | ` <result> = call [ret attrs] <ty> <name>(<...args>) ` | 函数调用 |
253
+ | ` call ` | ` <result> = call [ret attrs] <ty> <name>(<...args>) ` | 函数调用 |
254
254
| ` alloca ` | ` <result> = alloca <type> ` | 分配内存 |
255
- | ` load ` | ` <result> = load <ty>, <ty>* <pointer> ` | 读取内存 |
256
- | ` store ` | ` store <ty> <value>, <ty>* <pointer> ` | 写内存 |
257
- | ` getelementptr ` | ` <result> = getelementptr <ty>, * {, [inrange] <ty> <idx>}* ` < br > ` <result> = getelementptr inbounds <ty>, <ty>* < ptrval>{, [inrange] <ty> <idx>}*` | 计算目标元素的位置(数组部分会单独详细说明) |
258
- | ` phi ` | ` <result> = phi [fast-math-flags] <ty> [ <val0>, <label0>], ... ` | / |
259
- | ` zext..to ` | ` <result> = zext <ty> <value> to <ty2> ` | 将 ` ty ` 的 ` value ` 的type扩充为 ` ty2 ` |
260
- | ` trunc..to ` | ` <result> = trunc <ty> <value> to <ty2> ` | 将 ` ty ` 的 ` value ` 的type缩减为 ` ty2 ` |
261
- | ` br ` | ` br i1 <cond>, label <iftrue>, label <iffalse> ` <br > ` br label <dest> ` | 改变控制流 |
262
- | ` ret ` | ` ret <type> <value> ` , ` ret void ` | 退出当前函数,并返回值 |
255
+ | ` load ` | ` <result> = load <ty>, ptr <pointer> ` | 读取内存 |
256
+ | ` store ` | ` store <ty> <value>, ptr <pointer> ` | 写内存 |
257
+ | ` getelementptr ` | ` <result> = getelementptr <ty>, ptr < ptrval>{, <ty> <idx>}* ` | 计算目标元素的位置(数组部分会单独详细说明) |
258
+ | ` phi ` | ` <result> = phi [fast-math-flags] <ty> [<val0>, <label0>], ... ` | / |
259
+ | ` zext..to ` | ` <result> = zext <ty> <value> to <ty2> ` | 将 ` ty ` 的 ` value ` 的 type 扩充为 ` ty2 ` (zero extend) |
260
+ | ` trunc..to ` | ` <result> = trunc <ty> <value> to <ty2> ` | 将 ` ty ` 的 ` value ` 的 type 缩减为 ` ty2 ` (truncate) |
261
+ | ` br ` | ` br i1 <cond>, label <iftrue>, label <iffalse> ` <br > ` br label <dest> ` | 改变控制流 |
262
+ | ` ret ` | ` ret <type> <value> ` , ` ret void ` | 退出当前函数,并返回值 |
263
263
264
- ### (4)LLVM 构建单元
264
+ ### (4)LLVM IR 构建单元
265
265
266
- 在 C/C++ 中,一个 ` .c ` /` .cpp ` 文件是一个编译单元,在 LLVM 中也是一样,一个 ` .ll ` 文件对应一个 ` Module ` 。在前的示例中大家可能已经注意到了,LLVM 有着非常严格清晰的结构,如下图。
266
+ 在 C/C++ 中,一个 ` .c ` /` .cpp ` 文件是一个编译单元,在 LLVM IR 中也是一样,一个 ` .ll ` 文件对应一个 ` Module ` 。在前的示例中大家可能已经注意到了,LLVM IR 有着非常严格清晰的结构,如下图。
267
267
268
268
![ llvm-0-3] ( imgs/chapter07-1/llvm-overview.svg )
269
269
270
270
> 细心的同学可能注意到了图中使用的箭头和斜体,不用怀疑,它们分别表示的就是面向对象中的继承与抽象类。
271
271
272
- 在 LLVM 中,一个 ` Module ` 由若干 * ` GlobalValue ` * 组成,而一个 * ` GlobalValue ` * 可以是** 全局变量** (` GlobalVariable ` ),也可以是** 函数** (` Function ` )。一个函数由若干** 基本块** (` BasicBlock ` )组成,与大家在理论课上学习的基本块是一样的。在基本块内部,则是由若干** 指令** (` Instruction ` )组成,也是 LLVM 的基本组成。
272
+ 在 LLVM IR 中,一个 ` Module ` 由若干 * ` GlobalValue ` * 组成,而一个 * ` GlobalValue ` * 可以是** 全局变量** (` GlobalVariable ` ),也可以是** 函数** (` Function ` )。一个函数由若干** 基本块** (` BasicBlock ` )组成,与大家在理论课上学习的基本块是一样的。在基本块内部,则是由若干** 指令** (` Instruction ` )组成,也是 LLVM IR 的基本组成。
273
273
274
- 可以发现,LLVM 中所有类都直接或间接继承自 ` Value ` ,因此在 LLVM 中,有** “一切皆 Value”** 的说法。通过规整的继承关系,我们就得到了 LLVM 的类型系统。为了表达 ` Value ` 之间的引用关系,LLVM 中还有一种特殊的 ` Value ` 叫做 ` User ` ,其将其他 ` Value ` 作为参数。例如,对于下面的代码:
274
+ 可以发现,LLVM IR 中所有类都直接或间接继承自 ` Value ` ,因此在 LLVM IR 中,有** “一切皆 Value”** 的说法。通过规整的继承关系,我们就得到了 LLVM IR IR 的类型系统。为了表达 ` Value ` 之间的引用关系,LLVM IR 中还有一种特殊的 ` Value ` 叫做 ` User ` ,其将其他 ` Value ` 作为参数。例如,对于下面的代码:
275
275
276
276
``` llvm
277
277
A: %add1 = add nsw i32 %a, %b
@@ -281,11 +281,11 @@ C: %sum = add nsw i32 %add1, %add2
281
281
282
282
其中,A 是一条 ` Instruction ` ,更具体的,是一个 ` BinaryOperator ` ,它在代码中的体现就是 ` %add1 ` ,即指令的返回值,也称作一个** 虚拟寄存器** 。` Instruction ` 继承自 ` User ` ,因此它可以将其他 ` Value ` (` %a ` 和 ` %b ` )作为参数。因此,在 ` %add1 ` 与 ` %a ` 、` %b ` 之间分别构成了 ` Use ` 关系。紧接着,` %add1 ` 又和 ` %add2 ` 一起作为 ` %sum ` 的参数,从而形成了一条 ` Use ` 链。
283
283
284
- > LLVM 是可以用非数字作为变量标识符的,但二者不能混用。
284
+ > LLVM IR 是可以用非数字作为变量标识符的,但二者不能混用。
285
285
286
- 这种指令间的关系正是 LLVM 的核心之一,实现了 SSA 形式的中间代码。这样的形式可以方便 LLVM 进行分析和优化,例如,在这样的形式下,可以很容易地识别出,` %add1 ` 和 ` %add2 ` 实际上是同一个值,因此可以进行替换。同时,如果一个 ` Value ` 没有 ` Use ` 关系,那么它很可能就是可以删除的冗余代码。
286
+ 这种指令间的关系正是 LLVM IR 的核心之一,实现了 SSA 形式的中间代码。这样的形式可以方便 LLVM IR 进行分析和优化,例如,在这样的形式下,可以很容易地识别出,` %add1 ` 和 ` %add2 ` 实际上是同一个值,因此可以进行替换。同时,如果一个 ` Value ` 没有 ` Use ` 关系,那么它很可能就是可以删除的冗余代码。
287
287
288
- > 同学们可以根据自己的需要自行设计数据类型。但如果对代码优化效果要求不高,这部分内容其实没有那么重要,可以直接面向 AST 生成代码。
288
+ > 同学们可以根据自己的需要自行设计数据类型。但如果对代码优化效果要求不高,这部分内容其实没有那么重要,可以直接面向 AST 生成代码。(你甚至不需要中间代码就能生成目标代码。)
289
289
290
290
### (5)一些说明
291
291
@@ -298,9 +298,9 @@ LLVM 是一个非常庞大的系统,这里介绍的仅仅是它的冰山一角
298
298
299
299
这一部分代码生成的目标是根据语法、语义分析生成的 AST,构建出 LLVM IR(或是四元式)。想继续生成 MIPS 代码的同学也可以先生成 LLVM IR 来检验自己中间代码的正确性。
300
300
301
- 大家可能会发现,` clang ` 生成的 LLVM IR 中,虚拟寄存器是按数字命名的。LLVM 限制了一个函数内所有数字命名的虚拟寄存器必须** 严格从 0 开始递增** 。其实,这些数字就是每个(对应了虚拟寄存器的) ` Value ` 在 ` Function ` 内的顺序,实现起来并没有想象中那么复杂,可以参考 LLVM 中的 ` SlotTracker ` 类。如果不想严格按照数字命名,那么就需要使用非数字的字符串命名,两种方式不能混用。
301
+ 大家可能会发现,` clang ` 生成的 LLVM IR 中,虚拟寄存器是按数字命名的。LLVM IR 限制了一个函数内所有数字命名的虚拟寄存器必须** 严格从 0 开始递增** 。其实,这些数字就是每个(对应了虚拟寄存器的) ` Value ` 在 ` Function ` 内的顺序,实现起来并没有想象中那么复杂,可以参考 LLVM IR 中的 ` SlotTracker ` 类。如果不想严格按照数字命名,那么就需要使用非数字的字符串命名,两种方式不能混用。
302
302
303
- LLVM 的架构比较复杂,为了方便同学们理解,我们定义了一个较为简单的语法 tolangc,并为它实现了一套相对简单的 LLVM IR 数据结构,大家可以进行参考。
303
+ LLVM IR 的架构比较复杂,为了方便同学们理解,我们定义了一个较为简单的语法 tolangc,并为它实现了一套相对简单的 LLVM IR 数据结构,大家可以进行参考。
304
304
305
305
## 三、主函数与常量表达式
306
306
@@ -392,16 +392,16 @@ define dso_local i32 @main() {
392
392
CompUnit → {Decl} MainFuncDef
393
393
Decl → ConstDecl | VarDecl
394
394
ConstDecl → 'const' BType ConstDef { ',' ConstDef } ';'
395
- BType → 'int'
396
- ConstDef → Ident '=' ConstInitVal
395
+ BType → 'int' | 'char'
396
+ ConstDef → Ident '=' ConstInitVal
397
397
ConstInitVal → ConstExp
398
398
ConstExp → AddExp
399
399
VarDecl → BType VarDef { ',' VarDef } ';'
400
400
VarDef → Ident | Ident '=' InitVal
401
401
InitVal → Exp
402
402
```
403
403
404
- 在 LLVM 中,全局变量使用的是和函数一样的全局标识符 `@` ,所以全局变量的写法其实和函数的定义几乎一样。在本次的实验中,全局变/常量声明中指定的初值表达式必须是**常量表达式**,例如:
404
+ 在 LLVM IR 中,全局变量使用的是和函数一样的全局标识符 `@` ,所以全局变量的写法其实和函数的定义几乎一样。在本次的实验中,全局变/常量声明中指定的初值表达式必须是**常量表达式**,例如:
405
405
406
406
```c
407
407
// 以下都是全局变量
@@ -418,6 +418,8 @@ char b = 2 + 3;
418
418
419
419
可以看到,对于全局变量中的常量表达式,在生成的 LLVM IR 中需要直接算出其** 具体的值** ,同时,也需要完成必要的类型转换。此外,全局变量对应的是一个地址,使用时需要结合 ` load ` /` store ` 指令。
420
420
421
+ > 需要注意的是,我们的文法中存在 ` int ` 和 ` char ` 两个不同大小的类型,因此我们需要选择 LLVM IR 中合适的类型,即 ` i32 ` 和 ` i8 ` 。LLVM IR 中,不同类型的变量不能直接运算,因此会涉及类型转换,具体内容见类型转换部分。
422
+
421
423
### (2)局部变量与作用域
422
424
423
425
本章内容涉及文法包括:
@@ -443,6 +445,8 @@ store i32 %2, i32* %1
443
445
444
446
注意到,对于局部变量,我们首先需要通过 ` alloca ` 指令分配一块内存,才能对其进行 ` load ` /` store ` 操作。
445
447
448
+ > 与全局变量相同,我们同样需要为 ` int ` 和 ` char ` 选择对应的类型。
449
+
446
450
### (3)符号表设计
447
451
448
452
这一章将主要考虑变量,包括** 全局变量** 和** 局部变量** 以及** 作用域** 的说明。不可避免地,同学们需要进行符号表的设计。
@@ -469,7 +473,7 @@ int main() {
469
473
}
470
474
```
471
475
472
- 如果需要将上述代码转换为 LLVM,应当怎么考虑呢?直观来看,` a ` 和 ` b ` 是** 全局变量** ,` c ` 是** 局部变量** 。直观上来说,过程是首先将全局变量 ` a ` 和 ` b ` 进行赋值,然后进入到 ` main ` 函数内部,对 ` c ` 进行赋值。那么生成的 ` main ` 函数的 LLVM IR 可能如下。
476
+ 如果需要将上述代码转换为 LLVM IR ,应当怎么考虑呢?直观来看,` a ` 和 ` b ` 是** 全局变量** ,` c ` 是** 局部变量** 。直观上来说,过程是首先将全局变量 ` a ` 和 ` b ` 进行赋值,然后进入到 ` main ` 函数内部,对 ` c ` 进行赋值。那么生成的 ` main ` 函数的 LLVM IR 可能如下。
473
477
474
478
``` llvm
475
479
@a = dso_local global i32 1
@@ -491,7 +495,7 @@ define dso_local i32 @main() {
491
495
492
496
这时候就有问题了,在 AST 中,不同地方的标识符之间是毫无关系的,那么如何确定代码中出现的 ` a ` 是谁,以及出现的多个 ` b ` 是不是同一个变量?这时候符号表的作用就体现出来了。简单来说,符号表类似于一个索引,通过它可以很快速的找到标识符对应的变量。
493
497
494
- 在 SysY 中,我们遵循** 先声明,后使用** 的原则,因此可以在第一次遇见变量声明时,将标识符与对应的 LLVM ` Value ` 添加到符号表中,那么之后再次遇到标识符时就可以获得最初的声明,找不到的话说明出现了使用未定义变量的错误。
498
+ 在 SysY 中,我们遵循** 先声明,后使用** 的原则,因此可以在第一次遇见变量声明时,将标识符与对应的 LLVM IR 中的 ` Value ` 添加到符号表中,那么之后再次遇到标识符时就可以获得最初的声明,找不到的话说明出现了使用未定义变量的错误。
495
499
496
500
> 注意,虚拟寄存器的数字并不是符号表的一部分,它们只是输出时的标记。因此,只需要在输出 LLVM IR 时,遍历一遍 ` Function ` 内的所有 ` Value ` ,对其标号即可。
497
501
@@ -506,7 +510,7 @@ int c = 3;
506
510
int main () {
507
511
int d = 4;
508
512
int e = 5;
509
- { // blockA
513
+ { // BlockA
510
514
int a = 7;
511
515
int e = 8;
512
516
int f = 9;
@@ -516,9 +520,9 @@ int main() {
516
520
}
517
521
```
518
522
519
- 在上面的程序中,在 ** blockA ** 中,` a ` 的值为 ` 7 ` ,覆盖了全局变量 ` a = 1 ` ,` e ` 覆盖了 ` main ` 中的 ` e = 5 ` ,而在 ` main ` 的最后一行,` f ` 并不存在覆盖,因为 ` main ` 外层不存在其他 ` f ` 的定义。
523
+ 在上面的程序中,在 BlockA 中,` a ` 的值为 ` 7 ` ,覆盖了全局变量 ` a = 1 ` ,` e ` 覆盖了 ` main ` 中的 ` e = 5 ` ,而在 ` main ` 的最后一行,` f ` 并不存在覆盖,因为 ` main ` 外层不存在其他 ` f ` 的定义。
520
524
521
- 同样的,下面给出上述程序的 LLVM 代码:
525
+ 同样的,下面给出上述程序的 LLVM IR 代码:
522
526
523
527
``` llvm
524
528
@a = dso_local global i32 1
@@ -547,6 +551,8 @@ define dso_local i32 @main() {
547
551
548
552
<img src =" imgs/chapter07-1/llvm-2-1.png " style =" zoom :50% ;" />
549
553
554
+ 所有的变量都会唯一对应一个中间代码的值,因此在中间代码里不会有重名问题。
555
+
550
556
### (4)测试样例
551
557
552
558
源程序如下。
@@ -570,7 +576,7 @@ int main() {
570
576
}
571
577
```
572
578
573
- LLVM 参考如下:
579
+ LLVM IR 参考如下:
574
580
575
581
``` llvm
576
582
@a = dso_local global i32 1
@@ -630,13 +636,13 @@ Stmt → LVal '=' 'getint''('')'';'
630
636
```
631
637
632
638
633
- 首先添加**库函数**的调用,在实验的 LLVM 代码中,库函数的声明如下:
639
+ 首先添加**库函数**的调用,在实验的 LLVM IR 代码中,库函数的声明如下:
634
640
635
641
```llvm
636
642
declare i32 @getint()
637
- declare i32 @getch ()
643
+ declare i32 @getchar ()
638
644
declare void @putint(i32)
639
- declare void @putchar (i8)
645
+ declare void @putch (i8)
640
646
declare void @putstr(i8*)
641
647
```
642
648
@@ -648,49 +654,50 @@ declare void @putstr(i8*)
648
654
649
655
``` c
650
656
int main () {
651
- int a, b;
657
+ int a;
658
+ char b;
652
659
a = getint();
653
660
b = getchar();
654
- printf ("Hello: %d, %d ", a, b);
661
+ printf ("Hello: %d, %c ", a, b);
655
662
return 0;
656
663
}
657
664
```
658
665
659
- 生成的 LLVM 代码如下:
666
+ 生成的 LLVM IR 代码如下:
660
667
661
668
``` llvm
662
669
declare i32 @getint()
663
- declare i32 @getch ()
670
+ declare i32 @getchar ()
664
671
declare void @putint(i32)
665
- declare void @putchar (i32)
672
+ declare void @putch (i32)
666
673
declare void @putstr(i8*)
667
674
668
675
669
676
@.str = private unnamed_addr constant [8 x i8] c"Hello: \00", align 1
670
677
@.str.1 = private unnamed_addr constant [3 x i8] c", \00", align 1
671
- @.str.2 = private unnamed_addr constant [2 x i8] c"\0A\00", align 1
672
678
673
679
define dso_local i32 @main() {
674
680
%1 = alloca i32
675
- %2 = alloca i32
681
+ %2 = alloca i8
676
682
%3 = call i32 @getint()
677
683
store i32 %3, i32* %1
678
- %4 = call i32 @getchar()
679
- store i32 %4, i32* %2
680
- %5 = load i32, i32* %2
681
- %6 = load i32, i32* %1
684
+ %4 = call i32 @getchar() ; 注意 getchar 的返回值类型
685
+ %5 = trunc i32 %4 to i8 ; 不可避免地进行一次类型转换
686
+ store i8 %5, i8* %2
687
+ %6 = load i8, i8* %2
688
+ %7 = load i32, i32* %1
682
689
call void @putstr(i8* getelementptr inbounds ([8 x i8], [8 x i8]* @.str, i64 0, i64 0))
683
- call void @putint(i32 %6 )
690
+ call void @putint(i32 %7 )
684
691
call void @putstr(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str.1, i64 0, i64 0))
685
- call void @putchar(i32 %5)
686
- call void @putstr(i8* getelementptr inbounds ([2 x i8], [2 x i8]* @.str.2, i64 0, i64 0) )
692
+ %8 = zext i8 %6 to i32
693
+ call void @putch(i32 %8 )
687
694
ret i32 0
688
695
}
689
696
```
690
697
691
698
不难看出,` call i32 @getint() ` 即为调用 ` getint ` 的语句,` call i32 @getchar() ` 即为调用 ` getchar ` 的语句,对于其他的任何函数的调用也是像这样去写。
692
699
693
- 对于 ` printf ` ,则需要将其转化为多条语句,将格式字符串与变量值分别输出。对于格式字符串的输出,可以直接调用 ` putstr ` ,比较方便,之后也可以直接编译为 MIPS 中的 4 号系统调用。这种方式需要配合全局的字符串常量,不过由于仅需考虑输出字符串这一种情况,因此可以将输出硬编码。注意 LLVM 中需要对 ` \n ` 和 ` \0 ` 进行转义 。当然,也可以转化为多个 ` putchar ` 的调用。
700
+ 对于 ` printf ` ,则需要将其转化为多条语句,将格式字符串与变量值分别输出。对于格式字符串的输出,可以直接调用 ` putstr ` ,比较方便,之后也可以直接编译为 MIPS 中的 4 号系统调用。这种方式需要配合全局的字符串常量,不过由于仅需考虑输出字符串这一种情况,因此可以将输出硬编码。注意 LLVM IR 中需要对 ` \n ` 和 ` \0 ` 等进行转义 。当然,也可以转化为多个 ` putch ` 的调用。
694
701
695
702
696
703
### (2)函数定义与调用
@@ -709,10 +716,6 @@ UnaryExp → PrimaryExp | Ident '(' [FuncRParams] ')' | UnaryOp UnaryExp
709
716
710
717
其实之前的 main 函数也是一个函数,即主函数。这里将其拓广到一般函数。对于一个函数,其特征包括**函数名**,**函数返回类型**和**参数**。在本实验中,函数返回类型有`char`、`int` 和 `void` 三种,参数类型有 `char` 和 `int` 两种。
711
718
712
- > 在这里我们讨论一下 `char` 类型数据的处理。
713
- >
714
- > 我们从文法中可以看出,`char` 类型的数据在输入时因为字面量的存在,需要和 `int` 类型区分;在赋值时需要截断高位的数字。在计算和输出时,`int` 和 `char` 并无区别,都作为常量按数字处理。因此一种简单的处理方法是,在语义分析中,我们可以将 `int` 和 `char` 都视为统一的数字,**用 i32** 作为 LLVM 中的**统一数据类型**存储使用,在赋值时特殊处理。因此,函数的类型在中间代码翻译时可以简化为 `void` 和 `i32`,函数参数类型可以统一成 `i32`。当然,最好是能够实现 `char` 类型的处理,将其翻译成 `i8`。
715
-
716
719
`FuncFParams` 之后的 `Block` 则与之前主函数内处理方法一样。值得一提的是,由于每个**临时寄存器**和**基本块**占用一个编号,所以没有参数的函数的第一个临时寄存器的编号应该从 **1** 开始,因为函数体入口占用了一个编号 0。而有参数的函数,参数编号从 **0** 开始,**进入 Block 后需要跳过一个基本块入口的编号**(可以参考测试样例)。
717
720
718
721
> 当然,如果全部采用字符串编号寄存器,上述问题都不会存在。
@@ -744,13 +747,13 @@ int main() {
744
747
}
745
748
```
746
749
747
- LLVM 输出参考:
750
+ LLVM IR 输出参考:
748
751
749
752
``` llvm
750
753
declare i32 @getint()
754
+ declare i32 @getchar()
751
755
declare void @putint(i32)
752
- declare i32 @getch()
753
- declare void @putchar(i32)
756
+ declare void @putch(i32)
754
757
declare void @putstr(i8*)
755
758
756
759
@a = dso_local global i32 1000
@@ -807,7 +810,7 @@ LAndExp → EqExp | LAndExp '&&' EqExp
807
810
LOrExp → LAndExp | LOrExp ' ||' LAndExp
808
811
```
809
812
810
- > 在条件语法中,需要同学们进行条件的判断与选择。这时采用数字编号的同学可能会对基本块的标号产生疑惑,因为需要跳转到之后还未构建的基本块。你可能回想到** 回填** 操作,但是之后每次优化后 LLVM 都会发生变化,因此并不实际。所以最佳的做法还是使用 LLVM 中的 ` SlotTracker ` ,因为编号其实只用在输出中,所以只需要在输出前,遍历每个 ` Function ` 中的所有 ` Value ` ,按顺序为它们分配编号即可。
813
+ > 在条件语法中,需要同学们进行条件的判断与选择。这时采用数字编号的同学可能会对基本块的标号产生疑惑,因为需要跳转到之后还未构建的基本块。你可能回想到** 回填** 操作,但是之后每次优化后 LLVM IR 都会发生变化,因此并不实际。所以最佳的做法还是使用 LLVM IR 中的 ` SlotTracker ` ,因为编号其实只用在输出中,所以只需要在输出前,遍历每个 ` Function ` 中的所有 ` Value ` ,按顺序为它们分配编号即可。
811
814
812
815
要写出条件语句,首先要理清楚逻辑。在上述文法中,最重要的莫过于下面这一条语法:
813
816
@@ -820,13 +823,13 @@ Stmt → 'if' '(' Cond ')' Stmt1 [ 'else' Stmt2 ] BasicBlock3
820
823
<img src =" imgs/chapter07-1/llvm-4-1.png " style =" zoom :50% ;" />
821
824
822
825
823
- 首先进行 Cond 结果的判断,如果结果为 ** 1** 则进入 ** Stmt1** ,如果 Cond 结果为** 0** ,若文法有 ` else ` 则将进入 ** Stmt2** ,否则进入下一条文法的基本块 ** BasicBlock3** 。在 Stmt1 或 Stmt2 执行完成后都需要跳转到 BasicBlock3。对于一个 LLVM 程序来说,对一个含 ` else ` 的条件分支,其基本块构造可以如右上图所示。如果能够理清楚基本块跳转的逻辑,那么在写代码的时候就会变得十分简单。
826
+ 首先进行 Cond 结果的判断,如果结果为 ** 1** 则进入 ** Stmt1** ,如果 Cond 结果为** 0** ,若文法有 ` else ` 则将进入 ** Stmt2** ,否则进入下一条文法的基本块 ** BasicBlock3** 。在 Stmt1 或 Stmt2 执行完成后都需要跳转到 BasicBlock3。对于一个 LLVM IR 程序来说,对一个含 ` else ` 的条件分支,其基本块构造可以如右上图所示。如果能够理清楚基本块跳转的逻辑,那么在写代码的时候就会变得十分简单。
824
827
825
828
这时候再回过头去看 Cond 里面的代码,即 LOr 和 LAnd,Eq 和 Rel。不难发现,其处理方式和加减乘除非常像,除了运算结果都是 1 位(i1)而非 32 位(i32)。同学们可能需要用到 ` trunc ` 或者 ` zext ` 指令进行类型转换。
826
829
827
830
### (2)短路求值
828
831
829
- 可能有的同学会认为,反正对于 LLVM 来说,跳转与否只看 Cond 的值,所以只要把 Cond 算完结果就行,不会影响正确性。不妨看一下下面这个例子:
832
+ 可能有的同学会认为,反正对于 LLVM IR 来说,跳转与否只看 Cond 的值,所以只要把 Cond 算完结果就行,不会影响正确性。不妨看一下下面这个例子:
830
833
831
834
``` c
832
835
int a = 5 ;
@@ -842,7 +845,7 @@ int main() {
842
845
}
843
846
```
844
847
845
- 如果要将上面这段代码翻译为 LLVM,同学们会怎么做?如果按照传统方法,即先** 统一计算 Cond** ,则一定会执行一次 ` change() ` 函数,把全局变量的值变为 ** 6** 。但事实上,由于短路求值的存在,在读完 1 后,整个 ` Cond ` 的值就** 已经被确定** 了,即无论 ` 1 || ` 后面跟的是什么,都不影响 ` Cond ` 的结果,那么根据短路求值,后面的东西就不应该执行。所以上述代码的输出应当为 ** 5** 而不是 6,也就是说, LLVM 不能够单纯的把 ` Cond ` 计算完后再进行跳转。这时候就需要对 ` Cond ` 的跳转逻辑进行改写。
848
+ 如果要将上面这段代码翻译为 LLVM IR ,同学们会怎么做?如果按照传统方法,即先** 统一计算 Cond** ,则一定会执行一次 ` change() ` 函数,把全局变量的值变为 ** 6** 。但事实上,由于短路求值的存在,在读完 1 后,整个 ` Cond ` 的值就** 已经被确定** 了,即无论 ` 1 || ` 后面跟的是什么,都不影响 ` Cond ` 的结果,那么根据短路求值,后面的东西就不应该执行。所以上述代码的输出应当为 ** 5** 而不是 6,也就是说, LLVM IR 不能够单纯的把 ` Cond ` 计算完后再进行跳转。这时候就需要对 ` Cond ` 的跳转逻辑进行改写。
846
849
847
850
改写之前同学们不妨思考一个问题,即什么时候跳转。根据短路求值,只要条件判断出现“短路”,即不需要考虑后续与或参数的情况下就已经能确定值的时候,就可以进行跳转。或者更简单的来说,当 ** LOrExp 值为 1** 或者 ** LAndExp 值为 0** 的时候,就已经没有必要再进行计算了。
848
851
@@ -901,13 +904,13 @@ int main() {
901
904
}
902
905
```
903
906
904
- 参考 LLVM 如下。
907
+ 参考 LLVM IR 如下。
905
908
906
909
``` llvm
907
910
declare i32 @getint()
908
- declare i32 @getch ()
911
+ declare i32 @getchar ()
909
912
declare void @putint(i32)
910
- declare void @putchar (i32)
913
+ declare void @putch (i32)
911
914
declare void @putstr(i8*)
912
915
913
916
@a = dso_local global i32 1
@@ -1067,13 +1070,13 @@ int main() {
1067
1070
}
1068
1071
```
1069
1072
1070
- 参考 LLVM 如下。
1073
+ 参考 LLVM IR 如下。
1071
1074
1072
1075
``` llvm
1073
1076
declare i32 @getint()
1074
- declare i32 @getch ()
1077
+ declare i32 @getchar ()
1075
1078
declare void @putint(i32)
1076
- declare void @putchar (i32)
1079
+ declare void @putch (i32)
1077
1080
declare void @putstr(i8*)
1078
1081
1079
1082
@.str = private unnamed_addr constant [7 x i8] c"round \00", align 1
@@ -1197,9 +1200,15 @@ LVal → Ident {'[' Exp ']'}
1197
1200
`getelementptr` 指令的工作是计算地址。其本身不对数据做任何访问与修改。其语法如下:
1198
1201
1199
1202
```llvm
1200
- <result> = getelementptr <ty>, <ty>* <ptrval>, { <ty> <index >}*
1203
+ <result> = getelementptr <ty>, <ty>* <ptrval>{, [inrange] <ty> <idx >}*
1201
1204
```
1202
1205
1206
+ > 也可以为 ` getelementptr ` 的参数添加括号,如下。
1207
+ >
1208
+ > ``` llvm
1209
+ > <result> = getelementptr (<ty>, <ty>* <ptrval>{, [inrange] <ty> <idx>}*)
1210
+ > ```
1211
+
1203
1212
现在来理解一下上面这一条指令。第一个 `<ty>` 表示的是第一个索引所指向的类型,有时也是**返回值的类型**。第二个 `<ty>` 表示的是后面的指针基地址 `<ptrval>` 的类型, `<ty> <index>` 表示的是一组索引的类型和值,在本实验中索引的类型为 `i32`。索引指向的基本类型确定的是增加索引值时指针的偏移量。
1204
1213
1205
1214
说完理论,不如结合一个实例来讲解。考虑数组 `a[5]`,需要获取 `a[3]` 的地址,有如下写法:
@@ -1220,12 +1229,12 @@ LVal → Ident {'[' Exp ']'}
1220
1229
例如,我们有如下的全局数组。
1221
1230
1222
1231
``` c
1223
- int a[1 + 2 + 3 + 4 ] = {1, 1 + 1, 1 + 3 - 1, 0, 0, 0, 0, 0, 0, 0};
1232
+ int a[1 + 2 + 3 + 4 ] = { 1, 1 + 1, 1 + 3 - 1, 0, 0, 0, 0, 0, 0, 0 };
1224
1233
int b[ 20] ;
1225
1234
char c[ 8] = "foobar";
1226
1235
```
1227
1236
1228
- 对应的 LLVM 中的数组如下。
1237
+ 对应的 LLVM IR 中的数组如下。
1229
1238
1230
1239
```llvm
1231
1240
@a = dso_local global [10 x i32] [i32 1, i32 2, i32 3, i32 0, i32 0, i32 0, i32 0, i32 0, i32 0, i32 0]
@@ -1243,10 +1252,14 @@ char c[8] = "foobar";
1243
1252
1244
1253
对于局部数组,在定义的时候同样需要使用 ` alloca ` 指令,其存取指令同样采用 ** load 和 store** ,只是在此之前需要采用 ` getelementptr ` 获取数组内应位置的地址。
1245
1254
1246
- 字符数组的字符串常量初始化,可以自行设计实现。LLVM 中,全局字符数组的字符串常量初始化可以直接通过字符串赋值,局部字符数组则也需要通过 ` alloca ` 指令分配内存空间,逐个元素初始化。不要忘记字符串常量末尾的结束符 '\00' 和填充符号 '\00'。
1255
+ 字符数组的字符串常量初始化,可以自行设计实现。LLVM IR 中,全局字符数组的字符串常量初始化可以直接通过字符串赋值,局部字符数组则也需要通过 ` alloca ` 指令分配内存空间,逐个元素初始化。不要忘记字符串常量末尾的结束符 '\00' 和填充符号 '\00'。
1247
1256
1248
1257
对于数组传参,其中涉及到维数的变化问题,例如,对于参数中** 含维度的数组** ,同学们可以参考上述 ` getelementptr ` 指令自行设计,因为该指令很灵活,所以下面的测试样例仅仅当一个参考。同学们可以将自己生成的 LLVM IR 使用 ` lli ` 编译后自行查看输出比对。
1249
1258
1259
+ 此外,你可能注意到,教程中生成的 ` getelementptr ` 指令中添加了 ` inbound ` 。当指定 ` inbound ` 时,如果下标访问超过了数组的实际大小,那么 ` getelementptr ` 就会返回一个 “poison” 值,而不是正常计算得到的地址,算是一种越界检查。官网对此的描述如下。
1260
+
1261
+ > "With the ` inbounds ` keyword, the result value of the GEP is ` poison ` if the address is outside the actual underlying allocated object and not the address one-past-the-end."
1262
+
1250
1263
### (3)测试样例
1251
1264
1252
1265
源代码如下。
@@ -1265,13 +1278,13 @@ int main() {
1265
1278
}
1266
1279
```
1267
1280
1268
- 参考 LLVM 如下。
1281
+ 参考 LLVM IR 如下。
1269
1282
1270
1283
``` llvm
1271
1284
declare i32 @getint()
1272
- declare i32 @getch ()
1285
+ declare i32 @getchar ()
1273
1286
declare void @putint(i32)
1274
- declare void @putchar (i32)
1287
+ declare void @putch (i32)
1275
1288
declare void @putstr(i8*)
1276
1289
1277
1290
@a = dso_local global [6 x i32] [i32 1, i32 2, i32 3, i32 4, i32 5, i32 6]
@@ -1338,13 +1351,13 @@ int main()
1338
1351
}
1339
1352
```
1340
1353
1341
- 对于 ` char ` 类型的溢出,我们的实验中统一按照无符号处理,即 ` char ` 的取值范围是 0 ~ 255。那么对于上面的源代码 ,可能的 LLVM 如下。
1354
+ 当 ` int ` 类型值赋给 ` char ` 类型变量时,需要进行截断。在 LLVM IR 中,我们可以直接使用 ` trunc ` 指令将 ` i32 ` 转换为 ` i8 ` 。同理,当 ` char ` 类型值赋给 ` int ` 类型变量时,需要进行扩展。由于我们文法中限制了 ` char ` 的取值大于零,所以我们使用无符号扩展 ` zext ` 即可。因此那么对于上面的源代码 ,可能的 LLVM IR 如下。
1342
1355
1343
1356
``` llvm
1344
1357
declare i32 @getint()
1345
- declare i32 @getch ()
1358
+ declare i32 @getchar ()
1346
1359
declare void @putint(i32)
1347
- declare void @putchar (i32)
1360
+ declare void @putch (i32)
1348
1361
declare void @putstr(i8*)
1349
1362
1350
1363
@.str = private unnamed_addr constant [5 x i8] c"a = \00", align 1
@@ -1387,3 +1400,4 @@ a = 255, b = 2
1387
1400
b = 1
1388
1401
```
1389
1402
1403
+ 此外,参数传递也可以视为特殊的赋值,当 ` int ` 类型值传递给 ` char ` 类型参数(或反之)时,同样需要进行类型转换。当然,如果是数组,那么是没法进行转换的,如果出现,则应该报错。
0 commit comments