iOS 关于符号该知道的事儿
符号表是什么
符号表 debugger Symbols,是Xcode编译项目生成的.dSYM文件
通俗点说,我们定义的方法和变量都有名称,保存这些名称的表就是符号表。
符号绑定及其过程
ASLD和重定向
image是镜像,本地编译完的可执行文件运行到内存后就是镜像。UIKit、Foundation等动态库的可执行文件拷贝到内存就是镜像文件。
断点输出image list指令可以查看项目可执行文件的首地址

每次machO运行的时候,这个首地址位置都不一样,原因是系统会在虚拟地址前增加一个随机偏移量(ASLR)
通过符号的地址和这个首地址相减,可以得到一个偏移,这个偏移值无论重新运行多少次不会改变。
接下来我们来说一下什么是重定向:ASLR + 偏移值 = 内存值
-
内部函数地址通过重定向可以计算
-
外部函数(
NSLog等动态库函数)无法计算函数地址
符号的加载时机
DYLD动态链接器加载image,MachOview里面看到的Load Commands就是工程依赖库的基本信息,里面能看到很多系统库。加载完依赖库以后才会加载应用。
符号表使用了PIC技术,就是位置代码独立。首次启动应用但还未调用NSLog方法时,我们依旧可以在Dynamic Symbol table的Indirect Symbols里面能看到NSLog及其一些信息,但是这个时候其实还未进行符号绑定,里面记录的也不是真实的函数地址。
直到首次使用该方法时DYLD才会去做符号绑定(告诉当前应用这个函数的真实地址),下次就可以直接访问符号表进行调用。
这个就是符号的懒加载
符号绑定的流程
每个外部方法调用,首先要找到外部符号的桩。就是在symbol pointer stubs表找到存储的数据,这里存储的是跳转指令及传参。
接下来就需要去懒加载表(Lazy Symbol Pointers)里查找方法的相关信息。
如果是首次调用该方法,懒加载表记录的是Non Lazy Symbol Pointers里dyld_stub_binder的方法地及第一步的传参。
dyld_stub_binder会到动态库里获取真实的方法地址,替换掉懒加载表的数据然后调用。
后续的每次调用就可以直接通过已绑定成功的函数地址调用函数,不需要再次绑定。
怎么去除符号
为什么要去除符号
去除符号后安装包会变小,应用更加安全
符号分几种
- 全局符号(导出符号),一般提供给外部用,做为动态库用。所以动态库不能全部去掉符号。
- 间接符号(导入符号),从
Foundation等动态库过来的函数,需要做符号绑定的,所以不可以去掉
APP自己的方法一般不需要提供外部使用,所以除了间接符号,其他符号可以去除。
作用于本文件,static修饰函数的符号是本地符号。
作用于本文件,没有static的函数,可以提供给其他文件调用的,就是全局符号。
本地符号如果不使用,符号不会生成。
全局符号不使用也会生成符号。
通过objcdump –macho -t symboldemo1指令可以查看符号表
-
前缀
l = local,本地符号 -
前缀
g = gobal,全局符号
Xcode相关的配置
通过Xcode的设置可以控制我们需要保留在符号表的符号
Build Setting -> Target -> Deployment postprocessing 设置为NO编译时保留符号,打包的时候才去除。
Build Setting -> strip Style
All Symbol一般APP用,只剩下间接符号Non-Global Symbol,一般给动态库用,只剩全局符号Debugging Symbol,编译时会做去符号操作,此时debug也无法断点
怎么恢复符号
逆向工程会有恢复符号的技术,什么能恢复符号呢?
OC有动态语言的特性,runtime可以通过字符串获得类、方法……
上述变量明即便设置了去除符号,在MachO文件中依旧会以字符串方式保存。
使用restore_symbol工具(搜索下载,找不到留言),可以恢复OC和swift的本地符号。
-
swift属于动态派发所以可以恢复 -
OC有动态语言特性,所以可以恢复 -
C函数没有动态特性所以不可以恢复
怎么修改符号
fishhook框架
fishHook 是 Facebook 提供的一个动态修改链接 mach-O 文件的工具
通过 修改符号表的指向 实现 勾起方法 的效果
代码很简单,fishHook 提供了两个钩子方法及一个钩子结构体,
一般如下构建rebinding结构体传入rebind_symbols函数即可
- (void)viewDidLoad {
[super viewDidLoad];
// hook NSLog
struct rebinding nslog = {};
// 方法名
nslog.name = "NSLog";
// 替换进去的方法
nslog.replacement = new_NSLog;
// 被替换的方法指针
nslog.replaced = (void *)&sys_NSlog;
// 创建结构体数组
struct rebinding rbs[1] = {nslog};
// hook
rebind_symbols(rbs, 1);
}
// 系统NSLog的函数地址
static void (*sys_NSlog)(NSString *format, ...);
// 定义新的NSLog
void new_NSLog(NSString *format, ...) {
format = [format stringByAppendingString:@"\n hook Succ"];
sys_NSlog(format);
}
//点击一下 就输出
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"test");
}
fishhook的流程
fishhook 是通过重新绑定符号实现功能的,首先我们看一下怎么利用符号找到方法的名字
懒加载符号表(Lazy Symbol Pointers)和间接符号表(Indirect Symbols)的记录顺序是一致的。
在间接符号表(Lazy Symbol Pointers)的Data段,记录着这个符号在总符号表(Symbols)的编号。
根据编号查找总符号表,可以看到方法所属的库(Library)以及在字符串表(String Table)记录的偏移(string table index)。
到字符串表里面使用string table index加上字符串表第一行的pFile地址得出对应行,就能找到对应函数名。
那么其实只要反向操作上述过程,就可以通过方法名找到符号所在位置!
-
1、
fishhook在字符串表(String Table)使用.分割字符串,对比传入的方法名找出所在行,再用所在行减去首行pFile算出string table index -
2、在总符号表(
String Table)遍历对比string table index查出符号在总符号表的位置(symbols index) -
3、在间接符号表(
Indirect Symbols)用symbols index查找该符号偏移值(indirect symbols index) -
4、通过符号在懒加载符号表(
Lazy Symbol Pointers)和间接符号表(Indirect Symbols)位置一一对应的关系就可以就找到符号在懒加载符号表的位置。 -
5、最终修改的值是懒加载符号表(
Lazy Symbol Pointers)里记录的值
后续调用方法的时候先找桩,找到桩就会找lazy symbol pointer执行,此时找到的就是修改后的方法地址了。
解决第三方库的符号冲突
动态库的情况
当两个动态库都有同名方法的时候,动态库方法的调用是先找到方法所在库,再去找方法符号。
所以找到了A库就不会到B库找符号。动态库之间不存在符号冲突
静态库的情况
两个静态库都有同名方法的时候
- 1、如果静态库没有被使用到,就不会被链接,其链接的最小单位是类
- 2、链接器链接到A符号之后就不会再链接同名的A符号了
如果静态库里面添加分类,那情况就会不一样了。分类是动态添加的,分类在编译时不会被链接。
静态库链接的时候也不确定动态库是否使用分类代码,所以不会链接分类,运行就会崩溃找不到该方法。
所以解除上述问题的办法就是添加Other Link Flags添加-ObjC参数,这样编译的时候就会连同分类里面的代码把所有OC代码编译进去。
两个静态库都有分类的时候,可以通过Build Setting -> Other Link Flags参数使用-forceload指定库加载代码,以解决冲突。
碰到两个静态库都有动态代码需要链接的时候,又不提供源码的时候,使用llvmobjcopy修改库的二进制文件添加前缀以解决冲突。
转载自:https://juejin.cn/post/7051507256470274056