静态代码插桩
代码插桩是指根据一定的策略在代码中插入桩点来统计代码覆盖的技术手段.一般可以分为三个粒度:
- 函数(function): 按照函数为单位进行插桩;
- 基本块(basic block): 按照代码执行单元进行分组的执行单元,单元内部的代码执行次数一定是相同的;
- 边界(Edge): 按照代码执行路径进行插桩。
针对iOS来说,clang支持以上粒度的插桩方式。这里先介绍一些函数粒度的插桩实现.
函数覆盖
Clang 是一个高度模块化开发的轻量级编译器。可以通过设置Clang的编译参数实现静态插桩.
- 在Xcode->Build Settings中搜索"Other C Flags",然后在其中添加
// 基本块覆盖可以使用参数: -fsanitize-coverage=bb,trace-pc-guard
// 边缘覆盖可以使用参数: -fsanitize-coverage=edge,trace-pc-guard
-fsanitize-coverage=func,trace-pc-guard
同时在任意实现文件中添加一下两个函数:
// 哨兵初始化函数,其中[*start,*end)表示了哨兵的标志,这里可以理解为每个哨兵guard是一个指针,保存了一个uint32_t的整形数据来作为自己的标记
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.
for (uint32_t *x = start; x < stop; x++) {
*x = ++N; // Guards should start from 1.
}
}
// 当每个函数开始调用时会被插入该回调,所以在方法调用开始就会执行该回调。函数中guard就是__sanitizer_cov_trace_pc_guard_init中[start, end)区间中一个
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
printf("guard = %p, *guard=%d", guard, *guard);
// 在这里可以尝试获取到执行函数的信息
}
然后运行项目会发现,回调正常执行.那么问题来了,如何在回调函数中获取到当前执行函数的信息呢?
获取当前执行函数的信息
在汇编语言中,如果函数包含了子函数,则方法执行过程中会在进行bl跳转指令之前将下一条指令的地址保存在特定的寄存器中(x30寄存器),而clang中封装了这样的获取方法:
// 返回当前函数或其调用者的返回地址,LEVEL表示调用层级:
// 0: 表示当前函数的返回地址;
// 1:表示当前函数调用者的返回地址,依次类推
void *__builtin_return_address(int LEVEL);
拿到改地址之后如何获取该方法的信息呢?系统同样提供了开放的方法。在 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 *);
这样就可以通过函数内部的地址获取到函数的符号信息:
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
// 获取寄存器中下一条指令的地址
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);
}
再次启动工程就可以发现,已经获取到了项目中执行方法的符号信息.
fname=/private/var/containers/Bundle/Application/9B098A1A-27A2-48C4-8402-B528A872C362/Demo001.app/Demo001
fbase=0x100820000
sname=main
saddr=0x1008257d4
fname=/private/var/containers/Bundle/Application/9B098A1A-27A2-48C4-8402-B528A872C362/Demo001.app/Demo001
fbase=0x100820000
sname=-[AppDelegate application:didFinishLaunchingWithOptions:]
saddr=0x100825520
fname=/private/var/containers/Bundle/Application/9B098A1A-27A2-48C4-8402-B528A872C362/Demo001.app/Demo001
fbase=0x100820000
sname=-[SceneDelegate window]
saddr=0x100825d08
fname=/private/var/containers/Bundle/Application/9B098A1A-27A2-48C4-8402-B528A872C362/Demo001.app/Demo001
fbase=0x100820000
sname=-[SceneDelegate setWindow:]
saddr=0x100825d60
fname=/private/var/containers/Bundle/Application/9B098A1A-27A2-48C4-8402-B528A872C362/Demo001.app/Demo001
fbase=0x100820000
sname=-[SceneDelegate window]
saddr=0x100825d08
fname=/private/var/containers/Bundle/Application/9B098A1A-27A2-48C4-8402-B528A872C362/Demo001.app/Demo001
fbase=0x100820000
sname=-[SceneDelegate window]
saddr=0x100825d08
fname=/private/var/containers/Bundle/Application/9B098A1A-27A2-48C4-8402-B528A872C362/Demo001.app/Demo001
fbase=0x100820000
sname=-[SceneDelegate scene:willConnectToSession:options:]
saddr=0x1008259f4
fname=/private/var/containers/Bundle/Application/9B098A1A-27A2-48C4-8402-B528A872C362/Demo001.app/Demo001
fbase=0x100820000
sname=-[SceneDelegate window]
saddr=0x100825d08
fname=/private/var/containers/Bundle/Application/9B098A1A-27A2-48C4-8402-B528A872C362/Demo001.app/Demo001
fbase=0x100820000
sname=-[SceneDelegate window]
saddr=0x100825d08
fname=/private/var/containers/Bundle/Application/9B098A1A-27A2-48C4-8402-B528A872C362/Demo001.app/Demo001
fbase=0x100820000
sname=-[SceneDelegate window]
saddr=0x100825d08
fname=/private/var/containers/Bundle/Application/9B098A1A-27A2-48C4-8402-B528A872C362/Demo001.app/Demo001
fbase=0x100820000
sname=-[ViewController setImageView:]
saddr=0x10082541c
fname=/private/var/containers/Bundle/Application/9B098A1A-27A2-48C4-8402-B528A872C362/Demo001.app/Demo001
fbase=0x100820000
sname=-[ViewController viewDidLoad]
saddr=0x100824538
fname=/private/var/containers/Bundle/Application/9B098A1A-27A2-48C4-8402-B528A872C362/Demo001.app/Demo001
fbase=0x100820000
sname=-[SceneDelegate sceneWillEnterForeground:]
saddr=0x100825c20
fname=/private/var/containers/Bundle/Application/9B098A1A-27A2-48C4-8402-B528A872C362/Demo001.app/Demo001
fbase=0x100820000
sname=-[SceneDelegate sceneDidBecomeActive:]
saddr=0x100825b38
保存符号
既然获得了符号,就需要对符号进行保存,以留作后续使用.这里尝试保存在第一个界面正式出现之前的所有符号。
为了方便操作将上述两个方法从main中移动到第一个控制器中.
- 由于回调函数可能出在多个线程中,为了线程安全,同时减少频繁加锁开锁的系统开销使用原子队列来进行数据存储.
需要注意的是,原子队列只能添加结构体节点,不能直接添加oc对象节点。
#import <libkern/OSAtomic.h>
// 向队列中添加节点
void OSAtomicEnqueue( OSQueueHead *__list, void *__new, size_t __offset);
// 可以使用循环从院子队列中取出元素
void* OSAtomicDequeue( OSQueueHead *__list, size_t __offset);
- 初始化原子队列,并定义结构体节点:
static OSQueueHead queueHead = OS_ATOMIC_QUEUE_INIT;
// 定义节点
struct SYNode {
char *symbol; // 记录符号
struct SYNode *next; // 记录符号的写一个地址
};
- 向节点中写入数据:
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 *symbol = malloc(sizeof(char) * strlen(info.dli_sname));
strcpy(symbol, info.dli_sname);
struct SYNode *node = malloc(sizeof(struct SYNode));
*node = (struct SYNode){symbol, NULL};
// 向节点中写入数据
OSAtomicEnqueue(&queueHead, node, offsetof(struct SYNode, next));
}
- 在控制器的viewDidAppear方法中,获取院子队列中的符号信息,并进行本地保存:
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
NSMutableArray<NSString *> *allSymbols = [NSMutableArray array];
struct SYNode *node = OSAtomicDequeue(&queueHead, offsetof(struct SYNode, next));
while (node) {
NSLog(@"--->%s", node->symbol);
[allSymbols addObject:[NSString stringWithUTF8String:node->symbol]];
free(node);
node = OSAtomicDequeue(&queueHead, offsetof(struct SYNode, next));
}
NSString *cacheRootPath = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, true).firstObject;
NSString *path = [cacheRootPath stringByAppendingPathComponent:@"symbols.txt"];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
BOOL success = [allSymbols writeToFile:path atomically:true];
NSLog(@"写入%@", success ? @"成功" : @"失败");
});
}
- 使用Xcode连接真机,启动应用直至第一个控制器界面家在完成.使用快捷键cmd+shift+2进入Devices and Simulators界面,选择对应应用并点击Download Containers选择保存路径下载文件。
- 在下载的.xcappd中右键显示包内容,在AppData->Library->Caches路径下即可保存的.txt文件.
由于队列的特性,这里的符号与实际调用顺序是相反的。
这样就可查看到在应用首个控制器显示之前系统调用的所有符号,从而为应用启动优化奠定基础.
静态插桩作用
通过静态插桩,可以查看项目中的代码执行情况,进而为项目优化提供依据.
- 重排二进制文件:可以根据启动时调用的方法,存储在.order文件中,认为干预二进制文件的生成,优化启动速度;
- 删除无用代码:可以根据项目中方法的执行情况,查看方法的覆盖率,将没有使用到的方法进行删除,减少二进制文件的大小;
- 跟踪方法调用顺序:可以将调用的符号进行保存来查看应用中方法的调用顺序,跟踪异常。