专栏首页酷酷的哀殿llvm 编译器高级用法:第三方库插桩

llvm 编译器高级用法:第三方库插桩

本文字数:3141

预计阅读时间:25分钟

一、背景

最近看到一篇有意思的技术文章:《抖音研发实践:基于二进制文件重排的解决方案 APP启动速度提升超15%》。

原文结尾提到该方案无法覆盖100%的符号:

基于静态扫描+运行时trace的方案仍然存在少量瓶颈:

initialize hook不到

部分block hook不到

C++通过寄存器的间接函数调用静态扫描不出来

目前的重排方案能够覆盖到80%~90%的符号,未来我们会尝试编译期插桩等方案来进行100%的符号覆盖,让重排达到最优效果。

实际上,除上面的场景外,抖音研发团队的方案还存在一些无法覆盖的场景:

  • 无法覆盖代码行级别的检测
    • 当某些复杂的函数存在 if/else/switch 等场景时,开发者可以将函数拆成多个子函数进行优化
  • OC/C 语言的函数调用同样很难被静态扫描
  • 无法对第三方的静态库或者动态库进行有效处理
  • 无法检测 __attribute__((constructor)) 修饰的函数

今天我们将尝试通过 llvmIR 配合实现解决上面提到的各类场景。

二、效果展示

本质上,上面提到的各类场景,都可以通过 对代码进行 基本块(BasicBlock-Level) 级别插桩 的方式解决。

为了方便读者能够继续将本文全部阅读下去,我们先看看一个给 微信SDK 插桩的实际效果。

基本块(BasicBlock-Level) 的概念会在下一章节进行讲解

1、微信SDK

微信SDK(OpenSDK1.8.7.1)[1] 提供了3个公开的头文件,其中 WXApi.h 的暴露了一个类方法 [WXApi registerApp: universalLink:]

/*! @brief 微信Api接口函数类
 *
 * 该类封装了微信终端SDK的所有接口
 */
@interface WXApi : NSObject

/*! @brief WXApi的成员函数,向微信终端程序注册第三方应用。
 *
 * 需要在每次启动第三方应用程序时调用。
 * @attention 请保证在主线程中调用此函数
 * @param appid 微信开发者ID
 * @param universalLink 微信开发者Universal Link
 * @return 成功返回YES,失败返回NO。
 */
+ (BOOL)registerApp:(NSString *)appid universalLink:(NSString *)universalLink;

@end

2、 main.m

新建一个工程,添加回调函数并增加对微信SDK的接口调用:

@import Darwin;
int main(int argc, char * argv[]) {
  // 调用微信SDK
    [WXApi registerApp:@"App" universalLink:@"link"];
    return 0;
}

// 提供回调函数
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
    Dl_info info;
  // 获取当前函数的返回地址 
)
    void *PC = __builtin_return_address(0);
   // 根据返回地址,获取相关的信息 
    dladdr(PC, &info);
   // 打印与 PC 最近的符号名称
    printf("guard:%p 开始执行:%s \n", PC, info.dli_sname);
}

更多内容,可以阅读参考资料的相关链接 dladdr[2] __builtin_return_address[3]

3、运行

通过在 __sanitizer_cov_trace_pc_guard 函数增加断点,我们可以看到下面的调用栈:

整理后的流程图如下所示:

我们可以很容易地从流程图看出来:

微信SDK 调用了开发者提供的回调函数 __sanitizer_cov_trace_pc_guard


下面,我们开始进入正题。

三、插桩与代码覆盖率

为了强调一下本文与抖音技术方案的区别,我们需要先了解一下插桩中常用的代码覆盖率计量单位。

通常情况下,代码覆盖率有 3 种计量单位:

  • 函数(Fuction-Level)
  • 基本块(BasicBlock-Level)
  • 边界(Edge-Level)

1、函数(Fuction-Level)

函数(Fuction-Level) 比较容易理解,就是记录哪些函数执行过。是一种粗糙但高效的统计方式。

从抖音的技术文章看,他们勉强算是做到了这个级别的代码覆盖率检测。

2、基本块(BasicBlock-Level)

基本块(BasicBlock) 通常是只包含顺序执行的代码块。

以下面的代码为例:

void foo(int *a) {
  if (a)
    *a = 0;
}

通过编译器将代码转为汇编时,它会被拆成3个部分:

每个部分都是一个 基本块(BasicBlock)

代码行覆盖率可以通过 基本块(BasicBlock-Level) 级别的代码插桩实现。

3、边界(Edge-Level)

边界(Edge) 的概念比较难理解,我们仍然以上面的代码为例进行说明。

上面的代码包含3个 基本块(BasicBlock)ABC

即使代码行覆盖测试报告显示 ABC 三块都被执行过,我们仍然无法得到以下结论:

路径A-->C 出现过

此时,我们可以添加一个虚拟路径 D

如果测试报告显示 虚拟路径 D 被执行过,则 路径A-->C 就一定出现过;反之, 路径A-->C 就一定没有出现过。

路径覆盖率可以通过 边界(Edge) 级别的代码插桩实现。

三、SanitizerCoverage

根据 llvm 的官方文档 SanitizerCoverage[4],我们可以搭配 -fsanitize-coverage=trace-pc-guard 或者其它编译参数控制编译器插入不同级别的

下面,我们以 -fsanitize-coverage=trace-pc-guard 为例进行演示效果:

1、配置 编译开关

2、准备源码文件

// 文件 A
int f(void) __attribute__((constructor));

int f(void) {
    NSLog(@" int f() __attribute__((constructor)) 被调用");
    return 0;
}
// 文件 ViewController.mm
#import <string>

static std::string cxx_static_str("cxx_static_str");

+ (void)load {
    NSLog(@"load 被执行");
}

// 文件 main.m
@import Darwin;

void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
                                         uint32_t *stop) {
    static uint32_t N;  // Counter for the guards.
    if (start == stop || *start) return;  // Initialize only once.
    printf("INIT: %p %p\n", start, stop);
    for (uint32_t *x = start; x < stop; x++)
    *x = ++N;
}

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
    Dl_info info;
    
    void *PC = __builtin_return_address(0);
    dladdr(PC, &info);
    printf("guard:%p 开始执行:%s \n", PC, info.dli_sname);
}

void foo(int *a) {
    if (a)
        *a = 0;
}


int main(int argc, char * argv[]) {
    dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"main block");
    });
    int i=0;
    foo(&i);
    
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

3、运行

运行日志如下所示,我们可以发现以下场景都能够被正常覆盖:

  • load 方法
  • c++ 变量
  • __attribute__((constructor)) 修饰的函数
  • 函数 foo 的两个 基本块(BasicBlock-Level)
  • block

四、编译流程简析

我们先通过一个简单例子,看看源码是如何成为二进制文件的。

1、准备源码文件

命令行输入:

cat <<EOF > main.m
int main() {
  return 0;
}
EOF

2、打印构建顺序

命令行输入:

xcrun clang main.m -save-temps -v -mllvm -debug-pass=Structure -fsanitize-coverage=trace-pc-guard

输出如下所示(有删减):

clang -cc1 -E --fsanitize-coverage-type=3 -fsanitize-coverage-trace-pc-guard main.mi -x objective-c main.m

clang -cc1 -emit-llvm-bc -disable-llvm-passes -fsanitize-coverage-type=3 -fsanitize-coverage-trace-pc-guard -o main.bc -x objective-c-cpp-output main.mi

clang -cc1 -S -fsanitize-coverage-type=3 -fsanitize-coverage-trace-pc-guard -o main.s -x ir main.bc

clang -cc1as -o main.o main.s

ld -o a.out -L/usr/local/lib main.o

整理后,如下图所示:

因为 main.bc 是二进制版本的 bitcode,可读性比较差。

开发者可以通过 llvm-dis main.bc -o - 命令转为更具有可读性的版本:

; ModuleID = 'main.bc'
source_filename = "~/main.m"
target datalayout = "e-m:o-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-apple-macosx10.15.0"

; Function Attrs: noinline nounwind optnone ssp uwtable
define i32 @main() #0 {
  %1 = alloca i32, align 4
  store i32 0, i32* %1, align 4
  ret i32 0
}

attributes #0 = { noinline nounwind optnone ssp uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "darwin-stkchk-strong-link" "disable-tail-calls"="false" "frame-pointer"="all" "less-precise-fpmad"="false" "min-legal-vector-width"="0" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "probe-stack"="___chkstk_darwin" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+cx8,+fxsr,+mmx,+sahf,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }

!llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7}
!llvm.ident = !{!8}

!0 = !{i32 2, !"SDK Version", [3 x i32] [i32 10, i32 15, i32 6]}
!1 = !{i32 1, !"Objective-C Version", i32 2}
!2 = !{i32 1, !"Objective-C Image Info Version", i32 0}
!3 = !{i32 1, !"Objective-C Image Info Section", !"__DATA,__objc_imageinfo,regular,no_dead_strip"}
!4 = !{i32 1, !"Objective-C Garbage Collection", i8 0}
!5 = !{i32 1, !"Objective-C Class Properties", i32 64}
!6 = !{i32 1, !"wchar_size", i32 4}
!7 = !{i32 7, !"PIC Level", i32 2}
!8 = !{!"Apple clang version 12.0.0 (clang-1200.0.32.21)"}

再与 main.s 文件的内容对照一下:

	.section	__TEXT,__text,regular,pure_instructions
	.build_version macos, 10, 15	sdk_version 10, 15, 6
	.globl	_main                   ## -- Begin function main
	.p2align	4, 0x90
_main:                                  ## @main
	.cfi_startproc
## %bb.0:
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset %rbp, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register %rbp
	subq	$16, %rsp
	leaq	l___sancov_gen_(%rip), %rdi
	callq	___sanitizer_cov_trace_pc_guard
	## InlineAsm Start
	## InlineAsm End
	xorl	%eax, %eax
	movl	$0, -4(%rbp)
	addq	$16, %rsp
	popq	%rbp
	retq
	.cfi_endproc
                                        ## -- End function
	.p2align	4, 0x90         ## -- Begin function sancov.module_ctor_trace_pc_guard
_sancov.module_ctor_trace_pc_guard:     ## @sancov.module_ctor_trace_pc_guard
	.cfi_startproc
## %bb.0:
	pushq	%rax
	.cfi_def_cfa_offset 16
	leaq	section$start$__DATA$__sancov_guards(%rip), %rax
	leaq	section$end$__DATA$__sancov_guards(%rip), %rcx
	movq	%rax, %rdi
	movq	%rcx, %rsi
	callq	___sanitizer_cov_trace_pc_guard_init
	popq	%rax
	retq
	.cfi_endproc
                                        ## -- End function
	.section	__DATA,__sancov_guards
	.p2align	2               ## @__sancov_gen_
l___sancov_gen_:
	.space	4

	.section	__DATA,__mod_init_func,mod_init_funcs
	.p2align	3
	.quad	_sancov.module_ctor_trace_pc_guard
	.no_dead_strip	l___sancov_gen_
	.section	__DATA,__objc_imageinfo,regular,no_dead_strip
L_OBJC_IMAGE_INFO:
	.long	0
	.long	64

.subsections_via_symbols

通过两份文件对比,我们可以发现经过 backend 流程后,___sanitizer_cov_trace_pc_guard 相关的调用才开始出现。

所以,我们可以得到第一个重要的结论:

在具有 bc 文件 的情况下,就可以通过 backend 流程 进行插桩处理。

再结合我们之前发过的公众号文章:检查第三方库是否包含 bitcode 信息,我们可以得到第二个结论:

通过导出第三方库的 bitcode,我们可以实现任意 cpu 架构下的插桩。

五、实战

讲解完基础知识后,我们开始以 微信SDKOpenSDK1.8.7.1) 为例进行实际讲解。

1、对微信SDK进行处理

检测 微信SDK 的文件类型

命令行输入:

file ~/Downloads/OpenSDK1.8.7.1/libWeChatSDK.a

输出如下:

~/Downloads/OpenSDK1.8.7.1/libWeChatSDK.a: Mach-O universal binary with 4 architectures: [i386:current ar archive] [arm_v7] [x86_64] [arm64]
~/Downloads/OpenSDK1.8.7.1/libWeChatSDK.a (for architecture i386): current ar archive
~/Downloads/OpenSDK1.8.7.1/libWeChatSDK.a (for architecture armv7): current ar archive
~/Downloads/OpenSDK1.8.7.1/libWeChatSDK.a (for architecture x86_64): current ar archive
~/Downloads/OpenSDK1.8.7.1/libWeChatSDK.a (for architecture arm64): current ar archive

因为 微信SDK包含多个架构,所以需要先用 lipo 命令导出一份单架构文件

lipo -thin armv7 ~/Downloads/OpenSDK1.8.7.1/libWeChatSDK.a -o ~/Downloads/OpenSDK1.8.7.1/libWeChatSDK_armv7.a

检测单架构文件的类型

命令行输入:

file -b ~/Downloads/OpenSDK1.8.7.1/libWeChatSDK_armv7.a

输出如下:

current ar archive

因为 libWeChatSDK_armv7.aar 文件,通过 tar 命令解压缩

tar -xf ~/Downloads/OpenSDK1.8.7.1/libWeChatSDK_armv7.a

产出12个 .o 文件

tree
.
├── AppCommunicate.o
├── AppCommunicateData.o
├── WXApi+ExtraUrl.o
├── WXApi+HandleOpenUrl.o
├── WXApi.o
├── WXApiObject.o
├── WXLogUtil.o
├── WapAuthHandler.o
├── WeChatApiUtil.o
├── WeChatIdentityHandler.o
├── WechatAuthSDK.o
└── base64.o

0 directories, 12 files

依次判断 .o 文件的类型并进行处理 命令行输入:

file -b AppCommunicate.o

输出:

Mach-O object arm_v7

通过 segedit 命令导出 bitcode

segedit AppCommunicate.o -extract __LLVM __bitcode .AppCommunicate.bc

通过 clangbitcode 转为 .s 文件

注意事项: 为了避免编译器错误:fatal error: error in backend: Cannot select: intrinsic %llvm.objc.clang.arc.use,这里需要传入 -O1 或者更高级别的优化开关,以启用 -objc-arc-contract Pass

xcrun clang -O1 -target armv7-apple-ios7 -S AppCommunicate.bc -o AppCommunicate.s -fsanitize-coverage=trace-pc-guard -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS14.1.sdk

截取 AppCommunicate.s 部分内容如下:

Ltmp0:
	.loc	9 16 0 prologue_end     ; AppCommunicate/AppCommunicate.m:16:0
Lloh0:
	adrp	x0, l___sancov_gen_@PAGE
Ltmp1:
	;DEBUG_VALUE: +[AppCommunicate getDataPasteboardName]:self <- [DW_OP_LLVM_entry_value 1] $x0
Lloh1:
	add	x0, x0, l___sancov_gen_@PAGEOFF
	bl	___sanitizer_cov_trace_pc_guard
Ltmp2:
	;DEBUG_VALUE: +[AppCommunicate getDataPasteboardName]:_cmd <- [DW_OP_LLVM_entry_value 1] $x1

2、Demo

将处理后的文件直接放到工程中:

3、运行

我们仍然用本文开头的代码进行演示。

如下所示,可以通过 console 区域看到微信SDK内部的执行流程

总结

首先,我们先回顾一下本文的重点知识:

  • 代码覆盖率 分为 函数(Fuction-Level)基本块(BasicBlock-Level)边界(Edge-Level) 三种级别。
  • llvm 编译器 通过 SanitizerCoverage 支持以上三种级别的代码覆盖率插桩。
  • 通过导出第三方库的 bitcode,我们可以实现任意cpu架构下的插桩。

本文通过介绍 代码覆盖率SanitizerCoverage编译流程 ,并以 微信SDK 为例,对如何实现第三方SDK插桩进行了详细的讲解。

参考资料

[1]

微信SDK(OpenSDK1.8.7.1): https://developers.weixin.qq.com/doc/oplatform/Downloads/iOS_Resource.html

[2]

dladdr: https://gcc.gnu.org/onlinedocs/gcc/Return-Address.html

[3]

__builtin_return_address: https://gcc.gnu.org/onlinedocs/gcc/Return-Address.html

[4]

SanitizerCoverage: https://releases.llvm.org/10.0.0/tools/clang/docs/SanitizerCoverage.html#instrumentation-points

本文分享自微信公众号 - 酷酷的哀殿(kukudeaidian)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-12-18

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • iOS 代码染色原理及技术实践

    随着业务的迅速发展,业务代码逻辑的复杂度增加。QA 测试的质量对于产品上线后的稳定性更加重要。一般 QA 测试的工作流程分为两大项:自动化测试和人工测试。这两种...

    ios-lan
  • iOS 覆盖率检测原理与增量代码测试覆盖率工具实现

    对苹果开发者而言,由于平台审核周期较长,客户端代码导致的线上问题影响时间往往比较久。如果在开发、测试阶段能够提前暴露问题,就有助于避免线上事故的发生。代码覆盖率...

    美团技术团队
  • iOS 增量代码覆盖率检测实践

    本文介绍了对iOS覆盖率检测算法的研究,分享一种可以嵌入到现有开发流程中,并对开发透明的增量代码测试覆盖率工具的实现。

    美团技术团队
  • AddressSanitizer算法及源码解析

    AddressSanitizer是Google用于检测内存各种buffer overflow(Heap buffer overflow, Stack buffe...

    Linux阅码场
  • 静态拦截iOS对象方法调用的简易实现

    最近出现了几篇关于二进制重排启动优化的文章。所有方案中都需要事先统计所有的函数调用情况,并根据函数调用的频次来进行代码的重排。

    欧阳大哥2013
  • Ruby 与 clang

    笔者在使用 `rbenv`[1] 安装 ruby 时,遇到一个头文件缺失导致无法编译失败的问题。

    酷酷的哀殿
  • 有赞iOS精准测试实践

    近几年有赞零售业务快速发展,为了满足日益增多的业务需求,2019年起零售客户端发版改成了每周一次,在质量保障方面,技术团队要面对更大的挑战。故此我们团队做了很多...

    有赞coder
  • 一些值得学习的Fuzzer开源项目

    之前GitHub上有人整理过一个叫Awesome-Fuzzing的资料,整理了关于Fuzzing技术的电子书、视频、工具、教程以及用于练习的漏洞程序。整体上不错...

    泉哥
  • 如何持续的自我提升

    今天,笔者从 百度App Objective-C/Swift 组件化混编之路(二)- 工程化 时,就被灌输了一个”新知识“:module 会供链接器使用 。

    酷酷的哀殿
  • 都有Python了,还要什么编译器!

    诚然,编译器可以为你生成高性能的代码,但是你真的需要编译器吗?另一种方法是用 Assembly 编写程序,虽然有点夸大,但这种方法有两个主要缺陷:

    AI科技大本营
  • 深度课堂:全角度解读神经网络编译器

    近年来,以机器学习、深度学习为核心的AI技术得到迅猛发展,深度神经网络在各行各业得到广泛应用:

    BBuf
  • 效能优化实践:C/C++单元测试万能插桩工具

    ? 作者:mannywang,腾讯安全平台后台开发 研发效能是一个涉及面很广的话题,它涵盖了软件交付的整个生命周期,涉及产品、架构、开发、测试、运维,每个环节...

    腾讯技术工程官方号
  • 初识LLVM&Clang-开发Xcode插件

    Xcode现在使用的编译器就是LLVM。LLVM比以前使用的GCC编译器速度快好几倍。并且LLVM可以编译 Kotlin,Ruby,Python,Haskell...

    用户6094182
  • 关于深度学习编译器,这些知识你需要知道

    近年来,以机器学习、深度学习为核心的AI技术得到迅猛发展,深度神经网络在各行各业得到广泛应用:

    AI科技大本营
  • 【Android】函数插桩(Gradle + ASM)

    第一次看到插桩,是在Android开发高手课中。看完去查了一下:“咦!还有这东西,有点意思”。

    Gavin-ZYX
  • 说说编译插桩

    说到这里,有必要说一下Java字节码和Dalvik 字节码 java字节码可以参考这篇文章作为了解一文让你明白Java字节码,里面演示了如何将字节码反过来解析...

    提莫队长
  • 图解 Rust 编译器与语言设计 | Part1:Rust 编译过程与宏展开

    想必读者朋友们都已经看到了 《Rust 日报》里的消息:微软、亚马逊、Facebook等巨头,都在组建自己的 Rust 编译器团队,都在战略性布局针对 Rust...

    张汉东
  • MIT开源高性能自动微分框架Enzyme:速度提升4.5倍

    当前,PyTorch、TensorFlow 等机器学习框架已经成为了人们开发的重要工具。计算反向传播、贝叶斯推理、不确定性量化和概率编程等算法的梯度时,我们需要...

    机器之心
  • 麻省理工新框架 | MIT开源高性能自动微分框架,速度提升4.5倍(附框架源码)

    当前,PyTorch、TensorFlow 等机器学习框架已经成为了人们开发的重要工具。计算反向传播、贝叶斯推理、不确定性量化和概率编程等算法的梯度时,我们需要...

    计算机视觉研究院

扫码关注云+社区

领取腾讯云代金券