iOS高级进阶系列之-MachO与dyld

3,169 阅读13分钟

系列文章:OC底层原理系列OC基础知识系列Swift底层探索系列iOS高级进阶系列

前言

按计划这篇文章讲一下脚本的实际应用,但是自己整理下来发现有些脚本在之前的文章已经写过了,文章写了一半多发现可写的内容太少了,撑不起来一篇文章,所以临时决定写一下dyld和lldb的内容。

Mach-o分析

之前文章iOS高级进阶系列之-项目开发基础(下)Mach-O与链接器,Symbol已经讲过Mach-o,我们说了Mach-o结构,说了它的Header,这里我们在深入的讲一下

代码分析

  • 我们创建一个test.m文件,然后将它编译成可执行文件,我们先看下我们test.m文件内容 image.png
  • 我们将test.m编译成可执行文件 image.png
  • 我们查看可执行文件里的内容 image.png
  • 查看下.o文件 image.png

和上面的可执行文件有不同:左边虚拟内存地址变成了偏移量

  • 这种情况还不是很明显,下面我们改变下test.m文件 image.png
  • 同样的操作,读取.o文件 image.png

发现内容多了不少,此时偏移更加明显.o内容加载是按写的顺序进行

  • 分析下此时的.o文件
    • 我们看下红框的内容,调用的两个方法testtest_1,前面都是e8开头,这个e8就是固定的机器码都是指callq
    • e8后面的00 00 00 00这个叫什么呢?这里说个概念,这个就是静地址相对位移调用指令(偏移量),也就是00 00 00 00 + 后面的48就等于我们test地址
    • 我们看到test的其实地址是0,而我们红框内的方法没有这个地址,这说明这个地址虚拟地址非真实地址,怎么办?可执行文件里我们知道有虚拟地址,我们告诉指令器需要把偏移地址写进来,这样我们就能拿到真实地址。此时我们需要把test放到重定位符号表里,告诉链接器这里需要重定位
  • 重定位符号表 image.png
  • 分析
    • 此时49需要重定位(test_1),test_1之前的位置为4800 00 00 00 就是49的位置,也就是告诉链接器,到这个位置需重新定位
    • 再看下global地址是3b,而在之前是39c7是3a05位3b,那么后面的fc ff ff ff就是需要重定位的位置
  • 编译可执行文件 image.png

此时在看我们的main函数,发现前面不是跟之前一样的,不再是偏移量,而是分配了虚拟内存地址

  • 分析
    • 此时我们查看test,上面说了b8 ff ff ff + 100003fa8就是test的内存地址,此时是ffffffb8,开头是ff说明是补码为负,通过补码拿原码就是取反+1,所以前面的ffffff不用看了,只需要看b8就可以了
      image.png
    • 此时取反就变成了01000111,此时再加1
      image.png
    • 转成16进制 image.png
    • 此时的原码就是0x48,然后上面说了需要+下面的100003fa8就能拿到原地址 image.png
    • 此时的0x0000000100003f60就是test的原地址 image.png
  • 查看global的值(展示当前文件所有二进制内容) image.png

看最下面__data,有个0a000000就是一开始付的值100008000就是它的位置

  • 计算它的位置 image.png

等于上面打印的位置

总结

此时知道Mach-o内部是怎么找数据有一个比较清晰的认识了吧!就是通过这样的偏移量来找到初始的地址进行调用

调试信息

dSYM文件

  • dSYM:就是保存按DWARF格式保存调试信息的文件
  • DWARF:是一种被众多编译器和调试器使用的用于支持源代码级别调试的调试文件格式

调试信息如何生成dSYM

  • 1.读取debug map
  • 2.从.o文件中加载__DWARF
  • 3.重新定位所有地址
  • 4.最后将全部的DWARF打包成dSYM Bundle

解释

  • 1.生成调试信息 image.png image.png

此时调试信息在__DWARF段

  • 2.生成可执行文件 image.png

此时搜索__DWARF搜不到的,就是在链接的时候会将__DWARF删除,放到其它位置

  • 3.查看__DWARF在可执行文件中的位置 image.png

放到符号表里面,上面说了DWARF是一个文件格式,如果我们按照一定的格式将符号写入到文件中就可以

  • 4.生成dSYM文件 使用指令:clang -g1 test.m -o test就可以生成dSYM文件 image.png
  • 5.查看dSYM文件 image.png

发现这里面保存着符号的完整信息,将所在文件,符号地址,名称

项目实际应用

随便弄了个项目,模拟向日常开发中如何去使用dSYM文件

  • 先看下VC的代码 image.png

这么写代码,在2秒后就会执行test_dwarf方法,test_dwarf方法里面的数组就会越界

  • 崩溃信息 image.png

此时的崩溃信息非常的清晰,将崩溃代码名称给出来,之所以给出来是因为这些都被还原了。如果不想让它还原需要脱符号

  • 脱符号设置 image.png image.png

这么设置后,Xcode进行了脱符号处理,此时再运行 image.png 发现给到的不再具体崩溃名称了,下面我们把地址还原成我们的符号

地址还原成符号

上面截图的地址偏移后的地址,我们在查看headers的时候,看到首地址是0x0000000100000000,那么即使偏移也是按照0x0000000100000000地址以macho为单位进行偏移image.png image.png

我们需要使用方法偏移后的地址减去系统对自身库偏移地址,就能得到真实虚拟地址。之所以要真实的虚拟地址就是因为dSYM保存的就是真是的虚拟地址

  • 获取真实的虚拟地址 image.png

此时我们拿到了偏移的地址

  • 还原成符号
    • 在这之前写了个脚本,将dSYM和包复制到同一个文件中 image.png
    • 运行自动生成了dSYM image.png
    • 查找符号 image.png

dSYM文件保存是没有偏移虚拟地址

ASLR

我们接着上面的代码继续,我们添加如下代码: image.png 下面我们来操作一下,看看获得的真正虚拟地址,是否能再dSYMz中查找到 image.png

  • 下面我们去看一下dSYM文件,查找一下这个地址 image.png

找到这个地址,说明ASLR是根据Macho的二进制文件中的image进行偏移的,这个东西有什么用呢?

ASLR应用

我写了一个项目,项目中引入了TestFramework,下面看下TestFramework里面有什么 image.png

就这一个方法,并做打印

  • 我们看下TestFramework如何被引入的 image.png

意思就是如果使用Source=1进行引入,则引入的是.h和.m文件,如果不是用则引入的是Framework

  • 直接使用pod install进行引入 image.png
  • 使用Source=1进行引入 image.png
  • 此时在项目里调用组件的方法 image.png
  • 下面我们运行项目,在lj_test加断点,看看能不能跳到我们组件里面去 image.png
  • 点击下一步 image.png

发现跳到组件中去,那么为什么能跳到这里呢?

  • 看看二进制文件信息,从中找原因 image.png

这个路径是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设置断点 image.png

我们发现设置不上

  • 通过:br set -n dyldbootstrap::start -s dyld设置断点 image.png

发现设置成功

  • 3.通过禁掉白名单的方式设置断点 image.png

发现通过禁掉白名单后,用之前使用的b dyldbootstrap::start也可以设置成功,禁掉后如何恢复呢?看到禁掉命令最后是0恢复将0改为1就行了。而且这次设置只在当前的这一次lldb中生效退出后就失效

上面介绍了几种环境变量,下面我们来试试

  • 打印dyld内部几乎所有发生的调用 image.png

这就打印了我们项目在dyld做了哪些事情

  • 打印在应用程序启动期间正在加载所有动态库 image.png

test在运行过程中使用了这么多动态库

  • 打印当前程序的segment信息 image.png

截取部分,可以看到有__TEXT,__DATA的地址区间

使用dyld进行调试

我们使用dyld来调试我们的test,同时打印调试信息 image.png

在main函数前,和之前一样,但是执行main函数打印了一些相关信息

dyld加载程序过程

这部分在OC底层原理之-App启动过程(dyld加载流程)中讲过dyld的加载流程,这里要补充一下内容

执行流程

  • 有系统进程函数在执行dyld之前会进行初始化一下内容:
    • 加载文件从磁盘到内存中(1.这样后面读取速度更快。2.这样下次启动速度更快)
    • 解析当前可执行文件的mach header,判断当前Mach-o文件是否可用
    • 如果可用,则根据mach header解析load commands根据解析结果将程序各个部分加载程序到指定的地址空间,同时设置保护标志
      image.png image.png

      r-x,rw-就是保护标志

    • LC_LOAD_DYLINKER中加载dyld
    • dyld开始工作

image.png

dyld到底做了什么

dyld: 动态链接程序

libdyld.dylib: 我们的程序提供在Runtime期间能使用动态链接功能

  • 1.执行自身初始化配置加载环境;LC_DYLD_INFO_ONLY image.png
    1. 加载当前程序链接所有动态库到指定的内存中;LC_LOAD_DYLIB image.png
    1. 搜索所有动态库绑定需要在调用程序之前用的符号(非懒加载符号);LC_DYSYMTAB
    1. 在indirect symbol table中将需要绑定导入符号真实地址替换;LC_DYSYMTA(间接符号表)
      image.png
    1. 向程序提供在Runtime时使用dyld的接口函数(存在libdyld.dylib中,由LC_LOAD_DYLIB提供);
    1. 配置Runtime执行所有动态库/image中使用的全局构造函数
    1. dyld调用程序入口函数,开始执行程序。 LC_MAIN image.png

我们看到test的main函数地址是16256,下面我们看下是不是main函数所在位置 image.png

dyld执行

上面说了dyld启动前做了一些事情,那么dyld启动后又会做哪些? 下面我整理了一下dyld后面的流程 image.png 其中会调用一个__dyld_start()方法,来说明dyld正式开始工作,我们来看下这个方法

  • 我们先对__dyld_statr()方法打断点 image.png

然后发现我们断点是不成功,原因是这个是用汇编实现

  • 通过正则方法来打断点 image.png

我们发现断点成功,但是地址一看就是假地址,所以实际并没有断成功,运行直接运行结束退出。但我想说的是正则在探索底层上面也是非常有用

  • 缓存命中 缓存检视就是之前将dyld说的共享缓存,如果在共享缓存找到了就直接返回完成dyld配置把控制权给可执行文件入口函数main函数,然后把main函数地址返回给libdyld。为什么返回给它? image.png

通过打印我们看到,将main函数地址返回给libdyld后libdyld会调用start函数

  • 缓存未命中
    • 1.插入动态库(它并非我们应用程序链接的动态库)
    • 2.链接动态库(程序需要链接的动态库)
    • 3.链接插入的库
    • 4.应用插入函数
    • 5.绑定符号
    • 6.libSystem_initializer(),读取LCMain找到入口函数地址
    • 7.通过LC_MAIN查找设置程序入口函数将胶水地址设置成入口函数地址,否则胶水地址为0执行失败(在此之前:libSystem的libSystem_initializer()方法会先于main函数执行)

插入动态库

  • 准备 image.png

工程里有一个Inject.m文件,里面做了一个构造函数,构造函数里只做了一句打印,我们已经给它包装成一个动态库 image.png 动态库具体内容

看些另一个项目 image.png

项目中什么都没有引入 image.png 设置:DYLD_INSERT_LIBRARIES,并将动态库Inject赋值给它

  • 运行TestInject image.png

看到执行在Inject里的打印了,这就是插入动态库的地址通过环境变量来插入动态库

插入函数

  • 准备 image.png

写了宏,它就是用来hook函数的,24行将NSLog替换成我们写的my_NSLog,之前我们做方法替换的时候用的是方法交换,这里用的就是dyld提供的插入函数

上面的宏看起来乱七八糟的,我们可以转换一下 image.png

  • 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引入这个动态库 image.png
  • 运行TestInject image.png

我们发现NSLog以经换成我们自定义的打印了。这个就是插入函数

说明

上面的方法是不能上架的,但是可以作为探索别人的源码的一种方式

写到最后

这边文章写得时间比较长,因为写的过程中自己思考了一些问题。因为自己文章是自己探索的东西,所以写作时间很长,自己先探索,再将探索过程记录下来,有问题还要去解决,解决过程也要写下来,很慢!自己规划的东西很多,照这么下去,啥时候自己规划的内容才能弄完?所以决定后面不再写自己探索的内容了,会写一些项目应用的文章,像这种打基础的技术文章不再写了。感谢大家的支持