iOS-底层原理29-启动优化二进制重排

《iOS底层原理文章汇总》

上一篇文章《iOS-底层原理28-启动优化》介绍了二进制重排能减少缺页中断PageFault的页数,优化应用程序的启动时间,那启动时刻调用了哪些方法呢?此篇文章将分析启动时刻调用的方法。进而将启动时刻调用的方法尽量都放在集中的页中,从而减少启动时间。

clang插桩:参考官方文档Tracing PCs

  • 1.添加标记,Build Settings -> 搜索Other C Flags,添加-fsanitize-coverage=trace-pc-guard
fsanitize-coverage=trace-pc-guard@2x.png
  • 2.编译,会报两个符号找不到的错___sanitizer_cov_trace_pc_guard_init和___sanitizer_cov_trace_pc_guard,添加这两个方法,说明配置了上面的标记位后,调用了这两个函数
    根据官方文档添加这两个函数。
sanitizer_cov_trace_pc_guard@2x.png

报错的方法找不到的情况,删掉方法__sanitizer_symbolize_pc,注释掉不用的代码,能正常运行,此时运行,打印出结果,start和end的值分别为0x104ca1100和0x104ca1118

__sanitizer_symbolize_pc@2x.png
增加两个方法@2x.png
打印结果@2x.png

开始地址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..........
stop往前读4个字节@2x.png

在ViewController增加一个方法touchBegin,此时发现结尾处的数据为07,这说明了啥?结尾处的数据存的就是方法的个数吗?继续在ViewController.m文件里面增加函数,block。无论是函数,方法,block都能获取到。


增加方法touchsBegan@2x.png
增加函数和block@2x.png

增加一个属性呢?增加了三个,setter,getter方法,还有一个cxx的析构函数。

增加属性@2x.png
  • 综上,上述哨兵函数能监听方法,函数,block,属性。

1.获取符号地址:监听方法的调用

点击屏幕,调用了touchBegan方法,点一下监听一次

39.gif

若touchBegan中调用了其他方法,则继续监听到了其他方法,__sanitizer_cov_trace_pc_guard全免捕捉到了函数,方法,block的调用

tochBegan-test-block@2x.png
40.gif

那么__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)

OtherCFlags@2x.png
__sanitizer_cov_trace_pc_guard@2x.png
41.gif

避开load方法if (!*guard) return;,验证下,注释掉此行代码,在ViewController类中重写load方法,发现会多走一次方法void __sanitizer_cov_trace_pc_guard(uint32_t *guard),从而多打印一次guard: 0x100e99388 0 PC

避开load方法@2x.png
42.gif

我们可以利用上面的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) 
PC@2x.png
  • 点击屏幕touchBegan再次进行验证,touchBegan方法的内存地址和PC内存地址是否一致,验证结果一致
PC和touchBegan内存地址@2x.png
  • 查看汇编代码,touchBegan怎么调用到函数void __sanitizer_cov_trace_pc_guard(uint32_t *guard)中去,void __sanitizer_cov_trace_pc_guard(uint32_t *guard)调用完毕,返回ret,还要返回到touchBegan方法中
touchBegan.png
touchBegan调用__sanitizer_cov_trace_pc_guard.png
__sanitizer_cov_trace_pc_guard.png

void __sanitizer_cov_trace_pc_guard(uint32_t *guard)调用完毕,返回ret,还要返回到touchBegan方法中

返回touchBegan.png

函数调用栈:调用栈里面的内存地址并不是函数的开始位置,查看汇编代码可以得知函数栈中的touchBegan的地址为0x0000000102fa82f0,汇编代码中touchBegan的地址为0x102fa82c4,两者并不相等,所以函数栈中的地址并不是touchBegan的开始地址,而是上一个函数void __sanitizer_cov_trace_pc_guard(uint32_t *guard)调用后的返回地址

函数栈中地址并不是touchBegan的开始位置.png

这是有原因的,因为lldb的bt也是用的这个函数__builtin_return_address(0),验证如下:删除Other C Flags里面的哨兵函数标记-fsanitize-coverage=trace-pc-guard

删除guard监听.png

  • 在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
test.png
touchBegan函数调用栈.png
touchBegan调用test返回.png
  • 断点在block中查看bt,发现函数调用栈中的touchBegan的地址为0x000000010270c578,和test函数中的地址不一致,因为0x000000010270c578和0x00000001001ec560根本就不是touchBegan的起始地址,而是test和block的返回地址,返回到了touchBegan中,这就lldb的原理
touchBegan调用block.png

blr

block中的地址.png

2.获取符号

综上,通过void *PC = __builtin_return_address(0);,PC为当前函数返回到上一个调用的地址,0代表我当前函数回到哪里去,1代表我上一个函数回到哪里去

address中参数为1返回上上个函数的地址@2x.png
  • 由上得知PC指向了当前函数返回到上一个函数调用的地址,怎么能拿到函数的符号呢?

引入#import <dlfcn.h>,通过dladdr(PC, &info);函数获取到函数所在的MachO文件名和地址,以及函数符号名和地址

touchBegan调用test()和block()@2x.png
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能查看
dis-s-地址@2x.png
  • 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的内存地址了,逆向班会讲
image-list@2x.png
动态库内存地址首地址@2x.png

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循环中取符号方法,一直跳不出循环,什么导致循环无法停止呢?
取函数符号死循环.png
43.gif

因为while循环也被方法void __sanitizer_cov_trace_pc_guard(uint32_t *guard)hook了,循环一次hook一次,点击屏幕,在循环中进入汇编查看,多的一次__sanitizer_cov_trace_pc_guard属于循环,bl是条件跳转,b是无条件跳转,一次循环也会被hook一次,只要是跳转(bl和b的汇编指令),就会被hook

touchBeganhook三次@2x.png

解决办法:在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文件一致
cloud.order放入工程根目录@2x.png
Build Settings order file配置@2x.png

将Build Settings中Write Link Map File选项改为Yes,运行程序,在Product同级目录Intermediates.noindex/Demo.build/Debug-iphoneos/Demo.build/Demo-LinkMap-normal-arm64.txt中查看文件中每一个方法的排列顺序

函数按cloud.order文件执行顺序@2x.png
44.gif
  • 6.若调用了swift的函数呢?应该怎么处理,在Build Settings中设置-sanitize-coverage=func -sanitize=undefined
swift配置Other Swift Flags@2x.png
45.gif
order文件中的swift函数@2x.png

推荐阅读更多精彩内容