關於LLVM,這些東西你必須知道!

發表於2017-02-08

只要你和程式碼打交道,瞭解編譯器的工作流程和原理定會讓你受益無窮,無論是分析程式,還是基於它寫自己的外掛,甚至學習一門全新的語音。通過本文,將帶你瞭解LLVM,並使用LLVM來完成一些有意思的事情。

一、什麼是LLVM?

The LLVM Project is a collection of modular and reusable compiler and toolchain technologies.

簡單來說,LLVM專案是一系列分模組、可重用的編譯工具鏈。它提供了一種程式碼編寫良好的中間表示(IR),可以作為多種語言的後端,還可以提供與變成語言無關的優化和針對多種cpu的程式碼生成功能。

先來看下LLVM架構的主要組成部分:

  • 前端:前端用來獲取原始碼然後將它轉變為某種中間表示,我們可以選擇不同的編譯器來作為LLVM的前端,如gcc,clang。
  • Pass(通常翻譯為“流程”):Pass用來將程式的中間表示之間相互變換。一般情況下,Pass可以用來優化程式碼,這部分通常是我們關注的部分。
  • 後端:後端用來生成實際的機器碼。

雖然如今大多數編譯器都採用的是這種架構,但是LLVM不同的就是對於不同的語言它都提供了同一種中間表示。傳統的編譯器的架構如下:

11script_1482136450962LLVM的架構如下:

12script_1482136601642當編譯器需要支援多種原始碼和目標架構時,基於LLVM的架構,設計一門新的語言只需要去實現一個新的前端就行了,支援新的後端架構也只需要實現一個新的後端就行了。其它部分完成可以複用,就不用再重新設計一次了。

二、安裝編譯LLVM

這裡使用clang作為前端:

1.直接從官網下載:http://releases.llvm.org/download.html

2.svn獲取

3.git獲取

最新的LLVM只支援cmake來編譯了,首先安裝cmake。

編譯:

編譯時間比較長,而且編譯結果會生成20G左右的檔案。

編譯完成後,就能在build/bin/目錄下面找到生成的工具了。

三、從原始碼到可執行檔案

我們在開發的時候的時候,如果想要生成一個可執行檔案或應用,我們點選run就完事了,那麼在點選run之後編譯器背後又做了哪些事情呢?

我們先來一個例子:

上面這個檔案,我們可以通過命令列直接編譯,然後連結:

拷貝到手機執行:

大家不會以為就這樣就完了吧,當然不是,我們要繼續深入剖析。

3.1 預處理(Preprocess)

這部分包括macro巨集的展開,import/include標頭檔案的匯入,以及#if等處理。

可以通過執行以下命令,來告訴clang只執行到預處理這一步:

執行完這個命令之後,我們會發現匯入了很多的標頭檔案內容。

可以看到上面的預處理已經把巨集替換了,並且匯入了標頭檔案。但是這樣的話會引入很多不會去改變的系統庫比如Foundation,所以有了pch預處理檔案,可以在這裡去引入一些通用的標頭檔案。

後來Xcode新建的專案裡面去掉了pch檔案,引入了moduels的概念,把一些通用的庫打成modules的形式,然後匯入,預設會加上-fmodules引數。

這樣的話,只需要@import一下就能匯入對應庫的modules模組了。

3.2 詞法分析 (Lexical Analysis)

在預處理之後,就要進行詞法分析了,將預處理過的程式碼轉化成一個個Token,比如左括號、右括號、等於、字串等等。

3.3 語法分析 (Semantic Analysis)

根據當前語言的語法,驗證語法是否正確,並將所有節點組合成抽象語法樹(AST)

語法樹直觀圖:

13script_1482150626825

3.4 IR程式碼生成 (CodeGen)

CodeGen負責將語法樹從頂至下遍歷,翻譯成LLVM IR,LLVM IR是Frontend的輸出,也是LLVM Backerend的輸入,橋接前後端。

可以在中間程式碼層次去做一些優化工作,我們在Xcode的編譯設定裡面也可以設定優化級別-O1,-O3,-Os。 還可以去寫一些自己的Pass,這裡需要解釋一下什麼是Pass。

Pass就是LLVM系統轉化和優化的工作的一個節點,每個節點做一些工作,這些工作加起來就構成了LLVM整個系統的優化和轉化。

3.5 生成位元組碼 (LLVM Bitcode)

我們在Xcode7中預設生成bitcode就是這種的中間形式存在, 開啟了bitcode,那麼蘋果後臺拿到的就是這種中間程式碼,蘋果可以對bitcode做一個進一步的優化,如果有新的後端架構,仍然可以用這份bitcode去生成。

14script_1482218230417

3.6 生成相關彙編

3.7 生成目標檔案

15script_1482218636504

3.8 生成可執行檔案

3.9 整體流程

16script_1482219005958

四、可以用Clang做什麼?

4.1 libclang進行語法分析

可以使用libclang裡面提供的方法對原始檔進行語法分析,分析它的語法樹,遍歷語法樹上面的每一個節點。可以用於檢查拼寫錯誤,或者做字串加密。

來看一段程式碼的使用:

然後我們就可以在printVisitor這個函式裡面去遍歷輸入檔案的語法樹了。

我們也通過通過python去呼叫用clang:

那麼基於語法樹的分析,我們可以針對字串做加密:

11859001-2f61aa201798594e

從左上角的明文字串,處理成右下角的介個樣子~

4.2 LibTooling

對語法樹有完全的控制權,可以作為一個單獨的命令使用,如:clang-format

我們也可以自己寫一個這樣的工具去遍歷、訪問、甚至修改語法樹。 目錄:llvm/tools/clang/tools

上面的程式碼通過遍歷語法樹,去修改裡面的方法名和返回變數名:

那麼,我們看到LibTooling對程式碼的語法樹有完全的控制,那麼我們可以基於它去檢查命名的規範,甚至做一個程式碼的轉換,比如實現OC轉Swift。

4.3 ClangPlugin

對語法樹有完全的控制權,作為外掛注入到編譯流程中,可以影響build和決定編譯過程。目錄:llvm/tools/clang/examples

我們可以基於ClangPlugin做些什麼事情呢?我們可以用來定義一些編碼規範,比如程式碼風格檢查,命名檢查等等。下面是我寫的判斷類名前兩個字母是不是大寫的例子,如果不是報錯。(當然這只是一個例子而已。。。)

18script_1482318703701

五、動手寫Pass

5.1 一個簡單的Pass

前面我們說到,Pass就是LLVM系統轉化和優化的工作的一個節點,當然我們也可以寫一個這樣的節點去做一些自己的優化工作或者其它的操作。下面我們來看一下一個簡單Pass的編寫流程:

1.建立標頭檔案

寫入內容:

2.建立原始檔

CMakeLists.txt:

LLVMBuild.txt:

SimplePass.cpp:

修改.../Transforms/LLVMBuild.txt, 加上剛剛寫的模組Obfuscation

修改.../Transforms/CMakeLists.txt, 加上剛剛寫的模組Obfuscation

編譯生成:LLVMSimplePass.dylib

因為Pass是作用於中間程式碼,所以我們首先要生成一份中間程式碼:

然後載入Pass優化:

對比中間程式碼:

這裡寫的Pass只是把a+b簡單的替換成了a-(-b),只是一個演示,怎麼去寫自己的Pass,並且作用於程式碼。

5.2 將Pass加入PassManager管理

上面我們是單獨去載入Pass動態庫,這裡我們將Pass加入PassManager,這樣我們就可以直接通過clang的引數去載入我們的Pass了。

首先在llvm/lib/Transforms/IPO/PassManagerBuilder.cpp新增標頭檔案。

然後新增如下語句:

然後在populateModulePassManager這個函式中新增如下程式碼:

最後在IPO這個目錄的LLVMBuild.txt中新增庫的支援,否則在編譯的時候會提示連結錯誤。具體內容如下:

修改Pass的CMakeLists.txt為靜態庫形式:

最後再編譯一次。

那麼我們可以這麼去呼叫:

基於Pass,我們可以做什麼? 我們可以編寫自己的Pass去混淆程式碼,以增加他人反編譯的難度。

19script_1482320959711

我們可以把程式碼左上角的樣子,變成右下角的樣子,甚至更加複雜~

六、總結

上面說了那麼說,來總結一下:

1.LLVM編譯一個原始檔的過程:

預處理 -> 詞法分析 -> Token -> 語法分析 -> AST -> 程式碼生成 -> LLVM IR -> 優化 -> 生成彙編程式碼 -> Link -> 目標檔案

2.基於LLVM,我們可以做什麼?

  1. 做語法樹分析,實現語言轉換OC轉Swift、JS or 其它語言,字串加密。
  2. 編寫ClangPlugin,命名規範,程式碼規範,擴充套件功能。
  3. 編寫Pass,程式碼混淆優化。

這篇只是一個簡單的入門介紹,個人還需要深入去學習LLVM,再給大家分享,如有問題,歡迎拍磚~

相關文章