1、LLVM设计与使用
了解如何把C语言代码编译为LLVM IR(Intermediate representation)及其他多种形式。
1.1 模块化设计
与其他编译器(如 GNU Compiler Collection) 不同,LLVM 设计目标是成为一系列的库。
让我们先准备一段测试代码作为优化器输入:
define i32 @test1(i32 %A) {
%B = add i32 %A, 0
ret i32 %B
}
define internal i32 @test(i32 %X, i32 %dead) {
ret i32 %X
}
define i32 @caller() {
%A = call i32 @test(i32 123, i32 456)
ret i32 %A
}
-
试试指令合并优化:
sudo opt -S -instcombine testfile.ll -o output1.ll
-
试试参数消除优化:
sudo opt -S -deadargelim testfile.ll -o output2.ll
-
原理:
- 不同的优化 pass 整体风格一致
- 每个 pass 编译成 .o 后链接得到一个库
- pass 之间的依赖关系由
LLVM pass管理器
管理
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XIsoZpc0-1655738865598)(…/picture/pass-rela.png)]
更多:
codeGen 也采用模块设计理念,将代码分解为多个独立pass:
指令选择、寄存器分配、指令调度、代码布局优化、代码发射等
IR的跟多信息参阅 我翻译的IR参考 或 原文
1.2 交叉编译Clang/LLVM
编译二进制文件的机器叫主机(host),运行生成二进制的平台叫目标平台(target). 为相同平台编译代码我们称为本机编译(native assembler), 不同称为 交叉编译(cross-compiler).
环境依赖略过,记录一下编译命令:
cmake -G Ninja<LLVM源码目录> \
-DCMAKE_CROSSCOMPILING=True \
-DCMAKE_INSTALL_PREFIX=<工具链安装目录(可选)> \
-DLLVM_TABLEGEN=<已安装的LLVM工具链目录 >/llvm-tblgen \
-DCLANG_TABLEGEN=<已安装的LLVM工具链目录 >/clang-tblgen \
-DLLVM_DEFAULT_TARGET_TRIPLE=arm-linux-gnueabihf \
-DLLVM_TARGET_ARCH=ARM \
-DLLVM_TARGETS_TO_BUILD=ARM \
-DCMAKE_CXX_FLAGS='-target armv7a-linux-gnueabihf -mcpu=cortex-a9
-I/usr/arm-linux-gnueabihf/include/c++/4.x.x/arm-linux-gnueabihf/
-I/usr/arm-linux-gnueabihf/include/ -mfloat-abi=hard -ccc-gcc-name
arm-linux-gnueabihf-gcc'
使用了 DCMAKE_INSTALL_PREFIX 会在 install-dir 创建 sysroot
注:ARM 平台的库还是得自己下载一份或自己编译构建.
1.3 将C源码转换为LLVM汇编码
sudo clang -emit-llvm -S multiply.c -o multiply.ll
sudo clang -cc1 -emit-llvm testfile.c -o testfile.ll # 通过 cc1 生成
- 原理:
- 从词法分析开始(源码分解为token流:标识符、字面量、运算符)
- token 流传递非词法分析器:在语言的
CFG(context free grammar)
指导下组织成AST(抽象语法树)
- 语义分析(检查正确性)
- 生成 IR
1.4 将LLVM IR转换为 bitcode
bitcode
也叫字节码:
- 位流 (
bitstream
) - 编码格式
define i32 @mult(i32 %a, i32 %b) #0 {
%1 = mul nsw i32 %a, %b
ret i32 %1
}
- sudo llvm-as test.ll -o test.bc
- 生成如下二进制(hexdump -C test.bc)
Offset: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 00000000: 42 43 C0 DE 35 14 00 00 05 00 00 00 62 0C 30 24 BC@^5.......b.0$ 00000010: 4D 59 BE 66 7D FB B4 4F 1B C8 24 44 01 32 05 00 MY>f}{4O.H$D.2.. 00000020: 21 0C 00 00 02 01 00 00 0B 02 21 00 02 00 00 00 !.........!..... 00000030: 16 00 00 00 07 81 23 91 41 C8 04 49 06 10 32 39 ......#.AH.I..29 ......
- 原理:
llvm-as
就是 LLVM 的汇编器,将 LLVM IR 转为 bitcode, 类似把汇编转为可执行文件- 为了做到这点,引入了
区块(block)
和记录(record)
的概念- 区块: 标识位流的区域,如函数体,符号表, 每个区块内容对应一个特定ID, 如函数ID是12
- 记录:由记录码和一个整数值组成,描述了指令、全局变量描述符、类型描述中的实体
- 可参考 官网描述
- 为了做到这点,引入了
1.5 将LLVM bitcode转换为目标平台汇编码
- 可通过 llc 或 clang 前端 获得:
sudo llc test.bc -o test.s # 默认消除帧指针 sudo clang -S test.bc -o test.s -fomit-frame-pointer # 消除帧指针
- 原理:llc把LLVM输入编译为特定架构的汇编语言,如不指定默认生成本机汇编码
- 生成可执行文件需使用汇编器和链接器
- -march=architechture 可生成特定架构的汇编码
- -mcpu=cpu 可指定 cpu
1.6 将LLVM bitcode转回为LLVM汇编码
sudo llvm-dis test.bc -o test.ll
- 原理:
- llvm-dis 是反汇编器,省略文件名时会从标准输入读取
1.7 转换LLVM IR
- 执行转换pass
opt -passname input.ll -o output.ll
clang -emit-llvm -S multiply.c -o multiply.ll # 得到未进行优化的输出
sudo opt -mem2reg -S multiply.ll -o multiply1.ll # 优化内存访问,从局部变量提升到寄存器
- 原理:opt 作为 LLVM 的优化和分析工具,可使用多个 pass
- adce 无用代码 消除
- bb-vectorize 基本快向量化
- constprop 简单常量传播
- dce 无用代码消除
- deadargelim 无用参数消除
- globaldce 无用全局变量消除
- globalopt 全局变量优化
- gvn 全局变量编号
- inline 函数内联
- instcombine 冗余指令合并
- licm 循环常量代码外提
- loop-unswitch 循环外提
- loweratomic 原子内建函数 lowering
- lowerinvoke invode 指令lowering
- mem2reg 内存访问优化
-->可帮助理解C指令映射IR指令
- memcpyopt Memcpy优化
- simplifycfg 简化 CFG
- sink 代码提升
- tailcallelim 尾调用消除
1.8 链接LLVM bitcode
- 准备测试文件
int func(int a) { a = a*2; return a; }
#include<stdio.h> extern int func(int a); int main() { int num = 5; num = func(num); printf("number is %d\n", num); return num; }
sudo clang -emit-llvm -S test1.c -o test1.ll sudo clang -emit-llvm -S test2.c -o test2.ll sudo llvm-as test1.ll -o test1.bc sudo llvm-as test2.ll -o test2.bc sudo llvm-link test1.bc test2.bc -o output.bc
- 原理:与传统链接器一致,会解析这个文件中引用的符号。不同的是,不会生成一个二进制文件,只会链接 bitcode 文件
1.9 执行LLVM bitcode
lli output.bc
- 这会执行 output.bc 文件。输出显示在标准输出
- 原理:使用即时编译器(JIT)执行,不存在则用解释器执行
1.10 使用C语言前端——Clang
clang作为编译器驱动,可得到可执行文件,使用 -E 可做预处理器,可用下面的命令获得抽象语法树
clang -cc1 test.c -ast-dump # -cc1 保证只使用前端,而不是驱动器
可用 -S 生成汇编码
clang test.c -S -emit-llvm -o -
- 原理:clang 可作为预处理器、编译器驱动、前端以及代码驱动器,因此取决于你指定的参数