QCon北京「鸿蒙专场」火热来袭!即刻报名,与创新同行~ 了解详情
写点什么

基于 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:482756

评论

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

毕业总结

流火

毕业季 | 华为专家亲授面试秘诀:如何拿到大厂高薪offer?

华为云开发者联盟

面试 工作 offer 大厂 毕业

微服务服务稳定性治理

阿泽🧸

微服务治理 7月月更

纪念成为首个DAYU200三方demo贡献者

坚果

HarmonyOS OpenHarmony OpenHarmony应用开发 7月月更 harmony

BOOTSTRAP

Jason199

bootstrap 7月月更

一个实习生的CnosDB之旅

CnosDB

IoT 时序数据库 开源社区 CnosDB infra

如何使用物联网低代码平台进行个人设置?

AIRIOT

低代码 物联网 低代码,项目开发

透过华为军团看科技之变(六):智慧公路

脑极体

4. 对象映射 - Mapping.Mapstercover

MASA技术团队

C# .net 微软 框架 Framework

K临近算法介绍

乌龟哥哥

7月月更

创业团队如何落地敏捷测试,提升质量效能?丨声网开发者创业讲堂 Vol.03

声网

创业讲堂 生态专栏

hive数据导入:从查询数据导入

怀瑾握瑜的嘉与嘉

大数据 hive 7月月更

linux下清理系统缓存并释放内存

入门小站

Linux

架构师毕业总结

凯博无线

九章云极DataCanvas公司蝉联中国机器学习平台市场TOP 3

九章云极DataCanvas

vue-axios(三)

小恺

7月月更

认识线程

zarmnosaj

7月月更

RPA进阶(二)Uipath应用实践

No Silver Bullet

RPA 7月月更 Uipath

精耕渠道共谋发展 福昕携手伟仕佳杰开展新产品培训大会

联营汇聚

若干互联网暴露面的收敛及处置建议

穿过生命散发芬芳

7月月更 互联网暴露

实例讲解将Graph Explorer搬上JupyterLab

华为云开发者联盟

人工智能 GES

【直播回顾】战码先锋首期8节直播完美落幕,下期敬请期待!

OpenHarmony开发者

Open Harmony

华为云专家详解GaussDB(for MySQL)新特性

华为云开发者联盟

数据库 云计算 后端 算子

亚马逊云科技 Community Builder 申请窗口开启

亚马逊云科技 (Amazon Web Services)

build 亚马逊云

使用环信提供的uni-app Demo,快速实现一对一单聊

环信

即时通讯 uni-app集成 环信im

Windows11 ARM版本安装Python环境

IT蜗壳-Tango

IT蜗壳教学 7月月更

网络编程常用的几种字符编码

HoneyMoose

极简,利用Docker仅两行命令就能下载和编译OpenJDK11

程序员欣宸

Java Openjdk 7月月更

BlocProvider 为什么感觉和 Provider 很相似?

岛上码农

flutter ios 安卓 移动端开发 7月月更

JavaScript 设计模式之代理模式

devpoint

JavaScript 设计模式 代理模式 7月月更

AirServer2022最新版功能介绍及下载

茶色酒

AirServer AirServer2022

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