阅读视图
启动优化clang插桩(二)
一、前言
上一篇的方法给到的是个数,但不是符号,个数并没有什么作用,甚至给了全部的符号也没什么用,因为二进制重排仅仅需要的是启动阶段所需要的符号,这就需要下面这个函数:
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
}
添加断点,点击箭头,可以看到绿色框中的函数调用栈:
函数调用栈跟之前讲到的
app名称.LinkMap-normal-arm64.txt
文件里面的数据格式一样。
把上面断点过掉,给touchBegan
方法添加断点::
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
///
}
点击上面的绿色箭头:
这里就出现了touchesBegan
,那大概推出下面这个函数是系统每调用一个方法,都会调用这个__sanitizer_cov_trace_pc_guard
函数:
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
}
二、调试验证
验证是否为真给下面两个函数添加打印方法:
void test(void) {
NSLog(@"%s",__func__);
}
void (^block) (void) = ^{
NSLog(@"%s",__func__);
};
同时在touchesBegan
方法里面为添加打印和调用两个函数:
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"%s",__func__);
test();
block();
}
这个追踪方法也添加打印:
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
NSLog(@"%s",__func__);
}
cmd + r
启动,先清空打印,点击屏幕:
TraceDemo[20273:399962] __sanitizer_cov_trace_pc_guard
TraceDemo[20273:399962] -[ViewController touchesBegan:withEvent:]
TraceDemo[20273:399962] __sanitizer_cov_trace_pc_guard
TraceDemo[20273:399962] test
TraceDemo[20273:399962] __sanitizer_cov_trace_pc_guard
TraceDemo[20273:399962] block_block_invoke
可以看出先__sanitizer_cov_trace_pc_guard
再调用方法、函数、block
,这说明不管是方法、函数、block
它都会去回调这个函数,而且这个函数的调用是我们在调用这个函数之前,也就是这个函数拦截或者hook
了所有的方法、函数包括block
, 这就搞定了我们没有其他操作就能拦截到app启动时候调用了那些方法和函数
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
NSLog(@"%s",__func__);
}
我们在程序启动时候__sanitizer_cov_trace_pc_guard
拦截到的方法函数:
把这些写入到
.order
文件里面这样二进制重排就搞定了
而_sanitizer_cov_trace_pc_guard
,这个函数是如何做到这一点的?给_sanitizer_cov_trace_pc_guard
添加断点,在Xcode的Debug
选择Debug WorkFlow
选择显示汇编,选择main
:
可以看到在main
之前,系统插入了_sanitizer_cov_trace_pc_guard
这个符号:
在AppDelegate
页面也是插入了这个符号:
在SceneDelegate
同样如此:
也就是说,在编译器clang
添加下面这个标记后,编译器会给函数方法前面都会调用_sanitizer_cov_trace_pc_guard
这个函数:
这样,我们确实在打断点看到启动阶段的所有符号。
三、如何把符号都打印出来
先打开注释的代码:
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
NSLog(@"%s",__func__);
if (!*guard) return;
void *PC = __builtin_return_address(0);
// char PcDescr[1024];
// printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}
上面的PC
指的是上一个函数的地址,有了这个函数地址,就可以拿到这个函数的符号,这里需要用到dladdr
函数:
#import <dlfcn.h>
dladdr(const void *, Dl_info *)
,这个函数可以获得一个函数的名称以及地址,第一个参数传入PC,第二个参数定义一个变量 Dl_info info
传进去:
Dl_info info;
dladdr(PC, &info);
查看Dl_info
,它是一个结构体:
typedef struct dl_info {
const char *dli_fname; /* Pathname of shared object */
void *dli_fbase; /* Base address of shared object */
const char *dli_sname; /* Name of nearest symbol */
void *dli_saddr; /* Address of nearest symbol */
} Dl_info;
dli_sname
就是所需要符号
删除其他打印,把调试代码改为如下:
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
uint32_t *stop) {
static uint64_t N;
if (start == stop || *start) return;
printf("INIT: %p %p\n", start, stop);
for (uint32_t *x = start; x < stop; x++)
*x = ++N;
NSLog(@"%d",N);
}
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
if (!*guard) return;
void *PC = __builtin_return_address(0);
Dl_info info;
dladdr(PC, &info);
printf("dli_sname -- %s\n",info.dli_sname);
}
运行:
dli_sname -- main
dli_sname -- -[AppDelegate application:didFinishLaunchingWithOptions:]
dli_sname -- -[SceneDelegate window]
dli_sname -- -[SceneDelegate setWindow:]
dli_sname -- -[SceneDelegate window]
dli_sname -- -[SceneDelegate scene:willConnectToSession:options:]
dli_sname -- -[SceneDelegate window]
dli_sname -- -[ViewController viewDidLoad]
dli_sname -- -[SceneDelegate sceneWillEnterForeground:]
dli_sname -- -[SceneDelegate sceneDidBecomeActive:]
得到了启动时候所需要的所有符号和启动顺序,拿到这些符号之后,把它复制粘贴到.order
文件中,就可以实现之前需要的目标,就是拿到把启动所需要的符号和顺序加载在前面的page
里面,就实现了二进制重排。在放进.order
文件之前,需要把重复的符号删掉,并且对于函数或者block
,需要在前面加个“_”
,这样整个clang
插桩就已经完成。
四、获取符号方式优化
但是,上面手动的方法不够灵活,应该让计算机去做这些操作,用代码完成上面的操作。
目标:
- 去掉重复的符号
- 如果是函数就前面加上
“_”
- 生成一个
.order
文件
那第一步就是要对下面的符号进行收集,然后存储起来:
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
// NSLog(@"%s",__func__);
if (!*guard) return;
void *PC = __builtin_return_address(0);
Dl_info info;
dladdr(PC, &info);
printf("dli_sname -- %s\n",info.dli_sname);
}
这就需要有一个全局的容器来存放,并且这里会涉及到多线程的情况,因为一个app启动不大可能只有一个线程在跑,因为函数、方法和
block
在哪个线程跑,这个获取符号的回调函数也在那个线程运行,所以在符号收集的时候,需要考虑线程安全问题。
所以,这边使用线程安全的原子队列,导入头文件:
#import <libkern/OSAtomic.h>
定义一个全局容器结构体:
typedef struct {
void * pc;
void *next;
} SYNode;
然后在回调函数里面进行操作:
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
if (!*guard) return;
void *PC = __builtin_return_address(0);
//开辟空间
SYNode *node = malloc(sizeof(SYNode));
//赋值
*node = (SYNode){PC,NULL};
//结构体存入原子队列
OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, next ));
}
offsetof
里面的next
是下一个存储位置的偏移量,这样我们就把符号都放进了symbolList
里面,在合适的位置把它取出来就行。
启动优化clang插桩(一)
启动优化clang插桩(一)
一、了解Clang
首先到Clang
地址:Clang Documentation
PCs
指的是CPU
的寄存器,用来存储将要执行的下一条指令的地址,Tracing PCs
就是跟踪CPU
将要执行的代码。
二、如何使用
网页下拉有个Example
使用之前要在工程添加标记:
编译器就会在每一行代码的边缘插入这一段函数:
__sanitizer_cov_trace_pc_guard(&guard_variable)
打开实例demo
,在Build Settings
搜索 Other c Flag
填入 -fsanitize-coverage=trace-pc-guard
项目会报未定义符号的错:
这就需要去定义这两个符号,先把这两个函数复制过来:
先把代码复制进ViewController
extern "C" 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.
}
// This callback is inserted by the compiler on every edge in the
// control flow (some optimizations apply).
// Typically, the compiler will emit the code like this:
// if(*guard)
// __sanitizer_cov_trace_pc_guard(guard);
// But for large functions it will emit a simple call:
// __sanitizer_cov_trace_pc_guard(guard);
extern "C" void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
if (!*guard) return; // Duplicate the guard check.
// If you set *guard to 0 this code will not be called again for this edge.
// Now you can get the PC and do whatever you want:
// store it somewhere or symbolize it and print right away.
// The values of `*guard` are as you set them in
// __sanitizer_cov_trace_pc_guard_init and so you can make them consecutive
// and use them to dereference an array or a bit vector.
void *PC = __builtin_return_address(0);
char PcDescr[1024];
// This function is a part of the sanitizer run-time.
// To use it, link with AddressSanitizer or other sanitizer.
__sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr));
printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}
把头文件也粘贴进来:
#include <stdint.h>
#include <stdio.h>
#include <sanitizer/coverage_interface.h>
两个方法里面都有
extern “C”
,extern “C”
的主要作用是为了能够正确实现C++
去调用其他C语言的代码,加上extern “C”
就会指示作用域内的代码按照C语言区编译,而不是C++
,这个extern “C”
在OC项目里没什么用,直接删除
此时还会包一个错误:
这个__sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr));
函数没有什么作用,直接删除即可。
三、代码调试
cmd + r
运行,此时终端会打印一些信息:
删除两个函数里面的注释,先注释第二个的内容,然后运行
INIT: 0x1025c5478 0x1025c54f0
这是运行打印得到的地址,就是函数(uint32_t *start, uint32_t *stop)
的start
和stop
两个指针的地址
stop
存储的就是我们工程里面符号的个数
for (uint32_t *x = start; x < stop; x++)
*x = ++N;
看一下这个
for
循环,start
会先复制给*x
,x++
就是内存平移,按照uint32_t
的大小去平移,而uint32_t
的定义是typedef unsigned int uint32_t;
是无符号整型,占4
个字节,所以每次按4
个字节平移。
start
和stop
里面存的是什么,打断点调试:
先看start
:
INIT: 0x1042a5278 0x1042a52e0
(lldb) x 0x1042a5278
0x1042a5278: 01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00 ................
0x1042a5288: 05 00 00 00 06 00 00 00 07 00 00 00 08 00 00 00 ................
(lldb)
由于uint32_t
按4
个字节来存储发现start
就是 0 1 2 3 4…
,再看stop
,由于stop
的已经是结束位置,读取的数据是在start
和stop
之间的数据,所以需要向前平移4
个字节得到其真实数据。
(lldb) x (0x1042a52e0-4)
0x1042a52dc: 1a 00 00 00 00 00 00 00 00 00 00 00 fe f1 29 04 ..............).
0x1042a52ec: 01 00 00 00 00 00 00 00 00 00 00 00 90 40 2a 04 .............@*.
(lldb)
可以得到1a
就是26
,也可以循环外面打印结果:
可以得到:
TraceDemo[16814:301325] 26
也是26
个符号。
四、测试验证方法
可以验证一下,添加一个函数:
void test(void) {
NSLog(@"%s",__func__);
}
符号变成27
:
TraceDemo[16911:304537] 27
再添加一个block
:
void (^block) (void) = ^{
NSLog(@"%s",__func__);
};
符号变成28
:
TraceDemo[16933:305465] 28
添加一个数据类型属性:
@property (nonatomic ,assign) int age;
由于系统自动生成getter、setter方法,符号变成30
TraceDemo[16975:306816] 30
添加一个对象属性:
@property (nonatomic ,copy) NSString *str;
符号变成33
:
TraceDemo[17041:308780] 33
对象属性由于ARC,系统自动除了生成getter、setter方法外还生成了cxx_destruct()析构函数
添加一个方法:
- (void)test{
}
符号变成34
:
TraceDemo[17114:311256] 34
在其他类AppDelegate
类中添加一个属性:
@interface AppDelegate : UIResponder <UIApplicationDelegate>
@property (nonatomic, strong) NSString *name;
@end
符号变成37
:
TraceDemo[17266:316294] 37
符号变成37
,
结论
这就说明了通过这个方法整个项目里的符号,它都能捕获到。