一、前言
首先先跟大家说个抱歉,在上篇《如何通过Mach-O实现static函数的动态调用》中由于调研不够严谨,没考虑到Xcode在打包时会将符号表strip的情况(在这里要感谢@walreal的及时指正)。由于正式版APP中不存在符号表,因此导致整个方案不可行。为了解决这个问题,特地左思右想换了个实现方案。
二、为什么会strip符号表
为什么会剔除掉符号表?如果剔除了符号表,那么程序运行时如何才能找到地址呢?查看walreal的留言及自身验证得知,实际上Mach-O保存了动态链接的符号表,静态链接的符号表已经被剥离到.dSYM文件中。从这点就可以看出,静态链接的函数实际上是不需要符号的,因为一旦编译完成后,地址即已确定,不能确定地址的才会保留符号表。因此可以说,符号表只是程序员的调试工具,并不完全具备函数调用的映射意义。保留符号表不但没有意义而且会造成安全隐患和应用包的增大。
三、.dSYM
大家都知道,在接入bugly等崩溃统计工具时,会要求开发者上传dSYM文件。有了符号表,无论是通过xcrun atos 命令还是dwarfdump 命令,我们都能根据地址获取到相应的符号。dSYM实际上是一个压缩包,其内部包含了一个Mach-O文件,这是一个胖二进制文件,为了方便查看,首先通过lipo命令将其按架构拆分
lipo xxx.app.dSYM/Contents/Resources/DWARF/xxx -thin arm64 -output xxx_arm64
拆分完成后,我们可以清楚地看到这个文件内容。
在文件的符号表中,我们可以看到静态函数确实存储在符号表中
除了符号表外,还有一些debug相关的secion也存储在dSYM中。也就是说之前的方案是从当前应用获取符号表,现在可能需要从外界获取到符号表后,将获取到的函数地址以字符串的形式下发到应用中,而不是下发文件名和符号名。
三、实现
首先将拆分后的文件直接加载到内存中,为了读取文件中的符号表,我创建了一个Mac应用,整个应用主要是读取文件并获取符号信息。在有了前一篇文章的基础后,在内存中加载文件的难度并不大,很容易就可以获取到该文件的符号表和字符串表
加载文件并获取文件header
//加载文件并获取文件header
NSString *path = @"/Users/a58/Desktop/xxx_armv7";
NSData *data = [NSData dataWithContentsOfFile:path];
void *pt = (void*)[data bytes];
mach_header_t *header = (mach_header_t*)pt;
获取符号表LoadCommand
//获取符号表LoadCommand
pt = pt+sizeof(mach_header_t);
for (uint i = 0; i < header->ncmds; i++, pt += cur_seg_cmd->cmdsize) {
cur_seg_cmd = (struct segment_command *)pt;
if (cur_seg_cmd->cmd == LC_SYMTAB) {
symtab_cmd = (struct symtab_command*)cur_seg_cmd;
}
}
获取符号表和字符串表
//获取符号表和字符串表
struct nlist *symtab = (struct nlist *)((uintptr_t)header+symtab_cmd->symoff);
char *strtab = (char *)((uintptr_t)header + symtab_cmd->stroff);
之后的流程与前文一致。但是,经过查看符号表发现,dSYM符号表中并没有存储任何文件相关的信息,在前文中,我们通过匹配文件来定位这个符号到底是不是我们所要的文件中的符号(因为static 在不同的文件中符号相同,需要区别同名符号是否是我们所指文件中的符号),这就意味着我们获取到的地址可能并不是我们所需要的地址。比如,在58中,某符号在项目中共存在48处,因此仅靠符号名无法准确获取到地址。
四、debug_info
使用过apple 的crash 分析工具的同学可能会清除,在获取崩溃后apple能帮你自动定位到crash发生的文件类甚至精确到某行代码。这是因为在dSYM文件中,存在着相关调试信息,
虽然在MachOView工具中我们看不出什么,但是通过
dwarfdump --debug-info /Users/a58/Desktop/OS2JS_Demo.app.dSYM/Contents/Resources/DWARF/OS2JS_Demo
命令查看其符号化后的形式,
//随意截取的一个函数
0x00001ac5: TAG_subprogram [47] *
AT_low_pc( 0x00000001000063f0 )//函数起始地址
AT_high_pc( 0x00000014 )
AT_frame_base( reg31 )
AT_object_pointer( {0x00001ade} )
AT_name( "-[AppDelegate .cxx_destruct]" )
AT_decl_file( "/Users/a58/Desktop/OS2JS_Demo/OS2JS_Demo/AppDelegate.m" )//文件信息
AT_decl_line( 27 )
AT_prototyped( true )
AT_artificial( true )
AT_APPLE_optimized( true )
因此可以断定,我们所需要的文件信息其实都在debug_info section中。起初,我的第一想法是获取到符号地址后,反查地址所对应的debug_info文件信息,从而判断该符号地址是否是我需要的地址。但是很遗憾,直接解析debug_info的二进制数据对我来说是十分复杂的,这需要对DWARF格式数据有相当的了解才行。因此偷懒借助dwarfdump来实现地址获取,通过
dwarfdump /Users/a58/Desktop/MachOtest.app.dSYM/Contents/Resources/DWARF/MachOtest --name=xxx
可以获取到指定变量/函数名的debug信息,信息中包括文件信息、变量偏移地址等。
文件信息与符号表的对照关系如下:
这个方式比我们遍历符号表省力太多,我们不需要关注符号规则,只需要一个命令即可获取到地址。获取到函数地址后,下发到应用中,在应用内计算出其真实运行时的地址即可。
为了验证可行性,我写了个demo,在demo中,我定义了一个static 变量,一个static函数
static int s_idata = 100;
static int printData(){
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"" message:[NSString stringWithFormat:@"%d",s_idata] delegate:[UIApplication sharedApplication].delegate cancelButtonTitle:nil otherButtonTitles:@"OK", nil];
[alert show];
return s_idata;
}
然后通过动态输入地址,修改s_idata的值,并动态调用printData。dSYM文件分析如下:
输入动态参数,模仿脚本调用,将s_idata修改为0xc8(200),并动态调用static 函数
s_idata值被修改,并且函数正确执行
校验计算过程,运行地址计算正确