二进制文件重排的解决方案 APP启动速度提升

启动是App给用户的第一印象,对用户体验至关重要。修改代码在二进制文件的布局可以提高启动性能,

原理

虚拟内存

在早期的计算机中 , 并没有虚拟内存的概念 , 任何应用被从磁盘中加载到运行内存中时 , 都是完整加载和按序排列的 .
那么因此 , 就会出现两个问题 :

  • 安全问题 : 由于在内存条中使用的都是真实物理地址 , 而且内存条中各个应用进程都是按顺序依次排列的 . 那么在 进程1 中通过地址偏移就可以访问到 其他进程 的内存 .
  • 效率问题 : 随着软件的发展 , 一个软件运行时需要占用的内存越来越多 , 但往往用户并不会用到这个应用的所有功能 , 造成很大的内存浪费 , 而后面打开的进程往往需要排队等待 .
虚拟内存工作原理

引用了虚拟内存后 , 在我们进程中认为自己有一大片连续的内存空间实际上是虚拟的 , 也就是说从 0x000000 ~ 0xffffff 我们是都可以访问的 . 但是实际上这个内存地址只是一个虚拟地址 , 而这个虚拟地址通过一张映射表映射后才可以获取到真实的物理地址 .

可以理解为 , 系统对真实物理内存访问做了一层限制 , 只有被写到映射表中的地址才是被认可可以访问的 .
例如 , 虚拟地址 0x000000 ~ 0xffffff 这个范围内的任意地址我们都可以访问 , 但是这个虚拟地址对应的实际物理地址是计算机来随机分配到内存页上的 .

虚拟内存解决进程间安全问题原理:

引用虚拟内存后就不存在通过偏移可以访问到其他进程的地址空间的问题了 .

因为每个进程的映射表是单独的 , 在你的进程中随便你怎么访问 , 这些地址都是受映射表限制的 , 其真实物理地址永远在规定范围内 , 也就不存在通过偏移获取到其他进程的内存空间的问题了 .
而且实际上 , 每次应用被加载到内存中 , 实际分配的物理内存并不一定是固定或者连续的 , 这是因为内存分页以及懒加载以及 ASLR 所解决的安全问题 .

cpu 寻址过程

引入虚拟内存后 , cpu 在通过虚拟内存地址访问数据的过程如下 :

  • 通过虚拟内存地址 , 找到对应进程的映射表 .
  • 通过映射表找到其对应的真实物理地址 , 进而找到数据 .

这个过程被称为 地址翻译 , 这个过程是由操作系统以及 cpu 上集成的一个 硬件单元 MMU 协同来完成的 .

虚拟内存解决效率问题

刚刚提到虚拟内存和物理内存通过映射表进行映射 , 但是这个映射并不可能是一一对应的 , 那样就太过浪费内存了 . 为了解决效率问题 , 实际上真实物理内存是分页的 . 而映射表同样是以页为单位的 .

换句话说 , 映射表只会映射到一页 , 并不会映射到具体每一个地址 .

linux 系统中 , 一页内存大小为 4KB , 在不同平台可能各有不同 .

  • Mac OS 系统中 , 一页为 4KB ,
  • iOS 系统中 , 一页为 16KB .

我们可以使用 pagesize 命令直接查看 .

Page Fault

  • 当应用被加载到内存中时 , 并不会将整个应用加载到内存中 . 只会放用到的那一部分 . 也就是懒加载的概念 , 换句话说就是应用使用多少 , 实际物理内存就实际存储多少 .

  • 当应用访问到某个地址 , 映射表中为 0 , 也就是说并没有被加载到物理内存中时 , 系统就会立刻阻塞整个进程 , 触发一个我们所熟知的 缺页中断 - Page Fault

当一个缺页中断被触发 , 操作系统会从磁盘中重新读取这页数据到物理内存上 , 然后将映射表中虚拟内存指向对应 ( 如果当前内存已满 , 操作系统会通过置换页算法 找一页数据进行覆盖 , 这也是为什么开再多的应用也不会崩掉 , 但是之前开的应用再打开时 , 就重新启动了的根本原因 ).

通过App Store渠道分发的App,Page Fault还会进行签名验证,所以一次Page Fault的耗时比想象的要多:


image.png

重排

在了解了内存分页会触发中断异常 Page Fault 会阻塞进程后 , 我们就知道了这个问题是会对性能产生影响的 .

编译器在生成二进制代码的时候,默认按照链接的Object File(.o)顺序写文件,按照Object File内部的函数顺序写函数。

静态库文件.a就是一组.o文件的ar包,可以用ar -t查看.a包含的所有.o

image.png

简化问题:假设我们只有两个page:page1/page2,其中绿色的method1和method3启动时候需要调用,为了执行对应的代码,系统必须进行两个Page Fault。

但如果我们把method1和method3排布到一起,那么只需要一个Page Fault即可,这就是二进制文件重排的核心原理。

实际项目中的做法是将启动时需要调用的函数放到一起 ( 比如 前10页中 ) 以尽可能减少 page fault , 达到优化目的 . 而这个做法就叫做 : 二进制重排 .

存在的问题

为了完成重排,有以下几个问题要解决:

  • 重排效果怎么样 - 要想看到优化效果 , 就应该知道如何获取启动阶段的page fault次数 , 以此来帮助我们查看优化前以及优化后的效果 .
  • 如何重排 - 让链接器按照指定顺序生成Mach-O
  • 重排成功了没 - 拿到当前二进制的函数布局
  • 重排的内容 - 获取启动时候用到的函数
    • hook objc_MsgSend ( 只能拿到 oc 以及 swift 加上 @objc dynamic 修饰后的方法 ) .
    • 静态扫描 mach-o 特定段和节里面所存储的符号以及函数数据 . (静态扫描 , 主要用来获取 load 方法 , c++ 构造(有关 c++ 构造 , 参考 从头梳理 dyld 加载流程 这篇文章有详细讲述和演示 ) .
    • clang 插桩 ( 完美版本 , 完全拿到 swift , oc , c , block 全部函数 )

获取启动阶段的page fault次数

如果想查看真实 page fault 次数 , 应该将应用卸载 , 查看第一次应用安装后的效果 , 或者先打开很多个其他应用 .
因为之前运行过 app , 应用其中一部分已经被加载到物理内存并做好映射表映射 , 这时再启动就会少触发一部分缺页中断 , 并且杀掉应用再打开也是如此 .
其实就是希望将物理内存中之前加载的覆盖/清理掉 , 减少误差 .

System Trace

日常开发中性能分析是用最多的工具无疑是Time Profiler,但Time Profiler是基于采样的,并且只能统计线程实际在运行的时间,而发生Page Fault的时候线程是被blocked,所以我们需要用一个不常用但功能却很强大的工具:System Trace。

选中主线程,在VM Activity中的File Backed Page In次数就是Page Fault次数,并且双击还能按时序看到引起Page Fault的堆栈:


image.png

当然 , 你可以通过添加 DYLD_PRINT_STATISTICS 来查看 pre-main 阶段总耗时来做一个侧面辅证 .

signpost

在Instrument中已经能拿到某个时间段的Page In次数,可以通过os_signpost和启动映射起来呢?

os_signpost是iOS 12开始引入的一组API,可以在Instruments绘制一个时间段,代码也很简单

os_log_t logger = os_log_create("com.bytedance.tiktok", "performance");
os_signpost_id_t signPostId = os_signpost_id_make_with_pointer(logger,sign);
//标记时间段开始
os_signpost_interval_begin(logger, signPostId, "Launch","%{public}s", "");
//标记结束
os_signpost_interval_end(logger, signPostId, "Launch");

有多少个Mach-O,就会有多少个Load和C++静态初始化阶段,用signpost相关API对对应阶段打点,方便跟踪每个阶段的优化效果。

二进制重排具体如何操作

ld

Xcode使用的链接器件是ld,ld有一个不常用的参数-order_file,通过man ld可以看到详细文档:

Alters the order in which functions and data are laid out. For each section in the output file, any symbol in that section that are specified in the order file file is moved to the start of its section and laid out in the same order as in the order file file.

改变函数和数据的排列顺序。对于输出文件中的每个section,在order file中指定的该section中的任何符号都将移动到其section的开头,并按照与order file中相同的顺序排列。

可以看到,order_file中的符号会按照顺序排列在对应section的开始,完美的满足了我们的需求。

我们可以通过这个参数配置一个 order 文件的路径 .Xcode的GUI也提供了order_file选项:Build Settings -> Linking -> Order File
在这个 order 文件中 , 将你需要的符号按顺序写在里面 .
当工程 build 的时候 , Xcode 会读取这个文件 , 打的二进制包就会按照这个文件中的符号顺序进行生成对应的 mach-O .

如果order_file中的符号实际不存在 , ld会忽略这些符号,如果提供了link选项 -order_file_statistics,会以warning的形式把这些没找到的符号打印在日志里。

这种方式不会影响上架 ? 首先 , objc 源码自己也在用这种方式 .二进制重排只是重新排列了所生成的 macho 中函数表与符号表的顺序 .

通过 Linkmap 查看自己工程的符号顺序

Linkmap是iOS编译过程的中间产物,记录了二进制文件的布局,通过Xcode的Build Settings里开启Write Link Map File 设置输出与否 , 默认是 no .

修改完毕后 clean 一下 , 运行工程 , Products - show in finder, 找到 macho 的上上层目录.

linkmap主要包括三大部分:

  • Object Files 生成二进制用到的link单元的路径和文件编号

  • Sections 记录Mach-O每个Segment/section的地址范围

  • Symbols 按顺序记录每个符号的地址范围

# Symbols:
# Address   Size        File  Name
0x100005460 0x000000B0  [  2] -[MQTTCFSocketDecoder init]
0x100005510 0x000000C0  [  2] -[MQTTCFSocketDecoder open]
0x1000055D0 0x00000050  [  2] -[MQTTCFSocketDecoder dealloc]
0x100005620 0x000000A0  [  2] -[MQTTCFSocketDecoder close]
0x1000056C0 0x00000740  [  2] -[MQTTCFSocketDecoder stream:handleEvent:]
0x100005E00 0x00000020  [  2] -[MQTTCFSocketDecoder state]
0x100005E20 0x00000020  [  2] -[MQTTCFSocketDecoder setState:]
0x100005E40 0x00000020  [  2] -[MQTTCFSocketDecoder error]
0x100005E60 0x00000040  [  2] -[MQTTCFSocketDecoder setError:]
0x100005EA0 0x00000020  [  2] -[MQTTCFSocketDecoder stream]
0x100005EC0 0x00000040  [  2] -[MQTTCFSocketDecoder setStream:]
0x100005F00 0x00000030  [  2] -[MQTTCFSocketDecoder delegate]
0x100005F30 0x00000030  [  2] -[MQTTCFSocketDecoder setDelegate:]
0x100005F60 0x0000005C  [  2] -[MQTTCFSocketDecoder .cxx_destruct]
0x100005FC0 0x00000120  [  3] -[YXPhotographyMainNavView initWithFrame:]
0x1000060E0 0x000001E0  [  3] -[YXPhotographyMainNavView isRepeatClickDetail:]
0x1000062C0 0x000001E0  [  3] -[YXPhotographyMainNavView resetDetailItemStatus]
0x1000064A0 0x000002C0  [  3] -[YXPhotographyMainNavView setDetailIndex:]
0x100006760 0x00000270  [  3] -[YXPhotographyMainNavView setMainType:]
0x1000069D0 0x00000F00  [  3] -[YXPhotographyMainNavView setUpView]

符号顺序明显是按照 Compile Sources 的文件顺序来排列的 .

最左侧地址就是 实际代码地址而并非符号地址 , 二进制重排并非只是修改符号地址 , 而是利用符号顺序 , 重新排列整个代码在文件的偏移地址 , 将启动需要加载的方法地址放到前面内存页中 , 以此达到减少 page fault 的次数从而实现时间上的优化 ,
可以利用 MachOView 查看排列前后在 _text 段 ( 代码段 ) 中的源码顺序来帮助理解 .

简单操作

来到工程根目录 , 新建一个文件 touch lb.order . 随便挑选几个启动时就需要加载的方法 ,

-[YXCenterTitleNavView .cxx_destruct]
+[YXRemoteVideoFile cacheContentType:]
+[YXRemoteVideoFile tmpContentType:]
+[YXRemoteVideoFile cacheFileExit:]
+[YXRemoteVideoFile cacheFilePath:]
+[YXRemoteVideoFile cacheFileSize:]
+[YXRemoteVideoFile tempFileExit:]
+[YXRemoteVideoFile tempFilePath:]
+[YXRemoteVideoFile tempFileSize:]
+[YXRemoteVideoFile clearTempFile:]

写到该文件中 , 保存 , 配置 order_file 文件路径 .
重新运行 , 查看 .Linkmap

# Symbols:
# Address   Size        File  Name
0x100005460 0x00000085  [ 23] -[YXCenterTitleNavView .cxx_destruct]
0x1000054F0 0x00000110  [ 24] +[YXRemoteVideoFile cacheContentType:]
0x100005600 0x00000110  [ 24] +[YXRemoteVideoFile tmpContentType:]
0x100005710 0x000000D0  [ 24] +[YXRemoteVideoFile cacheFileExit:]
0x1000057E0 0x00000110  [ 24] +[YXRemoteVideoFile cacheFilePath:]
0x1000058F0 0x00000170  [ 24] +[YXRemoteVideoFile cacheFileSize:]
0x100005A60 0x000000D0  [ 24] +[YXRemoteVideoFile tempFileExit:]
0x100005B30 0x000000D0  [ 24] +[YXRemoteVideoFile tempFilePath:]
0x100005C00 0x00000170  [ 24] +[YXRemoteVideoFile tempFileSize:]
0x100005D70 0x000000C0  [ 24] +[YXRemoteVideoFile clearTempFile:]
0x100005E30 0x000000B0  [  2] -[MQTTCFSocketDecoder init]
0x100005EE0 0x000000C0  [  2] -[MQTTCFSocketDecoder open]

我们所写的方法已经被放到最前面了 , 至此 , 生成的 macho 中距离首地址偏移量最小的代码就是我们所写的这三个方法

获取启动时用的函数符号。

首先排除了解析Instruments(Time Profiler/System Trace) trace文件方案,因为他们都是基于特定场景采样的,大多数符号获取不到。最后选择了静态扫描+运行时Trace结合的解决方案。

Load

Objective C的符号名是+-[Class_name(category_name) method:name:],其中+表示类方法,-表示实例方法。

linkmap里记录了所有的符号名,所以只要扫一遍linkmap的__TEXT,__text,正则匹配("^\+\[.*\ load\]$")可以拿到所有的load方法符号。

C++静态初始化

C++并不像Objective C方法那样,大部分方法调用编译后都是objc_msgSend,也就没有一个入口函数去运行时hook。

可以用-finstrument-functions在编译期插桩“hook”,这套方案需要修改依赖三方库的构建过程。二进制文件重排在没有业界经验可供参考,不确定收益的情况下,选择了并不完美但成本最低的静态扫描方案。

  1. 扫描linkmap的__DATA,__mod_init_func,这个section存储了包含C++静态初始化方法的文件,获得文件号[ 5]。
//__mod_init_func
0x100008060    0x00000008  [  5] ltmp7
//[  5]对应的文件
[  5] .../Build/Products/Debug-iphoneos/libStaticLibrary.a(StaticLibrary.o)
  1. 通过文件号,解压出.o。
lipo libStaticLibrary.a -thin arm64 -output arm64.a
ar -x arm64.a StaticLibrary.o
  1. 通过.o,获得静态初始化的符号名_demo_constructor。
objdump -r -section=__mod_init_func StaticLibrary.o

StaticLibrary.o:    file format Mach-O arm64

RELOCATION RECORDS FOR [__mod_init_func]:
0000000000000000 ARM64_RELOC_UNSIGNED _demo_constructor
  1. 通过符号名,文件号,在linkmap中找到符号在二进制中的范围:
0x100004A30    0x0000001C  [  5] _demo_constructor
  1. 通过起始地址,对代码进行反汇编:
objdump -d --start-address=0x100004A30 --stop-address=0x100004A4B demo_arm64 
_demo_constructor:
100004a30:    fd 7b bf a9     stp x29, x30, [sp, #-16]!
100004a34:    fd 03 00 91     mov x29, sp
100004a38:    20 0c 80 52     mov w0, #97
100004a3c:    da 06 00 94     bl  #7016 
100004a40:    40 0c 80 52     mov w0, #98
100004a44:    fd 7b c1 a8     ldp x29, x30, [sp], #16
100004a48:    d7 06 00 14     b   #7004 
  1. 通过扫描bl指令扫描子程序调用,子程序在二进制的开始地址为:100004a3c +1b68(对应十进制的7016)。
100004a3c:    da 06 00 94     bl  #7016 
  1. 通过开始地址,可以找到符号名和结束地址,然后重复5~7,递归的找到所有的子程序调用的函数符号。

小坑

STL里会针对string生成初始化函数,这样会导致多个.o里存在同名的符号,例如:

__ZNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEC1IDnEEPKc

类似这样的重复符号的情况在C++里有很多,所以C/C++符号在order_file里要带着所在的.o信息:

//order_file.txt
libDemoLibrary.a(object.o):__GLOBAL__sub_I_demo_file.cpp

局限性
branch系列汇编指令除了bl/b,还有br/blr,即通过寄存器的间接子程序调用,静态扫描无法覆盖到这种情况。

Local符号

做C++静态初始化扫描的时候,会扫描出很多类似l002的符号。这是由于依赖方输出静态库的时候裁剪了local符号。导致__GLOBAL__sub_I_demo_file.cpp 变成了l002。

需要静态库出包的时候保留local符号,CI脚本不要执行strip -x,同时Xcode对应target的Build Setting -> Deployment -> Strip Style修改为Debugging symbol

静态库保留的local符号会在宿主App生成IPA之前裁剪掉,所以不会对最后的IPA包大小有影响。宿主App的Strip Style要选择All Symbols,宿主动态库选择Non-Global Symbols。

Objective C方法

绝大部分Objective C的方法在编译后会走objc_msgSend,所以通过fishhook(https://github.com/facebook/fishhook) hook这一个C函数即可获得Objective C符号。由于objc_msgSend是变长参数,所以hook代码需要用汇编来实现:https://www.jianshu.com/p/f0ecf5ca5114

//代码参考InspectiveC
__attribute__((__naked__))
static void hook_Objc_msgSend() {
    save()
    __asm volatile ("mov x2, lr\n");
    __asm volatile ("mov x3, x4\n");
    call(blr, &before_objc_msgSend)
    load()
    call(blr, orig_objc_msgSend)
    save()
    call(blr, &after_objc_msgSend)
    __asm volatile ("mov lr, x0\n");
    load()
    ret()
}

子程序调用时候要保存和恢复参数寄存器,所以save和load分别对x0~x9, q0~q9入栈/出栈。call则通过寄存器来间接调用函数:

#define save() \
__asm volatile ( \
"stp q6, q7, [sp, #-32]!\n"\
...

#define load() \
__asm volatile ( \
"ldp x0, x1, [sp], #16\n" \
...

#define call(b, value) \
__asm volatile ("stp x8, x9, [sp, #-16]!\n"); \
__asm volatile ("mov x12, %0\n" :: "r"(value)); \
__asm volatile ("ldp x8, x9, [sp], #16\n"); \
__asm volatile (#b " x12\n");

在before_objc_msgSend中用栈保存lr,在after_objc_msgSend恢复lr。由于要生成trace文件,为了降低文件的大小,直接写入的是函数地址,且只有当前可执行文件的Mach-O(app和动态库)代码段才会写入:

iOS中,由于ALSR的存在,在写入之前需要先减去偏移量slide:

IMP imp = (IMP)class_getMethodImplementation(object_getClass(self), _cmd);
unsigned long imppos = (unsigned long)imp;
unsigned long addr = immpos - macho_slide

获取一个二进制的__text段地址范围:

unsigned long size = 0;
unsigned long start = (unsigned long)getsectiondata(mhp,  "__TEXT", "__text", &size);
unsigned long end = start + size;

获取到函数地址后,反查linkmap既可找到方法的符号名。

Block

block是一种特殊的单元,block在编译后的函数体是一个C函数,在调用的时候直接通过指针调用,并不走objc_msgSend,所以需要单独hook。

通过Block的源码可以看到block的内存布局如下:

struct Block_layout {
    void *isa;
    int32_t flags; // contains ref count
    int32_t reserved;
    void  *invoke;
    struct Block_descriptor1 *descriptor;
};
struct Block_descriptor1 {
    uintptr_t reserved;
    uintptr_t size;
};

其中invoke就是函数的指针,hook思路是将invoke替换为自定义实现,然后在reserved保存为原始实现。

//参考 https://github.com/youngsoft/YSBlockHook
if (layout->descriptor != NULL && layout->descriptor->reserved == NULL)
{
    if (layout->invoke != (void *)hook_block_envoke)
    {
        layout->descriptor->reserved = layout->invoke;
        layout->invoke = (void *)hook_block_envoke;
    }
}

由于block对应的函数签名不一样,所以这里仍然采用汇编来实现hook_block_envoke:

__attribute__((__naked__))
static void hook_block_envoke() {
    save()
    __asm volatile ("mov x1, lr\n");
    call(blr, &before_block_hook);
    __asm volatile ("mov lr, x0\n");
    load()
    //调用原始的invoke,即resvered存储的地址
    __asm volatile ("ldr x12, [x0, #24]\n");
    __asm volatile ("ldr x12, [x12]\n");
    __asm volatile ("br x12\n");
}

在before_block_hook中获得函数地址(同样要减去slide)。

intptr_t before_block_hook(id block,intptr_t lr)
{
    Block_layout * layout = (Block_layout *)block;
    //layout->descriptor->reserved即block的函数地址
    return lr;
}

同样,通过函数地址反查linkmap既可找到block符号。

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

  • initialize hook不到
  • 部分block hook不到
  • C++通过寄存器的间接函数调用静态扫描不出来
    目前的重排方案能够覆盖到80%~90%的符号,可以尝试编译期插桩等方案来进行100%的符号覆盖,让重排达到最优效果。
image.png

设置条件触发流程

工程注入Trace动态库,选择release模式编译出.app/linkmap/中间产物

运行一次App到启动结束,Trace动态库会在沙盒生成Trace log

以Trace Log,中间产物和linkmap作为输入,运行脚本解析出order_file

clang 插桩

官方文档 https://clang.llvm.org/docs/SanitizerCoverage.html
clang 插桩主要有两个实现思路 , 一是自己编写 clang 插件 , 另外一个就是利用 clang 本身已经提供的一个工具 or 机制来实现我们获取所有符号的需求 . 本文是第二种思路

新建一个工程,按照文档指示来走 ,测试和使用一下这个静态插桩代码覆盖工具的机制和原理 .

  1. 首先 , 添加编译设置 .
    Build Settings -> Apple Clang - Custom Compiler Flags -> Other C Flags 中 , 添加
-fsanitize-coverage=trace-pc-guard
  1. 添加 hook 代码 .
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
                                                    uint32_t *stop) {
  static uint64_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;  // Guards should start from 1.
}

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
  if (!*guard) return;  // Duplicate the guard check.

  void *PC = __builtin_return_address(0);
  char PcDescr[1024];
  //__sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr));
  printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}

写在空工程的 ViewController.m 里的打印

INIT: 0x108435788 0x1084357d4
guard: 0x1084357ac a PC 
guard: 0x1084357a0 7 PC \340\272�a\377�
guard: 0x1084357c8 11 PC 
guard: 0x1084357cc 12 PC �
guard: 0x1084357c8 11 PC �
guard: 0x1084357c8 11 PC �
guard: 0x1084357b0 b PC YNu \377�
guard: 0x1084357c8 11 PC  
guard: 0x1084357c8 11 PC \300�}\347\376�
guard: 0x1084357c8 11 PC 
guard: 0x1084357c0 f PC \377\377
guard: 0x1084357b8 d PC \377\377

代码命名 INIT 后面打印的两个指针地址叫 start 和 stop . 通过 lldb 来查看下从 start 到 stop 这个内存地址里面所存储的到底是啥 .

INIT: 0x10bcbd788 0x10bcbd7d4
guard: 0x10bcbd7ac a PC 
guard: 0x10bcbd7a0 7 PC \340\272�a\377�
guard: 0x10bcbd7c8 11 PC 
guard: 0x10bcbd7cc 12 PC �
guard: 0x10bcbd7c8 11 PC �
guard: 0x10bcbd7c8 11 PC �
guard: 0x10bcbd7b0 b PC YNu \377�
guard: 0x10bcbd7c8 11 PC  
guard: 0x10bcbd7c8 11 PC \300\204\364\343\376�
guard: 0x10bcbd7c8 11 PC 
guard: 0x10bcbd7c0 f PC \377\377
guard: 0x10bcbd7b8 d PC \377\377
(lldb) x 0x10bcbd788
0x10bcbd788: 01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00  ................
0x10bcbd798: 05 00 00 00 06 00 00 00 07 00 00 00 08 00 00 00  ................
(lldb) x 0x10bcbd7a8
0x10bcbd7a8: 09 00 00 00 0a 00 00 00 0b 00 00 00 0c 00 00 00  ................
0x10bcbd7b8: 0d 00 00 00 0e 00 00 00 0f 00 00 00 10 00 00 00  ................
(lldb) x 0x10bcbd7c8
0x10bcbd7c8: 11 00 00 00 12 00 00 00 13 00 00 00 00 00 00 00  ................
0x10bcbd7d8: 50 94 d2 0b 01 00 00 00 00 00 00 00 00 00 00 00  P...............
(lldb) 

发现存储的是从 1 到 19 这个序号 . 那么我们来添加一个 oc 方法 .

再次运行查看 .

INIT: 0x10419d7a8 0x10419d7f8
guard: 0x10419d7d0 b PC 
guard: 0x10419d7c4 8 PC \340\272�a\377�
guard: 0x10419d7ec 12 PC 
guard: 0x10419d7f0 13 PC �
guard: 0x10419d7ec 12 PC �
guard: 0x10419d7ec 12 PC �
guard: 0x10419d7d4 c PC YNu \377�
guard: 0x10419d7ec 12 PC  
guard: 0x10419d7ec 12 PC \300\204\246\353\376�
guard: 0x10419d7ec 12 PC 
guard: 0x10419d7e4 10 PC \377\377
guard: 0x10419d7dc e PC \377\377
(lldb) x 0x10419d7a8
0x10419d7a8: 01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00  ................
0x10419d7b8: 05 00 00 00 06 00 00 00 07 00 00 00 08 00 00 00  ................
(lldb) x 0x10419d7c8
0x10419d7c8: 09 00 00 00 0a 00 00 00 0b 00 00 00 0c 00 00 00  ................
0x10419d7d8: 0d 00 00 00 0e 00 00 00 0f 00 00 00 10 00 00 00  ................
(lldb) x 0x10419d7e8
0x10419d7e8: 11 00 00 00 12 00 00 00 13 00 00 00 14 00 00 00  ................
0x10419d7f8: 50 94 20 04 01 00 00 00 00 00 00 00 00 00 00 00  P. .............
(lldb) 

发现从 13 变成了 14 . 也就是说存储的 1 到 19 这个序号变成了 1 到 20 .
那么我们再添加一个 c 函数 , 一个 block , 和一个触摸屏幕方法来看下 .

void testCFun(){
    NSLog(@"C函数");
}

- (void)testOCFunc{
    
}

void (^testBlock)(void) = ^(){
    NSLog(@"block");
};
//
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{

}
INIT: 0x10fe987c0 0x10fe9881c
guard: 0x10fe987f4 e PC 
guard: 0x10fe987e8 b PC \340\272�a\377�
guard: 0x10fe98810 15 PC 
guard: 0x10fe98814 16 PC �
guard: 0x10fe98810 15 PC �
guard: 0x10fe98810 15 PC �
guard: 0x10fe987f8 f PC YNu \377�
guard: 0x10fe98810 15 PC  
guard: 0x10fe98810 15 PC \300\324\326\337\376�
guard: 0x10fe98810 15 PC 
guard: 0x10fe98808 13 PC \377\377
guard: 0x10fe98800 11 PC \377\377
(lldb) x 0x10fe9881c-0x4
0x10fe98818: 17 00 00 00 00 00 00 00 50 44 f0 0f 01 00 00 00  ........PD......
0x10fe98828: 90 30 e9 0f 01 00 00 00 00 00 00 00 00 00 00 00  .0..............
(lldb) 

变成了23 , 可以得到一个猜想 , 这个内存区间保存的就是工程所有符号的个数 .

其次 , 点击四次屏幕,

guard: 0x10163e7e4 a PC 
guard: 0x10163e7e4 a PC \275\223�a\377�
guard: 0x10163e7e4 a PC \275\223�a\377�
guard: 0x10163e7e4 a PC \275\223�a\377�

我们在触摸屏幕方法调用了 c 函数 , c 函数中调用了 block . 之后点击四次屏幕 ,

guard: 0x10f0557ec a PC 
guard: 0x10f0557e0 7 PC 
2021-01-07 17:24:58.234205+0800 WhatFuck[58381:5771061] C函数
guard: 0x10f0557e8 9 PC 
2021-01-07 17:24:58.234330+0800 WhatFuck[58381:5771061] block
guard: 0x10f0557ec a PC \275\223�a\377�
guard: 0x10f0557e0 7 PC 7\331���
2021-01-07 17:25:14.187490+0800 WhatFuck[58381:5771061] C函数
guard: 0x10f0557e8 9 PC \267T�a\377�
2021-01-07 17:25:14.187779+0800 WhatFuck[58381:5771061] block
guard: 0x10f0557ec a PC \275\223�a\377�
guard: 0x10f0557e0 7 PC 7\331���
2021-01-07 17:25:15.655994+0800 WhatFuck[58381:5771061] C函数
guard: 0x10f0557e8 9 PC \267T�a\377�
2021-01-07 17:25:15.656135+0800 WhatFuck[58381:5771061] block
guard: 0x10f0557ec a PC \275\223�a\377�
guard: 0x10f0557e0 7 PC 7\331���
2021-01-07 17:25:15.985723+0800 WhatFuck[58381:5771061] C函数
guard: 0x10f0557e8 9 PC \267T�a\377�
2021-01-07 17:25:15.985874+0800 WhatFuck[58381:5771061] block

发现我们实际调用几个方法 , 就会打印几次 guard : .

类似我们埋点统计所实现的效果 . 在触摸方法添加一个断点查看汇编 :

通过汇编我们发现 , 在每个函数调用的第一句实际代码 ( 栈平衡与寄存器数据准备除外 ) , 被添加进去了一个 bl 调用到 __sanitizer_cov_trace_pc_guard 这个函数中来 .
这也是静态插桩的原理和名称由来 .

`-[ViewController touchesBegan:withEvent:]:
    0x101ce3910 <+0>:   pushq  %rbp
    0x101ce3911 <+1>:   movq   %rsp, %rbp
    0x101ce3914 <+4>:   subq   $0x40, %rsp
    0x101ce3918 <+8>:   leaq   0x7ecd(%rip), %rax
    0x101ce391f <+15>:  movq   %rdi, -0x28(%rbp)
    0x101ce3923 <+19>:  movq   %rax, %rdi
    0x101ce3926 <+22>:  movq   %rsi, -0x30(%rbp)
    0x101ce392a <+26>:  movq   %rdx, -0x38(%rbp)
    0x101ce392e <+30>:  movq   %rcx, -0x40(%rbp)
    0x101ce3932 <+34>:  callq  0x101ce41ce               ; symbol stub for: __sanitizer_cov_trace_pc_guard
    0x101ce3937 <+39>:  movq   -0x28(%rbp), %rax
    0x101ce393b <+43>:  movq   %rax, -0x8(%rbp)
    0x101ce393f <+47>:  movq   -0x30(%rbp), %rcx
    0x101ce3943 <+51>:  movq   %rcx, -0x10(%rbp)
    0x101ce3947 <+55>:  movq   $0x0, -0x18(%rbp)
    0x101ce394f <+63>:  leaq   -0x18(%rbp), %rdx
    0x101ce3953 <+67>:  movq   -0x38(%rbp), %rsi
    0x101ce3957 <+71>:  movq   %rdx, %rdi
    0x101ce395a <+74>:  callq  0x101ce4216               ; symbol stub for: objc_storeStrong
    0x101ce395f <+79>:  movq   $0x0, -0x20(%rbp)
    0x101ce3967 <+87>:  leaq   -0x20(%rbp), %rax
    0x101ce396b <+91>:  movq   -0x40(%rbp), %rcx
    0x101ce396f <+95>:  movq   %rax, %rdi
    0x101ce3972 <+98>:  movq   %rcx, %rsi
    0x101ce3975 <+101>: callq  0x101ce4216               ; symbol stub for: objc_storeStrong
->  0x101ce397a <+106>: callq  0x101ce4222               ; symbol stub for: testCFun
    0x101ce397f <+111>: xorl   %r8d, %r8d

静态插桩总结

静态插桩实际上是在编译期就在每一个函数内部二进制源数据添加 hook 代码 ( 我们添加的 __sanitizer_cov_trace_pc_guard 函数 ) 来实现全局的方法 hook 的效果 .

究竟是直接修改二进制在每个函数内部都添加了调用 hook 函数这个汇编代码 , 还是只是类似于编译器在所生成的二进制文件添加了一个标记 , 然后在运行时如果有这个标记就会自动多做一步调用 hook 代码呢 ?

使用 hopper 来看下生成的 mach-o 二进制文件 .

                     -[ViewController touchesBegan:withEvent:]:
0000000100001910         push       rbp                                         ; Objective C Implementation defined at 0x1000082e0 (instance method), DATA XREF=0x1000082e0
0000000100001911         mov        rbp, rsp
0000000100001914         sub        rsp, 0x40
0000000100001918         lea        rax, qword [0x1000097ec]
000000010000191f         mov        qword [rbp+var_28], rdi
0000000100001923         mov        rdi, rax
0000000100001926         mov        qword [rbp+var_30], rsi
000000010000192a         mov        qword [rbp+var_38], rdx
000000010000192e         mov        qword [rbp+var_40], rcx
0000000100001932         call       imp___stubs____sanitizer_cov_trace_pc_guard
0000000100001937         mov        rax, qword [rbp+var_28]
000000010000193b         mov        qword [rbp+var_8], rax
000000010000193f         mov        rcx, qword [rbp+var_30]
0000000100001943         mov        qword [rbp+var_10], rcx
0000000100001947         mov        qword [rbp+var_18], 0x0
000000010000194f         lea        rdx, qword [rbp+var_18]
0000000100001953         mov        rsi, qword [rbp+var_38]                     ; argument "value" for method imp___stubs__objc_storeStrong
0000000100001957         mov        rdi, rdx                                    ; argument "addr" for method imp___stubs__objc_storeStrong
000000010000195a         call       imp___stubs__objc_storeStrong
000000010000195f         mov        qword [rbp+var_20], 0x0
0000000100001967         lea        rax, qword [rbp+var_20]
000000010000196b         mov        rcx, qword [rbp+var_40]
000000010000196f         mov        rdi, rax                                    ; argument "addr" for method imp___stubs__objc_storeStrong
0000000100001972         mov        rsi, rcx                                    ; argument "value" for method imp___stubs__objc_storeStrong
0000000100001975         call       imp___stubs__objc_storeStrong
000000010000197a         call       imp___stubs__testCFun
000000010000197f         xor        r8d, r8d
0000000100001982         mov        esi, r8d                                    ; argument "value" for method imp___stubs__objc_storeStrong
0000000100001985         lea        rax, qword [rbp+var_20]
0000000100001989         mov        rdi, rax                                    ; argument "addr" for method imp___stubs__objc_storeStrong
000000010000198c         call       imp___stubs__objc_storeStrong
0000000100001991         xor        r8d, r8d
0000000100001994         mov        esi, r8d                                    ; argument "value" for method imp___stubs__objc_storeStrong
0000000100001997         lea        rax, qword [rbp+var_18]
000000010000199b         mov        rdi, rax                                    ; argument "addr" for method imp___stubs__objc_storeStrong
000000010000199e         call       imp___stubs__objc_storeStrong
00000001000019a3         add        rsp, 0x40
00000001000019a7         pop        rbp
00000001000019a8         ret
                        ; endp
00000001000019a9         nop        dword [rax]

的确是函数内部 一开始就添加了 调用额外方法的汇编代码 . 这也是我们为什么称其为 " 静态插桩 " .

获取所有函数符号

原理大体上了解了 , 那么如何才能拿到函数的符号

思路

我们现在知道了 , 所有函数内部第一步都会去调用 __sanitizer_cov_trace_pc_guard 这个函数 . 那么函数嵌套时 , 在跳转子函数时都会保存下一条指令的地址在 X30 ( 又叫 lr 寄存器) 里 .

例如 , A 函数中调用了 B 函数 , 在 arm 汇编中即 bl + 0x**** 指令 , 该指令会首先将下一条汇编指令的地址保存在 x30 寄存器中 ,]

然后在跳转到 bl 后面传递的指定地址去执行 . ( 提示 : bl 能实现跳转到某个地址的汇编指令 , 其原理就是修改 pc 寄存器的值来指向到要跳转的地址 , 而且实际上 B 函数中也会对 x29 / x30 寄存器的值做保护防止子函数又跳转其他函数会覆盖掉 x30 的值 , 当然 , 叶子函数除外 . ) .

当 B 函数执行 ret 也就是返回指令时 , 就会去读取 x30 寄存器的地址 , 跳转过去 , 因此也就回到了上一层函数的下一步 .
这种思路来实现实际上是可以的 . 我们所写的 __sanitizer_cov_trace_pc_guard 函数中的这一句代码 :

void *PC = __builtin_return_address(0); 

它的作用其实就是去读取 x30 中所存储的要返回时下一条指令的地址 . 所以他名称叫做__builtin_return_address . 换句话说 , 这个地址就是我当前这个函数执行完毕后 , 要返回到哪里去 .

也就是说 , 我们现在可以在 __sanitizer_cov_trace_pc_guard 这个函数中 , 通过 __builtin_return_address 数拿到原函数调用 __sanitizer_cov_trace_pc_guard 这句汇编代码的下一条指令的地址 .

根据内存地址获取函数名称

拿到了函数内部 下一行代码 的地址 , 需要获取函数名称

熟悉安全攻防 , 逆向的同学可能会清楚 . 我们为了防止某些特定的方法被别人使用 fishhook hook 掉 , 会利用 dlopen 打开动态库 , 拿到一个句柄 , 进而拿到函数的内存地址直接调用 .

是不是跟我们这个流程有点相似 , 只是我们好像是反过来的 . 其实反过来也是可以的 .

dlopen 相同 , 在 dlfcn.h 中有一个方法如下 :

typedef struct dl_info {
        const char      *dli_fname;     /* 所在文件 */
        void            *dli_fbase;     /* 文件地址 */
        const char      *dli_sname;     /* 符号名称 */
        void            *dli_saddr;     /* 函数起始地址 */
} Dl_info;

//这个函数能通过函数内部地址找到函数符号
int dladdr(const void *, Dl_info *);

实验一下 , 先导入头文件#import <dlfcn.h> , 然后修改代码如下 :

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
    if (!*guard) return;  // Duplicate the guard check.
    
    void *PC = __builtin_return_address(0);
    Dl_info info;
    dladdr(PC, &info);
    
    printf("fname=%s \nfbase=%p \nsname=%s\nsaddr=%p \n",info.dli_fname,info.dli_fbase,info.dli_sname,info.dli_saddr);
    
    char PcDescr[1024];
    printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}

打印结果

INIT: 0x10d8447d0 0x10d84482c
fname=/Users/geneqiao/Library/Developer/CoreSimulator/Devices/E85FCCF7-A3B0-473F-9759-4172A466E9BD/data/Containers/Bundle/Application/45D702CA-A27F-4AC6-8326-46859B66D239/WhatFuck.app/WhatFuck 
fbase=0x10d83b000 
sname=main
saddr=0x10d83cca0 
guard: 0x10d844804 e PC 
fname=/Users/geneqiao/Library/Developer/CoreSimulator/Devices/E85FCCF7-A3B0-473F-9759-4172A466E9BD/data/Containers/Bundle/Application/45D702CA-A27F-4AC6-8326-46859B66D239/WhatFuck.app/WhatFuck 
fbase=0x10d83b000 
sname=-[AppDelegate application:didFinishLaunchingWithOptions:]
saddr=0x10d83c9a0 
guard: 0x10d8447f8 b PC \340\272�a\377�
fname=/Users/geneqiao/Library/Developer/CoreSimulator/Devices/E85FCCF7-A3B0-473F-9759-4172A466E9BD/data/Containers/Bundle/Application/45D702CA-A27F-4AC6-8326-46859B66D239/WhatFuck.app/WhatFuck 
fbase=0x10d83b000 
sname=-[SceneDelegate window]
saddr=0x10d83d070 
guard: 0x10d844820 15 PC 
fname=/Users/geneqiao/Library/Developer/CoreSimulator/Devices/E85FCCF7-A3B0-473F-9759-4172A466E9BD/data/Containers/Bundle/Application/45D702CA-A27F-4AC6-8326-46859B66D239/WhatFuck.app/WhatFuck 
fbase=0x10d83b000 
sname=-[SceneDelegate setWindow:]
saddr=0x10d83d0c0 
guard: 0x10d844824 16 PC �
fname=/Users/geneqiao/Library/Developer/CoreSimulator/Devices/E85FCCF7-A3B0-473F-9759-4172A466E9BD/data/Containers/Bundle/Application/45D702CA-A27F-4AC6-8326-46859B66D239/WhatFuck.app/WhatFuck 
fbase=0x10d83b000 
sname=-[SceneDelegate window]
saddr=0x10d83d070 
guard: 0x10d844820 15 PC �
fname=/Users/geneqiao/Library/Developer/CoreSimulator/Devices/E85FCCF7-A3B0-473F-9759-4172A466E9BD/data/Containers/Bundle/Application/45D702CA-A27F-4AC6-8326-46859B66D239/WhatFuck.app/WhatFuck 
fbase=0x10d83b000 
sname=-[SceneDelegate window]
saddr=0x10d83d070 
guard: 0x10d844820 15 PC �
fname=/Users/geneqiao/Library/Developer/CoreSimulator/Devices/E85FCCF7-A3B0-473F-9759-4172A466E9BD/data/Containers/Bundle/Application/45D702CA-A27F-4AC6-8326-46859B66D239/WhatFuck.app/WhatFuck 
fbase=0x10d83b000 
sname=-[SceneDelegate scene:willConnectToSession:options:]
saddr=0x10d83cd70 
guard: 0x10d844808 f PC YNu \377�
fname=/Users/geneqiao/Library/Developer/CoreSimulator/Devices/E85FCCF7-A3B0-473F-9759-4172A466E9BD/data/Containers/Bundle/Application/45D702CA-A27F-4AC6-8326-46859B66D239/WhatFuck.app/WhatFuck 
fbase=0x10d83b000 
sname=-[SceneDelegate window]
saddr=0x10d83d070 
guard: 0x10d844820 15 PC \300�<\342\376�
fname=/Users/geneqiao/Library/Developer/CoreSimulator/Devices/E85FCCF7-A3B0-473F-9759-4172A466E9BD/data/Containers/Bundle/Application/45D702CA-A27F-4AC6-8326-46859B66D239/WhatFuck.app/WhatFuck 
fbase=0x10d83b000 
sname=-[SceneDelegate window]
saddr=0x10d83d070 
guard: 0x10d844820 15 PC �\225\204
�
fname=/Users/geneqiao/Library/Developer/CoreSimulator/Devices/E85FCCF7-A3B0-473F-9759-4172A466E9BD/data/Containers/Bundle/Application/45D702CA-A27F-4AC6-8326-46859B66D239/WhatFuck.app/WhatFuck 
fbase=0x10d83b000 
sname=-[SceneDelegate window]
saddr=0x10d83d070 
guard: 0x10d844820 15 PC 
fname=/Users/geneqiao/Library/Developer/CoreSimulator/Devices/E85FCCF7-A3B0-473F-9759-4172A466E9BD/data/Containers/Bundle/Application/45D702CA-A27F-4AC6-8326-46859B66D239/WhatFuck.app/WhatFuck 
fbase=0x10d83b000 
sname=-[SceneDelegate sceneWillEnterForeground:]
saddr=0x10d83cf90 
guard: 0x10d844818 13 PC \377\377
fname=/Users/geneqiao/Library/Developer/CoreSimulator/Devices/E85FCCF7-A3B0-473F-9759-4172A466E9BD/data/Containers/Bundle/Application/45D702CA-A27F-4AC6-8326-46859B66D239/WhatFuck.app/WhatFuck 
fbase=0x10d83b000 
sname=-[SceneDelegate sceneDidBecomeActive:]
saddr=0x10d83ceb0 
guard: 0x10d844810 11 PC \377\377

收集符号

多线程问题

由于项目各个方法肯定有可能会在不同的线程执行 , 因此 __sanitizer_cov_trace_pc_guard 这个函数也有可能受多线程影响 , 所以不能简简单单用一个数组来接收所有的符号就搞定了 .

考虑到这个方法会来特别多次 , 使用锁会影响性能 , 这里使用苹果底层的原子队列 ( 底层实际上是个栈结构 , 利用队列结构 + 原子性来保证顺序 ) 来实现

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    //遍历出队
    while (true) {
        //offsetof 就是针对某个结构体找到某个属性相对这个结构体的偏移量
        SymbolNode * node = OSAtomicDequeue(&symboList, offsetof(SymbolNode, next));
        if (node == NULL) break;
        Dl_info info;
        dladdr(node->pc, &info);
        
        printf("%s \n",info.dli_sname);
    }
}
//原子队列
static OSQueueHead symboList = OS_ATOMIC_QUEUE_INIT;
//定义符号结构体
typedef struct{
    void * pc;
    void * next;
}SymbolNode;

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
    if (!*guard) return;  // Duplicate the guard check.
    void *PC = __builtin_return_address(0);
    SymbolNode * node = malloc(sizeof(SymbolNode));
    *node = (SymbolNode){PC,NULL};
    
    //入队
    // offsetof 用在这里是为了入队添加下一个节点找到 前一个节点next指针的位置
    OSAtomicEnqueue(&symboList, node, offsetof(SymbolNode, next));
}

上述这种 clang 插桩的方式 , 会在循环中同样插入 hook 代码 .

当确定了队列入队和出队都是没问题的 , 对应的保存和读取也是没问题的 ,但会死循环 .因为dladdr()会走这个函数

通过汇编会查看到 一个带有 while 循环的方法 , 会被静态加入多次 __sanitizer_cov_trace_pc_guard 调用 , 导致死循环.

解决方案

Other C Flags 修改为如下 ,代表仅针对 func 进行 hook . 再次运行 .

-fsanitize-coverage=func,trace-pc-guard
load 方法

load 方法时 , __sanitizer_cov_trace_pc_guard 函数的参数 guard 是 0.

上述打印并没有发现 load . 因此 屏蔽掉 __sanitizer_cov_trace_pc_guard 函数中的

if (!*guard) return;

load 方法就有了 .

如果我们希望从某个函数之后/之前开始优化 , 通过一个全局静态变量 , 在特定的时机修改其值 , 在 __sanitizer_cov_trace_pc_guard 这个函数中做好对应的处理即可 .

使用注意

  • 由于用的先进后出原因 , 我们要倒叙一下
  • 需要做去重 .
  • order 文件格式要求c 函数 , block 调用前面还需要加 _ , 下划线 .
  • 写入文件即可 .

全部代码

#import "ViewController.h"
#import <dlfcn.h>
#import <libkern/OSAtomic.h>
@interface ViewController ()
@end

@implementation ViewController
+ (void)load{
    
}
- (void)viewDidLoad {
    [super viewDidLoad];
    testCFunc();
    [self testOCFunc];
}
- (void)testOCFunc{
    NSLog(@"oc函数");
}
void testCFunc(){
    LBBlock();
}
void(^LBBlock)(void) = ^(void){
    NSLog(@"block");
};

void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
                                         uint32_t *stop) {
    static uint64_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;  // Guards should start from 1.
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSMutableArray<NSString *> * symbolNames = [NSMutableArray array];
    while (true) {
        //offsetof 就是针对某个结构体找到某个属性相对这个结构体的偏移量
        SymbolNode * node = OSAtomicDequeue(&symboList, offsetof(SymbolNode, next));
        if (node == NULL) break;
        Dl_info info;
        dladdr(node->pc, &info);
        
        NSString * name = @(info.dli_sname);
        
        // 添加 _
        BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
        NSString * symbolName = isObjc ? name : [@"_" stringByAppendingString:name];
        
        //去重
        if (![symbolNames containsObject:symbolName]) {
            [symbolNames addObject:symbolName];
        }
    }

    //取反
    NSArray * symbolAry = [[symbolNames reverseObjectEnumerator] allObjects];
    NSLog(@"%@",symbolAry);
    
    //将结果写入到文件
    NSString * funcString = [symbolAry componentsJoinedByString:@"\n"];
    NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"lb.order"];
    NSData * fileContents = [funcString dataUsingEncoding:NSUTF8StringEncoding];
    BOOL result = [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
    if (result) {
        NSLog(@"%@",filePath);
    }else{
        NSLog(@"文件写入出错");
    }
    
}
//原子队列
static OSQueueHead symboList = OS_ATOMIC_QUEUE_INIT;
//定义符号结构体
typedef struct{
    void * pc;
    void * next;
}SymbolNode;

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
    //if (!*guard) return;  // Duplicate the guard check.
    
    void *PC = __builtin_return_address(0);
    
    SymbolNode * node = malloc(sizeof(SymbolNode));
    *node = (SymbolNode){PC,NULL};
    
    //入队
    // offsetof 用在这里是为了入队添加下一个节点找到 前一个节点next指针的位置
    OSAtomicEnqueue(&symboList, node, offsetof(SymbolNode, next));
}
@end

wift 工程 / 混编工程问题

通过如上方式适合纯 OC 工程获取符号方式 .

由于 swift 的编译器前端是自己的 swift 编译前端程序 , 因此配置稍有不同 .

搜索 Other Swift Flags , 添加两条配置即可 :

  • -sanitize-coverage=func
  • -sanitize=undefined

swift 类通过上述方法同样可以获取符号 .

cocoapod 工程问题

对于 cocoapod 工程引入的库 , 由于针对不同的 target . 那么我们在主程序中的 target 添加的编译设置 Write Link Map File , -fsanitize-coverage=func,trace-pc-guard 以及 order file 等设置肯定是不会生效的 . 解决方法就是针对需要的 target 去做对应的设置即可 .

对于直接手动导入到工程里的 sdk , 不管是 静态库 .a 还是 动态库 , 默认主工程的设置就可以了 , 是可以拿到符号的 .

手动导入的三方库如果没有导入并且使用的话 , 是不会加载的 . 添加了 load 方法也是如此 .

参考链接

https://mp.weixin.qq.com/s/Drmmx5JtjG3UtTFksL6Q8Q

https://juejin.cn/post/6844904130406793224

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335