一. 使用LLVM编译程序
1. 编译C程序:
编写 C 程序 HelloLLVM.c:
#include <stdio.h>
int main() {
printf("Hello LLVM!\n");
}
执行编译命令:
clang HelloLLVM.c -o HelloLLVM
运行结果:
./HelloLLVM
Hello LLVM!
2. 编译 C++ 程序:
类似的,编写 C++ 程序 HelloLLVM.cpp:
#include <iostream>
using namespace std;
int main() {
cout << "Hello LLVM!" << std::endl;
}
使用 clang++ 编译:
clang++ HelloLLVM.cpp -o HelloLLVM
运行结果:
./HelloLLVM
Hello LLVM!
3. 拆解编译过程:
我们前面展示的编译过程是通过 Clang 进而调用 LLVM 编译出最终的程序,这其中包括了多个步骤。Clang 提供了众多编译参数,通过这些参数我们可以控制编译的过程,使其只进行其中的某一个或多个步骤。
clang --help | grep 'Only run'
-c Only run preprocess, compile, and assemble steps
-E Only run the preprocessor
-S Only run preprocess and compilation steps
我们通过以上对帮助文档的过滤可以找到拆分步骤的参数。
3.1. 预处理
假设我们有一个自定义的头文件 print.h。
void print(const char *);
在 HelloLLVM.c 中 include 这个头文件。
#include "print.h"
int main() {
print("Hello LLVM!\n");
}
执行下面的命令:
clang -E HelloLLVM.c -o HelloLLVM.e
cat HelloLLVM.e
预处理的结果:
在开头是一些注释,紧接着就是 print.h 的内容,被原封不动的插入到了预处理结果中。
3.2. 生成汇编代码
原博命令:
clang -S HelloLLVM.e -o HelloLLVM.s
但是我没成功。。
3.3. 汇编
这一步将把汇编代码翻译成机器码:
clang -c HelloLLVM.c -o HelloLLVM.o
file HelloLLVM.o
HelloLLVM.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
这一步生成的文件通常成为对象文件,更专业的说法可能是可重定位文件。之所以说可重定向是因为这个文件的符号在下一步过程中会被放在一个更大的文件中,那么符号的位置也自然会被重新确立位置。
3.4. 链接
这是我们刚才提到的 print.h 对应的 print.c 文件。
#include <stdio.h>
void print(const char * str)
{
printf("%s", str);
}
把前面的 print 函数也编译成对象文件
clang -c print.c -o print.o
file print.o
print.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
链接过程实际上是使用 ld 命令,不过这个命令需要配合许多参数一起使用,我们很少会手动调用,一般是通过编译工具来帮助我们完成,这里我们使用 clang 来完成链接过程
clang HelloLLVM.o print.o -o HelloLLVM
file HelloLLVM
HelloLLVM: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, not stripped
clang HelloLLVM.o print.o HelloLLVM --verbose
获得编译器更多输出。
验证一下链接出来的可执行程序:
./HelloLLVM
Hello LLVM!
3.5. 中间表示
实际上作为一般的编译器都可以拆解成以上步骤:预处理、输出汇编、汇编、链接。而 LLVM 有强大的中间表示,这种中间表示可以以物理的形式存在,而非想 gcc 只能在内存中临时存在。
不过 clang 本身并没有开放这样的功能,clang 留了一个“后门”,通过它你可以直接控制 clang 背后的引擎程序(clang命令 实际上是一个调度程序,实际的工作都由它来完成)
clang -cc1 HelloLLVM.c -emit-llvm
cat HelloLLVM.ll
LLVM 的中间表示:
打开这个“后门”就是通过加 -cc1 参数,
clang -c
和clang -cc1
是Clang编译器的两个不同选项,用于不同的编译步骤。
-
clang -c
:这是Clang的编译选项,用于将源代码文件编译为目标文件(二进制文件)。当你执行clang -c <source_file>
时,Clang会进行以下步骤:- 预处理:执行宏展开、条件编译等预处理操作。
- 编译:将预处理后的源代码编译为汇编代码。
- 汇编:将汇编代码转换为目标文件(二进制文件),这个目标文件可以被链接器使用。
例如,执行以下命令将
source.c
编译为object.o
目标文件:clang -c source.c -o object.o
-
clang -cc1
:这是Clang的底层编译器驱动程序,用于控制Clang的内部编译过程。-cc1
选项用于手动指定Clang的各个编译阶段,可以对源代码进行逐步的编译和转换。它通常与其他选项结合使用,用于进行高级的编译控制和分析。例如,执行以下命令将
source.c
进行逐步编译:clang -cc1 -triple x86_64-unknown-linux-gnu -emit-llvm -o source.bc -x c source.c
-cc1
选项告诉Clang使用底层编译器驱动程序,-emit-llvm
选项告诉Clang生成LLVM字节码,-o source.bc
指定生成的字节码文件名为source.bc
,-x c
指定输入源代码类型为C语言。
总结来说,clang -c
是用于将源代码编译为目标文件的简化选项,而clang -cc1
是Clang底层编译器驱动程序的选项,用于手动控制编译过程和进行更高级的编译控制。
参数 -emit-llvm 表示输出 LLVM 中间表示,默认会保存在同名的 .ll 文件中。
这里我们看到,.ll 文件是一个文本形式,我们还可以将其转换成二进制形式。
lvm-as HelloLLVM.ll
file HelloLLVM.bc
HelloLLVM.bc: LLVM IR bitcode
这个过程是可逆的,通过以下命令完成。
llvm-dis HelloLLVM.bc
中间表示也可以被编译。
clang -c HelloLLVM.ll -o HelloLLVM.o
或者
clang -c HelloLLVM.bc -o HelloLLVM.o
llc 是 llvm 的后端,可以用来把 中间表示编译成汇编。
llc HelloLLVM.bc -o HelloLLVM.s
这与前面用 clang -S 是一样的效果。
3.6. 中间表示链接
程序指令在优化的时候,单看独立的文件有时候不能很好的进行优化,所以 llvm-link 提供了把独立的 IR 文件链接在一起的功能。
我们先生成 print.c 对应的 IR 文件。
clang -cc1 -emit-llvm print.c
print.c:1:10: fatal error: 'stdio.h' file not found
#include <stdio.h>
^~~~~~~~~
1 error generated.
按照刚才的方式,结果报错了,原因是 我们加入了 -cc1 参数,使用了背后的引擎,没有调度工具 clang 帮助我们添加指定头文件目录的工作。但这也不难,我们通过 --verbose 看看 clang 都加了那些参数。
clang -c print.c --verbose
获得编译器的更多输出  把里面的参数复制出来,并修改 -emit-obj 为 -emit-llvm,去掉 -o print.o。
-cc1 -triple x86_64-unknown-linux-gnu -emit-llvm -mrelax-all -disable-free -disable-llvm-verifier -discard-value-names -main-file-name print.c -mrelocation-model static -mthread-model posix -mdisable-fp-elim -fmath-errno -masm-verbose -mconstructor-aliases -munwind-tables -fuse-init-array -target-cpu x86-64 -v -dwarf-column-info -debugger-tuning=gdb -coverage-notes-file /home/cc16-04/test/print.gcno -resource-dir /usr/local/bin/../lib/clang/4.0.0 -internal-isystem /usr/local/include -internal-isystem /usr/local/bin/../lib/clang/4.0.0/include -internal-externc-isystem /usr/include/x86_64-linux-gnu -internal-externc-isystem /include -internal-externc-isystem /usr/include -fdebug-compilation-dir /home/cc16-04/test -ferror-limit 19 -fmessage-length 80 -fobjc-runtime=gcc -fdiagnostics-show-option -x c print.c
得到 print.ll 文件,把两个 IR 文件链接起来
llvm-link print.ll HelloLLVM.ll -S -o all.ll
查看 all.ll
cat all.ll
同时出现了 print 和 main 两个定义
3.7.语法分析
clang可以输出程序的抽象语法树(也可以以图象形式输出。)
clang -cc1 -ast-dump HelloLLVM.c
3.8. 生成控制流图
首先需要安装graphviz
sudo apt install graphviz
对于中间表达的*.ll文件
opt -dot-cfg all.ll
生成了main和print各自的dot文件,以main为例
dot -Tpng .cfg.main.dot -o main.png
得到
参考:1. https://github.com/tuoxie007/play_with_llvm/blob/master/ch02.md
2. (24条消息) LLVM 编译器学习笔记之二十九 -- 控制流程CFG_llvm控制流图_清钟沁桐的博客-CSDN博客