LLVM(Low Level Virtual Machine)概述
- LLVM是架構編譯器(compiler)的框架系統,以C++編寫而成,用於優化以任意程序語言編寫的程序的編譯時間(compile-time),鏈接時間(link-time),運行時間(run-time)以及空閒時間(idle-time),對開發者保持開放,並兼容已有腳本。
傳統編譯器設計
編譯器前端
- 編譯器前端的任務是解析源代碼,它會進行:詞法分析,語法分析,語意分析,檢查源代碼是否存在錯誤,然後建構抽象語法樹(Abstract Syntax Tree,AST),LLVM的前端還會生成中間代碼(intermediate representation,IR)。
優化器(Optimizer)
- 優化器負責進行各種優化,改善代碼運行時間,例如消除冗余計算等。
後端(Backend)/代碼生成器(CodeGenerator)
- 將代碼映射到目標指令集,生成機器語言,並且進行機器相關的代碼優化。
iOS的編譯器架構
Objective C/C/C++使用的編譯器前端是Clang,Swift是Swift,後端都是LLVM。
LLVM的設計
當編譯器決定支持多種源語言或多種硬件架構時,LLVM最重要的地方就來了。其他的編譯器如GCC,它方法非常成功,但由於他是作為整體應用程序設計的,因此他們的用途受到了很大的限制。
LLVM設計的最重要的方面是,使用通用的代碼表示形式(IR),他是用來在編譯器中表示代碼的形式,所以LLVM可以為任何編程語言獨立編寫前端,並且可以為任意硬件架構獨立編寫後端。
- 簡單來說LLVM的設計就是將前後端分離,無論前端或是後端改變都不會影響另一個。
Clang簡介
- clang是LLVM項目中的一個子項目,他是基於LLVM架構圖的輕量級編譯器,誕生之初是為了替代GCC提供更快的編譯速度,他是
負責C、C++、OC語言的編譯器
,屬於整個LLVM架構中的編譯器前端,對於開發者來說,研究Clang可以給我們帶來很多好處。
LLVM編譯流程
- 新建一個文件,寫下如下程式碼
int test(int a,int b){
return a + b + 3;
}
int main(int argc, const char * argv[]) {
int a = test(1, 2);
printf("%d",a);
return 0;
}
- 通過命令可以打印源碼編譯流程
//************終端命令************
clang -ccc-print-phases main.m
//************编译流程************
//0 - 輸入文件,找到文件的路徑
+- 0: input, "main.m", objective-c
//1 - 預處理階段:這個過程處理包括宏的替換,頭文件的導入
+- 1: preprocessor, {0}, objective-c-cpp-output
//2 - 編譯階段:進行詞法分析,語法分析,檢測語法是否正確,最終生成IR
+- 2: compiler, {1}, ir
//3 - 後端:這裡LLVM會通過一個個的pass去優化,每個pass做一些事情,最終生成彙編代碼
+- 3: backend, {2}, assembler
//4 - 彙編代碼生成目標文件
+- 4: assembler, {3}, object
//5 - 鏈接:鏈接需要的動態庫和靜態庫,生成可執行文件
+- 5: linker, {4}, image(镜像文件)
//6 - 綁定:通過不同的架構,生成對應的可執行文件
6: bind-arch, "x86_64", {5}, image
以下針對上述流程,分別解釋個個流程,其中input主要是輸入文件,即找到源文件的路徑
preprocessor預處理編譯階段
- 這個過程處理包括宏的替換,頭文件的導入,可以執行以下命令,執行完畢可以看到頭文件的導入和紅的替換
//在終端直接查看替換結果
clang -E main.m
//生成對應的文件查看替換後的源碼
clang -E main.m >> main2.m
- 需要注意的是:
-
typedef
在給數據取別名時,在預處理階段不會被替換掉
-
define
則在預處理階段時會被替換,所以經常被用來進行代碼混淆,目的是為了app安全,實現邏輯是:將app中的核心類,核心方法等用系統相似的名稱進行取別名了,然後在預處理階段就被替換了,來達到代碼混淆的目的。
-
編譯階段
- 編譯階段主要是進行詞法,語法的分析檢查,然後生成中間代碼IR
1.詞法分析
- 預處理完成後就會進行詞法分析,這裡會把代碼切成一個個token,比如大小括號,等於號還有字符串等。
- 可以通過下面的命令查看
clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
- 如果頭文件找不到,指定sdk
clang -isysroot (自己SDK路径) -fmodules -fsyntax-only -Xclang -dump-tokens main.m
clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.1.sdk/ -fmodules -fsyntax-only -Xclang -dump-tokens main.m
- 以下是代碼的詞法分析結果
2.語法分析
詞法分析完成後就是語法分析,它的任務是驗證語法是否正確,在詞法分析的基礎上將單詞序列合成各類詞法短語,如程序,語句,表達式等等,然後將所有節點組成抽象語法樹
(Abstract Syntax Tree—>AST),語法分析程序判斷程序在結構上是否正確
- 可以通過下面終端命令查看語法分析結果
clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
- 如果導入頭文件找不到,可以指定SDK
clang -isysroot (自己SDK路径) -fmodules -fsyntax-only -Xclang -ast-dump main.m
clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.1.sdk/ -fmodules -fsyntax-only -Xclang -ast-dump main.m
- 下面是語法分析的結果
其中,主要說明幾個關鍵字的含義
- -FunctionDecl 函數
- -ParmVarDecl 參數
- -CallExpr 調用一個函數
- -BinaryOperator 運算符
3.生成中間代碼IR
完成以上步驟後,就開始生成中間代碼IR了,代碼生成器(Code Generation)會將語法數自頂向下遍歷逐步翻成LLVM IR。
- 可以通過下面命令生成.ll的文本文件,查看IR代碼。OC代碼在這一步進行runtime橋接,property合成,ARC處理等
clang -S -fobjc-arc -emit-llvm main.m
/*
以下是IR基本語法
@ 全局標示
% 局部標示
alloca 開闢空間
align 內存
i32 32bit,4個字節
store 寫入內存
load 讀取數據
call 調用函數
ret 返回
*/
- 下面是生成的中間代碼.ll文件
- 其中,test函數的參數解釋為
- IR文件在OC中是可以優化的,一般設置是在
target - Build Setting - Optimization Level
(優化器等級)中設置。LLVM的優化級別分別是-O0 -O1 -O2 -O3 -Os
(第一個是大寫英文字母—>O),下面是帶優化的生成中間代碼IR的命令
clang -Os -S -fobjc-arc -emit-llvm main.m -o main.ll
- 這是優化後的中間代碼
- xcode7以後開啟bitcode,Apple會進一步優化,生成.bc的中間代碼,我們通過優化後的IR代碼生成.bc代碼
clang -emit-llvm -c main.ll -o main.bc
後端
LLVM在後端主要是通過一個個的pass去優化,每個Pass做一些事情,最終生成彙編代碼
- 生成彙編代碼
- 我們通過最終的
.bc或者.ll代碼
生成彙編代碼
clang -S -fobjc-arc main.bc -o main.s clang -S -fobjc-arc main.ll -o main.s
- 生成彙編代碼也可以進行優化
clang -Os -S -fobjc-arc main.m -o main.s
此時查看生成的main.s文件的格式為彙編代碼
生成目標文件
- 目標文件的生成,是彙編器以彙編代碼作為插入,將彙編代碼轉換為機器代碼,最後輸出目標文件(object file)
clang -fmodules -c main.s -o main.o
- 可以通過nm命令查看下main.o中的符號
$xcrun nm -nm main.o
- 以下是main.o中的符號,其文件格式為
目標文件
- _printf函數是一個
undefined 、external
-
undefined
表示在當前文件暫時找不到符号_printf
-
external
表示這個符號是外部可以訪問
的
鏈接
鏈接主要是鏈接需要的動態庫和靜態庫,生成可執行文件,其中
- 靜態庫會和可執行文件合併
- 動態庫是獨立的
連接器把編譯生成的.o文件和.dyld .a文件鏈接,生成一個mach-o文件
clang main.o -o main
查看鏈接之後的符號
$xcrun nm -nm main
結果如下所示,其中的undefined
表示會在運行時進行動態綁定
- 通過命令查看
main
是什麼格式,此時是mach-o可執行文件
綁定
綁定主要是通過不同的架構生成對應的mach-O格式的可執行文件
總結
編譯流程如下圖所示
- 註:生成的.ll(IR)進行優化和之後生成的.s(彙編)再進行優化是一樣的,優化流程只有一次。
Clang插件開發
準備工作
- 下載路徑
llvm | 镜像站使用帮助 | 清华大学开源软件镜像站 | Tsinghua Open Source Mirror
- 下載LLVM項目
git clone <https://mirrors.tuna.tsinghua.edu.cn/git/llvm/llvm.git>
- 在LLVM項目
git clone <https://mirrors.tuna.tsinghua.edu.cn/git/llvm/llvm.git>
- 在LLVM的projects目錄下載
compiler-rt、libcxx、libcxxabi
cd ../projects
git clone <https://mirrors.tuna.tsinghua.edu.cn/git/llvm/compiler-rt.git>
git clone <https://mirrors.tuna.tsinghua.edu.cn/git/llvm/libcxx.git>
git clone <https://mirrors.tuna.tsinghua.edu.cn/git/llvm/libcxxabi.git>
- 在clang的tools下安裝extra工具
cd ../tools/clang/tools
git clone <https://mirrors.tuna.tsinghua.edu.cn/git/llvm/clang-tools-extra.git>
LLVM編譯
由於最新的LLVM只支持cmake來編譯所以需要安裝cmake
安裝cmake
查看brew是否安裝cmake,如果已經安裝,則跳過下面步驟
brew list
通過brew安装cmake
brew install cmake
編譯LLVM
有兩種編譯方式
- 通過xcode編譯LLVM
- 通過ninja編譯LLVM
通過xcode編譯LLVM
- cmake編譯成Xcode項目
mkdir build_xcode
cd build_xcode
cmake -G Xcode ../llvm
- 使用xcode編譯Clang
- 選擇自動創建Schemes
- 選擇手動創Schemes,然後編譯Clang,ClangTooling即可
- 編譯(CMD + B),選擇
clang
進行編譯,預計1+小時
通過ninja編譯LLVM
- 使用
ninja
進行便意則還需要安裝ninja
,使用以下命令安裝ninja
brew install ninja
- 在LLVM源碼根目錄下新建一個
build_ninja
目錄,最終會在build_ninja
目錄下生成build.ninja - 在LLVM源碼根目錄下新建
llvm_release
目錄,最終編譯文件會在llvm_release
文件夾路徑下
cd llvm_build
//注意DCMAKE_INSTALL_PREFIX后面不能有空格
cmake -G Ninja ../llvm -DCMAKE_INSTALL_PREFIX= 安装路径(本机为/ Users/xxx/xxx/LLVM/llvm_release)
- 一次執行編譯,安裝指令
ninja
ninja install
創建插件
- 在
/llvm/tools/clang/tools
下新建插件ACPlugin
- 在
/llvm/tools/clang/tools
目錄下CMakeLists.txt
文件,新增add_clang_subdirectory(ACPlugin)
,此處的ACPlugin
即為上一步創建的插件名稱
、
- 在
ACPlugin
目錄下新建兩個文件,分別是ACPlugin.cpp
和CMakeLists.txt
,並在CMakeLists.txt
中加上以下代碼
//通過終端在ACPlugin目錄下創建
touch ACPlugin.cpp
touch CMakeLists.txt
//CMakeList.txt中添加以下代碼
add_llvm_library( CJLPlugin MODULE BUILDTREE_ONLY
ACPlugin.cpp
)
- 接下來利用cmake重新生成Xcode項目,在
build_xcode
目錄下執行以下命令
cmake -G Xcode ../llvm
-
最後可以在LLVM的xcode項目中可以看到
Loadable modules
目錄下由自定義的ACPlugin目錄了,然後可以在裡面編寫插件代碼了
編寫插件代碼
- 在ACPlugin目錄下的ACPlugin.cpp文件中,加入以下代碼
#include <iostream>
#include "clang/AST/AST.h"
#include "clang/AST/DeclObjC.h"
#include "clang/AST/ASTConsumer.h"
#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/Frontend/FrontendPluginRegistry.h"
using namespace clang;
using namespace std;
using namespace llvm;
using namespace clang::ast_matchers;
//命名空间,和插件同名
namespace CJLPlugin {
//第三步:扫描完毕的回调函数
//4、自定义回调类,继承自MatchCallback
class CJLMatchCallback: public MatchFinder::MatchCallback {
private:
//CI传递路径:CJLASTAction类中的CreateASTConsumer方法参数 - CJLConsumer的构造函数 - CJLMatchCallback的私有属性,通过构造函数从CJLASTConsumer构造函数中获取
CompilerInstance &CI;
//判断是否是用户源文件
bool isUserSourceCode(const string filename) {
//文件名不为空
if (filename.empty()) return false;
//非xcode中的源码都认为是用户的
if (filename.find("/Applications/Xcode.app/") == 0) return false;
return true;
}
//判断是否应该用copy修饰
bool isShouldUseCopy(const string typeStr) {
//判断类型是否是NSString | NSArray | NSDictionary
if (typeStr.find("NSString") != string::npos ||
typeStr.find("NSArray") != string::npos ||
typeStr.find("NSDictionary") != string::npos/*...*/)
{
return true;
}
return false;
}
public:
CJLMatchCallback(CompilerInstance &CI) :CI(CI) {}
//重写run方法
void run(const MatchFinder::MatchResult &Result) {
//通过result获取到相关节点 -- 根据节点标记获取(标记需要与CJLASTConsumer构造方法中一致)
const ObjCPropertyDecl *propertyDecl = Result.Nodes.getNodeAs<ObjCPropertyDecl>("objcPropertyDecl");
//判断节点有值,并且是用户文件
if (propertyDecl && isUserSourceCode(CI.getSourceManager().getFilename(propertyDecl->getSourceRange().getBegin()).str()) ) {
//15、获取节点的描述信息
ObjCPropertyDecl::PropertyAttributeKind attrKind = propertyDecl->getPropertyAttributes();
//获取节点的类型,并转成字符串
string typeStr = propertyDecl->getType().getAsString();
// cout<<"---------拿到了:"<<typeStr<<"---------"<<endl;
//判断应该使用copy,但是没有使用copy
if (propertyDecl->getTypeSourceInfo() && isShouldUseCopy(typeStr) && !(attrKind & ObjCPropertyDecl::OBJC_PR_copy)) {
//使用CI发警告信息
//通过CI获取诊断引擎
DiagnosticsEngine &diag = CI.getDiagnostics();
//通过诊断引擎 report报告 错误,即抛出异常
/*
错误位置:getBeginLoc 节点开始位置
错误:getCustomDiagID(等级,提示)
*/
diag.Report(propertyDecl->getBeginLoc(), diag.getCustomDiagID(DiagnosticsEngine::Warning, "%0 - 这个地方推荐使用copy!!"))<< typeStr;
}
}
}
};
//第二步:扫描配置完毕
//3、自定义CJLASTConsumer,继承自ASTConsumer,用于监听AST节点的信息 -- 过滤器
class CJLASTConsumer: public ASTConsumer {
private:
//AST节点的查找过滤器
MatchFinder matcher;
//定义回调类对象
CJLMatchCallback callback;
public:
//构造方法中创建matcherFinder对象
CJLASTConsumer(CompilerInstance &CI) : callback(CI) {
//添加一个MatchFinder,每个objcPropertyDecl节点绑定一个objcPropertyDecl标识(去匹配objcPropertyDecl节点)
//回调callback,其实是在CJLMatchCallback里面重写run方法(真正回调的是回调run方法)
matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"), &callback);
}
//实现两个回调方法 HandleTopLevelDecl 和 HandleTranslationUnit
//解析完一个顶级的声明,就回调一次(顶级节点,相当于一个全局变量、函数声明)
bool HandleTopLevelDecl(DeclGroupRef D){
// cout<<"正在解析..."<<endl;
return true;
}
//整个文件都解析完成的回调
void HandleTranslationUnit(ASTContext &context) {
// cout<<"文件解析完毕!"<<endl;
//将文件解析完毕后的上下文context(即AST语法树) 给 matcher
matcher.matchAST(context);
}
};
//2、继承PluginASTAction,实现我们自定义的Action,即自定义AST语法树行为
class CJLASTAction: public PluginASTAction {
public:
//重载ParseArgs 和 CreateASTConsumer方法
bool ParseArgs(const CompilerInstance &ci, const std::vector<std::string> &args) {
return true;
}
//返回ASTConsumer类型对象,其中ASTConsumer是一个抽象类,即基类
/*
解析给定的插件命令行参数。
- param CI 编译器实例,用于报告诊断。
- return 如果解析成功,则为true;否则,插件将被销毁,并且不执行任何操作。该插件负责使用CompilerInstance的Diagnostic对象报告错误。
*/
unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef iFile) {
//返回自定义的CJLASTConsumer,即ASTConsumer的子类对象
/*
CI用于:
- 判断文件是否使用户的
- 抛出警告
*/
return unique_ptr<CJLASTConsumer> (new CJLASTConsumer(CI));
}
};
}
//第一步:注册插件,并自定义AST语法树Action类
//1、注册插件
static FrontendPluginRegistry::Add<CJLPlugin::CJLASTAction> CJL("CJLPlugin", "This is CJLPlugin");
其原理主要分三步
-
【第一步】註冊插件,並自定義AST語法樹Atction類
- 繼承自
PluginASTAction
自定義ASTAction,需要重載兩個方法ParseArgs
和CreateASTConsumer
,其中的重點方法是CreateASTConsumer,方法中有個參數CI即編譯實例對象,主要用於以下兩個方面- 用於判斷文件是否是用戶的
- 用於拋出警告
- 通過
FrontendPluginRegistry
註冊插件,需要關聯插件名與自定義的ASTAction類
- 繼承自
-
【第二步】掃描配置完畢
- 繼承自ASTConsumer類,實現自定義的子類ACASTConsumer,有兩個參數
MatchFinder
對象matcher
以及CJLMatchCallback
自定義的回調對象callback
- 實現構造函數,主要是創建
MatchFinder
對象,以及將CI傳遞給回調對象 - 實現兩個回調方法
-
HandleTopLevelDecl
:解析完一個頂級的聲明,就回調一次 -
HandleTranslationUnit
:整個文件都解析完成的回調,將文件解析完畢後的上下文context
(即AST語法樹)給matcher
-
- 繼承自ASTConsumer類,實現自定義的子類ACASTConsumer,有兩個參數
-
【第三步】掃描完畢的回調函數
- 繼承自
MatchFinder::MatchCallback
,自定義回調類ACMatchCallback
- 定義
CompilerInstance
私有屬性,用於接收ASTConsumer
類傳遞過來的CI
信息 - 重寫run方法
- 通過result,根據節點標記,獲取相對應節點,此時的比較需要與ACASTConsumer構造方法中一致
- 判斷節點有值,屏且是用戶文件即
isUserSourceCode
私有方法 - 獲取節點的描述信息
- 獲取解職點的類型,並轉成字符串
- 判斷應該使用copy,但是沒有使用copy
- 通過CI獲取診斷引擎
- 通過診斷引擎報告錯誤
所以,綜上所述,clang插件開發的流程圖如下
- 繼承自
- 然後再終端測試插件
//命令格式
自己编译的clang文件路径 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.1.sdk/ -Xclang -load -Xclang 插件(.dyld)路径 -Xclang -add-plugin -Xclang 插件名 -c 源码路径
//例子
/Users/XXX/Desktop/build_xcode/Debug/bin/clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.1.sdk/ -Xclang -load -Xclang /Users/XXXX/Desktop/build_xcode/Debug/lib/CJLPlugin.dylib -Xclang -add-plugin -Xclang CJLPlugin -c /Users/XXXX/Desktop/XXX/XXXX/测试demo/testClang/testClang/ViewController.m
Xcode集成插件
加載插件
打開測試項目,在target->Build Settings -> Other C Flags
添加以下內容
Xclang -load -Xclang (.dylib)動態庫路徑 -Xclang -add-plugin -Xclang CJLPlugin
-
在
Build Settings
欄目中新增兩項用戶定義的設置,分別是CC
和CXX
-
CC
對應的是自己編譯的clang
的絕對路徑 -
CXX
對應的是自己編譯的clang++
的絕對路徑
-
接下來在Build Settings
中搜尋index
,將Enable Index-Wihle-Building Functionality
的Default
改為NO
- 最後,重新編譯測試項目,會出現下面的效果。