2012年10月8日 星期一

LLVM 寫一個 pass - 教學入門篇

注意!! 這篇文章並非官方的 Writing an LLVM Pass 的中文翻譯版本!!


而是精簡版, 採用建置方式是將 pass 的程式碼獨立於 LLVM 的 Source tree 外
這樣的好處是可避免與整串 LLVM 搞在一起, 並且對於整體建置流程會較為清楚

這篇文章對於讀者有些假設 :
  1. 已經會建置 LLVM (不會? 建置與安裝 LLVM + Clang)
  2. 對於 Makefile 有一點基礎知識 (沒有? 快來惡補)


1. 先從 LLVM 抄一個 Hello Pass

#define DEBUG_TYPE "hello"
#include "llvm/Pass.h"
#include "llvm/Function.h"
#include "llvm/Support/raw_ostream.h"
#include "llvm/ADT/Statistic.h"
using namespace llvm;

namespace {
  // Hello - The first implementation, without getAnalysisUsage.
  struct Hello : public FunctionPass {
    static char ID; // Pass identification, replacement for typeid
    Hello() : FunctionPass(ID) {}

    virtual bool runOnFunction(Function &F) {
      errs() << "Hello: ";
      errs() << F.getName() << '\n';
      return false;
    }
  };
}

char Hello::ID = 0;
static RegisterPass<Hello> X("hello", "Hello World Pass");
將上列程式碼儲存成 Hello.cpp 或著任何你喜歡的檔名
原程式碼上面有一些授權宣告跟註解, 基於文章版面配置將那部份移除, 其詳細授權見LLVM Developer Policy
說明?能建置成功前先把細節拋一邊吧

2. 建置 Makefile

LLVM_CONFIG=llvm-config

CXX=`$(LLVM_CONFIG) --bindir`/clang
CXXFLAGS=`$(LLVM_CONFIG) --cppflags` -fPIC -fno-rtti
LDFLAGS=`$(LLVM_CONFIG) --ldflags`

all: hello.so

hello.so: Hello.o
        $(CXX) -shared Hello.o -o hello.so $(LDFLAGS) -fPIC

Hello.o: Hello.cpp
        $(CXX) -c Hello.cpp -o Hello.o $(CXXFLAGS)

clean:
        rm -f *.o hello.so
把上面內容抄到與 Hello.cpp 同目錄下的 Makefile 中
注意一下 Makefile 建置動作前面的是 Tab 字元不是空白字元, 相當重要
這邊唯一要注意的是 LLVM_CONFIG 要設定為 你安裝的 llvm 路徑裡面的那個 llvm-config
例如 LLVM 裝在家目錄底下, 那麼 LLVM_CONFIG 就設定為 /home/kito/bin/llvm-config

3. 建置與測試

接著執行 make,
make
應該會吐出下面兩行
`llvm-config --bindir`/clang -c Hello.cpp -o Hello.o `llvm-config --cppflags` -fPIC
`llvm-config --bindir`/clang -shared Hello.o -o hello.so `llvm-config --ldflags` -fPIC
然後目錄下應該會出現 Hello.so 才對
ls
# Hello.cpp  Hello.o  hello.so  Makefile
接著寫個 Hello World 的小程式
#include <stdio.h>

int main () {
  printf("Hello LLVM!\n");
}
將以上簡單內容存檔成 HelloWorld.c 接著把 Hello World 產生 .bc 檔, 加上 -emit-llvm 告訴 llvm 你要產生 bitcode
clang -c -emit-llvm -o HelloWorld.bc HelloWorld.c
最後步驟就是把你的 HelloWorld.bc 丟到前面寫的 Hello Pass !
opt -load ./hello.so -hello HelloWorld.bc -o /dev/null
後面 -o /dev/null 的意思是叫 opt 把經過最佳化或分析的結果直接丟掉
執行完後 LLVM 就會跟你說 Hello 了!
Hello: main
main 是你 Hello World 中的 Function 名稱

4. 回頭看一下 Hello Pass

DEBUG_TYPE 是 LLVM 拿來統計該 pass 被呼叫幾次或著是使用 DEBUG 輸出除錯訊息的時候用的
#define DEBUG_TYPE "hello"
include 要用到的 LLVM 的
#include "llvm/Pass.h"
#include "llvm/Function.h"
#include "llvm/Support/raw_ostream.h"
#include "llvm/ADT/Statistic.h"
把 llvm namespace 的東西都弄進來, 雖然這是不好的習慣, 不過由於方便起見直接 using 下去吧XD
using namespace llvm;
匿名 namespace, 避免 export 裡面的 symbol,
聽無上一句在講啥就先跳過就可以了
namespace {
Hello 繼承 FunctionPass, 代表 Hello Pass 的輸入是以 Function 為單位
  // Hello - The first implementation, without getAnalysisUsage.
  struct Hello : public FunctionPass {
現階段下面這行就照抄吧
給 LLVM 用來識別這個 Pass 的 id
    static char ID; // Pass identification, replacement for typeid
Constructor
    Hello() : FunctionPass(ID) {}
這邊則是整個 pass 的精華所在, LLVM 會以一次一個 Function 為單位, 餵進來給你玩
大致上裡面的行為就是將餵進來的 Function 的名稱印出來而已
其中 return false 是告訴 LLVM 你沒有修改任何東西
    virtual bool runOnFunction(Function &F) {
      errs() << "Hello: ";
      errs() << F.getName()) << '\n';
      return false;
    }
class 跟 namespace 的結束, 沒啥好說的
  };
}
定義下一下 Hello::ID 的實體
char Hello::ID = 0;
跟 LLVM 註冊你的這個 Pass, 第一個參數是 pass name, 第二個則是說明用的文字
static RegisterPass<hello> X("hello", "Hello World Pass");

5. 解析 Makefile

先指定 llvm-config 執行檔的所在位置, 如果 llvm-config 已經加到 PATH 中的話那就填 llvm-config 就好
LLVM_CONFIG=llvm-config
llvm-config 這隻程式可以幫你產生出大部分你所需的 flag
用 --help 可以檢視所有可用選項
llvm-config --help
指定編譯器使用 clang, 單純個人喜好XD
CXX=`$(LLVM_CONFIG) --bindir`/clang
指定編譯的參數, 大部分從 llvm-config --cppflags 即可,
但由於我們現在是要編譯的是 Shared Library 所以要加 -fPIC
不知道 Shared Library 是啥? 快去弄一本程式設計師的自我修養來 K 阿~~
-fno-rtti 則是指定不產生 Run-time Typeinfo 的資訊,加了此選項後 typeid 及 dynamic_cast 會無法使用,基於實作成本 LLVM 在這方面有實作自己一套的 RTTI 機制
CXXFLAGS=`$(LLVM_CONFIG) --cppflags` -fPIC -fno-rtti
指定 Link 的參數, 大部分從 llvm-config --ldflags 即可,
現在是要編譯的是 Shared Library 所以加 -shared
LDFLAGS=`$(LLVM_CONFIG) --ldflags` -shared 
我們總共要建置 hello.so
all: hello.so
hello.so 怎麼生勒?, 生之前要先生出 Hello.o
然後下面一行則是建置規則
hello.so: Hello.o
        $(CXX) Hello.o -o hello.so $(LDFLAGS)
Hello.o 的建置規則, 以及其相依 Hello.cpp
Hello.o: Hello.cpp
        $(CXX) -c Hello.cpp -o Hello.o $(CXXFLAGS)
寫 Makefile 時寫個 clean 的 rule 是個好習慣 :)
clean:
        rm -f *.o hello.so

參考

[1] Writing an LLVM Pass
[2] 程式設計師的自我修養-連結、載入、程式庫

Update Note

  1. (11/16) 在較新版的 llvm-config 無提供 -fno-rtti 選項,必須自行在 CXXFLAGS 那邊自行添加
  2. (11/22) 上面提到那個項目目前不確定是 bug 還是怎樣,cmake 出來的 llvm-config 不會有 -fno-rtti,但 configure 建置出來的 llvm-config 則會有