专栏首页ASTMatcherASTMatcher分析函数调用链(下)
原创

ASTMatcher分析函数调用链(下)

上一篇文章(ASTMatcher分析函数调用链(上))讲到ASTMatcher的原理以及创建,本文将详细介绍ASTMatcher获取函数调用链在iOS app中的应用。

一、ASTMatcher部分

1、无消息调用的函数定义获取

上篇中的ASTMatcher只能获取有消息调用的函数定义,那没有消息调用的函数定义就无法匹配到,所以无消息调用的函数定义也需要获取

DeclarationMatcher AllFuncMatcher = objcMethodDecl(
            hasAncestor(objcImplementationDecl().bind("allClass"))
        ).bind("allSelector");

同理也需要获取匹配节点

2、category类的消息调用获取

objcImplementationDecl()中并不包括category类,所以category类需要单独写Matcher匹配

DeclarationMatcher CategoryFuncLinkMatcher = objcMethodDecl(
    hasAncestor(objcCategoryImplDecl().bind("myCategoryClass"))
    ,forEachDescendant(objcMessageExpr().bind("categoryFuncCaller"))
        ).bind("myCategorySelector");

同理也需要获取匹配节点

3、缓存文件目录

本文将分析到的消息调用、函数定义和类声明写到缓存文件中,这里需要适配多业务,缓存目录不能写死在同一个文件夹,所以缓存目录通过参数传递:

    rootPath = argv[1];
    std::vector<std::string> sv;
    split(rootPath, sv, '/');
    for (const auto& s : sv) {
        fileName = s;
    }

Matcher命令行中必须通过-extra-arg参数传递:

~/clang-llvm/build/bin/func-call ~/www/CYHTest/get_func_link/demoB.m -extra-arg=~/www/AST_Matcher_Result/func-category-new -- -I ~/www/CYHTest/get_func_link/

二、脚本部分

分析整个app的函数调用关系,需要遍历每个.m和.mm文件,并且每个.m和.mm文件引用的头文件目录必须 -I 给Matcher,所以需要拿到每个.m和.mm以及其对应的.h文件的引用文件路径集合。

1、.h文件与其所在目录的对应关系

# 获取头文件所在目录,为后续参数-I做准备
def get_head_file_relation(workspace):
    head_dir_relation = {}
    for root, dirs, files in os.walk(workspace):
        for file_one in files:
            if file_one.endswith(".h"):
                head_dir_relation[file_one] = root
    return head_dir_relation

2、.m和.mm文件引用头文件对应关系

# 获取每个.m或者.mm文件import的头文件所在目录,输出为字典
def get_import_file_dict(file_list, head_dir_relation):
    ...

3、屏蔽条件语句

ASTMatcher分析不到条件语句中的代码,所以条件语句全部屏蔽

def do_work(ROOT):
    line_records = {}
    for root, dirs, files in os.walk(ROOT):
        for file in files:
            file_name, file_extension = os.path.splitext(file)
            if file_extension in ['.m', '.mm']:
                path = os.path.join(root, file)
                line_records[path] = pre_edit(path)
    with open("line_record.txt", 'wb') as fp:
        json.dump(line_records, fp)

def pre_edit(path):
    line_record = []
    new_lines = []
    with open(path, 'rt') as fp:
        lines = fp.readlines()
        reg = re.compile(r'^\s*\#if|^\s*\#else|^\s*\#endif|^\s*\#error')
        for index, line in enumerate(lines):
            for m in reg.finditer(line):
                line = line[:m.start()] + '//' + line[m.start():]
                line_record.append(index)
                break
            new_lines.append(line)
    with open(path, 'wt') as fp:
        fp.writelines(new_lines)
    return line_record

4、分析对应app工程

获取所有.m和.mm文件list,循环通过func-call分析(这里使用func-call和func-call-category-only两个ASTMatcher,3.5章节有解释)

for file_one in file_list:
    try:
        header_one_file_str = " -I ".join(file_head_relation[file_one])
        terminal = "~/clang-llvm-6.0/build/bin/func-call {} -extra-arg={} -- -I {}".format(file_one, ast_matcher_cache_dir, header_one_file_str)
        status, output = commands.getstatusoutput(terminal)
        if status == 0:
            print 'success:', number
            number = number + 1
        else:
            print 'fail:', fail, ":", file_one
            fail = fail + 1
            fail_file_list.append(file_one)
    except Exception as e:
        except_number = except_number + 1
        except_file_list.append(file_one)
        print "except file:", e, ":", file_one
        continue

for file_one_category in file_list:
    try:
        header_one_file_str = " -I ".join(file_head_relation[file_one_category])
        terminal = "~/clang-llvm/build/bin/func-call-category-only {} -extra-arg={} -- -I {}".format(file_one_category, ast_matcher_cache_dir, header_one_file_str)
        status, output = commands.getstatusoutput(terminal)
        if status == 0:
            print 'success category:', number
            number = number + 1
        else:
            print 'fail category:', fail, ":", file_one_category
            fail = fail + 1
            fail_file_list.append(file_one_category)
    except Exception as e:
        except_number = except_number + 1
        except_file_list.append(file_one_category)
        print "except file category:", e, ":", file_one_category
        continue

5、分析结果以及DB

三、踩过的一些坑

1、无法获取objcCategoryImplDecl()

解决:原来clang6.0不支持获取objcCategoryImplDecl(),使用的clang版本比较旧,更新至最新clang8.0即可。

2、category无法获取类名,只能获取别名

解决:ObjCCategoryImplDecl()->getIdentifier()->getName()方法只能获取category类的别名,通过ObjCCategoryImplDecl()->getCategoryDecl()->getClassInterface()->getNameAsString()获取类名,曲线救国~

const ObjCCategoryImplDecl *categoryImplDecl = Result.Nodes.getNodeAs<ObjCCategoryImplDecl>("myCategoryClass");
std::string categoryName = categoryImplDecl->getIdentifier()->getName();
ObjCCategoryDecl *categoryDecl = categoryImplDecl->getCategoryDecl();
if(!categoryDecl->IsClassExtension())
{   
    //这里有两种情况,一种是Extension,一种是Category。这里是处理Category
    ObjCInterfaceDecl *itfcdecl = categoryDecl->getClassInterface();
    outCategoryMessage << itfcdecl->getNameAsString() << "::";
 }

在clang更新到clang8.0后,sudo ninja构建时报错CMake Error at tools/clang/tools/extra/func-category/CMakeLists.txt:6 (target_link_libraries):

解决:CMakeLists.txt target_link_libraries中加入PRIVATE,见链接:https://stackoverflow.com/questions/47737558/build-llvm-clangtool

4、clang8.0生成的func-call获取调用链不全

clang8.0生成的func-call可执行文件获取调用链不全,具体原因没有去研究

解决:func-call使用clang6.0生成的可执行文件,func-call-category-only使用clang8.0生成的可执行文件。这里还要注意func-call-category-only依赖clang8.0的环境,所以机器上clang环境还得是clang8.0。

这就导致了普通函数的获取与category类函数的获取不能在同一个ASTMatcher中,所以需要两个ASTMatcher来分析整个工程,这里后续再优化~

5、categroy类函数通过分析all file得到

app category类函数的获取有两种方法,一是遍历整个工程目录,拿到文件名中有“+”号的category文件list,然后循环分析;二是分析整个app所有.m和.mm文件。方式一速度快,但不是以“+”号命名的category类无法获取;方式二会增加分析时间,但可以分析到所有category类,数据比较完整。

解决:本文为了数据完整性,牺牲分析时间,采用方式二。

四、ASTMatcher无法分析的情况

1、系统方法

由于系统方法在我们调用链中没有用处,所以这里就放弃了对系统方法的获取,包括系统类的category类以及方法。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • ASTMatcher分析函数调用链(上)

    clang是llvm的编译器前端,是一个C语言、C++、Objective-C、Objective-C++语言的轻量级编译器,基本工作是进行词法分析、语法分析,...

    adding
  • composer 实现自动加载原理

    一般在框架中都会用到composer工具,用它来管理依赖。其中composer有类的自动加载机制,可以加载composer下载的库中的所有的类文件。那么comp...

    lin_zone
  • 数据结构之线段树

    1、什么是线段树(也称为区间树)Segment Tree。为什么使用线段树,线段树解决了什么问题,对于有一类问题,我们关心的是线段(或者区间)。

    别先生
  • python中redis查看剩余过期时间以及用正则通配符批量删除key的方法

    用户1214487
  • 扭曲你的数据,让其变得具有视觉吸引力

    经常有这样的情况,你用数据画出图像有看起来会很丑,如何让你的图像变得好看一点呢?本文给大家介绍如何扭曲你的数据,在不影响结果和其他属性的情况下,使得你数据画出来...

    YingJoy_
  • 使用Jmeter导出导入接口自动化案例中的自定义变量

    jmeter技术研究
  • 应急响应系列之利用ProcessMonitor进行恶意文件分析

    最近几年,伴随着数字货币的兴起,促进了经济利益驱动型黑客行为的暴增。各位做安服和应急的小伙伴不可避免的会与各种勒索病毒、挖矿病毒以及各种蠕虫病毒打交道,通过分析...

    FB客服
  • go test 测试用例那些事(二) mock

    关于go的单元测试,之前有写过一篇帖子go test测试用例那些事,但是没有说go官方的库mock,很有必要单独说一下这个库,和他的实现原理。 mock主要的...

    lpxxn
  • API测试工具SoapUI & Postman对比分析

    最近公司要引入API测试工具,经过调查和了解,最终决定在SoapUI 和 Postman两种工具之间做一个选择,两种工具在业界都很有名,相信很多人两种工具也都曾...

    葡萄城控件
  • 使用pypiserver搭建私有源

    py3study

扫码关注云+社区

领取腾讯云代金券