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