前言
目前在做一些編譯相關(guān)調(diào)研。先前寫過篇《深入剖析 iOS 編譯 Clang / LLVM》和《深入剖析 iOS 編譯 Clang / LLVM 直播的 Slides》,內(nèi)容偏理論。本篇著重對(duì) LLVM 的使用,理論內(nèi)容會(huì)很少,主要是說下如何使用 llvm 來做些事情,會(huì)有詳細(xì)的操作步驟和工程示例。
代碼新陳代謝
昨天看了昨天和今天 WWDC22 的 session,看到了蘋果為包體積也做了很多工作,甚至不惜改 C ABI的 call convention 來達(dá)到此目的。
我很早前就做過一個(gè)方案,可以說是一個(gè)更好處理代碼新陳代謝的方案,那就先說下這個(gè)。
方案總體介紹
靜態(tài)檢查無法分析真實(shí)使用場(chǎng)景里代碼是不是真的用了,或用的是否多。
動(dòng)態(tài)檢查來說,以前檢查的方式有通過埋點(diǎn)查看相應(yīng)代碼是否有用到,還可以通過類的 isInitialized 方法來統(tǒng)計(jì)類是否被用到。第一個(gè)方案成本高,第二個(gè)方案范圍太大,如果類都很大,那么檢查結(jié)果的意義就沒了。因此,需要一個(gè)能夠動(dòng)態(tài)檢查函數(shù)和代碼塊級(jí)別是否使用的方法。
一些現(xiàn)有方案和其不可用的地方
下面列兩個(gè)已有可檢查比類更小粒度的方案。
gcov
clang 使用 -fprofile-instr-generate -fcoverage-mapping ,swiftc 使用 -profile-generate -profile-coverage-mapping 生成 .profraw 文件。llvm-profdata merge 轉(zhuǎn)成 .profdata。編譯時(shí)每個(gè)文件會(huì)用 GCOVProfiling 生成 .gcno 包含計(jì)數(shù)和源碼的映射關(guān)系,運(yùn)行時(shí)用的是 GCDAProfiling 處理回調(diào)記錄運(yùn)行時(shí)執(zhí)行了哪些代碼。最后 llvm-cov 轉(zhuǎn)成報(bào)告,生成工具是 gcov,生成的報(bào)告可以看到哪些代碼有用到,哪些沒有用。
gcov 對(duì)于線下測(cè)試夠用,但無法放到線上使用。
SanitizerCoverage 插樁回調(diào)函數(shù)
SanitizerCoverage 是 libfuzzer 使用的代碼覆蓋技術(shù),使用 -fsanitize-coverage=trace-pc-guard 這個(gè)編譯 flag 插入不同級(jí)別的樁,會(huì)在程序控制流圖的每條邊插入__sanitizer_cov_trace_pc_guard。
如果只對(duì)函數(shù)插樁,使用 -fsanitize-coverage=func,trace-pc-guard,只對(duì)基本塊用 -fsanite-coverage=bb,no-prune,trace-pc-guard。swift 使用 -sanitize-coverage=func 和 -sanitize=undefined 編譯 flags。
在回調(diào)函數(shù) __sanitizer_cov_trace_pc_guard_init 和 __sanitizer_cov_trace_pc_guard 里實(shí)現(xiàn)自己要干的事情,比如對(duì)當(dāng)前插樁地址符號(hào)化,運(yùn)行后就可以得到運(yùn)行時(shí)調(diào)用了哪些方法。
使用 SanitizerCoverage 插樁,一個(gè)是編譯會(huì)很慢,另一個(gè)是插入范圍難控制,上線后各方面影響不可控。SanitizerCoverage 本是用于 fuzzing 測(cè)試的一個(gè) llvm pass,因此可以了解 SanitizerCoverage 使用的技術(shù),自建一個(gè)專門用于代碼新陳代謝的 pass 用來解決 SanitizerCoverage 和 gcov 不好用的問題。
自制可插入指令的 Pass
之所以在編譯中間層插入指令而不在編譯 frontend 插入代碼的原因是,這樣做的話能用類似 llvm-mctoll 二進(jìn)制轉(zhuǎn)中間層 IR 代碼的方式,可對(duì)第三方這樣沒有 frontend 源碼而只有生成的二進(jìn)制產(chǎn)物的庫(kù)進(jìn)行分析。
在函數(shù)中插入執(zhí)行指令執(zhí)行自定功能的方法是,用 IRBuilder 使用 SetInsertPoint 設(shè)置位置,CreateCall 插入指令,插入在塊的初始位置,用的是 dyn_cast<BinaryOperator>(&I) 。CreateCall 調(diào)用 LLVMContextFunctionCallee 來自 F.getParent()->getOrInsertFunction,其第一個(gè)參數(shù)就是要執(zhí)行我們自定義函數(shù)的函數(shù)名,第二個(gè)參數(shù) FunctionType 是通過 paramTypes 和 Type::getVoidTy 根據(jù) LLVMContext 而來。 使用編譯屬性可以指定要控制的函數(shù),pass 可用 getGlobalVariable 取到 llvm.global.annotations ,也就是所有編譯屬性。
F.getName().front() 為 \x01 表示的是 OC 方法,去掉這個(gè)前綴可得到方法名,.contains("_block") 是閉包函數(shù)。F.getName().startswith("_Z") 是 C++ 函數(shù)(_Z、__Z、___Z 都是)。使用 F.getName() 判讀讀取一個(gè)映射表進(jìn)行對(duì)比,也可以達(dá)到通過編譯屬性設(shè)置控制指定函數(shù)的效果。映射表里設(shè)置需要線上驗(yàn)證的函數(shù)集合。然后,處理函數(shù)和塊計(jì)數(shù)與源碼的映射關(guān)系,編譯加入處理自制 pass 記錄運(yùn)行時(shí)代碼執(zhí)行情況的回調(diào)。
使用
pass 代碼編譯生成 dylib 后,在 Xcode 中使用需要替換 clang 為編譯 pass 的 clang,編譯 pass 的版本也要對(duì)應(yīng)上。在 xconfig 中設(shè)置構(gòu)建命令選項(xiàng) OTHER_CFLAGS OTHER_CPLUSPLUSFLAGS 是 -Xclang -load -Xclang $pass,CC CXX 設(shè)置為替換的 clang。調(diào)試是用的 opt,可換成 opt scheme,在 Edit Scheme 里設(shè)置 opt 的啟動(dòng)參數(shù)。
llvm 14 后只能使用 new pm,legcy pm(pass manager) 通過 Xlang 給 clang 傳參,而 new pm 不行,new pm 的 pass 讓 clang 加載,一種方法是使用 -fpass-plugin,另一種是把 pass 加到 clang 的 pipeline 里,重新構(gòu)建對(duì)應(yīng)版本的 clang。具體來說就是 PassBuilder 的回調(diào) registerPipelineStartEPCallback 允許 ModulePassManager 使用 addPass 添加我們的 pass。
方案是這樣,接下來的內(nèi)容是偏實(shí)際的一些操作,你也可以跟著實(shí)踐下,畢竟本篇是說怎么使用 LLVM 嘛。
先看看 gcov 的用法。
生成代碼覆蓋率報(bào)告
命令行中開啟代碼覆蓋率的編譯選項(xiàng),參看官方指南:Source-based Code Coverage 。
通過一個(gè)例子實(shí)踐下。
建個(gè) C 代碼文件 main.m :
#include <stdio.h>
int main(int argc, char *argv[])
{
printf("hi there!\n");
return 0;
}
void foo() {
return;
}
加上代碼覆蓋率的編譯參數(shù)進(jìn)行編譯。
xcrun clang -fprofile-instr-generate -fcoverage-mapping main.m -o mainCoverage
運(yùn)行生成的 mainCoverage 會(huì)生成 default.profraw 文件,自定義文件名使用 LLVM_PROFILE_FILE="my.profraw" ./mainCoverage 命令。
對(duì)于 Swift 文件也沒有問題,建一個(gè) swift 文件 hi.swift
hi()
func hi() {
print("hi")
}
func f1() {
doNothing()
func doNothing() {}
}
通過 swiftc 來編譯
swiftc -profile-generate -profile-coverage-mapping hi.swift
從上面 clang 和 swiftc 的命令可以看出,clang 使用的是 -fprofile-instr-generate 和 -fcoverage-mapping 編譯 flags,swiftc 使用的是 -profile-generate 和 -profile-coverage-mapping 編譯 flags。
編譯出的可執(zhí)行文件 mainCoverage 和 hi 都會(huì)多出
生成代碼覆蓋率前建立索引,也就是生成 .profdata 文件。通過 xcrun 調(diào)用 llvm-prodata 命令。命令如下:
xcrun llvm-profdata merge -sparse my.profraw -o my.profdata
合并多個(gè) .profdata 文件使用下面的命令:
llvm-profdata merge one.profdata two.profdata -output all.profdata
使用 llvm-cov 命令生成行的報(bào)告
xcrun llvm-cov show ./mainCoverage -instr-profile=my.profdata
輸出:
1| |#include <stdio.h>
2| |
3| |int main(int argc, char *argv[])
4| 1|{
5| 1| printf("hi there!\n");
6| 1| return 0;
7| 1|}
8| |
9| 0|void foo() {
10| 0| return;
11| 0|}
上面的輸出可以看到,9到11行是沒有執(zhí)行的。
從文件層面看覆蓋率,可以通過下面的命令:
xcrun llvm-cov report ./mainCoverage -instr-profile=my.profdata
輸出的報(bào)告如下:
Filename Regions Missed Regions Cover Functions Missed Functions Executed Lines Missed Lines Cover Branches Missed Branches Cover
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
/Users/mingdai/Downloads/PTest/main.m 2 1 50.00% 2 1 50.00% 7 3 57.14% 0 0 -
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
TOTAL 2 1 50.00% 2 1 50.00% 7 3 57.14% 0 0 -
生成 JSON 的命令如下:
xcrun llvm-cov export -format=text ./mainCoverage -instr-profile=my.profdata > my.json
從生成的 json 文件可以看到這個(gè)生成的報(bào)告有5個(gè)統(tǒng)計(jì)項(xiàng),分別是函數(shù)、實(shí)例化、行、區(qū)域和分支。
更多報(bào)告生成選型參看 llvm-cov 官方說明 。
Xcode 配置生成代碼覆蓋率報(bào)告
在 Xcode 里開啟代碼覆蓋率,先選擇"Edit Scheme...",再在 Test 中的 Options 里勾上 Gather coverage for all targets 或 some targets。
在 Build Setting 中進(jìn)行設(shè)置,添加 -profile-generate 和 -profile-coverage-mapping 編譯 flags。
調(diào)用 llvm profile 的 c 函數(shù)生成 .profraw 文件。代碼見:
// MARK: - 代碼覆蓋率
func codeCoverageProfrawDump(fileName: String = "cc") {
let name = "\(fileName).profraw"
let fileManager = FileManager.default
do {
let documentDirectory = try fileManager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor:nil, create:false)
let filePath: NSString = documentDirectory.appendingPathComponent(name).path as NSString
__llvm_profile_set_filename(filePath.utf8String)
print("File at: \(String(cString: __llvm_profile_get_filename()))")
__llvm_profile_write_file()
} catch {
print(error)
}
}
codeCoverageProfrawDump 函數(shù)放到 applicationWillTerminate 里執(zhí)行,就可以生成在本次操作完后的代碼覆蓋率。
通過 llvm-cov report 命令將 .profraw 和生成的 Mach-O 文件關(guān)聯(lián)輸出代碼覆蓋率的報(bào)告,完整實(shí)現(xiàn)和調(diào)試看,參看 DaiMingCreationToolbox 里的 FundationFunction.swift 和 SwiftPamphletAppApp.swift 文件。
Fuzzing 介紹
另外,llvm 還提供另一種覆蓋率輸出,編譯參數(shù)是 -fprofile-arcs -ftest-coverage 和鏈接參數(shù) -lgcov,運(yùn)行程序后會(huì)生成 .gcda 和 .gcno 文件,使用 lcov 或 gcovr 就可以生成一個(gè) html 來查看覆蓋率。
之所以能夠輸出代碼覆蓋率,主要是 llvm 在編譯期間給函數(shù)、基本塊(IDA 中以指令跳轉(zhuǎn)當(dāng)分界線的每塊代碼)和邊界(較基本塊多了執(zhí)行邊界信息)插了樁。插樁的函數(shù)也有回調(diào),如果想使用插樁函數(shù)的回調(diào),有源碼可以使用 SanitizerCoverage, 官方說明見:SanitizerCoverage。
SanitizerCoverage 用的是 ModulePass,是 llvm 提供的 ModulePass、CallGraphSCCPass、FunctionPass、LoopPass、RegionPass 這幾個(gè)插樁 pass 中的一種。SanitizerCoverage 還應(yīng)用在 llvm 的 Fuzz 生成器 libfuzzer 上,libfuzzer 可以從硬件和 IR 層面進(jìn)行插樁獲取程序的覆蓋率。
Fuzzing 生成器的概念最早是威斯康星大學(xué) Barton Miller 教授在他的課上提出的,后應(yīng)用于安全測(cè)試領(lǐng)域,比如 PROTOS 測(cè)試集項(xiàng)目、網(wǎng)絡(luò)協(xié)議安全測(cè)試 SPIKE、最普遍應(yīng)用的文件 Fuzzing 技術(shù) Peach、語(yǔ)法模板 funfuzz 和 Dom fuzz 的 Domato、分析 llvm IR 符號(hào)執(zhí)行平臺(tái) Klee、源碼插樁和 QEMU 模式實(shí)現(xiàn)代碼覆蓋 fuzzing 的 AFL 和剛才我提到的 llvm 自帶基于 SanitizerCoverage 的 libfuzzer、挖掘系統(tǒng)內(nèi)核漏洞的系統(tǒng)函數(shù)調(diào)用模板 Fuzzing 庫(kù) syzkaller 和基于 libfuzzer 和 protobuf 做的 libprotobuf-mutator、組合了 libFuzzer,AFL++ 和 Honggfuzz 還有 ClusterFuzz 的平臺(tái) OSS-Fuzz。
其中 Spike 是網(wǎng)絡(luò)協(xié)議開源 Fuzzing 工具,由 Dave Aitel 編寫的,Dave Aitel 是《the Hacker's Handbook》(《黑客防范手冊(cè)》)和《the Shellcoder's Handbook》(《黑客攻防技術(shù)寶典:系統(tǒng)實(shí)戰(zhàn)篇》)的作者。網(wǎng)絡(luò)協(xié)議分析工具主要是 WireShark 和應(yīng)用層的 SockMon(特定進(jìn)程、協(xié)議、IP、函數(shù)抓包),和 IDA、OD 等工具結(jié)合找到軟件執(zhí)行的網(wǎng)絡(luò)命令分析數(shù)據(jù)包的處理過程。Spike 可以對(duì)數(shù)據(jù)發(fā)包收包,還可以構(gòu)造數(shù)據(jù)包自動(dòng)化做覆蓋更大的測(cè)試。
QEMU 是 2003 年 Fabrice Bellard 做的虛擬機(jī),包含很多架構(gòu)和硬件設(shè)備的模擬執(zhí)行,原理是 qemu TCG 模塊把機(jī)器代碼轉(zhuǎn)成 llvm IR,這個(gè)過程叫做反編譯,關(guān)于反編譯可以參考這篇論文《An In-Depth Analysis of Disassembly on Full-Scale x86/x64 Binaries》。之所以可以做到反編譯是因?yàn)闄C(jī)器指令和匯編指令是一一對(duì)應(yīng)的,可以先將機(jī)器指令翻譯成機(jī)器對(duì)應(yīng)的匯編,IR 實(shí)際上就是一個(gè)不遵循硬件設(shè)計(jì)的指令集,和硬件相關(guān)的匯編會(huì)按照 IR 的設(shè)計(jì)翻譯成機(jī)器無關(guān)的 IR 指令。這樣做的好處就是無論是哪個(gè)機(jī)器上的可執(zhí)行二進(jìn)制文件都能夠統(tǒng)一成一份標(biāo)準(zhǔn)的指令表示。IR 也可以設(shè)計(jì)成 DSL,比如 Ghidra 的 Sleigh 語(yǔ)言。
反編譯后,再將得到的 IR 轉(zhuǎn)成目標(biāo)硬件設(shè)備可執(zhí)行機(jī)器語(yǔ)言,IDA Pro 也是用的這個(gè)原理,IDA 的 IR 叫 microcode,IDA 的插件 genmc 專門用來顯示 microcode,HexRaysDeob 是利用 microcode 來做混淆的庫(kù)。
qemu 做的是沒有源碼的二進(jìn)制程序的分析,是一個(gè)完整的虛擬機(jī)工具,其中只有 tcg 模塊的一部分功能就可以實(shí)現(xiàn)模擬 CPU 執(zhí)行,執(zhí)行過程中插入分析的代碼就能夠方便的訪問寄存器,對(duì)地址或指令 hook,實(shí)現(xiàn)這些功能的庫(kù)是 Unicorn,還有功能更多些的 Qiling。Qiling 和 Unicorn 不同的是 Unicorn 只完成了 CPU 指令的仿真,而 Qiling 可以處理更高層次的動(dòng)態(tài)庫(kù)、系統(tǒng)調(diào)用、I/O 處理或 Mach-O 加載等,Qiling 還可以通過 Python 開發(fā)自己動(dòng)態(tài)分析工具,運(yùn)行時(shí)進(jìn)行 hotpatch,支持 macOS。基于 qemu 還有可以訪問執(zhí)行的所有代碼和數(shù)據(jù)做回放程序執(zhí)行過程的 PANDA、虛擬地址消毒劑 QASan、組合 Klee 和 qemu 的 S2E。
能夠使用 js 來開發(fā)免編譯功能的 Frida 也可以用于 Fuzzing,在 iOS 平臺(tái)上的 Fuzzing 參看1、2、3,使用工具見 iOS-messaging-tools。
更多 Fuzzing 資料可以參看 GitHub 上一份整理好的 Awesome-Fuzzing。
可見 Fuzzing 生成器應(yīng)用范圍非常廣,除了獲取代碼覆蓋率,還能夠進(jìn)行網(wǎng)絡(luò)安全分析和安全漏洞分析。本文主要是基于源碼插樁,源碼插樁庫(kù)主要是 libfuzzer、AFL++、honggfuzz、riufuzz(honggfuzz 二次開發(fā))。
AFL++ 在有源碼情況下原理和 libfuzzer 差不多,只是底層不是用的 SanitizerCoverage,而是自實(shí)現(xiàn)的一個(gè) pass,沒有源碼時(shí) AFL++ 用的就是 qemu 中 TCG 模塊的代碼,在反編譯為 IR 時(shí)進(jìn)行插樁。更多 AFL++ 應(yīng)用參見《What is AFL and What is it Good for?》
Fuzzing 除了代碼覆蓋率,還需要又能夠創(chuàng)建更多輸出條件,記錄執(zhí)行路徑,目標(biāo)和方向是找出程序運(yùn)行時(shí)在什么輸入條件和路徑下會(huì)有問題。但僅是檢測(cè)哪些代碼有用到,實(shí)際上只要用上 Fuzzing 的代碼覆蓋率就可以了。
SanitizerCoverage 插樁回調(diào)函數(shù)
那接下來實(shí)踐下 libfuzzer 中實(shí)現(xiàn)代碼覆蓋率的 SanitizerCoverage 技術(shù)。
命令行執(zhí)行
xcrun clang -fembed-bitcode main.m -save-temps -v -fsanitize-coverage=trace-pc-guard
使用 -fsanitize-coverage=trace-pc-guard 這個(gè)編譯 flag 插入不同級(jí)別的樁,會(huì)在程序控制流圖的每條邊插入:
__sanitizer_cov_trace_pc_guard(&guard_variable)
如果只對(duì)函數(shù)插樁,使用 -fsanitize-coverage=func,trace-pc-guard,只對(duì)基本塊用 -fsanite-coverage=bb,no-prune,trace-pc-guard。swift 使用 -sanitize-coverage=func 和 -sanitize=undefined 編譯 flags。
使用插樁函數(shù)回調(diào),先在 Xcode 的 Other C Flags 里添加 -fsanitize-coverage=trace-pc-guard。swift 就是在 Other Swift Flags 里添加 -sanitize-coverage=func 和 -sanitize=undefined 。
在回調(diào)函數(shù)里實(shí)現(xiàn)自己要干的事情,比如對(duì)當(dāng)前插樁地址符號(hào)化,代碼如下:
#import <dlfcn.h>
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
uint32_t *stop) {
static uint64_t N;
if (start == stop || *start) return;
printf("INIT: %p %p\n", start, stop);
for (uint32_t *x = start; x < stop; x++)
,*x = ++N;
}
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
if (!*guard) return;
void *PC = __builtin_return_address(0);
Dl_info info;
dladdr(PC, &info);
printf("調(diào)用了方法: %s \n", info.dli_sname);
}
運(yùn)行后就可以得到運(yùn)行時(shí)調(diào)用了哪些方法。
有了這些數(shù)據(jù)就可以統(tǒng)計(jì)哪些方法調(diào)用了,調(diào)用了多少次。通過和全源碼對(duì)比,取差集能夠找到運(yùn)行中沒有執(zhí)行的方法和代碼塊。其實(shí)利用 Fuzzing 的概念還可以做很多分析的工作,全面數(shù)據(jù)化觀測(cè)代碼執(zhí)行情況。可以到我的 GCDFetchFeed 工程中,打開 AppDelegate.m 里的兩個(gè)插樁回調(diào)方法的注釋來試用。
停止試用插樁,可以用 __attribute__((no_sanitize("coverage"))) 編譯屬性?;蛘咄ㄟ^黑名單或白名單,分別是 -fsanitize-coverage-ignorelist=blocklist.txt 和 -fsanitize-coverage-allowlist=allowlist.txt,范圍可以試文件夾、單個(gè)文件或者單個(gè)方法。
allowlist.txt 示例:
# 允許文件夾里所有文件
src:bar/*
# 特定源文件
src:foo/a.cpp
# 允許文件中所有函數(shù)
fun:*
blocklist.txt 示例:
# 禁用特定源文件
src:bar/b.cpp
# 禁用特定函數(shù)
fun:*myFunc*
上線前檢查出的沒有用到的代碼,并不表示上線后用戶不會(huì)用到,比如 AB 實(shí)驗(yàn)、用戶特殊設(shè)置、不常見 Case 等。這就可以利用 allowlist.txt 將部分不確定的代碼放到線上去檢測(cè),或者通過自動(dòng)插入埋點(diǎn)灰度檢測(cè),這些不確定的代碼不是主鏈路的,因此檢測(cè)影響范圍會(huì)很低。
SanitizerCoverage 本身是一個(gè) llvm pass,代碼在 llvm 工程的 llvm-project/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp 路徑下,那么怎么實(shí)現(xiàn)一個(gè)自定義的 pass 呢?
先把 llvm 裝到本地。
安裝 LLVM
使用 homebrew,命令如下:
brew install llvm@13
@13 表示 llvm 的版本。安裝后使用路徑在是 /usr/local/opt/llvm/,比如 cmake 構(gòu)建編譯環(huán)境可以使用下面的命令:
$LLVM_DIR=/usr/local/opt/llvm/lib/cmake/llvm cmake ..
可以用 Visual Studio Code 開發(fā) pass,安裝微軟的 C/C++ 的 extension,在 C/C++ Configurations 里把 /usr/local/opt/llvm/include/ 加入到包含路徑中。
llvm 的更新使用 brew upgrade llvm
llvm 也可以通過源碼來安裝,執(zhí)行如下命令即可:
git clone https://github.com/llvm/llvm-project.git
cd llvm-project
git checkout release/14.x
mkdir build
cd build
cmake -DCMAKE_BUILD_TYPE=Release -DLLVM_TARGETS_TO_BUILD=host -DLLVM_ENABLE_PROJECTS=clang ../llvm
cmake --build .
這里的 cmake 參數(shù) -DLLVM_ENABLE_PROJECTS=clang 表示也會(huì)構(gòu)建 clang 工具。如果還要加上 lld 以在構(gòu)建時(shí)能夠用自己的 pass,可以直接加成 -DLLVM_ENABLE_PROJECTS="clang;lld" 。
自定義安裝目錄的話,增加 -DCMAKE_INSTALL_PREFIX=/home/user/custom-llvm 。然后在設(shè)置路徑 export PATH=$PATH:/home/user/custom-llvm/bin 。
-G 編譯選項(xiàng)選擇 Ninja 編譯速度快。
各種設(shè)置整到一起:
cmake -G "Ninja" -DCMAKE_BUILD_TYPE=Release -DLLVM_TARGETS_TO_BUILD=host -DLLVM_ENABLE_PROJECTS="clang;lld" -DCMAKE_INSTALL_PREFIX=/Users/mingdai/Downloads/PTest/my-llvm-bin ../llvm
自制 Pass
Pass 介紹
llvm 屬于 multi-pass 編譯器,LLVM Pass 管理器是處理 pass 執(zhí)行的注冊(cè)和時(shí)序安排。曾有兩個(gè) pass 管理器,一個(gè)是 New Pass 管理器也叫 Pass 管理器,另一個(gè)是 Legacy Pass 管理器。New Pass 目前是默認(rèn)的管理器,Legacy Pass 在 LLVM 14 中被廢棄。Legacy 和 New 兩個(gè) pass 管理器在使用上最大的區(qū)別就是,Legacy 會(huì)注冊(cè)一個(gè)新的命令選項(xiàng),而 New Pass 只用定義一個(gè) pass。另外 Legacy 需要實(shí)現(xiàn) print 成員方法來打印,需要在通過 opt 通過傳遞 -analyze 命令行選項(xiàng)來運(yùn)行,而 New Pass 管理器是不用的,只需要實(shí)現(xiàn) printing pass。
總的來說
Legacy
- 基于繼承性
- 分析和打印 pass 之間沒有區(qū)別
- 注冊(cè)時(shí)加載所有需要的 pass
- 不變的 pass 執(zhí)行調(diào)度
- Transformation passes 定義了它們?cè)趫?zhí)行前保證保留的內(nèi)容
Legacy 的 pass 類
-
llvm::Passllvm::ModulePassllvm::FunctionPassllvm::PassRegistry
New
- 基于 CRTP、mixin 和 concept-model 的 idiom-based
- 在執(zhí)行過程中,根據(jù)需要有條件的加載依賴的 pass(更快、更有效)
- Transformation passes 在執(zhí)行后返回它們所保留的內(nèi)容
New 的 pass 類
llvm::PassInfoMixin<DerivedT>llvm::AnalysisInfoMixin<DerivedT>-
llvm::FunctionAnalysisManager- 別名類型
llvm::AnalysisManager<llvm::Function>
- 別名類型
-
llvm::ModuleAnalysisManager- 別名類型
llvm::AnalysisManager<llvm::Module>
- 別名類型
llvm::PreservedAnalysis
LLVM Pass 可以對(duì) LLVM IR 進(jìn)行優(yōu)化。優(yōu)化表現(xiàn)在 Pass 可以對(duì) IR 進(jìn)行分析和轉(zhuǎn)換,因此 Pass 主要也是分為分析(analysis)和轉(zhuǎn)換(transform)兩類。
分析里有數(shù)據(jù)流分析技術(shù),分為以下三種:
- Reaching-Definition Analysis 到達(dá)定值分析
- Live-Variable Analysis 活躍變量分析
- Available-Expression Analysis 可用表達(dá)式分析
一些常用的優(yōu)化方法,比如刪除計(jì)算結(jié)果不會(huì)使用的語(yǔ)句、刪除歸納變量、刪除公共子表達(dá)式、進(jìn)入循環(huán)前就對(duì)不管循環(huán)多少次都是同樣結(jié)果的表達(dá)式進(jìn)行求值、快的操作替換慢操作、用可推導(dǎo)出值是常量的表達(dá)式來替代表達(dá)式等。
編寫優(yōu)化的幾個(gè)方法。完整代碼參看這里。
插入新指令:
- 直接通過類或命名的構(gòu)造函數(shù)。
- 使用 llvm::IRBuilder<> 模板類。
刪除指令:
- llvm::Instruction::eraseFromParent() 成員函數(shù)
替換存在指令:
- llvm::ReplaceInstWithInst() 函數(shù)
- ~#include "llvm/Transforms/Utils/BasicBlockUtils.h"~
直接改指令
- llvm::User::setOperand() 成員函數(shù)
Value ? ConstantInt 類型轉(zhuǎn)換:
Type _t;
ConstantInt* val = dyn_cast<ConstantInt>(_t);
獲取 ConstantInt 類的值
ConstantInt* const_int;
uint64_t val = const_int->getZExtValue();
替換某個(gè)指令
Instruction inst;
// 替換,只是替換了引用,并沒刪
inst.replaceAllUsesWith(val);
// 刪除
if(inst->isSafeToRemove())
inst->eraseFromParent();
對(duì)應(yīng)的 IR 代碼
; 執(zhí)行前
%12 = load i32, i32* %2, align 4
%13 = add nsw i32 %12, 0
store i32 %13, i32* %3, align 4
; 只替換指令引用
%12 = load i32, i32* %2, align 4
%13 = add nsw i32 %12, 0
store i32 %12, i32* %3, align 4
%12 = load i32, i32* %2, align 4
store i32 %12, i32* %3, align 4
Instruction referencing instruction not embedded in a basic block!
%12 = load i32, i32* %2, align 4
<badref> = add nsw i32 %12, 0
建立新指令
// 取出第一個(gè)操作數(shù)
Value* val = inst.getOperand(0);
// 確定新指令的插入位置
IRBuilder<> builder(&inst);
// val << 1
Value* newInst = builder.CreateShl(val, 1);
// 替換指令
inst.replaceAllUsesWith(newInst);
Analysis pass 的 print pass 是基于一個(gè) Transformation pass,會(huì)請(qǐng)求原始 pass 分析的結(jié)果,并打印這些結(jié)果。會(huì)注冊(cè)一個(gè)命令行選項(xiàng) print<analysis-pass-name>。
實(shí)現(xiàn) pass 要選擇是 Analysis 還是 Transformation,也就是要對(duì)進(jìn)行輸入 IR 的分析還是進(jìn)行轉(zhuǎn)換來決定采用哪種。選擇 Transformation 通常繼承 PassInfoMixin。Analysis 繼承 AnalysisInfoMixin。
pass 生成的插件分為動(dòng)態(tài)和靜態(tài)的。靜態(tài)插件不需要在運(yùn)行時(shí)用 -load-pass-plugin 選項(xiàng)進(jìn)行加載,但需要在 llvm 工程中設(shè)置 CMake 重新構(gòu)建 opt。
做自己 pass 前可以先了解下 llvm 內(nèi)部的 pass 示例,可以先從兩個(gè)最基本的 Hello 和 Bye 來。比較實(shí)用的是一些做優(yōu)化的 pass,這些 pass 也是學(xué)習(xí)寫 pass ,了解編譯器如何工作的重要資源。許多 pass 都實(shí)現(xiàn)了編譯器開發(fā)理論中著名的概念。比如優(yōu)化 memcpy 調(diào)用(比如用 memset 替換)的 memcpyopt 、簡(jiǎn)化 CFG IRTransforms、總是內(nèi)聯(lián)用 alwaysinline 修飾的函數(shù)的 always-inline 、死代碼消除的 dce 和刪除未使用的循環(huán)的 loop-deletion。
自制插入指令 pass
接下來,怎么在運(yùn)行時(shí)插入指令來獲取我們需要代碼使用情況。完整代碼可以在這里 MingPass 拉下代碼參考進(jìn)行修改調(diào)試。
個(gè) pass 功能是在運(yùn)行時(shí)環(huán)境直接在特定位置執(zhí)行指定的函數(shù)。先寫個(gè)要執(zhí)行的函數(shù),新建個(gè)文件 loglib.m,代碼如下:
#include <stdio.h>
void runtimeLog(int i) {
printf("計(jì)算結(jié)果: %i\n", i);
}
再到 MingPass.cpp 中包含模塊頭文件
#include "llvm/IR/Module.h"
會(huì)用到 Module::getOrInsertFunction 函數(shù)來給 loglib.m 的 runtimeLog 做聲明。
更改 runOnFunction 函數(shù),代碼如下:
virtual bool runOnFunction(Function &F) {
// 從運(yùn)行時(shí)庫(kù)中獲取函數(shù)
LLVMContext &Context = F.getContext();
std::vector<Type*> paramTypes = {Type::getInt32Ty(Context)};
Type *retType = Type::getVoidTy(Context);
FunctionType *funcType = FunctionType::get(retType, paramTypes, false);
FunctionCallee logFunc = F.getParent()->getOrInsertFunction("runtimeLog", funcType);
for (auto &BB : F) {
for (auto &I : BB) {
if (auto *op = dyn_cast<BinaryOperator>(&I)) {
IRBuilder<> builder(op);
// 在 op 后面加入新指令
builder.SetInsertPoint(&BB, ++builder.GetInsertPoint());
// 在函數(shù)中插入新指令
Value* args[] = {op};
builder.CreateCall(logFunc, args);
return true;
} // end if
}
}
return false;
}
在 build 目錄下 make 出 pass 的 so 后,鏈接 main.m 和 loglib.m 的產(chǎn)物成可執(zhí)行文件,命令如下:
clang -c loglib.m
/usr/local/opt/llvm/bin/clang -flegacy-pass-manager -Xclang -load -Xclang build/src/libMingPass.so -c main.m
clang main.o loglib.o
./a.out
輸入數(shù)字4后,打印如下:
4
計(jì)算結(jié)果: 6
6
更多自制 pass
可以在這里查看,代碼里有詳細(xì)注釋。這里先留個(gè)白,后面再添加內(nèi)容。
IR
你會(huì)發(fā)現(xiàn)開發(fā) pass 需要更多的了解 IR,才可以更好的控制 LLVM 前端處理的高級(jí)語(yǔ)言。接下來我會(huì)說下那些高級(jí)語(yǔ)言的特性是怎么在 IR 里表現(xiàn)的。先介紹下 IR。
IR 介紹
LLVM IR(Intermediate Representation) 可以稱為中間代碼,是 LLVM 整個(gè)編譯過程的中間表示。
llvm ir 的基礎(chǔ)塊里的指令是不可跳轉(zhuǎn)到基礎(chǔ)塊的中間或尾部,只能從基礎(chǔ)塊的第一個(gè)指令進(jìn)入基礎(chǔ)塊。
下面是 ir 的幾個(gè)特點(diǎn):
- llvm ir 不是機(jī)器代碼而是生成機(jī)器代碼之前的一種有些看起來像高級(jí)語(yǔ)言的,比如函數(shù)和強(qiáng)類型,有些看起來像低級(jí)程序集,比如分支和基本塊。
- llvm ir 是強(qiáng)類型。
- llvm 沒有 sign 和 unsign 整數(shù)區(qū)別。
- 全局符號(hào)用 @ 符號(hào)開頭。
- 本地符號(hào)用 % 符號(hào)開頭。
- 必須定義和聲明所有符號(hào)。
IR 指令
常用指令
- alloca:分配??臻g
- load:從棧和全局內(nèi)存讀值
- store:將值寫到棧或全局內(nèi)存
- br:分支(條件或非條件)
- call:調(diào)用函數(shù)
- ret:從一個(gè)函數(shù)返回,可能會(huì)帶上一個(gè)返回值
- icmp/fcmp:比較整型或浮點(diǎn)值
- add/sub/mul:整數(shù)二進(jìn)制算術(shù)運(yùn)算
- fadd/fsub/fmul:浮點(diǎn)二進(jìn)制算術(shù)運(yùn)算
- sdiv/udiv/fdiv:有符號(hào)位整數(shù)/無符號(hào)位整數(shù)/浮點(diǎn)除法
- shl/shr:位向左/向右
- lshr/ashr:邏輯/算術(shù)右移
- and/or/xor:位邏輯運(yùn)算(沒有
not?。?/li>
常用特殊 ir 指令
- select:根據(jù)一個(gè)沒有 IR 級(jí)別分支的條件選擇一個(gè)值。
- phi:根據(jù)當(dāng)前基本塊前身選擇一個(gè)值。
- getelementpointer:獲取數(shù)組或結(jié)構(gòu)體里子元素的地址(不是值)。官方說明[[https://llvm.org/docs/GetElementPtr.html][The Often Misunderstood GEP Instruction]]。
- extractvalue:從一個(gè)數(shù)組或結(jié)構(gòu)體中提取一個(gè)成員字段的值(不是地址)。
- insertvalue:將一個(gè)值添加給數(shù)組或結(jié)構(gòu)體的成員字段。
ir 轉(zhuǎn)換指令
- bitcast:將一個(gè)值轉(zhuǎn)成給定類型而不改變它的位。
- trunc/fptrunc:將一個(gè)類型的整數(shù)/浮點(diǎn)值截?cái)酁橐粋€(gè)更小的整數(shù)/浮點(diǎn)類型。
- zext/sext/fpext:將一個(gè)值擴(kuò)展到一個(gè)更大的整數(shù)/浮點(diǎn)類型上。
- fptoui/fptosi:將一個(gè)浮點(diǎn)值轉(zhuǎn)換為無符號(hào)/有符號(hào)位的整數(shù)類型。
- uitofp/sitofp:將一個(gè)無符號(hào)/有符號(hào)位整數(shù)值轉(zhuǎn)換為浮點(diǎn)類型。
- ptrtoint:將指針轉(zhuǎn)成整數(shù)。
- inttoptr:將整數(shù)值轉(zhuǎn)成指針類型。
ir 庫(kù)的 header 地址在 include/llvm/IR ,源文件在 lib/IR ,文檔 llvm Namespace Reference。所有類和函數(shù)都在 llvm 命名空間里。
主要基礎(chǔ)類的說明如下:
- llvm::Module:ir 的容器類的最高級(jí)。
- llvm::Value:所有可作為其他值或指令操作數(shù)的基類。
- llvm::Constant
- llvm::ConstantDataArray (Constants.h)
- llvm::ConstantInt (Constants.h)
- llvm::ConstantFP (Constants.h)
- llvm::ConstantStruct (Constants.h)
- llvm::ConstantPointerNull (Constants.h)
- llvm::Function
- llvm::GlobalVariable
- llvm::BasicBlock
- llvm::Instruction
- Useful X-macro header: Instruction.def
- llvm::BinaryOperator (InstrTypes.h)
- add, sub, mul, sdiv, udiv, srem, urem
- fadd, fsub, fmul, fdiv, frem
- shl, lshr, ashr, and, or, xor
- llvm::CmpInst (InstrTypes.h)
- llvm::ICmpInst (Instructions.h)
- llvm::FCmpInst (Instructions.h)
- llvm::UnaryInstruction (InstrTypes.h)
- llvm::CastInst (Instrtypes.h)
- llvm::BitCastInst (Instructions.h)
- llvm::Constant
- llvm::Type:代表所有的 IR 數(shù)據(jù)類型,包括原始類型,結(jié)構(gòu)類型和函數(shù)類型。
C 調(diào)用 LLVM 接口
項(xiàng)目在:CLLVMCase
這是代碼:
/*
int sum(int a, int b) {
return a + b;
}
,*/
void csum() {
LLVMModuleRef module = LLVMModuleCreateWithName("sum_module");
LLVMTypeRef param_types[] = {LLVMInt32Type(), LLVMInt32Type()};
// 函數(shù)參數(shù)依次是函數(shù)的類型,參數(shù)類型向量,函數(shù)數(shù),表示函數(shù)是否可變的布爾值。
LLVMTypeRef ftype = LLVMFunctionType(LLVMInt32Type(), param_types, 2, 0);
LLVMValueRef sum = LLVMAddFunction(module, "sum", ftype);
LLVMBasicBlockRef entry = LLVMAppendBasicBlock(sum, "entry");
LLVMBuilderRef builder = LLVMCreateBuilder();
LLVMPositionBuilderAtEnd(builder, entry);
// IR 的表現(xiàn)形式有三種,一種是內(nèi)存中的對(duì)象集,一種是文本語(yǔ)言,比如匯編,一種是二進(jìn)制編碼字節(jié) bitcode。
LLVMValueRef tmp = LLVMBuildAdd(builder, LLVMGetParam(sum, 0), LLVMGetParam(sum, 1), "tmp");
LLVMBuildRet(builder, tmp);
char *error = NULL;
LLVMVerifyModule(module, LLVMAbortProcessAction, &error);
LLVMDisposeMessage(error);
// 可執(zhí)行引擎,如果支持 JIT 就用它,否則用 Interpreter。
LLVMExecutionEngineRef engine;
error = NULL;
LLVMLinkInMCJIT();
LLVMInitializeNativeTarget();
if (LLVMCreateExecutionEngineForModule(&engine, module, &error) != 0) {
fprintf(stderr, "Could not create execution engine: %s\n", error);
return;
}
if (error)
{
LLVMDisposeMessage(error);
return;
}
long long x = 5;
long long y = 6;
// LLVM 提供了工廠函數(shù)來創(chuàng)建值,這些值可以被傳遞給函數(shù)。
LLVMGenericValueRef args[] = {LLVMCreateGenericValueOfInt(LLVMInt32Type(), x, 0), LLVMCreateGenericValueOfInt(LLVMInt32Type(), y, 0)};
LLVMInitializeNativeAsmPrinter();
LLVMInitializeNativeAsmParser();
// 函數(shù)調(diào)用
LLVMGenericValueRef result = LLVMRunFunction(engine, sum, 2, args);
printf("%lld\n", LLVMGenericValueToInt(result, 0));
// 生成 bitcode 文件
if (LLVMWriteBitcodeToFile(module, "sum.bc") != 0) {
fprintf(stderr, "Could not write bitcode to file\n");
return;
}
LLVMDisposeBuilder(builder);
LLVMDisposeExecutionEngine(engine);
}
Swift 調(diào)用 LLVM 接口
llvm 的接口還可以通過 swift 來調(diào)用。
先創(chuàng)建一個(gè) module.modulemap 文件,創(chuàng)建 LLVMC.h 和 LLVMC.c 文件,自動(dòng)生成 SwiftLLVMCase-Bridging-Header.h。設(shè)置 header search paths 為 llvm 所在路徑 /usr/local/opt/llvm/include ,library search paths 設(shè)置為 /usr/local/opt/llvm/lib 。將 /usr/local/opt/llvm/lib/libLLVM.dylib 加到 Linked Frameworks and Libraries 里。
module.modulemap 內(nèi)容
module llvm [extern_c] {
header "LLVMC.h"
export *
}
LLVMC.h 里設(shè)置要用到的 llvm 的頭文件,比如:
#ifndef LLVMC_h
#define LLVMC_h
#include <stdio.h>
#include <llvm-c/Analysis.h>
#include <llvm-c/BitReader.h>
#include <llvm-c/BitWriter.h>
#include <llvm-c/Core.h>
#include <llvm-c/Disassembler.h>
#include <llvm-c/ExecutionEngine.h>
#include <llvm-c/IRReader.h>
#include <llvm-c/Initialization.h>
#include <llvm-c/Linker.h>
#include <llvm-c/Object.h>
#include <llvm-c/Support.h>
#include <llvm-c/Target.h>
#include <llvm-c/TargetMachine.h>
#include <llvm-c/Transforms/IPO.h>
#include <llvm-c/Transforms/PassManagerBuilder.h>
#include <llvm-c/Transforms/Scalar.h>
#include <llvm-c/Transforms/Vectorize.h>
#include <llvm-c/lto.h>
#endif /* LLVMC_h */
在 swift 中寫如下代碼試試
import Foundation
import llvm
func hiIR() {
let module = LLVMModuleCreateWithName("HiModule")
LLVMDumpModule(module)
LLVMDisposeModule(module)
}
hiIR()
執(zhí)行結(jié)果如下:
; ModuleID = 'HiModule'
source_filename = "HiModule"
下面一個(gè)簡(jiǎn)單的 c 函數(shù)
int sum(int a, int b) {
return a + b;
}
使用 llvm 的接口寫對(duì)應(yīng)的 IR 代碼如下:
func cSum() {
let m = Module(name: "CSum")
let bd = IRBuilder(module: m)
let f1 = bd.addFunction("sum", type: FunctionType([IntType.int32, IntType.int32], IntType.int32))
// 添加基本塊
let entryBB = f1.appendBasicBlock(named: "entry")
bd.positionAtEnd(of: entryBB)
let a = f1.parameters[0]
let b = f1.parameters[1]
let tmp = bd.buildAdd(a, b)
bd.buildRet(tmp)
m.dump()
}
dump 出對(duì)應(yīng) IR 如下:
; ModuleID = 'CSum'
source_filename = "CSum"
define i32 @sum(i32 %0, i32 %1) {
entry:
%2 = add i32 %0, %1
ret i32 %2
}
對(duì)于控制流函數(shù),比如下面的 swift 函數(shù):
func giveMeNumber(_ isBig : Bool) -> Int {
let re : Int
if !isBig {
// the fibonacci series (sort of)
re = 3
} else {
// the fibonacci series (sort of) backwards
re = 4
}
return re
}
使用 llvm 接口編寫 IR,代碼如下:
func controlFlow() {
let m = Module(name: "CF")
let bd = IRBuilder(module: m)
let f1 = bd.addFunction("calculateFibs", type: FunctionType([IntType.int1], FloatType.double))
let entryBB = f1.appendBasicBlock(named: "entry")
bd.positionAtEnd(of: entryBB)
// 給本地變量分配空間 let retVal : Double
let local = bd.buildAlloca(type: FloatType.double, name: "local")
// 條件比較 if !backward
let test = bd.buildICmp(f1.parameters[0], IntType.int1.zero(), .equal)
// 創(chuàng)建 block
let thenBB = f1.appendBasicBlock(named: "then")
let elseBB = f1.appendBasicBlock(named: "else")
let mergeBB = f1.appendBasicBlock(named: "merge")
bd.buildCondBr(condition: test, then: thenBB, else: elseBB)
// 指到 then block
bd.positionAtEnd(of: thenBB)
let thenVal = FloatType.double.constant(1/89)
bd.buildBr(mergeBB) // 到 merge block
// 指到 else block
bd.positionAtEnd(of: elseBB)
let elseVal = FloatType.double.constant(1/109)
bd.buildBr(mergeBB) // 到 merge block
// 指到 merge block
bd.positionAtEnd(of: mergeBB)
let phi = bd.buildPhi(FloatType.double, name: "phi_example")
phi.addIncoming([
(thenVal, thenBB),
(elseVal, elseBB)
])
// 賦值給本地變量
bd.buildStore(phi, to: local)
let ret = bd.buildLoad(local, type: FloatType.double, name: "ret")
bd.buildRet(ret)
m.dump()
}
輸出對(duì)應(yīng) IR 代碼:
; ModuleID = 'CF'
source_filename = "CF"
define double @giveMeNumber(i1 %0) {
entry:
%local = alloca i32, align 4
%1 = icmp eq i1 %0, false
br i1 %1, label %then, label %else
then: ; preds = %entry
br label %merge
else: ; preds = %entry
br label %merge
merge: ; preds = %else, %then
%phi_example = phi i32 [ 3, %then ], [ 4, %else ]
store i32 %phi_example, i32* %local, align 4
%ret = load i32, i32* %local, align 4
ret i32 %ret
}
這里有完整代碼 SwiftLLVMCase。
解釋執(zhí)行 bitcode(IR)
IR 的表現(xiàn)形式有三種,一種是內(nèi)存中的對(duì)象集,一種是文本語(yǔ)言,一種是二進(jìn)制編碼字節(jié) bitcode。
對(duì)于 Intel 芯片可以通過 Pin,arm 架構(gòu)可以用 DynamoRIO,目前 DynamoRIO 只支持 Window、Linux 和 Android 系統(tǒng),對(duì) macOS 的支持還在進(jìn)行中。另一種方式是通過基于 llvm 的 interpreter 開發(fā)來實(shí)現(xiàn)解釋執(zhí)行 bitcode,llvm 用很多 C++ 的接口在內(nèi)存中操作,將可讀的文本文件解析到內(nèi)存中,編譯過程文本的 IR 不會(huì)生成,只會(huì)生成一種緊湊的二進(jìn)制表示,也就是 bitcode。下面具體說下怎么做。
先構(gòu)建一個(gè)支持 libffi 的 llvm。編譯 llvm 源碼時(shí)加上 libffi 的選項(xiàng)來打開 DLLVM_ENABLE_FFI 的選項(xiàng)打開 libffi,編譯命令如下:
cmake -G Ninja -DLLVM_ENABLE_FFI:BOOL=ON ../llvm
創(chuàng)建一個(gè)項(xiàng)目。cmake 文件里注意設(shè)置自己的編譯生成的 llvm 路徑,還有 llvm 源碼路徑,設(shè)置這個(gè)路徑主要是為了用安裝 llvm 時(shí)沒有包含的 ExecutionEngine/Interpreter/Interpreter.h 頭文件。
實(shí)現(xiàn)方式是通過訪問 llvm 的 ExcutionEngine 進(jìn)行 IR 指令解釋執(zhí)行。聲明一個(gè)可訪問 ExcutionEngine 內(nèi)部的類 PInterpreter,代碼如下:
// 使用 public 訪問內(nèi)部
class PInterpreter : public llvm::ExecutionEngine,
public llvm::InstVisitor<llvm::Interpreter> {
public:
llvm::GenericValue ExitValue;
llvm::DataLayout TD;
llvm::IntrinsicLowering *IL;
std::vector<llvm::ExecutionContext> ECStack;
std::vector<llvm::Function*> AtExitHandlers;
};
然后聲明要用的方法。
class MInterpreter : public llvm::ExecutionEngine {
public:
llvm::Interpreter *interp;
PInterpreter *pItp;
llvm::Module *module;
explicit MInterpreter(llvm::Module *M);
virtual ~MInterpreter();
virtual void run();
virtual void execute(llvm::Instruction &I);
// 入口
virtual int runMain(std::vector<std::string> args,
char * const *envp = 0);
// 遵循 ExecutionEngine 接口
llvm::GenericValue runFunction(
llvm::Function *F,
const std::vector<llvm::GenericValue> &ArgValues
);
void *getPointerToNamedFunction(const std::string &Name,
bool AbortOnFailure = true);
void *recompileAndRelinkFunction(llvm::Function *F);
void freeMachineCodeForFunction(llvm::Function *F);
void *getPointerToFunction(llvm::Function *F);
void *getPointerToBasicBlock(llvm::BasicBlock *BB);
};
如上面代碼所示,因?yàn)橐獔?zhí)行 IR,所以用到獲取 IR 的函數(shù)和基本塊地址的方法,getPointerToFunction 和 getPointerToBasicBlock。最后再執(zhí)行指令時(shí),先打印出指令,然后進(jìn)行執(zhí)行,代碼如下:
class MingInterpreter : public MInterpreter {
public:
MingInterpreter(Module *M) : MInterpreter(M) {};
virtual void execute(Instruction &I) {
I.print(errs());
MInterpreter::execute(I);
}
};
完整代碼參看 MingInterpreter。
項(xiàng)目是基于 c 語(yǔ)言,可以使用 llvm include 里的 llvm-c/ExecutionEngine.h 接口頭文件,使用 c 來編寫。OC 和 Swift 項(xiàng)目還需要根據(jù)各自語(yǔ)言特性進(jìn)行開發(fā)完善解釋功能。