文章所涉及代码已托管至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提供了一些可选的命令行参数