文章所涉及程式碼已託管至github: github.com/L-Zephyr/cl…
在平時的開發中經常需要閱讀學習其他人的程式碼,當開始閱讀一份自己完全不熟悉的程式碼時,通常會遇到一些麻煩,因為我必須要先找到程式碼邏輯的入口點並沿著邏輯鏈路將其梳理一遍,一份程式碼檔案通常會伴隨著許多的方法呼叫,這一個階段往往是比較痛苦的,因為我必須花上許多時間來將這些方法之間的關係理清楚,這樣才能在我的大腦中生成一份邏輯關係圖。如果我們能自動生成原始碼中的方法呼叫圖(Call Graph),那樣一定會對原始碼閱讀有很大的幫助。
我們需要一個能夠自動生成原始碼方法呼叫圖的工具,那麼這個工具必須能夠理解並分析我們的程式碼,而最能理解程式碼的當然就是編譯器了。我們編譯Objective-C的程式碼所用的前端是Clang,Clang提供了一系列的工具來幫助我們分析原始碼,我們可以基於Clang來構建自己的工具。在這之前簡單介紹一些相關概念:
抽象語法樹
抽象語法樹(Abstract Syntax Code, AST)是原始碼語法結構的樹狀表示,其中的每一個節點都表示一個原始碼中的結構,AST在編譯中扮演了一個十分重要的角色,Clang分析輸入的原始碼並生成AST,之後根據AST生成LLVM IR(中間碼)。
我們可以使用Clang提供的工具clang-check
來檢視AST,建立一個程式碼檔案test.c
int square(int num) {
return num * num;
}
int main() {
int result = square(2);
}
複製程式碼
在終端執行命令clang-check -ast-dump test.m
,可以看到轉換後的AST結構:
|-FunctionDecl 0x7fa933840e00 </Users/lzephyr/Desktop/test.c:1:1, line:3:1> line:1:5 used square 'int (int)'
| |-ParmVarDecl 0x7fa93302f720 <col:12, col:16> col:16 used num 'int'
| `-CompoundStmt 0x7fa933840fa0 <col:21, line:3:1>
| `-ReturnStmt 0x7fa933840f88 <line:2:2, col:15>
| `-BinaryOperator 0x7fa933840f60 <col:9, col:15> 'int' '*'
| |-ImplicitCastExpr 0x7fa933840f30 <col:9> 'int' <LValueToRValue>
| | `-DeclRefExpr 0x7fa933840ee0 <col:9> 'int' lvalue ParmVar 0x7fa93302f720 'num' 'int'
| `-ImplicitCastExpr 0x7fa933840f48 <col:15> 'int' <LValueToRValue>
| `-DeclRefExpr 0x7fa933840f08 <col:15> 'int' lvalue ParmVar 0x7fa93302f720 'num' 'int'
`-FunctionDecl 0x7fa933841010 <line:5:1, line:7:1> line:5:5 main 'int ()'
`-CompoundStmt 0x7fa9338411f8 <col:12, line:7:1>
`-DeclStmt 0x7fa9338411e0 <line:6:2, col:24>
`-VarDecl 0x7fa9338410c0 <col:2, col:23> col:6 result 'int' cinit
`-CallExpr 0x7fa9338411b0 <col:15, col:23> 'int'
|-ImplicitCastExpr 0x7fa933841198 <col:15> 'int (*)(int)' <FunctionToPointerDecay>
| `-DeclRefExpr 0x7fa933841120 <col:15> 'int (int)' Function 0x7fa933840e00 'square' 'int (int)'
`-IntegerLiteral 0x7fa933841148 <col:22> 'int' 2
複製程式碼
###LibTooling和Clang Plugin
LibTooling
是一個庫,提供了對AST的訪問和修改的能力,LibTooling
可以用來編寫可獨立執行的程式,如我們上面所使用的clang-check
,LibTooling
提供了一系列便捷的方法來訪問語法樹。
Clang Plugin
與LibTooling
類似,對AST有完全的控制權,但是不同的是Clang Plugin
是作為外掛注入到編譯流程中的,並且可以嵌入xCode中。實際上使用LibTooling
編寫的獨立工具只需要經過少許的改動就可以變成Clang Plugin
來使用。
##訪問抽象語法樹
要獲得函式之間的呼叫關係,我們必須分析AST,Clang提供了兩種方法:ASTMatchers
和RecursiveASTVisitor
。
###ASTMatchers
ASTMatchers
提供了一系列的函式,以DSL的方式編寫匹配表示式來查詢我們感興趣的節點,並使用bind
方法繫結到指定的名稱上:
StatementMatcher matcher = callExpr(hasAncestor(functionDecl().bind("caller")),
callee(functionDecl().bind("callee")));
複製程式碼
上面的表示式匹配了原始碼中普通C函式的呼叫,並將呼叫者繫結到字串"caller",被呼叫者繫結到字串"callee",隨後在回撥方法中可以通過名稱caller和callee來獲取FunctionDecl
型別的物件:
class FindFuncCall : public MatchFinder::MatchCallback {
public :
virtual void run(const MatchFinder::MatchResult &Result) {
// 獲取呼叫者的函式定義
if (const FunctionDecl *caller = Result.Nodes.getNodeAs<clang::FunctionDecl>("caller")) {
caller->dump();
}
// 獲取被呼叫者的函式定義
if (const FunctionDecl *callee = Result.Nodes.getNodeAs<clang::FunctionDecl>("callee")) {
callee->dump();
}
}
};
int main(int argv, const char **argv) {
StatementMatcher matcher = callExpr(hasAncestor(functionDecl().bind("caller")),
callee(functionDecl().bind("callee")));
MatchFinder finder;
FindFuncCall callback;
finder.addMatcher(matcher, &callback);
// 執行Matcher
CommonOptionsParser OptionsParser(argc, argv, MyToolCategory);
ClangTool Tool(OptionsParser.getCompilations(), OptionsParser.getSourcePathList());
Tool.run(newFrontendActionFactory(&finder).get());
return 0;
}
複製程式碼
上述匹配表示式中的每一個函式(如callExpr)被稱為一個Matcher
,所有的Matcher
可以分為三類:
- Node Matchers:匹配表示式的核心,用來匹配特定型別的所有節點,所有的匹配表示式都是由一個
Node Matcher
來開始的,並且只有在Node Matcher
上可以呼叫bind
方法。Node Mathcher
可以包含任意數量的引數,在引數中傳入其他的Matcher來操縱匹配的節點,但是需要注意的是所有作為引數傳入的Matcher都會作用在同一個被匹配的節點上,如:
該matcher的含義是查詢名字為“MyClass”的c++類,DeclarationMatcher matcher = recordDecl(cxxRecordDecl().bind("class"), hasName("MyClass")); 複製程式碼
recordDecl
是一個Node Matcher
,匹配所有的class、struct和union的定義;hasName
匹配名字為"MyClass"的節點;cxxRecordDecl
匹配C++類定義的節點,並將其繫結到字串"class"上。 - Narrowing Matchers:顧名思義,這種Matcher提供了條件判斷能力用來縮小匹配範圍,如第二個例子中的
hasName
就是一個Narrowing Matcher
,只匹配名稱為"MyClass"的節點。 - Traversal Matchers:以當前匹配的節點作為起點,用來限定匹配表示式查詢的範圍。如第一個例子中的
hasAncestor
,在當前節點的祖先節點中進行下一步的匹配。
###RecursiveASTVisitor
RecursiveASTVisitor
是Clang提供的另一種訪問AST的方式,使用起來很簡單,你需要定義三個類,分別繼承自ASTFrontendAction
、ASTConsumer
和RecursiveASTVisitor
。
在自定義的MyFrontendAction中返回一個自定義的MyConsumer例項
class MyFrontendAction : public clang::ASTFrontendAction {
public:
virtual std::unique_ptr<clang::ASTConsumer> CreateASTConsumer(
clang::CompilerInstance &Compiler, llvm::StringRef InFile) {
return std::unique_ptr<clang::ASTConsumer>(new MyConsumer);
}
};
複製程式碼
在AST解析完畢後會呼叫MyConsumer的HandleTranslationUnit
方法,TranslationUnitDecl
是一個AST的根節點,ASTContext
中儲存了AST相關的所有資訊,獲取TranslationUnitDecl
並將其交給MyVisitor,我們主要的操作都在Visitor中完成
class MyConsumer : public clang::ASTConsumer {
public:
virtual void HandleTranslationUnit(clang::ASTContext &Context) {
Visitor.TraverseDecl(Context.getTranslationUnitDecl());
}
private:
MyVisitor Visitor;
};
複製程式碼
在Visitor中訪問感興趣的節點只需要重寫該型別節點的Visit方法就行了,比如我想訪問程式碼中所有的C++類定義,只需要重寫VisitCXXRecordDecl
方法,就可以訪問所有的的所有的C++類定義了
class MyVisitor : public RecursiveASTVisitor<FindNamedClassVisitor> {
public:
bool VisitCXXRecordDecl(CXXRecordDecl *decl) {
decl->dump();
return true; // 返回true繼續遍歷,false則直接停止
}
};
複製程式碼
之後在main函式中使用newFrontendActionFactory
建立ToolAction
就可以了:
Tool.run(newFrontendActionFactory<CallGraphAction>().get());
複製程式碼
##構建CallGraph工具
在Clang原始碼的Analysis
資料夾中提供了一個名為CallGraph
的類,參考這份原始碼的實現編寫了自己的CallGraph工具。其中核心部分主要為三個類:CallGraph
、CallGraphNode
和CGBuilder
:
- CallGraph:繼承自
RecursiveASTVisitor
,實現VisitFunctionDecl
和VisitObjCMethodDecl
方法,遍歷所有的C函式和Objective-C方法:
在bool VisitObjCMethodDecl(ObjCMethodDecl *MD) { if (isInSystem(MD)) { // 忽略系統庫中的定義 return true; } if (canBeCallerInGraph(MD)) { addRootNode(MD); // 新增一個Node到Roots } return true; } 複製程式碼
addRootNode
中將其封裝成CallGraphNode
物件並儲存在一個map型別的成員物件Roots
中。隨後獲取函式體(CompoundStmt
型別),將其傳遞給CGBuilder
查詢在函式體中被呼叫的方法。void CallGraph::addRootNode(Decl *decl) { CallGraphNode *Node = getOrInsertNode(decl); // 將decl封裝成Node,並新增到Roots中 // 初始化CGBuilder遍歷函式裡中所有的方法呼叫 CGBuilder builder(this, Node, Context); if (Stmt *Body = decl->getBody()) builder.Visit(Body); } 複製程式碼
- CallGraphNode:封裝了一個
Decl
型別的的例項(C函式或OC方法的定義),用來表示一個AST節點,所有被該函式所呼叫的其他函式會被新增到vector型別的成員變數CalledFunctions
中。class CallGraphNode { private: // C函式或OC方法的定義 Decl *decl; // 儲存所有被decl呼叫的Node SmallVector<CallGraphNode*, 5> CalledFunctions; ... 複製程式碼
- CGBuilder:繼承自
StmtVisitor
,初始化時獲取一個CallerNode,遍歷該CallerNode對應函式的函式體,查詢函式體中的方法呼叫:CallExpr
和ObjCMessageExpr
。CallExpr
表示普通的C函式呼叫,ObjCMessageExpr
表示Objective-C方法呼叫。獲取被呼叫函式的定義並封裝成CallGraphNode
型別,然後將其新增到CallerNode的CalledFunctions
中。class CGBuilder : public StmtVisitor<CGBuilder> { CallGraph *G; CallGraphNode *CallerNode; ASTContext &Context; public: void VisitObjCMessageExpr(ObjCMessageExpr *ME) { // 從ObjCMessageExpr中獲取被呼叫方法的Decl Decl *decl = ... // 將decl封裝在CallGraphNode中並新增到CallerNode的CalledFunctions中 addCalledDecl(decl); } ... 複製程式碼
目前只實現了一個基礎版本,支援C和Objecive-C,實現了最基本的功能,程式碼也比較簡單,之後會繼續優化並增加新的功能,所有程式碼已經託管到github上:https://github.com/L-Zephyr/clang-mapper
##使用
可以下載並自行編譯原始碼,或者直接使用release資料夾中預先編譯好的二進位制檔案clang-mapper
(使用Clang5.0.0編譯),由於採用了Graphviz
來生成呼叫圖,請確保在執行前已正確安裝了Graphviz
###編譯原始碼 關於如何編譯使用LibTooling編寫的工具,Clang官方文件中有詳細的說明
-
首先下載LLVM和Clang的原始碼。
-
將
clang-mapper
資料夾拷貝到llvm/tools/clang/tools/
中。 -
編輯檔案
llvm/tools/clang/tools/CMakeLists.txt
,在最後加上一句add_clang_subdirectory(clang-mapper)
-
建議採用外部編譯,在包含llvm資料夾的目錄下建立build資料夾,在build目錄中編譯原始碼
$ mkdir build $ cd build $ cmake -G 'Unix Makefiles' ../llvm $ make 複製程式碼
也可以按照文件中介紹的使用Ninja來編譯,編譯過程中會生成20多個G的中間檔案,編譯結束後在
build/bin/
中就能找到clang-mapper
檔案了,將其拷貝到/usr/local/bin
目錄下
###基本使用
傳入任意數量的檔案或是資料夾,clang-mapper
會自動處理所有檔案並在當前執行命令的路徑下生成函式的呼叫圖,以程式碼檔案的命名做區分。如下,我們用clang-mapper分析大名鼎鼎的AFNetworking的核心程式碼。我不希望將分析生成的結果和原始碼檔案混在一起,所以我建立了一個資料夾CallGraph並在該目錄下呼叫
$ cd ./AFNetworking-master
$ mkdir CallGraph
$ cd ./CallGraph
$ clang-mapper ../AFNetworking --
複製程式碼
之後程式會自動分析../AFNetworking
下的所有程式碼檔案,並在CallGraph目錄下生成對應的png檔案:
###命令列引數 clang-mapper提供了一些可選的命令列引數
- -graph-only:只生成png檔案,不保留dot檔案,這個是預設選項
- -dot-only:只生成dot檔案,不生成png檔案
- -dot-graph:同時生成dot檔案和png檔案
- -ignore-header:在iOS開發中標頭檔案通常只用來宣告,加上該選項可以忽略資料夾中的.h檔案
參考資料
- https://clang.llvm.org/docs/LibASTMatchersTutorial.html
- https://clang.llvm.org/docs/RAVFrontendAction.html
- https://clang.llvm.org/docs/LibASTMatchersReference.html
- https://clang.llvm.org/docs/IntroductionToTheClangAST.html