一. 使用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博客

浙公網安備 33010602011771號