iOS启动优化(二)

it2025-01-20  16

上一篇讲到了二进制重排的难点与核心点是如何找到启动过程中的所有符号表以及符号表的顺序,那么这一篇我们就来研究如何找到它!看过抖音二进制重拍文章的童鞋都知道,我们可以通过hook OC的系统方法 objc_msgSend,在objc_msgSend方法中,获取第二个参数SEL就可以拿到调用的方法!但是,这里会有坑点吧:难以hook所有的方法!今天我们就来看看Clang插桩的方法来hook所有启动时的符号表!Clang文档
1.首先,看文档我们知道,新建工程,在Build Setting 搜 other c flags,添加-fsanitize-coverage=trace-pc-guard:如图:

2.设置之后运行,如图会报两个错误:没有定义的符号

3.步骤1中设置参数之后,会调用两个方法
//编译器将这个回调函数作为模块构造函数插入到每个DSO中。 //“start”和“stop”对应于整个二进制文件(可执行文件或DSO文件)的开头和结尾。 //每个DSO至少调用一次回调,并且可以使用相同的参数多次调用。 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. } //这个回调函数由编译器插入到控制流的每一条边(应用了一些优化)。 //通常,编译器会发出这样的代码: extern "C" void __sanitizer_cov_trace_pc_guard(uint32_t *guard) { if (!*guard) return; 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); } //我们不需要extern "C",直接删掉即可!此时就可以运行了! //这样设置之后,编译器就会在所有的方法、函数、block内部起始部分插入调用该方法的一句代码,所有的方法、函数、block的调用,都会先调用__sanitizer_cov_trace_pc_guard这个方法,在执行方法、函数、block的内部代码!!!
4.运行成功了,但是怎么用呢?

举个例子,在我们刚创建的项目的ViewController中添加上面两个方法,并且添加方法、函数和block,然后运行查看调用情况:

void (^blockTest)(void) = ^(void) { }; void test() { blockTest(); } - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { test(); } //添加这样一个连续调用,点击屏幕,打印结果为: /* guard: 0x10dce1644 4 PC :u.S\377 guard: 0x10dce1640 3 PC \227\312\315 guard: 0x10dce163c 2 PC %N\354Q\377 */ //每次点击都会打印三次! //这里我们大致知道了这个方法确实hook了所有的方法、函数、block的调用!!!

知道这些貌似还是没什么用!!!!在__sanitizer_cov_trace_pc_guard中,有一句代码:void *PC = __builtin_return_address(0);通过打印PC和查看汇编,发现:

这个地址是执行完__sanitizer_cov_trace_pc_guard方法return跳转回到的地址!这里__builtin_return_address返回的就是这个地址!现在能拿到我们__sanitizer_cov_trace_pc_guard该方法所hook的方法的内部某一个地方,接下来通过改地址回去到我们的方法符号!!!

在dlfcn.h中,使用dladdr()可获取我们需要的符号表!

/*dladdr() 可确定指定的address 是否位于构成进程的进址空间的其中一个加载模块(可执行库或共享库)内。 如果某个地址位于在其上面映射加载模块的基址和该加载模块映射的最高虚拟地址之间(包括两端),则认为该地址在加载模块的范围内。 如果某个加载模块符合这个条件,则会搜索其动态符号表,以查找与指定的address 最接近的符号。最接近的符号是指其值等于,或最为接近但小于指定的address 的符号。 typedef struct dl_info { const char *dli_fname; void *dli_fbase; const char *dli_sname; void *dli_saddr; } Dl_info; dli_fname:一个指针,指向包含address的加载模块的文件名。每次调用dladdr() 后,该内存位置的内容都可能发生更改。 dli_fbase:文件地址 dli_sname: 一个指针,指向与指定的address最接近的符号的名称。该符号要么带有相同的地址,要么是带有低位地址的最接近符号。 dli_saddr:最接近符号的实际地址。*/ //方法经过改造后: void __sanitizer_cov_trace_pc_guard(uint32_t *guard) { if (!*guard) return; void *PC = __builtin_return_address(0); Dl_info dl_info; dladdr(PC, &dl_info); //这里只打印出dli_sname的值 printf("dli_sname:%s\n",dl_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 window] dli_sname:-[SceneDelegate scene:willConnectToSession:options:] dli_sname:-[SceneDelegate window] dli_sname:-[SceneDelegate window] dli_sname:-[SceneDelegate window] dli_sname:-[ViewController viewDidLoad] dli_sname:-[SceneDelegate sceneWillEnterForeground:] dli_sname:-[SceneDelegate sceneDidBecomeActive:] dli_sname:-[SceneDelegate window] dli_sname:-[SceneDelegate window] */

到这里终于拿到了调用过程中的符号表顺序!!!下面的事就是把这个顺序写到.order的文件里!

5.取出保存的符号

__sanitizer_cov_trace_pc_guard该方法调用队列肯定会出现在异步线程执行的情况,这里直接使用原子队列来操作!

//创建原子队列(先进后出)线程安全 static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT; typedef struct { void *pc; void *next; }SymbolNode; void __sanitizer_cov_trace_pc_guard(uint32_t *guard) { if (!*guard) return; void *PC = __builtin_return_address(0); SymbolNode *syNode = malloc(sizeof(SymbolNode)); *syNode = (SymbolNode){PC,NULL}; //存数据 OSAtomicEnqueue(&symbolList, syNode, offsetof(SymbolNode, next)); }

数据存储成功后,我们只要在合适的地方循环遍历,拿出*pc,获取到符号,写入order文件即可!

这里我直接在首页controller的viewWillAppear去做这个处理

- (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; while (YES) { SymbolNode *node = OSAtomicDequeue(&symbolList, offsetof(SymbolNode, next)); if (node == NULL) { break; } Dl_info dl_info; dladdr(node->pc, &dl_info); printf("%s \n",dl_info.dli_sname); } } /* 如果你的代码是这样直接取的,那么这里会有一个坑点,这里会进入一个死循环!!!! break不会走,因为前面设置的compile 中other c flags 的字段-fsanitize-coverage=trace-pc-guard需要修改, 如果使用此字段,while循环已被clang插入了__sanitizer_cov_trace_pc_guard代码的执行, 所以每一次减少一个node,while再次循环触发__sanitizer_cov_trace_pc_guard, 就又添加了一个node,导致node一直取不完,进入死循环! 解决办法:把我们设置的other c flags 设置为:-fsanitize-coverage=func,trace-pc-guard 设置之后,打印结果为: -[ViewController viewWillAppear:] -[ViewController viewDidLoad] -[SceneDelegate window] -[SceneDelegate window] -[SceneDelegate window] -[SceneDelegate scene:willConnectToSession:options:] -[SceneDelegate window] -[SceneDelegate window] -[SceneDelegate setWindow:] -[SceneDelegate window] -[AppDelegate application:didFinishLaunchingWithOptions:] main */
6.坑点

1.在ViewController中我们添加load方法;

2.现在我们在viewDidLoad方法中调用函数test(),test函数中调用了blockTest(),此时我们在次打印取出的符号表:

-[ViewController viewWillAppear:] blockTest_block_invoke test -[ViewController viewDidLoad] -[SceneDelegate window] -[SceneDelegate window] -[SceneDelegate window] -[SceneDelegate scene:willConnectToSession:options:] -[SceneDelegate window] -[SceneDelegate window] -[SceneDelegate setWindow:] -[SceneDelegate window] -[AppDelegate application:didFinishLaunchingWithOptions:] main //函数和block符号也有了,但是没有load的符号!!!! //这里是因为,在__sanitizer_cov_trace_pc_guard中有一句代码:if (!*guard) return; //正好,load的guard=0,所以没有被添加进去!这里我们需要注释掉此行代码!! //再次打印结果: -[ViewController viewWillAppear:] blockTest_block_invoke test -[ViewController viewDidLoad] -[SceneDelegate window] -[SceneDelegate window] -[SceneDelegate window] -[SceneDelegate scene:willConnectToSession:options:] -[SceneDelegate window] -[SceneDelegate window] -[SceneDelegate setWindow:] -[SceneDelegate window] -[AppDelegate application:didFinishLaunchingWithOptions:] main +[ViewController load]
7.order文件写入

将整个取出过程以及保存过程写在viewWillAppear里面

- (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; NSMutableArray <NSString *>*symbolNames = [NSMutableArray array]; while (YES) { SymbolNode *node = OSAtomicDequeue(&symbolList, offsetof(SymbolNode, next)); if (node == NULL) { break; } Dl_info dl_info; dladdr(node->pc, &dl_info); NSString *name = @(dl_info.dli_sname); //判断oc的类方法和对象方法 BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["]; //加符号_ NSString *symbolName = isObjc ? name : [@"_" stringByAppendingString:name]; [symbolNames addObject:symbolName]; } //取反 NSEnumerator *eatm = [symbolNames reverseObjectEnumerator]; //去重 NSMutableArray <NSString *>*symbolTemp = [NSMutableArray arrayWithCapacity:symbolNames.count]; NSString *symbolName; while (symbolName = [eatm nextObject]) { if (![symbolTemp containsObject:symbolName]) { [symbolTemp addObject:symbolName]; } } //将symbolTemp数组下入order NSString *symbolStr = [symbolTemp componentsJoinedByString:@"\n"]; NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"ClangTestOrder.order"]; NSData *fileData = [symbolStr dataUsingEncoding:NSUTF8StringEncoding]; [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileData attributes:nil]; NSLog(@"%@",filePath); }

获取到的order文件

结合上一篇的,我们重拍就完成了,先去查看linkMap:

将我们重排后的order文件放到根目录,并且在Build Setting -> Linking -> Order File 添加./ClangTestOrder.order,并且将我们做过的重排代码及Other c flags设置清除,再次运行,找到linkMap的文件: 到此我们重排完成!!! 谢谢!!!

最新回复(0)