最近出现了几篇关于二进制重排启动优化的文章。全部方案中都须要事先统计全部的函数调用状况,并根据函数调用的频次来进行代码的重排。python
这些函数调用中,OC对象的方法调用最多。统计OC对象的方法调用能够在运行时经过第三方库好比fishhook来Hook全部objc_msgSend调用来实现,也能够在编译后连接前经过静态插桩的方式来实现Hook拦截。ruby
对于静态插桩的实现通常有以下两个方案:bash
借助于LLVM语法树分析来实现代码插桩。函数
将源码编译为静态库,并经过修改静态库中.o目标文件的代码段来实现代码插桩。工具
上述的两个方法实现起来比较复杂,要么就要了解LLVM,要么就要熟悉目标文件中间字节码以及符号表相关的底层知识。post
本文所介绍的是第三种静态Hook方案,也是依赖于静态库这个前提来实现对objc_msgSend函数进行Hook,从而实如今编译前连接后的OC对象方法调用插桩。优化
这个方案实现的原理很简单。由于静态库其实只是一个编译阶段的中间产物,静态库目标文件中的全部引用的外部符号会保存到一张字符串表中,全部函数调用都只是记录了函数名称在字符串表的索引位置,在连接时会才会根据符号名称来替换为真实的函数调用指令。所以咱们能够将全部静态库字符串表中的objc_msgSend统一替换为另一个长度相同的字符串:hook_msgSend(名字任意只要长度一致并惟一)便可。而后在主工程源代码中实现一个名字为hook_msgSend的函数便可。这个函数必需要和objc_msgSend的函数签名保持一致,这样在连接时全部静态库中的objc_msgSend调用都会统一转化为hook_msgSend调用。ui
下面的是具体的实现步骤:spa
1. 在主工程中编写hook_msgSend的实现。code
hook_msgSend的函数签名要和objc_msgSend保持一致,而且要在主工程代码中实现,并且必需要用汇编代码实现。具体实现的逻辑和目前不少文章中介绍的对objc_msgSend函数的Hook实现保持一致便可。
不少对objc_msgSend进行Hook的实现实际上是不完整的,所以若是想彻底掌握函数调用ABI规则的话请参考:《深刻iOS系统底层之函数调用》
2. 将全部其余代码都统一编译为一个或多个静态库。
将源代码按功能编译为一个或多个静态库,而且主工程连接这些静态库。这种程序代码的组织方式已经很成熟了,最经常使用的方法是咱们能够借助代码依赖集成工具cocoapods来实现,这里就再也不赘述了。
3. 在主工程的Build Phases 中添加Run Script脚本。
咱们须要保证这个脚本必定要运行在连接全部静态库以前执行。所以能够放到Compile Sources 下面。
4. 实现静态库符号替换的Run Script脚本。
这是最为关键的一步,咱们能够实现一个符号替换的程序,而后在Run Script脚本中 执行这个符号替换程序。符号替换程序的输入参数就是主工程中所连接的全部静态库的路径。至于这个符号替换程序如何编写则没有限制,你能够用ruby编写也能够用python也能够用C语言编写。 不管用何种方法实现,你都须要首先了解一下静态库.a的文件结构。你能够从:《深刻iOS系统底层之静态库》一文中掌握到一个静态库文件的组成结构。了解了静态库文件的组成结构后,你的符号替换程序要作的事情就能够按以下步骤实现:
一)、 打开静态库.a文件。
二)、找到.a文件中的字符串表部分。字符串表的描述以下:
struct stringtab
{
int size; //字符串表的尺寸
char strings[0]; //字符串表的内容,每一个字符串以\0分隔。
};
复制代码
字符串表中的strings的内容就是一个个以\0分隔的字符串,这些字符串的内容其实就是这个目标文件所引用的全部外部和内部的符号名称。
三)、将字符串表中的objc_msgSend字符串替换为hook_msgSend字符串。
四)、保存并关闭静态库.a文件。
5. 编译、连接并运行你的主工程程序。
采用本文中所介绍的静态Hook方法的好处是咱们没必要Hook全部的OC方法调用,而是能够有选择的进行特定对象和类的方法调用拦截。所以这种技术不只能够应用代码重排统计上,还能够应用在其余的监控和统计应用中。由于这种机制能够避免程序在运行时进行objc_msgSend替换而产生的函数调用风暴问题。另外的一个点就是这个方法不局限于对objc_msgSend进行Hook,还能够对任意的其余函数进行Hook处理。所以这种技术也能够应用在其余方面。