前言
按计划这篇文章讲一下脚本的实际应用,但是自己整理下来发现有些脚本在之前的文章已经写过了,文章写了一半多发现可写的内容太少了,撑不起来一篇文章,所以临时决定写一下dyld和lldb的内容。
Mach-o分析
之前文章iOS高级进阶系列之-项目开发基础(下)Mach-O与链接器,Symbol已经讲过Mach-o
,我们说了Mach-o
的结构
,说了它的Header
,这里我们在深入的讲一下
代码分析
- 我们创建一个test.m文件,然后将它
编译成可执行文件
,我们先看下我们test.m文件内容
- 我们将test.m编译成可执行文件
- 我们查看
可执行文件里的内容
- 查看下
.o文件
和上面的可执行文件有不同:
左边
的虚拟内存地址变成了偏移量
。
- 这种情况还不是很明显,下面我们改变下test.m文件
- 同样的操作,读取.o文件
发现内容多了不少,此时
偏移更加明显
,.o内容加载是按写的顺序进行
的
- 分析下此时的.o文件
- 我们看下红框的内容,调用的
两个方法test
和test_1
,前面都是e8
开头,这个e8
就是固定的机器码
都是指callq
- e8后面的
00 00 00 00
这个叫什么呢?这里说个概念,这个就是静地址相对位移调用指令
(偏移量),也就是00 00 00 00 +
后面的48
就等于我们test
的地址
- 我们看到
test
的其实地址是0
,而我们红框内的方法没有这个地址
,这说明这个地址
是虚拟地址
而非真实地址
,怎么办?可执行文件里我们知道有虚拟地址
,我们告诉指令器
需要把偏移地址写进来
,这样我们就能拿到真实地址
。此时我们需要把test放到重定位符号表
里,告诉链接器这里需要重定位
的
- 我们看下红框的内容,调用的
- 重定位符号表
- 分析
- 此时
49
是需要重定位
的(test_1),test_1
之前的位置为48
,00 00 00 00
就是49的位置
,也就是告诉链接器
,到这个位置需
要重新定位
- 再看下
global地址是3b
,而在之前是39
,c7是3a
,05位3b
,那么后面的fc ff ff ff
就是需要重定位的位置
- 此时
- 编译可执行文件
此时在看我们的main函数,发现前面不是跟之前一样的,
不再是偏移量
,而是分配了虚拟内存地址
- 分析
- 此时我们
查看test
,上面说了b8 ff ff ff + 100003fa8
就是test的内存地址
,此时是ffffffb8
,开头是ff
说明是补码为负
,通过补码
拿原码就是取反+1
,所以前面的ffffff不用看
了,只需要看b8
就可以了
- 此时
取反就变成了01000111
,此时再加1
转成16进制
- 此时的
原码就是0x48
,然后上面说了需要+下面的100003fa8
就能拿到原地址
- 此时的
0x0000000100003f60
就是test的原地址
- 此时我们
查看global的值
(展示当前文件所有二进制内容)
看最下面
__data
,有个0a000000
就是一开始付的值
,100008000
就是它的位置
- 计算它的位置
等于上面打印的位置
总结
此时知道Mach-o内部是怎么找数据有一个比较清晰的认识了吧!就是通过这样的偏移量来找到初始的地址进行调用
调试信息
dSYM文件
dSYM
:就是保存按DWARF格式保存调试信息的文件
DWARF
:是一种被众多编译器和调试器使用的用于支持源代码级别调试的调试文件格式
调试信息如何生成dSYM
- 1.读取
debug map
- 2.从
.o
文件中加载__DWARF
- 3.
重新定位所有地址
- 4.最后将全部的
DWARF
打包成dSYM Bundle
解释
- 1.生成调试信息
此时
调试信息
都在__DWARF段
中
- 2.生成可执行文件
此时
搜索__DWARF
是搜不到
的,就是在链接的时候会将__DWARF删除
,放到其它位置
- 3.查看__DWARF在可执行文件中的位置
放到符号表里面,上面说了
DWARF是
一个文件格式
,如果我们按照
一定的格式将符号写入
到文件中就可以
了
- 4.生成dSYM文件
使用指令:clang -g1 test.m -o test
就可以生成dSYM文件
- 5.查看dSYM文件
发现这里面保存着符号的完整信息,将
所在
的文件,符号地址,名称
。
项目实际应用
随便弄了个项目,模拟向日常开发中如何去使用dSYM文件
- 先看下VC的代码
这么写代码,在2秒后就会执行test_dwarf方法,test_dwarf方法里面的数组就会越界
- 崩溃信息
此时的
崩溃信息
非常的清晰
,将崩溃代码名称
给出来,之所以给出来是因为这些都被还原
了。如果不想让它还原
就需要脱符号
- 脱符号设置
这么设置后,
Xcode
就进行了脱符号
处理,此时再运行
发现给到的
不再
是具体
的崩溃名称
了,下面我们把地址还原成我们的符号
地址还原成符号
上面截图的地址
是偏移后的地址
,我们在查看headers
的时候,看到首地址是0x0000000100000000
,那么即使偏移
也是按照0x0000000100000000
地址以macho为单位
进行偏移
的
我们需要使用方法
偏移
后的地址减去系统对自身库
的偏移地址
,就能得到真实
的虚拟地址
。之所以要真实的虚拟地址
就是因为dSYM保存的就是真是的虚拟地址
- 获取真实的虚拟地址
此时我们拿到了偏移的地址
- 还原成符号
- 在这之前写了个脚本,将dSYM和包复制到同一个文件中
- 运行自动生成了dSYM
- 查找符号
- 在这之前写了个脚本,将dSYM和包复制到同一个文件中
dSYM文件保存
的是没有偏移
的虚拟地址
ASLR
我们接着上面的代码继续,我们添加如下代码:
下面我们来操作一下,看看获得的真正虚拟地址
,是否能再dSYMz中查找到
- 下面我们去看一下
dSYM文件
,查找一下这个地址
找到这个地址,
说明ASLR是根据Macho的二进制文件中的image进行偏移
的,这个东西有什么用呢?
ASLR应用
我写了一个项目,项目中引入了TestFramework,下面看下TestFramework里面有什么
就这一个方法,并做打印
- 我们看下TestFramework如何被引入的
意思就是
如果使用Source=1进行引入
,则引入的是.h和.m文件
,如果不是用则引入的是Framework
- 直接使用
pod install进行引入
- 使用
Source=1进行引入
- 此时在项目里调用组件的方法
- 下面我们运行项目,在
lj_test加断点
,看看能不能跳到
我们组件里面去
- 点击下一步
发现
跳到组件中去
,那么为什么能跳到这里呢?
- 看看二进制文件信息,从中找原因
这个
路径
就是TestFramework
的路径
,所以它能找到源码
,就能跳进去
了
- 用途:
保存调试信息
,组件信息都是有用的,通过读路径
,就能找到源码
,当我们进行组件化
或者二进制化
的时候,通过调试信息来进入对应的源码中
dyld
如何调试dyld
- 1.第一种:如果
想调试dyld源代码
,需要准备带调试信息
的dyld/libdyld.dylib/ libclosured.dylib
,与系统做替换
,⻛险较大。 - 2.第二种:
lldb保留了一个库列表
,避免在按名称设置断点
时出现问题
,而dyld与libdyld.dylib就在该列表
上。有两种方式在可以强制在dyld上设置断点
:br set -n dyldbootstrap::start -s dyld
set set target.breakpoints-use-platform-avoid-list 0
- 第二种方式
无需查看代码、二进制文件
,而是通过dyld提供
的环境变量
来控制dyld在运行过程中输出有用信息
DYLD_PRINT_APIS:打印dyld内部几乎所有发生的调用
DYLD_PRINT_LIBRARIES:打印在应用程序启动期间正在加载的所有动态库
DYLD_PRINT_WARNINGS:打印dyld运行过程中的辅助信息
DYLD_*_PATH:显示dyld搜索动态库的目录顺序
DYLD_PRINT_ENV:显示dyld初始化的环境变量
DYLD_PRINT_SEGMENTS:打印当前程序的segment信息
DYLD_PRINT_STATISTICS:打印pre-main time
DYLD_PRINT_INITIALIZERS:显示都有initialiser
调试dyld
这边进入一个测试项目,我们对dyld设置断点
- 通过:
b dyldbootstrap::start设置断点
我们发现
设置不上
- 通过:
br set -n dyldbootstrap::start -s dyld设置断点
发现设
置成功
了
- 3.通过
禁掉白名单
的方式设置断点
发现通过
禁掉白名单
后,用之前使用的b dyldbootstrap::start也可以设置成功
,禁掉后如何恢复
呢?看到禁掉命令最后是0
,恢复将0改为1
就行了。而且这次设置
只在当前的这一次lldb中生效
,退出
后就失效
了
上面介绍了几种环境变量,下面我们来试试
打印dyld内部
几乎所有
发生的调用
这就
打印
了我们项目在dyld做了哪些事情
- 打印在
应用程序启动
期间正在加载
的所有动态库
test在运行过程中使用了这么多动态库
打印当前程序的segment信息
截取部分,可以看到
有__TEXT,__DATA的地址区间
使用dyld进行调试
我们使用dyld来调试我们的test,同时打印调试信息
在main函数前,和之前一样,但是
执行main函数
打印了一些相关信息
dyld加载程序过程
这部分在OC底层原理之-App启动过程(dyld加载流程)中讲过dyld的加载流程,这里要补充一下内容
执行流程
- 有系统进程函数在执行dyld之前会进行初始化一下内容:
加载文件从磁盘到内存中
(1.这样后面读取速度更快。2.这样下次启动速度更快)解析当前可执行文件的mach header,判断当前Mach-o文件是否可用
- 如果
可用
,则根据mach header
,解析load commands
。根据解析结果
,将程序各个部分加载程序到指定的地址空间
,同时设置保护标志
r-x,rw-就是保护标志
- 从
LC_LOAD_DYLINKER
中加载dyld
dyld开始工作
dyld到底做了什么
dyld: 动态链接程序
libdyld.dylib: 给
我们的程序提供在Runtime期间能使用动态链接功能
- 1.执行
自身初始化配置加载环境
;LC_DYLD_INFO_ONLY
-
加载
当前程序链接
的所有动态库
到指定的内存中;LC_LOAD_DYLIB
-
搜索所有
的动态库
,绑定
需要在调用程序之前用的符号
(非懒加载符号);LC_DYSYMTAB
-
在indirect symbol table中将需要绑定
的导入符号真实地址替换
;LC_DYSYMTA(间接符号表)
-
向程序提供在Runtime时使用dyld的接口函数
(存在libdyld.dylib中,由LC_LOAD_DYLIB提供);
-
配置Runtime
,执行所有动态库/image中使用的全局构造函数
;
-
dyld调用程序入口函数
,开始执行程序
。 LC_MAIN
我们看到test的main函数地址是16256,下面我们看下是不是main函数所在位置
dyld执行
上面说了dyld启动前做了一些事情,那么dyld启动后又会做哪些?
下面我整理了一下dyld后面的流程
其中会调用一个__dyld_start()方法
,来说明dyld正式开始工作
,我们来看下这个方法
- 我们先
对__dyld_statr()方法打断点
然后发现我们
断点是不成功
,原因是这个是用汇编实现
的
- 通过
正则方法来打断点
我们发现
断点成功
,但是地址
一看就是假地址
,所以实际并没有断成功
,运行直接运行结束退出
。但我想说的是正则在探索底层上面也是非常有用
的
- 缓存命中
缓存检视
就是之前将dyld说的共享缓存
,如果在共享缓存
中找到
了就直接返回
,完成dyld配置
,把控制权
交给可执行文件
的入口函数main函数
,然后把main函数
的地址返回给libdyld
。为什么返回给它?
通过打印我们看到,
将main函数地址返回给libdyld后
,libdyld会调用start函数
- 缓存未命中
- 1.
插入动态库
(它并非我们应用程序链接的动态库) - 2.
链接动态库
(程序需要链接的动态库) - 3.
链接插入的库
- 4.
应用插入函数
- 5.
绑定符号
- 6.
libSystem_initializer()
,读取LCMain
,找到入口函数地址
- 7.
通过LC_MAIN查找设置程序入口函数
,将胶水地址设置成入口函数地址
,否则胶水地址为0
,执行失败
(在此之前:libSystem的libSystem_initializer()方法会先于main函数执行)
- 1.
插入动态库
- 准备
工程里有一个Inject.m文件,里面做了一个
构造函数
,构造函数里只做了一句打印,我们已经给它包装成一个动态库
动态库具体内容
看些另一个项目
项目中什么都没有引入
设置:DYLD_INSERT_LIBRARIES,并将动态库Inject赋值给它
- 运行TestInject
看到执行在Inject里的打印了,这就是
插入动态库的地址
,通过环境变量来插入动态库
插入函数
- 准备
写了宏,它就是用来
hook函数
的,24行将NSLog替换成我们写的my_NSLog
,之前我们做方法替换
的时候用的是方法交换
,这里用的就是dyld提供的插入函数
上面的宏看起来乱七八糟的,我们可以转换一下
- 1.
__attribute__((used))告诉编译器我这个是偷偷使用的,不需要报警告
- 2.
struct { const void* replacement; const void* replacee; }就是声明一个结构体
- 3.
_interpose_NSLog结构体名称
- 4.
__attribute__ ((section("__DATA, __interpose")))这个就是将这个变量放入创建的section里
- 5.
{ (const void*) (unsigned long) &my_NSLog, (const void*) (unsigned long) &NSLog };就是初始化这个结构体,将my_NSLog和NSLog地址传进来
插入函数
就是当你写了就会替换
,这个是在dyld加载
的时候进行替换
(dyld会在DATA段判断__interpose是否有内容
,有就hook
),我们平时用的方法交换是运行时进行替换
的
- 在TestInject引入这个动态库
- 运行TestInject
我们发现NSLog以经换成我们自定义的打印了。这个就是插入函数
说明
上面的方法是不能上架
的,但是可以作为探索别人的源码
的一种方式
写到最后
这边文章写得时间比较长,因为写的过程中自己思考了一些问题。因为自己文章是自己探索的东西,所以写作时间很长,自己先探索,再将探索过程记录下来,有问题还要去解决,解决过程也要写下来,很慢!自己规划的东西很多,照这么下去,啥时候自己规划的内容才能弄完?所以决定后面不再写自己探索的内容了,会写一些项目应用的文章,像这种打基础的技术文章不再写了。感谢大家的支持