C以及Rust编译的过程
-
主流的编译器
-
GCC
- LLVM
-
C语言编译过程
-
LLVM编译过程
-
将C源码转为LLVM IR
* 将IR转化为BitCode
* 将BitCode转为目标平台汇编码
* 执行BitCode -
Rust编译过程
-
下一步做什么
主流的编译器
GCC
GCC编译器是由GNU开发的编译器,原名为GUN编译器,原本只能处理C语言随着发展,后续支持了C++,Java,Go等语言,所以改名为GNU编译器套件,GCC主要分为以下接口
- 前端接口: 将源码经过词法分析,语法分析生成与语言无关的低级中间语言表示层,然后经过优化后转化为RTL中间表示层
- 中间接口: 中间接口主要在RTL中间表示上进行各种优化,如循环优化,公共子表达式删除,指令重排等等
- 后端接口:GCC对每条RTL通过模板匹配方法调用对应的汇编模板生成汇编代码,生成的代码因处理器的不同而不同
LLVM
LLVM由C++编写,用于优化任意语言编写的程序,LLVM的命名最早源于Low Level Virtual Machine的缩写,LLVM代码有3种表示形式,IR,bitcode,汇编码,llvm提供了不同的优化Pass,对每个Pass的源码编译,得到一个Object文件,之后这些文件链接得到一个库,Pass之间由LLVM Pass管理器来统一管理
LLVM有很多其项目其中包括 LLVM Core libraries,Clang,LLD,LLDB,libc++ & libc++ ABI等等
C语言编译过程
一般的编译过程流程图大概是这样的
编译
汇编器
链接器
源代码
汇编语言
目标文件
可执行文件
但是不同的编译器有着不同的编译方式,下面我们使用LLVM对C语言编译的过程进行实践
LLVM编译过程
clang
llvm-as
llc
main.c
main.ll IR文件
main.bc bitCode
目标平台汇编码
我们开始准备LLVM的一些环境
- llvm
- llvm-as(Windows 安装llvm时没有这个文件,打开网站后输入llvm-as.exe搜索下载)
- llc (同llvm-as)
首先我们创建一个test.c文件然后输入以下内容
1
2
3
4
5
6
7
8 1int mult() {
2 int a = 5;
3 int b = 3;
4 int c = a * b;
5 return c;
6}
7
8
将C源码转为LLVM IR
输入一下命令
1
2
3 1clang -emit-llvm -S test.c -o test.ll
2
3
其中我们使用了clang作为前端进行编译,-emit-llvm用于LLVM IR写到.ll文件,-S表示仅运行预处理和编译步骤,-o参数用于将生成的内容输出到test.ll文件中
执行完毕后会在test.c同级目录下生成一个test.ll文件,将C语言代码分解为Token流(每个Token可表示标识符,字面量,运算符等等),Token流会传递给语法分析器,语法分析器使用CFG(上下文无关文法)组织成AST(抽象语法树),紧接着进行语义分析,然后生成IR
将IR转化为BitCode
我们使用一个较为简单的IR文件,内容如下
1
2
3
4
5
6
7 1// test.ll
2define i32 @mult(i32 %a, i32 %b) #0 {
3 %1 = mul nsw i32 %a, %b
4 ret i32 %1
5}
6
7
使用命令如下
1
2
3 1llvm-as test.ll -o test.bc
2
3
我们使用llvm-as(LLVM汇编器)将LLVM IR转为BitCode,-o参数用于将生成的BitCode输出到test.bc文件中
将BitCode转为目标平台汇编码
我们使用LLVM的静态编译器LLC把BitCode转为汇编码,命令如下
1
2
3 1llc test.bc -o test.s
2
3
或者我们可以使用Clang从BitCode文件生成汇编码,命令如下
1
2
3 1clang -S test.bc -o test.s -fomit-frame-pointer
2
3
我们使用了fomit-frame-pointer参数消除帧指针,因为Clang默认不消除帧指针,但是llc却默认消除帧指针
llc命令把LLVM的BitCode编译为指定架构的汇编语言,如果命令中没有指定任何架构默认生成的本机汇编码
执行BitCode
我们把test.c的内容换为以下内容
1
2
3
4
5
6
7
8
9
10 1#include<stdio.h>
2
3int main(){
4 int num = 5;
5 printf("number is %d\n", num);
6 return 0;
7}
8
9
10
然后我们按照之前的步骤将test.c转为BitCode
1
2
3
4 1$ clang -emit-llvm -S test.c -o test.ll
2$ llvm-as test.ll -o test.bc
3
4
注: 在Windows执行第2步是会出现以下错误
llvm-as: test.ll:31:62: error: expected ‘global’ or ‘constant’
@"??_C@_0O@BAPFBKAP@number?5is?5?$CFd?6?$AA@" = linkonce_odr dso_local unnamed_addr constant [14 x i8] c"number is %d\0A\00", comdat, align 1
错误原因等待解决
最后我们使用LLI命令来运行BitCode
1
2
3 1$ lli test.bc
2
3
LLI使用LLVM bitcode格式 作为输入并且使用即时编译器(JIT)执行,如果当前的架构不 存在JIT编译器,会用解释器执行
Rust编译过程
Rust使用的是rustc进行编译,编译的过程如下
语法分析,宏扩展
类型检查
转换
LLVM
链接
RustCode
HIR
MIR
LLVM IR
.o文件
可执行程序
详细过程如下
- 解析输入:将.rs文件作为输入并进行解析生成AST(抽象语法树)
- 名称解析,宏扩展和属性配置:解析完毕后处理AST,处理#[cfg]节点解析路径,扩展宏
- 转为HIR:名称解析完毕后将AST转换为HIR(高级中间表示),HIR比AST处理的更多,但是他不负责解析Rust的语法,例如((1+2)+3)和1+2+3在AST中会保留括号,虽然两者的含义相同但是会被解析成不同的树,但是在HIR中括号节点将会被删除,这两个表达式会以相同的方式表达
- 类型检查以及后续分析:处理HIR的重要步骤就是类型检查,例如使用x.f时如果我们不知道x的类型就无法判断访问的哪个f字段,类型检查会创建TypeckTables其中包括表达式的类型,方法的解析方式
- 转为MIR以及后置处理:完成类型检查后,将HIR转为MIR(中级中间表示)进行借用检查以及优化
- 转为LLVM IR和优化:LLVM进行优化,从而生成许多.o文件
- 链接: 最后将那些.o文件链接在一起
我们开始实践这一过程
首先我们创建一个Cargo项目
1
2
3 1~$ cargo new complier_test
2
3
main.rs文件中的内容如下
1
2
3
4
5 1fn main(){
2 println!("Hello");
3}
4
5
将源代码转为HIR
我们可以使用cargo的 -Zunpretty参数来生成hir,rustc命令没有找到。。
1
2
3 1~/complier_test$ cargo rustc -- -Zunpretty=hir-tree -o main.hir
2
3
然后在src目录下会生成main.hir文件
将源代码转为MIR
转为mir的过程我们可以使用rustc来完成
1
2
3 1~/complier_test/src$ rustc --emit mir -o main.mir main.rs
2
3
我们使用emit来生成emit用来生成mir,除此之外还可以使用emit来生成LLVM IR
1
2
3 1~/complier_test/src$ rustc --emit llvm-ir -o main.ll main.rs
2
3
转换为BitCode
然后我们可以使用llvm-as将IR转为BitCode
1
2
3 1~/complier_test/src$ llvm-as main.ll -o main.bc
2
3
或者我们可以使用rustc的emit参数生成
1
2
3 1~/complier_test/src$ rustc --emit llvm-bc -o main.bc
2
3
最后我们可以使用LLI来运行BitCode
1
2
3 1~/complier_test/src$ lli main.bc
2
3
下一步做什么
在下一篇文章我们使用Rust实现一个分词器,我们还需要掌握一些关于分词的理论知识