写点什么

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

评论

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

python中的GIL锁和互斥锁问题

半面人

Python

五十年前的一桩公案:数据库关系模型的流行史(上)

青菜年糕汤

数据库 分布式数据库 数据库规范 关系型数据库 数据库设计

人人都要有经营意识

Neco.W

创业 重新理解创业 公司管理

Java并发编程基础--线程

Java收录阁

Java 线程

写在2020年五四青年节

耿老的竹林

个人成长

Web3极客日报#134

谢锐 | Frozen

区块链 独立开发者 技术社区 Rebase Web3 Daily

笔记:《如何系统思考》之因果回路图

wiflish

思维方式

MySQL自增ID以及其他唯一ID方式分析

Bruce Duan

MySQL自增ID 唯一ID

游戏夜读 | 工具游戏的辉煌

game1night

NIO看破也说破(一)—— Linux/IO基础

小眼睛聊技术

Linux 架构 后端 Netty nio

Java并发编程系列——分布式锁

孙苏勇

Java zookeeper 并发编程 多线程 分布式锁

leetcode141. 环形链表

Damien

算法 链表 LeetCode

哲少荐书:这才是心理学

Jackey

心理学 读书

实战营第一战:FizzBuzz

escray

学习 CSD 认证实战营

我在极客大学算法训练营的收获

熊斌

极客时间 极客大学

leetcode8. 字符串转换整数 (atoi)

Damien

算法 数学

python oop 指南

志学Python

Python python 爬虫 oop

聊天机器人为什么这么难?

青菜年糕汤

人工智能 自然语言处理 搜索引擎 chatbot 聊天机器人

五十年前的一桩公案:数据库关系模型的流行史(下)

青菜年糕汤

数据库 分布式数据库 数据库规范 关系型数据库 数据库设计

Impala UDTF 功能实现

小鹏

大数据 hadoop cloudera 数据仓库

一文带你搞懂RPC核心原理

松花皮蛋me

微服务 RPC 远程调用

Web3极客日报#135

谢锐 | Frozen

区块链 独立开发者 技术社区 Rebase Web3 Daily

为什么厉害的人精力都那么好?

非著名程序员

程序员 程序人生 提升认知 精力管理

基准测试神器JMH —— 详解36个官方例子

捉虫大师

Java 性能 JMH

如何成为一个高效的问题解决者?

汪锋

冥想与呼吸法之于情绪控制

树上

情绪 冥想 呼吸法 呼吸 自我

中台是为了复用?未必!浅谈产业中台建设的特点与误区

孤岛旭日

架构 中台 企业中台 企业架构 产业互联网

Golang杂谈 - graceful shutdown为何离奇失效?

星语

后端 平滑重启 服务端 Go 语言

花更多的时间在自己的优势上

Neco.W

创业 自我管理 重新理解创业

Redis学习笔记(安装)

编程随想曲

redis

创投机会诞生在这四个核心变量中 | 2019年在某大学课堂做的一次讲演的实录

邓瑞恒Ryan

创业 管理 投资 行业资讯

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