使用 LLVM

前言

目前在做一些編譯相關(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 是通過 paramTypesType::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::Pass
    • llvm::ModulePass
    • llvm::FunctionPass
    • llvm::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è)最基本的 HelloBye 來。比較實(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::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ā)完善解釋功能。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容