|
|
@@ -99,6 +99,8 @@ int main() {
|
|
|
| Target Code | 目标代码 |
|
|
|
| Code Optimizer | 目标代码优化器 |
|
|
|
|
|
|
+### 词法分析
|
|
|
+
|
|
|
**扫描器**:简单地进行**词法分析**,运用类似有**限状态机**的算法可以将源代码的字符序序列分割成一系列的**记号**(`Token`)
|
|
|
|
|
|
通过 `Scanner` 扫描器的词法分析产生的记号 `Token` 一般分为一下几类:关键字、标识符、字面量(数字、字符串等)和特殊符号(加号、减号等)
|
|
|
@@ -130,8 +132,12 @@ array[index] = (index + 4) * (2 + 6);
|
|
|
| 6 | 数字 |
|
|
|
| `)` | 右圆括号 |
|
|
|
|
|
|
+### 语法分析
|
|
|
+
|
|
|
语法分析器(`Grammar Parser`) 对扫描器(`Scanner`) 产生的记号进行语法分析,从而产生语法树(`Syntax Tree`),整个过程采用上下文无关语法(`Context-Free Grammar`)
|
|
|
|
|
|
+对于 `int x = (1 + 2;` 这种错误就是在语法分析阶段被检查出来
|
|
|
+
|
|
|
通过**语法分析器**生成的**语法树**就是以**表达式**为节点的树
|
|
|
|
|
|

|
|
|
@@ -142,3 +148,121 @@ array[index] = (index + 4) * (2 + 6);
|
|
|
|
|
|
> 对于不同的语言,编译器的开发者只需要改变语法规则,而无需为每个编译器编写一个语法分析器
|
|
|
|
|
|
+### 语义分析
|
|
|
+
|
|
|
+语法分析仅仅完成了对表达式的语法层面的分析,但是并不了解这个语句是否真正有意义
|
|
|
+
|
|
|
+```cpp
|
|
|
+int* p1 = NULL;
|
|
|
+int* p2 = NULL;
|
|
|
+int p3 = p1 * p2;
|
|
|
+```
|
|
|
+
|
|
|
+比如上述代码在语法分析阶段是通过的,但是在语义分析阶段不能通过
|
|
|
+
|
|
|
+编译器能分析的语义是静态语义(`Static Semantic`),也就是编译器可以确定的语义。对应的还有动态语义(`dynamic Semantic`),只有在运行期才能确定的语义
|
|
|
+
|
|
|
+- 静态语义:类型检查、变量声明检查、控制流检查等。例如,编译器会检查变量是否在使用前声明,类型转换是否合法等。这些检查在编译时就能确定,因此称为静态语义
|
|
|
+- 动态语义:数组越界、除零错误等。这些错误只有在程序实际运行时才能被检测到,因此称为动态语义
|
|
|
+
|
|
|
+经过语义分析之后,语法树的表达式都会被标识上类型,如果有些类型需要做隐式转换,语义分析程序会在语法树中插入相应的转换节点
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+### 中间语言生成
|
|
|
+
|
|
|
+现代编译器有很多层次的优化,往往在**源代码级别**会有一个优化过程
|
|
|
+
|
|
|
+这里定义的**源码级优化器**(`Source Code Optimizer`)在不同的编译器中可能会有不同的定义或者一些其他的差异
|
|
|
+
|
|
|
+比如之前的例子 `array[index] = (index + 4) * (2 + 6);` 中 **(2 + 6)** 的值在编译期就可以被确定
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+> 上述的语法树经过优化了
|
|
|
+
|
|
|
+但是一般不会直接优化在语法树上做优化,这比较困难,**源代码优化器** 往往将整个语法树转换成**中间代码**(`Intermediate Code`),它是语法树的顺序表示,非常接目标代码
|
|
|
+
|
|
|
+> 中间代码一般与目标机器和运行时环境无关,比如不包含数据的尺寸、变量地址和寄存器的名字等
|
|
|
+
|
|
|
+中间代码有很多类型,在不同编译器中有不同的形式,比较常见的有:**三地址码**(`Three-Address Code` Or `TAC`)和 **P-代码**(`P-Code`)
|
|
|
+
|
|
|
+**三地址码** 是常见的中间表示形式,每条三地址码指令最多包含三个地址:两个操作数和一个结果
|
|
|
+
|
|
|
+1. 四元组表示,每条三地址码可以表示为一个四元组(`4-tuple`):运算符、操作数1、操作数2、结果
|
|
|
+2. 简单命令
|
|
|
+3. 线性表示,便于编译器进行顺序处理和优化
|
|
|
+
|
|
|
+```cpp
|
|
|
+array[index] = (index + 4) * (2 + 6);
|
|
|
+```
|
|
|
+
|
|
|
+上述代码以 **三地址码** 为例,会生成如下的中间代码
|
|
|
+
|
|
|
+```cpp
|
|
|
+t1 = 2 + 6
|
|
|
+t2 = index + 4
|
|
|
+t3 = t2 * t1
|
|
|
+array[index] = t3
|
|
|
+```
|
|
|
+
|
|
|
+**三地址码**的优化过程主要包括消除公共子表达式、常量折叠、赋值传播等技术
|
|
|
+
|
|
|
+所以**优化程序**在三地址码的基础上进行优化时
|
|
|
+
|
|
|
+- 常量折叠:会将 `2 + 6` 计算出来得到 `t1 = 8`,然后将后面代码中的 t1 替换成数字 3
|
|
|
+- 复制传播:省去一个临时变量 t3,因为 t2 可以重复利用
|
|
|
+
|
|
|
+最后得到优化后的三地址码如下
|
|
|
+
|
|
|
+```cpp
|
|
|
+t2 = index + 4
|
|
|
+t2 = t2 * 8
|
|
|
+array[index] = t2
|
|
|
+```
|
|
|
+
|
|
|
+至于什么是消除公共子表达式,例子如下
|
|
|
+
|
|
|
+```cpp
|
|
|
+// 原始三地址码
|
|
|
+t1 = b + c
|
|
|
+a = t1
|
|
|
+t2 = b + c
|
|
|
+d = t2
|
|
|
+t3 = a * d
|
|
|
+e = t3
|
|
|
+
|
|
|
+// 因为 t1 和 t2 表达式相同
|
|
|
+t1 = b + c
|
|
|
+a = t1
|
|
|
+d = t1 // 直接使用 t1 而不是重新计算 b + c
|
|
|
+t3 = a * d
|
|
|
+e = t3
|
|
|
+```
|
|
|
+
|
|
|
+### 目标代码生成与优化
|
|
|
+
|
|
|
+**源代码级优化器**产生的中间代码标志着下面的过程都属于**编译器后端**
|
|
|
+
|
|
|
+编译器后端负责将中间代码转换为目标机器代码,并进行各种优化以提高代码的执行效率。主要包括:**代码生成器**和**目标代码优化器**
|
|
|
+
|
|
|
+代码生成器将中间代码转换成目标机器代码,这个过程十分依赖目标机器,因为不同的机器有着不同的字长、寄存器、整数数据类型和浮点数数据类型等
|
|
|
+
|
|
|
+```x86asm
|
|
|
+movl index, %ecx ; 赋值 index 给 ecx
|
|
|
+addl $4, %ecx ; ecx = ecx + 4
|
|
|
+mull $8, %ecx ; ecx = ecx * 8
|
|
|
+movl index, %eax ; 赋值 index 给 eax
|
|
|
+movl %ecx, array(, eax, 4) ; array[index] = ecx
|
|
|
+```
|
|
|
+
|
|
|
+最后目标代码优化器对上述的目标代码进行优化,比如选择合适的寻址方式、使用位移来代替乘法运算、删除多余的指令等
|
|
|
+
|
|
|
+```x86asm
|
|
|
+movl index, %edx
|
|
|
+leal 32(, %edx, 8), %eax
|
|
|
+movl %eax, array(, %edx, 4)
|
|
|
+```
|
|
|
+
|
|
|
+> 乘法由一条相对复杂的 **基址比例变址寻址**(`Base Index Scale Addressing`) 的 lea 指令完成
|
|
|
+
|