文章所涉及代碼已託管至github: github.com/L-Zephyr/cl…html
在平時的開發中常常須要閱讀學習其餘人的代碼,當開始閱讀一份本身徹底不熟悉的代碼時,一般會遇到一些麻煩,由於我必需要先找到代碼邏輯的入口點並沿着邏輯鏈路將其梳理一遍,一份代碼文件一般會伴隨着許多的方法調用,這一個階段每每是比較痛苦的,由於我必須花上許多時間來將這些方法之間的關係理清楚,這樣才能在個人大腦中生成一份邏輯關係圖。若是咱們能自動生成源碼中的方法調用圖(Call Graph),那樣必定會對源碼閱讀有很大的幫助。前端
咱們須要一個可以自動生成源碼方法調用圖的工具,那麼這個工具必須可以理解並分析咱們的代碼,而最能理解代碼的固然就是編譯器了。咱們編譯Objective-C的代碼所用的前端是Clang,Clang提供了一系列的工具來幫助咱們分析源碼,咱們能夠基於Clang來構建本身的工具。在這以前簡單介紹一些相關概念:c++
抽象語法樹(Abstract Syntax Code, AST)是源代碼語法結構的樹狀表示,其中的每個節點都表示一個源碼中的結構,AST在編譯中扮演了一個十分重要的角色,Clang分析輸入的源碼並生成AST,以後根據AST生成LLVM IR(中間碼)。git
咱們可使用Clang提供的工具clang-check
來查看AST,建立一個代碼文件test.cgithub
int square(int num) {
return num * num;
}
int main() {
int result = square(2);
}
複製代碼
在終端執行命令clang-check -ast-dump test.m
,能夠看到轉換後的AST結構:bash
|-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
提供了一系列便捷的方法來訪問語法樹。app
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 Matcher
來開始的,而且只有在Node Matcher
上能夠調用bind
方法。Node Mathcher
能夠包含任意數量的參數,在參數中傳入其餘的Matcher來操縱匹配的節點,可是須要注意的是全部做爲參數傳入的Matcher都會做用在同一個被匹配的節點上,如:DeclarationMatcher matcher = recordDecl(cxxRecordDecl().bind("class"),
hasName("MyClass"));
複製代碼
該matcher的含義是查找名字爲「MyClass」的c++類,recordDecl
是一個Node Matcher
,匹配全部的class、struct和union的定義;hasName
匹配名字爲"MyClass"的節點;cxxRecordDecl
匹配C++類定義的節點,並將其綁定到字符串"class"上。hasName
就是一個Narrowing Matcher
,只匹配名稱爲"MyClass"的節點。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
:
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);
}
複製代碼
Decl
類型的的實例(C函數或OC方法的定義),用來表示一個AST節點,全部被該函數所調用的其餘函數會被添加到vector類型的成員變量CalledFunctions
中。class CallGraphNode {
private:
// C函數或OC方法的定義
Decl *decl;
// 保存全部被decl調用的Node
SmallVector<CallGraphNode*, 5> CalledFunctions;
...
複製代碼
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提供了一些可選的命令行參數