写点什么

基于 LLVM 的源码级依赖分析方案的设计与实现

  • 2020-03-04
  • 本文字数:4086 字

    阅读完需:约 13 分钟

基于LLVM的源码级依赖分析方案的设计与实现

1. 导读

随着业务快速发展,移动客户端技术架构也从单一的工程配置,转向模块化、组件化、动态化方向发展。越来越多的业务模块被拆分成独立组件 bundle,进行独立开发、构建、测试、发布、运营,但这也面临着许多挑战:


  • 如何保证众多的独立组件 bundle 能够准确无误快速集成到主工程、打包、提测、发布审核?

  • 如果删除或更新某个独立组件 bundle,将会对剩余的哪些 bundle 有影响?

  • 架构或产品优化时,哪些独立组件 bundle 可以删除/下线?


这就需要确定这些独立组件 bundle 之间的依赖关系。

2. 依赖分析的定义

简单地说,通过某种技术手段获取到某个复杂系统中各个子系统之间相互关系,并将这种关系数据化、图像化处理的过程,即依赖分析。

3. 常见的依赖分析方案

3.1 基于 Cocoapods 的依赖包分析

Cocoapods 是 iOS 业界提供,开源的、事实上的依赖管理标准工具,其 Podfile.lock 及 podspec 文件中均有显式的记录各个组件之间的依赖关系,因此只需要分析这些文件即可获取到依赖关系。

3.2 基于 #include 和 #import 头文件的依赖分析

众所周知,当某个源码文件 A 依赖另一个源码文件 B 时,必定会在 A 文件头部显式的添加上 #include 和 #importB。因此只需要扫描所有源码文件中的头文件引用关系即可获取到依赖关系。

3.3 基于 nm、otool 等命令行工具的符号依赖分析

nm 和 otool 常用于分析二进制文件中的符号信息,通过符号建立依赖关系。

3.4 三种符号依赖分析比较

三种方案各有优缺点:


方案优点缺点分析时机难度
Cocoapods简单直观,业内基础方案分析粒度大(以bundle为单位)编译前简单
头文件引用简单直观分析粒度中(以文件为单位),存在无效、循环依赖问题编译前简单
nm/otool简单直观分析粒度细(以符号为单位),编译混淆或优化(strip)的库查不到符号信息编译后简单


本文从编译原理角度,设计一种新的源码级别依赖分析方案。

4. 基于 LLVM 的依赖分析方案

The LLVM Project is a collection of modular and reusable compiler and toolchain technologies.


LLVM 项目是一系列分模块、可重用的编译工具链。它提供了一种代码编写良好的中间表示(IR),可以作为多种语言的后端,还可以提供与编程语言无关的优化和针对多种 CPU 架构的代码生成功能,举个例子来说明整个 LLVM 的编译过程:


// main.m#include <stdio.h>#define kPeer 3int main(int argc, const char * argv[]) {    int a = 1;    int b = 2;    int c = a + b + kPeer;    printf("%d",c);    return 0;}
// 执行命令 clang -ccc-print-phases main.m 输出0: input, "main.m", objective-c1: preprocessor, {0}, objective-c-cpp-output2: compiler, {1}, ir3: backend, {2}, assembler4: assembler, {3}, object5: linker, {4}, image6: bind-arch, "x86_64", {5}, image
复制代码


整体流程如图示:


4.1 预处理(Preprocessor)阶段

预处理包括:条件编译、源文件包含、宏替换、行控制、抛错、杂注和空指令。


clang-E main.m
复制代码

4.2 词法分析(Lexer)阶段

行词法分析:将预处理过的代码转化成一个个 Token,比如左括号、右括号、等于、字符串等等。


clang-fmodules-fsyntax-only-Xclang-dump-tokens main.m
复制代码

4.3 语法分析(AST)阶段

行语法分析:根据当前语言的语法,验证语法是否正确,并将所有节点组合成抽象语法树(AST)。


clang-fmodules-fsyntax-only-Xclang-ast-dump main.m
复制代码

4.4 中间代码(IR)生成阶段

CodeGen 负责将语法树从顶至下遍历,翻译成中间代码 IR,IR 是 LLVM Frontend 的输出,也是 LLVM Backerend 的输入,桥接前后端。


clang-S-fobjc-arc-emit-llvm main.m-o main.ll
复制代码

4.5 代码优化(Opt)阶段

例如 Xcode 中开启了 bitcode,那么苹果后台拿到的就是这种中间代码,苹果可以对 bitcode 做进一步的优化。


clang-emit-llvm-c main.m-o main.bc
复制代码

4.6 代码生成器(CodeGen)阶段

// 生成汇编代码clang-S-fobjc-arc main.m-o main.s
// 生成目标文件clang-fmodules-c main.m-o main.o
复制代码

4.7 链接成可执行文件

clang main.o-o main
复制代码


其中 IR 代码生成(CodeGen)阶段,会遍历整个 AST 语法树,在此处插桩记录下函数名 + 行号 + 文件路径 + 源码 hash 值等信息,即可生成依赖分析的元数据。

5. 如何进行 LLVM 插桩

针对 iOS 端的代码编译,LLVM 前端使用 Clang 编译器,要在中间代码(IR)阶段插桩即要进行 Clang Plugin 开发。

5.1 准备 Clang 开发工具链

可以选择自行编译的 Clang 开发工具链,如下操作:


#!/bin/shcd /optsudo mkdir llvmpushd llvm &&git clone -b release_80 git@github.com:llvm-mirror/llvm.git llvm &&git clone -b release_80 git@github.com:llvm-mirror/clang.git llvm/tools/clang &&git clone -b release_80 git@github.com:llvm-mirror/clang-tools-extra.git llvm/tools/clang/tools/extra &&git clone -b release_80 git@github.com:llvm-mirror/compiler-rt.git llvm/projects/compiler-rt &&popd &&sudo mkdir -v llvm_build &&pushd llvm_build &&cmake -DCMAKE_INSTALL_PREFIX=/opt/llvm_release  \      -DLLVM_TARGETS_TO_BUILD="X86;ARM;Mips;AArch64;WebAssembly" \      -DCMAKE_BUILD_TYPE=Release                \      -DLLVM_ENABLE_FFI=ON                      \      -DLLVM_ENABLE_RTTI=ON                     \      -DLLVM_BUILD_TESTS=OFF                    \      -DLLVM_INCLUDE_TESTS=OFF                  \      -Wno-dev -G Ninja ../llvm                &&ninja && ninja install && popd
复制代码


也可以选择已编译好的 Clang 开发工具链,下载地址:http://releases.llvm.org/

5.2 编写 Clang 插件

Clang 插件实际上一个动态链接库,因此使用 Xcode 创建一个 dylib 工程,将编译器指定到准备好的 Clang 工具链上即可开始,如下图示:



Clang Plugin 通常的入口点是 FrontendAction。FrontendAction 是一个接口,它允许用户指定的 actions 作为编译的一部分来执行。为了在 AST clang 上运行工具,AST clang 提供了方便的接口 ASTFrontendAction,它负责执行 action。剩下的唯一部分是实现 CreateASTConsumer 方法,该方法为每个翻译单元返回一个 ASTConsumer。继承它们即可实现遍历 AST 语法树的功能:


功能
clang::RecursiveASTVisitor遍历AST语法树的抽象基类
clang::PluginASTAction基于consumer的AST前端Action抽象基类
clang::ASTConsumer读取AST的抽象基类


识别 AST 语法树中的类名、方法名、调用关系,需使用 AST 中的以下类:


功能
clang::ObjCInterfaceDecl记录Object-C类声明信息
clang::ObjCCategoryDecl记录Object-C扩展类名信息
clang::ObjCMethodDecl记录Object-C类方法声明信息
clang::ObjCImplDecl记录Object-C类方法实现声明信息
clang::ObjCImplementationDecl记录Object-C类方法实现信息
clang::ObjCPropertyDecl记录Object-C类的属性声明信息
clang::ObjCProtocolDecl记录Object-C协议声明信息
clang::ObjCMessageExpr记录Object-C表达式信息

5.3 加载 Clang 插件

在编译参数 Other C/C++ Flag 中添加


-Xclang -load -Xclang /opt/llvm_release/plugins/libXXXPlugin.dylib -Xclang -add-plugin -Xclang XXXPlugin
复制代码

5.4 举个例子

以下代码实现遍历 AST 语法树中的所有 C++类名,并打印出来的功能:


#include "clang/AST/ASTConsumer.h"#include "clang/AST/RecursiveASTVisitor.h"#include "clang/Frontend/CompilerInstance.h"#include "clang/Frontend/FrontendAction.h"#include "clang/Tooling/Tooling.h"
using namespace clang;
class FindNamedClassVisitor : public RecursiveASTVisitor<FindNamedClassVisitor> {public: explicit FindNamedClassVisitor(ASTContext *Context) : Context(Context) {}
bool VisitCXXRecordDecl(CXXRecordDecl *Declaration) { llvm::outs() << "Found class: " << Declaration->getNameAsString() << "\n"; return true; }
private: ASTContext *Context;};
class FindNamedClassConsumer : public clang::ASTConsumer {public: explicit FindNamedClassConsumer(ASTContext *Context) : Visitor(Context) {}
virtual void HandleTranslationUnit(clang::ASTContext &Context) { Visitor.TraverseDecl(Context.getTranslationUnitDecl()); }private: FindNamedClassVisitor Visitor;};
class FindNamedClassAction : public clang::ASTFrontendAction {public: virtual std::unique_ptr<clang::ASTConsumer> CreateASTConsumer( clang::CompilerInstance &Compiler, llvm::StringRef InFile) { return std::unique_ptr<clang::ASTConsumer>( new FindNamedClassConsumer(&Compiler.getASTContext())); }};
复制代码


编译参数可使用 LLVM 为我们提供的 llvm-config 工具自动生成,执行


llvm-config --cxxflags --ldflags --system-libs --libs core
复制代码


其余额外依赖库自行根据功能添加。

6. 建立依赖关系元数据

通过加载定制化开发的 Clang Plugin,经过编译即可生成如下面格式的数据结构:


{    "+[GTMBase64 decodeBytes:length:]": {        "call": [            "+[GTMBase64 baseDecode:length:charset:requirePadding:]"        ],        "class": "GTMBase64",        "filename": "/Sources/Internal/Encode/GTMBase64.m",        "range": "11401-11553",        "sourceCode": "{return [self baseDecode:bytes length:length charset:kBase64DecodeChars requirePadding:YES];}"    }}
复制代码


其中:


key值描述
call标识调用链上的方法列表
class标识类名
filename标识编译单元文件名
range标识方法所在行号
sourceCode标识方法的实现源码


基于这些依赖元数据,经过后台系统加工处理,就可以准确地知道某个组件 bundle 与其他组件之间的关系,实现一套基于 LLVM 的依赖分析方案。

7. 小结

本文主要介绍了业内常见的依赖分析方案,并分享了一种基于 LLVM 的,从细粒度方法级别来实现依赖分析的方案,它能更准确反馈出各个独立组件 bundle 之间的关系,指导开发人员优化架构设计,可以应对未来“五独”技术进化带来的挑战。


2020-03-04 14:483041

评论

发布
暂无评论
发现更多内容

以太坊铭文聚合交易平台 Scorpio,铭文爆发的新推手?

股市老人

当前各类厂商纷纷入局低代码赛道,关于低代码未来的发展前景如何?

代码生成器研究

程序员指南|学会与大模型相处,提升个人开发效率

飞算JavaAI开发助手

程序员 软件开发 人工智能「 ChatGPT

智能监控,高效观测 IT 系统瓶颈

观测云

IT 智能监控

国际版Amazon Lightsail的功能解析

穿过生命散发芬芳

AWS Lightsail 轻量级服务器

埃森哲使用 Amazon CodeWhisperer 助力开发人员提高工作效率

亚马逊云科技 (Amazon Web Services)

Java Python 人工智能 S3 Amazon CodeWhisperer

C++ LibCurl实现Web指纹识别

不在线第一只蜗牛

c++ 编程 web socket LibC

编程新手如何提高编程能力?

代码生成器研究

AppLink结合金蝶云星空作订单信息同步流程

谷云科技RestCloud

零代码 APPlink

低代码PaaS开发平台

树上有只程序猿

低代码 PaaS 私有化部署

软件测试/测试开发丨人工智能时代软件测试的变化

测试人

人工智能 软件测试

2023-11-22:用go语言,给你一个长度为 n 下标从 0 开始的整数数组 nums。 它包含 1 到 n 的所有数字,请你返回上升四元组的数目。 如果一个四元组 (i, j, k, l) 满足

福大大架构师每日一题

福大大架构师每日一题

前言 | AI工程化部署

AIWeker

AI AI工程化部署

还在为考试重题、错题而头疼?大可不必!时习知新功能了解一下!

轶天下事

开源之夏 2023 | Databend 社区项目总结与分享

Databend

2024年企业软件定制开发必须了解的4大趋势

飞算JavaAI开发助手

数字化转型 企业 定制软件开发 技术服务

2023中关村论坛系列活动——英特尔智能医疗健康创新合作论坛在京成功举办

E科讯

让公有云服务“宁安如梦”的“定心丸”在哪里?

轶天下事

华为数字治理经验,首次披露!

轶天下事

SQL 中的 MIN 和 MAX 以及常见函数详解及示例演示

小万哥

MySQL 数据库 sql 程序员 后端开发

专访|OpenTiny 开源社区 常浩:完成比完美更重要

OpenTiny社区

开源 Vue 前端 富文本编辑器

2023 IoTDB Summit 应用实例议题详解 | 报名到场即送卫衣!

Apache IoTDB

乌卡时代确定性稀缺,企业多云战略最需看中什么?

轶天下事

编程到底难在哪里?

代码生成器研究

透过一台电视,看到万家星闪

脑极体

通信

阿里云崩了:企业未来该怎么选择云厂商?

轶天下事

企业如何选择一款高效的ETL工具

谷云科技RestCloud

ETL

低代码究竟能干什么?

代码生成器研究

基于LLVM的源码级依赖分析方案的设计与实现_文化 & 方法_高德技术_InfoQ精选文章