上一篇文章《iOS-底层原理28-启动优化》介绍了二进制重排能减少缺页中断PageFault的页数,优化应用程序的启动时间,那启动时刻调用了哪些方法呢?此篇文章将分析启动时刻调用的方法。进而将启动时刻调用的方法尽量都放在集中的页中,从而减少启动时间。
clang插桩:参考官方文档Tracing PCs
- 1.添加标记,Build Settings -> 搜索Other C Flags,添加-fsanitize-coverage=trace-pc-guard
- 2.编译,会报两个符号找不到的错___sanitizer_cov_trace_pc_guard_init和___sanitizer_cov_trace_pc_guard,添加这两个方法,说明配置了上面的标记位后,调用了这两个函数
根据官方文档添加这两个函数。
报错的方法找不到的情况,删掉方法__sanitizer_symbolize_pc
,注释掉不用的代码,能正常运行,此时运行,打印出结果,start和end的值分别为0x104ca1100和0x104ca1118
开始地址start为0x104919100,读取该开始地址下的内存值,一直读取16个字节,一排有16个字节,4个字节4个字节的读取,一直读到结尾,stop指向的地址为0x104919118,指向数据最后的端,要获取数据结尾处的数据应该往前再读4个字节,因为uint32_t是无符号整形占用4个字节,06表示的十进制数为6
(lldb) x 0x104919100
0x104919100: 01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00 ................
0x104919110: 05 00 00 00 06 00 00 00 78 2f a7 04 01 00 00 00 ........x/......
(lldb) x 0x104919118-0x4
0x104919114: 06 00 00 00 78 2f a7 04 01 00 00 00 00 00 00 00 ....x/..........
0x104919124: 00 00 00 00 43 75 91 04 01 00 00 00 00 00 00 00 ....Cu..........
在ViewController增加一个方法touchBegin,此时发现结尾处的数据为07,这说明了啥?结尾处的数据存的就是方法的个数吗?继续在ViewController.m文件里面增加函数,block。无论是函数,方法,block都能获取到。
增加一个属性呢?增加了三个,setter,getter方法,还有一个cxx的析构函数。
- 综上,上述哨兵函数能监听方法,函数,block,属性。
1.获取符号地址:监听方法的调用
点击屏幕,调用了touchBegan方法,点一下监听一次
若touchBegan中调用了其他方法,则继续监听到了其他方法,__sanitizer_cov_trace_pc_guard
全免捕捉到了函数,方法,block的调用
那么__sanitizer_cov_trace_pc_guard
为什么能捕捉到所有函数的调用呢,原理是什么?
进入汇编代码查看,sp操作栈空间,bl是跳转函数,断点进入test方法,查看汇编,发现只要是在Build Settings设置了让clang开辟这个功能-fsanitize-coverage=trace-pc-guard
,clang在读取所有代码的时候生成ir(中间代码)之前,都将__sanitizer_cov_trace_pc_guard
这个函数插入到每个函数、方法、block的开头或边缘,从而能进行监听,从而每一个方法,函数,block的调用都会来到此函数void __sanitizer_cov_trace_pc_guard(uint32_t *guard)
避开load方法if (!*guard) return;
,验证下,注释掉此行代码,在ViewController类中重写load方法,发现会多走一次方法void __sanitizer_cov_trace_pc_guard(uint32_t *guard)
,从而多打印一次guard: 0x100e99388 0 PC
我们可以利用上面的void __sanitizer_cov_trace_pc_guard(uint32_t *guard)
来进行clang插桩,目标是拿到所有调用的方法名
获取方法名:引入void *PC = __builtin_return_address(0);
,打印PC指针变量的地址,PC指向的地址0x0000000104b646f8和main函数的地址0x0000000104b646f8一致
INIT: 0x104b69388 0x104b693bc
(lldb) p PC
(void *) $0 = 0x0000000104b646f8
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = step over
* frame #0: 0x0000000104b64264 TraceDemo`__sanitizer_cov_trace_pc_guard(guard=0x0000000104b693b8) at ViewController.m:38:34
frame #1: 0x0000000104b646f8 TraceDemo`main(argc=0, argv=0x0000000000000000) at main.m:12
frame #2: 0x000000018212256c libdyld.dylib`start + 4
(lldb)
- 点击屏幕touchBegan再次进行验证,touchBegan方法的内存地址和PC内存地址是否一致,验证结果一致
- 查看汇编代码,touchBegan怎么调用到函数
void __sanitizer_cov_trace_pc_guard(uint32_t *guard)
中去,void __sanitizer_cov_trace_pc_guard(uint32_t *guard)
调用完毕,返回ret,还要返回到touchBegan方法中
void __sanitizer_cov_trace_pc_guard(uint32_t *guard)
调用完毕,返回ret,还要返回到touchBegan方法中
函数调用栈:调用栈里面的内存地址并不是函数的开始位置,查看汇编代码可以得知函数栈中的touchBegan的地址为0x0000000102fa82f0,汇编代码中touchBegan的地址为0x102fa82c4,两者并不相等,所以函数栈中的地址并不是touchBegan的开始地址,而是上一个函数void __sanitizer_cov_trace_pc_guard(uint32_t *guard)
调用后的返回地址
这是有原因的,因为lldb的bt也是用的这个函数__builtin_return_address(0)
,验证如下:删除Other C Flags里面的哨兵函数标记-fsanitize-coverage=trace-pc-guard
- 在touchBegan函数里面调用两个方法test()和block(),查看函数栈和汇编代码,如果函数栈中的touchBegan的地址为函数的首地址,则在同一个函数中,函数的地址不变,则block的函数栈中touchBegin的地址也为函数的首地址,两个值应该相等,若函数栈中的touchBegan的地址不为函数的首地址,为touchBegan上一个函数(test)或block()的返回地址,则查看test()和block()的汇编,就会发现不一致,两个值并不相等,所以得出test()和block()返回后地址并不是touchBegan函数的首地址,而是test()和block()调用的返回地址
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
test();
block();
}
void test(){
}
void (^block)(void)=^(void){
};
- 断点在test函数中查看bt,发现函数调用栈中touchBegan的地址为0x00000001001ec560
- 断点在block中查看bt,发现函数调用栈中的touchBegan的地址为0x000000010270c578,和test函数中的地址不一致,因为0x000000010270c578和0x00000001001ec560根本就不是touchBegan的起始地址,而是test和block的返回地址,返回到了touchBegan中,这就lldb的原理
blr
2.获取符号
综上,通过void *PC = __builtin_return_address(0);
,PC为当前函数返回到上一个调用的地址,0代表我当前函数回到哪里去,1代表我上一个函数回到哪里去
- 由上得知PC指向了当前函数返回到上一个函数调用的地址,怎么能拿到函数的符号呢?
引入#import <dlfcn.h>
,通过dladdr(PC, &info);
函数获取到函数所在的MachO文件名和地址,以及函数符号名和地址
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("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("\nguard: %p %x PC %s\n", guard, *guard, PcDescr);
}
fname:/var/containers/Bundle/Application/D1D94998-5B0B-49FB-B5AF-87783F492AC7/TraceDemo.app/TraceDemo
fbase:0x1003c4000
sname:-[ViewController touchesBegan:withEvent:]
saddr:0x1003cc268
guard: 0x1003d13a0 5 PC X\260m\261�
fname:/var/containers/Bundle/Application/D1D94998-5B0B-49FB-B5AF-87783F492AC7/TraceDemo.app/TraceDemo
fbase:0x1003c4000
sname:test
saddr:0x1003cc00c
guard: 0x1003d1394 2 PC \224\302<
fname:/var/containers/Bundle/Application/D1D94998-5B0B-49FB-B5AF-87783F492AC7/TraceDemo.app/TraceDemo
fbase:0x1003c4000
sname:block_block_invoke
saddr:0x1003cc028
guard: 0x1003d1398 3 PC �m=
- 此时拿到的地址是函数的起始地址,0x1003cc00c,为test函数的起始地址,通过dis -s 0x1003cc00c能查看
- TraceDemo的MachO的文件名为/var/containers/Bundle/Application/D1D94998-5B0B-49FB-B5AF-87783F492AC7/TraceDemo.app/TraceDemo,文件地址为0x1003c4000,同时可以通过image list镜像拿到,拿到文件和动态库的地址,都为虚拟地址。随机值怎么获取呢?ASLR的值,TraceDemo的MachO文件的首地址为0x00000001003c4000,则随机值为0x3c4000,本身为000,加上随机值3c4,从1开始为4个G的内存地址了,逆向班会讲
3.符号拿到之后,生成相应的order文件
注意:在Build Settings ->Other C Flags,添加-fsanitize-coverage=trace-pc-guard后,在任何地方写这两个方法void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,uint32_t *stop)
和void __sanitizer_cov_trace_pc_guard(uint32_t *guard)
都能监听到,在main.m文件中实现都行,且只需要实现一次,在哪里结束写在哪里
- 1.在监听方法里面存PC,启动结束的方法里面取PC,此处有个坑点,在touchBegan方法的while循环中取符号方法,一直跳不出循环,什么导致循环无法停止呢?
因为while循环也被方法void __sanitizer_cov_trace_pc_guard(uint32_t *guard)
hook了,循环一次hook一次,点击屏幕,在循环中进入汇编查看,多的一次__sanitizer_cov_trace_pc_guard
属于循环,bl是条件跳转,b是无条件跳转,一次循环也会被hook一次,只要是跳转(bl和b的汇编指令),就会被hook
解决办法:在Other C Flags中添加参数func,-fsanitize-coverage=func,trace-pc-guard,再次点击屏幕,不会产生循环,打印的方法如下
sname:-[ViewController touchesBegan:withEvent:]
sname:-[AppDelegate window]
sname:-[ViewController viewDidLoad]
sname:-[AppDelegate window]
sname:-[AppDelegate window]
sname:-[AppDelegate application:didFinishLaunchingWithOptions:]
sname:-[AppDelegate setWindow:]
sname:-[AppDelegate window]
sname:main
- 2.load没加载,被
if (!*guard) return;
直接return了,在void __sanitizer_cov_trace_pc_guard(uint32_t *guard)方法中删除这一句
sname:-[ViewController touchesBegan:withEvent:]
sname:-[AppDelegate window]
sname:-[ViewController viewDidLoad]
sname:-[AppDelegate window]
sname:-[AppDelegate window]
sname:-[AppDelegate application:didFinishLaunchingWithOptions:]
sname:-[AppDelegate setWindow:]
sname:-[AppDelegate window]
sname:main
sname:+[ViewController load]
- 3.去重,取反,不是OC方法添加_
BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
NSString * symbolName = isObjc ? name :[@"_" stringByAppendingString:name];
[symbolNames addObject:symbolName];
NSEnumerator * enumerator = [symbolNames reverseObjectEnumerator];
//创建一个新的数组
NSMutableArray * funcs = [NSMutableArray arrayWithCapacity:symbolNames.count];
NSString *name;
//去重!
while (name = [enumerator nextObject]) {
if (![funcs containsObject:name]) {//数组中不包含name
[funcs addObject:name];
}
}
//去除touchBegan
[funcs removeObject:[NSString stringWithFormat:@"%s",__FUNCTION__]];
- 4.写入cloud.order文件到沙盒中
//数组转成字符串
NSString * funcStr = [funcs componentsJoinedByString:@"\n"];
//字符串写入文件
//文件路径
NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"cloud.order"];
//文件内容
NSData * fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
[[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
- 5.将生成的cloud.order文件拖入工程中,按照cloud.order文件进行编译,重新运行,查看运行顺序是否和配置的OrderFile文件一致
将Build Settings中Write Link Map File选项改为Yes,运行程序,在Product同级目录Intermediates.noindex/Demo.build/Debug-iphoneos/Demo.build/Demo-LinkMap-normal-arm64.txt中查看文件中每一个方法的排列顺序
- 6.若调用了swift的函数呢?应该怎么处理,在Build Settings中设置
-sanitize-coverage=func -sanitize=undefined