网络知识 娱乐 ASTMatcher分析函数调用链(上)

ASTMatcher分析函数调用链(上)

一、方案对比

clang是llvm的编译器前端,是一个C语言、C++、Objective-C、Objective-C++语言的轻量级编译器,基本工作是进行词法分析、语法分析,生成抽象语法树(Abstract Syntax Code, AST)。要得到函数之间的调用关系,我们必须分析抽象语法树,clang提供了两种方法:ASTMatchersRecursiveASTVisitor,RecursiveASTVisitor有两种方式实现,一是clang plugin,二是libtooling

1、clang plugin

clang plugin:clang插件作为编译的一部分,在编译器运行时加载,很容易集成到构建环境中。这样通过替换xcode中clang编译器和加载clang插件分析AST,可以完全控制clang AST。编写插件有三步:自定义类继承、重载、注册插件。基于clang插件的一种iOS包大小瘦身方案 一文中有详细描述,具体这里不赘述。

clang plugin在编译器运行时能够拿到完整的AST,但替换的clang编译器会出现很多编译问题,导致业务接入成本和解决编译问题的人力成本大大加大。

2、libtooling

libtooling:代码本身是一个正常的C++程序,以正常的main()函数作为入口。其跟clang plugin不同,并不需要在编译器运行时加载,针对每个源程序生成相应的分析源码以及对应的AST,但同样的都是用RecursiveASTVisitor访问AST。需要定义三个类,继承自ASTFrontendAction、ASTConsumer和RecursiveASTVisitor。

libtooling分析AST无需编译,但整个过程需要逐层遍历,是由上至下的分析查找,并将系统类库和函数分析遍,还会存在重复分析,这样导致分析耗时特别长。

3、ASTMatcher

ASTMatcher:我们在写clang插件过程中,最大的痛点是在AST阶段快速找到自己想要的节点,RecursiveASTVisitor的方式需要递归遍历、逐层查找,不仅代码冗余,而且效率相对低下。而clang的ASTMatcher,速度快,可以让我们高效的匹配到我们想要的节点;其内部可以嵌套多个ASTMatcher,通过调用构造函数创建,或者构建成一个ASTMatchers的树,使得匹配更加具体准确;配合上clang-query的快速检验正确性,将使我们效率成倍提升。

存在的问题是ASTMatcher没有在编译阶段获取AST,获取的节点数据可能没有clang plugin数据全。

二、clang

1、下载clang

根据官方文档指引下载并安装clang:Tutorial for building tools using LibTooling and LibASTMatchers

2、clang分析AST

使用命令:clang -Xclang -ast-dump -fsyntax-only xxx.m。即可分析xxx.m的AST。

clang -Xclang -ast-dump -fsyntax-only ~/master/Classes/base/Data/ObjectSwizzleMethod/WebCoreCrashUI+SwizzleMethod.m

3、clang-query

clang-query作为clang的一个工具,可交互式检验Matcher正确性和有效性,可探索AST的结构和关系。

~/clang-llvm/build/bin/clang-query /Users/addbin/www/CYHTest/get_func_link/demoB.m --

三、ASTMatcher

ASTMatcher:允许用户编写一个程序来匹配AST节点并能通过访问节点的c++接口来获取该AST节点的属性、源位置等任何信息,其主要由宏与模板驱动,用法和函数式编程类似,其可实现简单精准高效的匹配。

在官网AST Matcher Reference中可以查看clang提供的所有不同类型的匹配器以及说明,主要分为三类(取自【clang】ASTMatcher & clang-query的描述):

Note Matchers:匹配特定类型节点 eg. objcPropertyDecl() :匹配OC属性声明节点 Narrowing Matchers:匹配具有相应属性的节点 eg.hasName()、hasAttr():匹配具有指定名称、attribute的节点 AST Traversal Matchers:允许在节点之间递归匹配 eg.hasAncestor()、hasDescendant():匹配祖、后代类节点

多数情况下会在Note Matchers的基础上,根据AST结构,有序交替组合narrowing Matchers、traversal matchers,直接匹配到我们感兴趣的节点。

1、AST

对于demoB.m文件:

#import "demoA.h"
#import "demoB.h"
@implementation Bus
- (void) drive
{
    Car *car = [[Car alloc] init];
    car.carName = @"Jeep Compass";
    car.carType = @"SUV";
}
@end

通过 clang -Xclang -ast-dump -fsyntax-only demoB.m得到其AST

2、创建ASTMatcher

获取函数调用,也需要获取函数被调用的函数名和类名。从上图AST分析,可以先拿到ObjCMessageExpr节点,然后获取ObjCMessageExpr节点的上一层:所在函数定义ObjCMethodDecl,最后得到ObjCMethodDecl节点上一层:所在类的声明ObjCImplementationDecl,这些节点都是我们需要的。

这里创建函数调用的ASTMatcher的策略如下:

(1)寻找想匹配的节点最外层的类:函数调用

(2)在 AST Matcher Reference 中查看所需要的Matcher匹配到需要的节点:objcMessageExpr()

(3)拿到函数调用后,还需要获取该函数调用的方法定义:objcMethodDecl(),以及类声明:objcImplementationDecl()

(4)创建匹配表达式,通过clang-query验证是否符合预期

所以函数调用的组合Matcher为:objcMessageExpr(hasAncestor(objcMethodDecl(hasAncestor(objcImplementationDecl()))))

使用clang-query验证:

match objcMessageExpr(hasAncestor(objcMethodDecl(hasAncestor(objcImplementationDecl()))))clang-query匹配结果如下:

为了后续获取匹配到的结果,一般会对匹配器进行绑定,只需要在匹配器中调用bind()方法:

match objcMessageExpr(hasAncestor(objcMethodDecl(hasAncestor(objcImplementationDecl().bind("myClass"))).bind("mySelector"))).bind("funcCaller")

clang-query验证匹配表达式没问题后,就可以写ASTMatcher了:

DeclarationMatcher FuncLinkMatcher = objcMethodDecl(
    hasAncestor(objcImplementationDecl().bind("myClass"))
    ,forEachDescendant(objcMessageExpr().bind("funcCaller"))
        ).bind("mySelector");

3、编写匹配函数

class Func_Call : public MatchFinder::MatchCallback {
public:
    virtual void run(const MatchFinder::MatchResult &Result)
    {
        ObjCMethodDecl const* methodDecl = Result.Nodes.getNodeAs<ObjCMethodDecl>("mySelector");
        cout << "begin=============" << "n";
        
        // 输出类名
        const ObjCImplementationDecl *classDecl = Result.Nodes.getNodeAs<ObjCImplementationDecl>("myClass");
        // ObjCInterfaceDecl *interfaceDecl = classDecl->getClassInterface();
        std::string implementationName = classDecl->getIdentifier()->getName();
        cout << implementationName << "::";
        
        //输出函数名
        if (methodDecl->isInstanceMethod())
        {
            std::string methodName = (methodDecl -> getSelector()).getAsString();
            cout << "-" << methodName << endl;
        }
        else if(methodDecl->isClassMethod())
        {
            std::string methodName = (methodDecl -> getSelector()).getAsString();
            cout << "+" << methodName << endl;
        }  

        //输出文件路径
        cout << "Path:" << rootPath << endl;

        // 输出被调用函数
        const ObjCMessageExpr * funcCaller = Result.Nodes.getNodeAs<ObjCMessageExpr>("funcCaller");
        std::string selector = (funcCaller -> getSelector()).getAsString();
        std::string className;
        if (funcCaller -> isInstanceMessage())
            {
                className = funcCaller -> getInstanceReceiver() -> getType().getAsString();
            }
        else if (funcCaller -> isClassMessage())
            {
                className = funcCaller -> getClassReceiver().getAsString();
            } 
        cout << "[" << className;
        cout << " " << selector << "]" << endl;
        cout << "end===============" << endl << endl << endl;   
    }
};

4、main()

int main(int argc, const char **argv) {
  CommonOptionsParser OptionsParser(argc, argv, MyToolCategory);
  ClangTool Tool(OptionsParser.getCompilations(),OptionsParser.getSourcePathList());
  
  Func_Call FuncCall;
  MatchFinder Finder;
  Finder.addMatcher(FuncLinkMatcher, &FuncCall);

  return Tool.run(newFrontendActionFactory(&Finder).get());
}

如何构造cpp文件和生成CMakeLists.txt文件在官方文档:Tutorial for building tools using LibTooling and LibASTMatchers中有讲到,这里不赘述。环境OK后,ninja下(本文使用的是ninja构建,也可用xcode构建),build/bin目录下就会生成对应的可执行文件。

5、使用ASTMatcher

文件中若import其他文件,ASTMatcher是分析不到的,这时你必须告诉ASTMatcher你import的文件来自哪里,所以被分析文件import的文件的目录必须通过参数 -I 传给ASTMatcher(同目录的文件引用不用 -I 传参),不然会报找不到对应头文件的错误,而且对应的消息发送不会被分析到。

ASTMatcher执行命令中必须加上参数 -- ,不然会报compilation-database:No such file or directory的错,或者可以通过-p参数为ASTMatcher加载编译数据库:compile_commands.json,这里没有深入研究。

~/clang-llvm/build/bin/func-call ~/www/CYHTest/get_func_link/demoB.m -- -I ~/www/CYHTest/get_func_link/

上述命令执行的结果如下:

四、结语

至此,ASTMatcher已经编写完成。很重要的一点是多了解AST Matcher Reference里提供的Matchers,配合clang-query快递验证匹配器的正确性,并且要多熟悉每个节点的使用。