在iOS項目中自動生成函數調用關係圖(CallGraph)

文章所涉及代碼已託管至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-checkLibTooling提供了一系列便捷的方法來訪問語法樹。app

Clang PluginLibTooling相似,對AST有徹底的控制權,可是不一樣的是Clang Plugin是做爲插件注入到編譯流程中的,而且能夠嵌入xCode中。實際上使用LibTooling編寫的獨立工具只須要通過少量的改動就能夠變成Clang Plugin來使用。函數

##訪問抽象語法樹 要得到函數之間的調用關係,咱們必須分析AST,Clang提供了兩種方法:ASTMatchersRecursiveASTVisitor工具

###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都會做用在同一個被匹配的節點上,如:
    DeclarationMatcher matcher = recordDecl(cxxRecordDecl().bind("class"),
    										hasName("MyClass"));
    複製代碼
    該matcher的含義是查找名字爲「MyClass」的c++類,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的方式,使用起來很簡單,你須要定義三個類,分別繼承自ASTFrontendActionASTConsumerRecursiveASTVisitor
在自定義的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工具。其中核心部分主要爲三個類:CallGraphCallGraphNodeCGBuilder

  • CallGraph:繼承自RecursiveASTVisitor,實現VisitFunctionDeclVisitObjCMethodDecl方法,遍歷全部的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對應函數的函數體,查找函數體中的方法調用:CallExprObjCMessageExprCallExpr表示普通的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官方文檔中有詳細的說明

  1. 首先下載LLVM和Clang的源碼。

  2. clang-mapper文件夾拷貝到llvm/tools/clang/tools/中。

  3. 編輯文件llvm/tools/clang/tools/CMakeLists.txt,在最後加上一句add_clang_subdirectory(clang-mapper)

  4. 建議採用外部編譯,在包含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
相關文章
相關標籤/搜索