1. 程式人生 > >LLVM第一彈——介紹及環境搭建(截止2018/9/14可用)

LLVM第一彈——介紹及環境搭建(截止2018/9/14可用)

       最近開始搞LLVM,感受到自己自學能力還是不夠,話不多說,下面我將從以下五個方面來介紹LLVM。分別是:(1)LLVM是什麼?(2)LLVM的組成部分;(3)LLVM+Clang環境搭建;(4)LLVM的執行過程;(5)LLVM Pass的構建執行過程;期間參考了以下優質的帖子(PS:都是大佬啊),會在每一章都有列出說明。

(一)LLVM是什麼??

        LLVM(low level virtual machine)從本質上來說,是一個開源編譯器框架,能夠提供程式語言的編譯期優化、連結優化、線上編譯優化、程式碼生成。LLVM有兩個特點:

     (1)LLVM有一個特定指令格式的IR語言,我們可以通過書寫Pass來對其IR進行優化。

     (2)可以作為多種語言的後端,提供與程式語言無關的優化和針對多種CPU的程式碼生成功能。

(二)LLVM的組成部分:

      LLVM主要由Clang前端、IR優化器(Pass)和LLVM後端構成。其功能分別是:

      clang前端:將平臺相關的原始碼生成與平臺無關的IR(llvm Bitcode)。

      IR優化器:主要對IR進行優化。

      llvm後端:將優化後的IR轉換為與平臺相關的彙編程式碼或者機器碼。

                                           

2.1  Clang前端:

       Clang前端以.c檔案為輸入,經語法詞法分析後解析為抽象語法數,最後通過LLVM內聯API變為LLVM IR。其功能為:詞法分析器:把輸入的程式程式碼切成token;語法分析器:接收token流解析為AST。

                                   

2.2 IR優化器:

       LLVM IR包含三種格式:一種是在記憶體中的編譯中間語言;一種是硬碟上儲存的二進位制中間語言(以.bc結尾),最後一種是可讀的中間格式(以.ll結尾)。這三種中間格式是完全相等的。LLVM IR是LLVM優化和進行程式碼生成的關鍵。根據可讀的IR,我們可以知道再最終生成目的碼之前,我們已經生成了什麼樣的程式碼。我們通過Pass來對IR進行相應的優化。

2.3 LLVM後端:

       Llvm clang編譯器主要是將各平臺原始碼編譯成與平臺無關的IR指令集,這將支撐對IR的優化及轉換操作,而llvm後端的主要工作是優化IR指令,並將這些與平臺無關的IR指令轉換成目標裝置相關的指令。

                                          

        由上圖所示,LLVM IR進入後端要經過pass優化,指令選擇,指令排程,暫存器分配,程式碼佈局優化以及彙編發行等過程。上述各過程都是pass優化的過程,普通(白色)pass可由使用者自定義,內建(灰色)pass由一系列小的pass構成,換句話說我們可以對每一個階段都可以進行不同程度的優化。同時無須為每個目標平臺編寫重複的程式碼。

2.3.1 指令選擇

                         

     指令選擇將記憶體中三地址結構的IR指令轉換成裝置相關的DAG節點,如上圖所示,指令選擇的主要過程有:

    (1) 構建初始DAG:該過程只是將LLVM IR指令簡單的轉換成不合法的SelectionDAG,利用SelectDAGISe類呼叫visit()函式遍歷IR建立DAGNode節點

   (2)優化 SelectionDAG:識別目標平臺支援的元指令

   (3)型別合法化:消除不支援的型別,利用TargetLowering介面

  (4)優化SelectionDAG:清除型別合法化後的冗餘

  (5)操作合法化:將操作進行合法化

  (6)優化 SelectionDAG:消除效率低下的運算元

  (7)轉換裝置無關的DAG到目標DAG(DAGNode轉換為MachineSDNode(目標平臺機器指令))

       注:機器指令由.td檔案描述。Tablegen就是用於記錄這些資訊的描述性語言。經過tablegen工具批量生成C++原始檔。它的好處就是減少我們描述的工作量。Tablegen主要由Class(類)和Definition(定義)組成。其中Class是用於描述構建其它記錄的抽象記錄,可以理解成模板。描述目標的共同特點以便批量生成記錄。Definition是具體的描述例項,不包含任何未定義的變數。

2.3.2 指令排程

         對DAG進行拓撲排序轉換為線性指令集儘可能的利用指令級的並行操作,提高執行效率.

2.3.3 暫存器分配

         由於IR擁有多個虛擬暫存器,因此需要將輸入的任意數目的暫存器重新分配為符合硬體要求的有限個數暫存器。

2.3.4 程式碼發行

        輸出彙編程式碼或二進位制程式碼

LLVM+Clang環境搭建:

      環境:Ubuntu16.04 32位

     gcc版本號:5.3.1

     Cmake版本號:3.5.1

     3.1新建LLVM資料夾:

mkdir LLVM

     3.2下載以下五個包:llvm-3.3.src、cfe-3.3.src、clang-tools-extra-3.3.src、compiler-rt-3.3.src、libcxx-3.3.src將其解壓至LLVM資料夾下。可以在這個連結或網頁找資源。下載連結:https://download.csdn.net/download/weixin_38244174/10667601

      3.3然後按下面的步驟組織,這樣可以將clang,clang-tools-extra和compiler-rt就可以和llvm一起編譯了。

mv cfe-3.3.src clang
mv clang/ llvm-3.3.src/tools/
mv clang-tools-extra-3.3.src extra
mv extra/ llvm-3.3.src/tools/clang/
mv compiler-rt-3.3.src compiler-rt
mv compiler-rt llvm-3.3.src/projects/

     3.4 在LLVM資料夾下新建build目錄:

mkdir buid
cd build

     3.5 配置並編譯,時間可能比較長,20min以上。

../llvm-3.3.src/configure --enable-optimized --enable-targets=host-only
make -j4
sudo make install

     3.6 驗證成功與否:

clang -help

        若顯示這樣則成功:

                            

)LLVM執行過程

                             

(一)理論基礎:

      1.如上圖所示,以c檔案為例,首先使用-emit-llvm命令告訴clang前端將.C檔案生成llvm的IR(LLVM IR主要有三種格式:一種是在記憶體中的編譯中間語言;一種是硬碟上儲存的二進位制中間語言(以.bc結尾),最後一種是可讀的中間格式(以.ll結尾)。這三種中間格式是完全相等的)。

      2.生成可執行檔案有兩種方式:

      a)使用llc命令將IR檔案(.bc檔案)生成目標檔案(.o檔案),通過系統連結器連結多個目標檔案,生成可執行檔案。(針對單個IR)

       b)使用llvm-link連結多個IR,使用llc命令將IR檔案生成目標檔案,之後生成可執行檔案。(針對多個IR)

(二)具體實踐:

對原始碼的編譯。

1.建立簡單的c語言原始碼檔案test.c

#include <stdio.h>

int main() {
  printf("hello llvm\n");
  return 0;
}

2.編譯可執行檔案

clang test.c -o test

3.生成LLVM位元組碼檔案

clang -O3 -emit-llvm test.c -c -o test.bc

4.生成LLVM視覺化位元組碼檔案

clang -O3 -emit-llvm test.c -S -o test.ll

5.執行可執行檔案

./test

6.執行位元組碼檔案

lli test.bc

7.反彙編位元組碼檔案

llvm-dis < test.bc | less

8.編譯位元組碼為彙編檔案

llc test.bc -o test.s

)LLVM執行過程

       該Pass功能是:opt命令列工具會動態的去載入動態連結庫,以執行Pass,之後Pass會遍歷每一個函式,輸出其函式名,但未對IR做任何改動。

步驟如下:

5.1 建立一個FuncBlockCount.cpp檔案,內容如下:

#include "llvm/IR/Function.h"
#include "llvm/Pass.h"
#include "llvm/Support/raw_ostream.h"
//引入llvm名稱空間,可以讓其實用LLVM當中的函式
using namespace llvm;

//建立一個匿名的名稱空間
namespace  {

    //宣告Pass
    struct FuncBlockCount : public FunctionPass
    {
        //宣告Pass的識別符號,會被LLVM用作識別Pass
        static char ID;
        //對父類進行初始化
        FuncBlockCount() : FunctionPass(ID){}

        //其實就是FunctionPass的一個虛擬函式,這裡對它進行了實現。一個FunctionPass的子類要想做一些實際的工作,就必須對這個虛擬函式進行實現。
        bool runOnFunction(Function &F) override {
            //errs()是一個LLVM提供的C++輸出流,我們可以用它來輸出到控制檯
            errs() << "Function "<<F.getName()<<'\n';
            //函式返回false說明它沒有改動函式F。之後,如果我們真的變換了程式,我們需要返回一個true。
            return false;
        }
    };
}
//初始化Pass ID
char FuncBlockCount::ID = 0;
//需要註冊Pass、填寫名稱和命令列引數
static RegisterPass<FuncBlockCount> X("funcblockcount","Function Block Count",false,false);//,最後兩個引數描述了它的行為:如果一個pass不修改CFG,那麼第三個引數將被設定為true;如果pass是一個分析傳遞,例如dominator樹(支配樹)傳遞,則true作為第四個引數提供。

5.2 使用以下命令編譯so檔案

g++ FuncBlockCount.cpp -fPIC -g -Wall -Wextra -std=c++11 `llvm-config --cxxflags ` -shared -o FuncBlockCount.so

5.3 測試檔案為(4)中的test.ll檔案;

5.4使用命令執行新Pass.

opt -load /home/kyriehe/Desktop/testpass/FuncBlockCount.so -funcblockcount test.ll

5.5若顯示下圖則說明執行正確。

5.6 將C語言測試程式碼編譯為LLVM IR的形式

clang -O0 -S -emit-llvm test.c -o test.ll