Build Settings
比如以下是一个单页面Demo项目的linkmap。
linkmap
linkmap主要包括三大部分:
-
Object Files 生成二进制用到的link单元的路径和文件编号
-
Sections 记录Mach-O每个Segment/p的地址范围
-
Symbols 按顺序记录每个符号的地址范围
ld
–
Xcode使用的链接器件是ld,ld有一个不常用的参数-order_file
,通过man ld
可以看到详细文档:
Alters the order in which functions and data are laid out. For each p in the output file, any symbol in that p that are specified in the order file file is moved to the start of its p and laid out in the same order as in the order file file.
可以看到,order_file中的符号会按照顺序排列在对应p的开始,完美的满足了我们的需求。
Xcode的GUI也提供了order_file选项:
order_file
如果order_file中的符号实际不存在会怎么样呢?
ld会忽略这些符号,如果提供了link选项-order_file_statistics
,会以warning的形式把这些没找到的符号打印在日志里。
获得符号
还剩下最后一个,也是最核心的一个问题,获取启动时候用到的函数符号。
我们首先排除了解析Instruments(Time Profiler/System Trace) trace文件方案,因为他们都是基于特定场景采样的,大多数符号获取不到。最后选择了静态扫描+运行时Trace结合的解决方案。
Load
Objective C的符号名是+-[Class_name(category_name) method:name:]
,其中+
表示类方法,-
表示实例方法。
刚刚提到linkmap里记录了所有的符号名,所以只要扫一遍linkmap的__TEXT,__text
,正则匹配("^\+\[.*\ load\]$"
)既可以拿到所有的load方法符号。
C++静态初始化
C++并不像Objective C方法那样,大部分方法调用编译后都是objc_msgSend
,也就没有一个入口函数去运行时hook。
但是可以用-finstrument-functions
在编译期插桩“hook”,但由于抖音的很多依赖由其他团队提供静态库,这套方案需要修改依赖的构建过程。二进制文件重排在没有业界经验可供参考,不确定收益的情况下,选择了并不完美但成本最低的静态扫描方案。
1. 扫描linkmap的__DATA,__mod_init_func
,这个p存储了包含C++静态初始化方法的文件,获得文件号[ 5]
。
1//__mod_init_func 20x100008060 0x00000008 [ 5] ltmp7 3//[ 5]对应的文件 4[ 5] …/Build/Products/Debug-iphoneos/libStaticLibrary.a(StaticLibrary.o)
2. 通过文件号,解压出.o。
1➜ lipo libStaticLibrary.a -thin arm64 -output arm64.a 2➜ ar -x arm64.a StaticLibrary.o
3. 通过.o,获得静态初始化的符号名_demo_constructor
。
1➜ objdump -r -p=__mod_init_func StaticLibrary.o 2 3StaticLibrary.o: file format Mach-O arm64 4 5RELOCATION RECORDS FOR [__mod_init_func]: 60000000000000000 ARM64_RELOC_UNSIGNED _demo_constructor
4. 通过符号名,文件号,在linkmap中找到符号在二进制中的范围:
10x100004A30 0x0000001C [ 5] _demo_constructor
5. 通过起始地址,对代码进行反汇编:
1➜ objdump -d --start-address=0x100004A30 --stop-address=0x100004A4B demo_arm64 2 3_demo_constructor: 4100004a30: fd 7b bf a9 stp x29, x30, [sp, #-16]! 5100004a34: fd 03 00 91 mov x29, sp 6100004a38: 20 0c 80 52 mov w0, #97 7100004a3c: da 06 00 94 bl #7016 8100004a40: 40 0c 80 52 mov w0, #98 9100004a44: fd 7b c1 a8 ldp x29, x30, [sp], #16 10100004a48: d7 06 00 14 b #7004
6. 通过扫描bl
指令扫描子程序调用,子程序在二进制的开始地址为:100004a3c +1b68(对应十进制的7016)。
1100004a3c: da 06 00 94 bl #7016
7. 通过开始地址,可以找到符号名和结束地址,然后重复5~7,递归的找到所有的子程序调用的函数符号。
小坑
STL里会针对string生成初始化函数,这样会导致多个.o里存在同名的符号,例如:
1__ZNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEC1IDnEEPKc
类似这样的重复符号的情况在C++里有很多,所以C/C++符号在order_file里要带着所在的.o信息:
1//order_file.txt 2libDemoLibrary.a(object.o):__GLOBAL__sub_I_demo_file.cpp
局限性
branch系列汇编指令除了bl/b,还有br/blr,即通过寄存器的间接子程序调用,静态扫描无法覆盖到这种情况。
Local符号
在做C++静态初始化扫描的时候,发现扫描出了很多类似l002的符号。经过一番调研,发现是依赖方输出静态库的时候裁剪了local符号。导致__GLOBAL__sub_I_demo_file.cpp
变成了l002。
需要静态库出包的时候保留local符号,CI脚本不要执行strip -x
,同时Xcode对应target的Strip Style修改为Debugging symbol:
Strip Style
静态库保留的local符号会在宿主App生成IPA之前裁剪掉,所以不会对最后的IPA包大小有影响。宿主App的Strip Style要选择All Symbols,宿主动态库选择Non-Global Symbols。
Objective C方法
绝大部分Objective C的方法在编译后会走objc_msgSend
,所以通过fishhook(https://github.com/facebook/fishhook) hook这一个C函数即可获得Objective C符号。由于objc_msgSend
是变长参数,所以hook代码需要用汇编来实现:
1//代码参考InspectiveC 2__attribute__((__naked__)) 3static void hook_Objc_msgSend() { 4 save() 5 __asm volatile ("mov x2, lr\n"); 6 __asm volatile ("mov x3, x4\n"); 7 call(blr, &before_objc_msgSend) 8 load() 9 call(blr, orig_objc_msgSend) 10 save() 11 call(blr, &after_objc_msgSend) 12 __asm volatile ("mov lr, x0\n"); 13 load() 14 ret() 15}
子程序调用时候要保存和恢复参数寄存器,所以save和load分别对x0~x9, q0~q9入栈/出栈。call则通过寄存器来间接调用函数:
1#define save() \ 2__asm volatile ( \ 3"stp q6, q7, [sp, #-32]!\n"\ 4... 5 6#define load() \ 7__asm volatile ( \ 8"ldp x0, x1, [sp], #16\n" \ 9... 10 11#define call(b, value) \ 12__asm volatile ("stp x8, x9, [sp, #-16]!\n"); \ 13__asm volatile ("mov x12, %0\n" :: "r"(value)); \ 14__asm volatile ("ldp x8, x9, [sp], #16\n"); \ 15__asm volatile (#b " x12\n");
在before_objc_msgSend
中用栈保存lr,在after_objc_msgSend
恢复lr。由于要生成trace文件,为了降低文件的大小,直接写入的是函数地址,且只有当前可执行文件的Mach-O(app和动态库)代码段才会写入:
iOS中,由于ALSR(https://en.wikipedia.org/wiki/Address_space_layout_randomization)的存在,在写入之前需要先减去偏移量slide:
1IMP imp = (IMP)class_getMethodImplementation(object_getClass(self), _cmd); 2unsigned long imppos = (unsigned long)imp; 3unsigned long addr = immpos - macho_slide
获取一个二进制的__text
段地址范围:
1unsigned long size = 0; 2unsigned long start = (unsigned long)getpdata(mhp, “__TEXT”, “__text”, &size); 3unsigned long end = start + size;
获取到函数地址后,反查linkmap既可找到方法的符号名。
Block
block是一种特殊的单元,block在编译后的函数体是一个C函数,在调用的时候直接通过指针调用,并不走objc_msgSend,所以需要单独hook。
通过Block的源码可以看到block的内存布局如下:
1struct Block_layout { 2 void *isa; 3 int32_t flags; // contains ref count 4 int32_t reserved; 5 void *invoke; 6 struct Block_descriptor1 *descriptor; 7}; 8struct Block_descriptor1 { 9 uintptr_t reserved; 10 uintptr_t size; 11};
其中invoke就是函数的指针,hook思路是将invoke替换为自定义实现,然后在reserved保存为原始实现。
1//参考 https://github.com/youngsoft/YSBlockHook 2if (layout->descriptor != NULL && layout->descriptor->reserved == NULL) 3{ 4 if (layout->invoke != (void *)hook_block_envoke) 5 { 6 layout->descriptor->reserved = layout->invoke; 7 layout->invoke = (void *)hook_block_envoke; 8 } 9}
由于block对应的函数签名不一样,所以这里仍然采用汇编来实现hook_block_envoke
:
1__attribute__((__naked__)) 2static void hook_block_envoke() { 3 save() 4 __asm volatile ("mov x1, lr\n"); 5 call(blr, &before_block_hook); 6 __asm volatile ("mov lr, x0\n"); 7 load() 8 //调用原始的invoke,即resvered存储的地址 9 __asm volatile ("ldr x12, [x0, #24]\n"); 10 __asm volatile ("ldr x12, [x12]\n"); 11 __asm volatile ("br x12\n"); 12}
在before_block_hook
中获得函数地址(同样要减去slide)。
1intptr_t before_block_hook(id block,intptr_t lr) 2{ 3 Block_layout * layout = (Block_layout *)block; 4 //layout->descriptor->reserved即block的函数地址 5 return lr; 6}
同样,通过函数地址反查linkmap既可找到block符号。
瓶颈
基于静态扫描+运行时trace的方案仍然存在少量瓶颈:
- initialize hook不到
尾声
最后,我再重复一次,如果你想成为一个优秀的 Android 开发人员,请集中精力,对基础和重要的事情做深度研究。
对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。 整理的这些架构技术希望对Android开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。
最后想要拿高薪实现技术提升薪水得到质的飞跃。最快捷的方式,就是有人可以带着你一起分析,这样学习起来最为高效,所以为了大家能够顺利进阶中高级、架构师,我特地为大家准备了一套高手学习的源码和框架视频等精品Android架构师教程,保证你学了以后保证薪资上升一个台阶。
当你有了学习线路,学习哪些内容,也知道以后的路怎么走了,理论看多了总要实践的。
进阶学习视频
附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题 (含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
套一二线互联网公司Android面试真题** (含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)
[外链图片转存中…(img-5xUIbKKE-1714646969969)]
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!