.o 文件mach-o
这将是一个低级的文章,但是我想您已经知道自从您登陆这里以来,对吗?
我想谈谈这个神秘的词Mach-O…
它是什么?
它是如何工作的?
真的是什么!?
要回答所有这些问题,我们必须深入挖掘并弄脏双手……
当我们使用Xcode构建应用程序时,很多事情会同时发生。 其中之一是将所有源代码转换为可执行文件。 该可执行文件包含将在CPU,iOS设备上的ARM处理器或Mac上的英特尔处理器上运行的字节码。
该可执行文件称为Mach-O。
好吧,这很简单又有趣,再见!
除非您想四处逛逛以了解内部构造😈
Mach-O格式
Mach-O是字节的二进制流,被分组为有意义的数据块。 这些块包含有关元数据的信息,例如字节顺序,CPU类型,块大小等。
有多种Mach-O类型,最典型的是您会看到这些
- 可执行文件-主应用程序二进制文件,例如Example.app/Example
- Dylib —动态库,如libSwiftCore.dylib
是的,我知道吧,潜伏在我们的鼻子底下!
因此,Mach-O文件分为几个部分,看起来像这样
但是在深入探讨细分市场之前,我们先来看一下其他内容
Mach-O标头
每个Mach-O文件都以定义文件结构的头结构开头。 它还包含有关文件类型和目标体系结构( armv7 , armv7s , i386等)的信息。
在标题结构的正下方是一堆加载命令,这些命令有助于文件的布局和链接。 另外,加载命令可以指定
文件在虚拟内存中的初始布局(我们将返回到此)
- 部门名称和地址
- 要加载的dylib
- “主要”功能地址
- 代码签名
而且,这就是完整标题的样子!
如您所见,Mach-O标头由一堆加载命令组成,这些命令定义了各节的地址,主函数以及要加载的从属二进制文件。
上面提到的地址实际上与加载Mach-O的内存地址有偏差。 这样做是因为,每次启动应用程序时,都会使用一种称为“ 自动空间布局随机化”的精巧技术或我们亲切地称为ASLR的启动技术来随机化起始内存地址。
这意味着您的应用程序进程启动时,您不知道它将从哪个地址开始。
让我们想象一下它的含义,假设您有一个全局变量在RAM中占用了一些内存地址,但是由于您不知道进程从何处开始,因此您可能无法确定该全局变量的内存地址!
您可能已经猜到,这样做是出于安全目的,否则,如果每次启动时所有地址都相同,那么破解二进制文件将变得非常容易!
区隔
让我们看一下Mach-O文件的各个部分
__PAGEZERO
这是可执行文件的第一部分,内部没有数据,因此不占用文件空间。 该段充满零以捕获NULL指针取消引用。 您可能已经遇到了EXC_BAD_ACCESS
崩溃,这恰恰是因为代码中的某些内容试图从此处访问数据,而这是不允许的。
顺便说一句,此段可能是隐藏恶意代码的好地方😉
__文本
该段包含可执行代码和只读数据。 将该段设置为只读,以便在将段映射到内存中时共享该段。 这主要用于框架,捆绑软件和共享库。
并且,由于__TEXT段是只读的,因此不需要将任何更改保存回磁盘。 如果内核需要释放内存,它将仅删除__TEXT页面并在需要时重新读取它们。
这就是iOS和OSX如此积极地缓存其动态库的原因。
__数据
该段包含可写数据(例如,全局变量,静态变量等),并且由于该段可写,因此逻辑上为与该库链接的每个进程复制框架或其他共享库的__DATA段。
如果您有使用Swift的经验,那么您必须熟悉写时复制 ,这实际上意味着在编辑引用的内容之前不要创建副本。 同样,在复制__DATA段时,直到某个进程对其进行修改后,该进程才真正收到该页的私有副本。
__OBJC
这是一个可选段,包含由Objective-C语言运行时支持库使用的数据。
__进口
这也是一个可选段,包含符号存根和指向可执行文件中未定义的符号的非惰性指针。 仅针对针对IA-32体系结构的可执行文件生成此段。
__LINKEDIT
此段包含链接器的原始数据( 链接e dito r ),例如符号和字符串表,压缩的动态链接信息,代码签名信息以及间接符号表-所有这些均占用装入命令指定的区域。
因此,现在我们已经了解了各个细分市场,让我们尝试着眼于全局,看一看如何将它们组合在一起。
全局图— DYLD
直到现在我们知道
- 如何生成Mach-O文件,以及使用其加载命令以各种方式链接依赖项。
- 加载命令用于映射内存命令中的段。
- 从
LC_MAIN
开始执行文件
好吧,这仅仅是信息,而该信息需要大脑进行处理。
这个大脑叫做Dyld!
让我告诉你一个秘密! 🤫
好吧,这不是一个秘密
当您通过点击应用程序图标启动应用程序时,而不是启动应用程序,内核会启动dyld
!
我知道,对吧! 这个家伙在这里很重要。
内核实际上将在某个随机地址空间加载dyld ,并且内核本身将具有自己的__TEXT段, __ DATA段……好了,您就知道了。
dyld的工作基本上是为我们加载和设置所有依赖的dylib。
加载dylib
dyld在这里读取Mach-O标头,以查找有关从属dylib的信息 。 然后,它在文件系统上找到该库文件并进行解析。
此过程是递归完成的,因为dylib A可以依赖dylib B,而dylib B可以依赖dylib C,因此它必须解析整个依赖关系图,最后将所有这些dylib的段映射到原始Mach-O标头。
而且,整个交易可能看起来像这样。
现在,请记住我们讨论过ASLR,以及如何不知道将哪个地址分配给应用程序中的所有变量。 dyld必须使用以下技术来解决此问题。
变基
__LINKEDIT部分包含所有需要移动的指针的位置。 Dyld将遍历所有这些指针,并根据您应用程序的起始地址对其进行移位。
请注意,要执行此操作,我们必须读取和写入数据页,从而导致这些页变脏,并且需要在写入时进行复制。 这就是为什么Rebasing在IO中非常昂贵的原因。
捆绑
对其他dylib函数的引用是通过绑定(例如NSLog,malloc等)进行固定的。
dyld加载依赖库后,需要搜索符号表并找到这些符号的实现。 因此,您的二进制文件中实际上有一个名为_NSLog
的字符串_NSLog
解析,而dyld将要做的就是查找符号表,并用从属库中这些函数的地址填充它。
这是计算复杂且昂贵的
对象运行时
所有Objc类定义都需要注册,为什么? 因为您可以从调用NSClassFromString(_:)
方法的字符串构造Objc类。
因此,dyld必须先构建此表,然后才能启动应用程序。
在方法列表中添加类别-这意味着,如果您已经在UIView上创建了一个类别并添加了许多新函数,则这些新函数将被添加到UIView的方法列表中。
它还可以确保选择器是唯一的。
运行初始化程序
此时将调用Objc +load
方法C ++静态初始化程序
这是以自下而上的方式发生的,因此基本上将首先初始化从属库。
哇! 完成所有这些操作之后,最后,您的main()
将执行。
而且,这就是难以捉摸的Mach-O文件背后的故事。
摘要
- 二进制文件使用的Mach-O格式__TEXT,__DATA和__LINKEDIT段。
- Dyld需要解析并加载所有动态库依赖项。
- Dyld需要修复内部和外部的所有指针(变基,绑定,设置运行时)。
- 运行静态初始化程序和
+load
方法。 - 然后,然后
main()
然后去哪儿?
- 阴暗面-https: //lowlevelbits.org/parsing-mach-o-files/
- 如果您想更深入地学习并从dylib中提取一些代码,请查阅objc.io中的本文 。
- 甚至摩尔文档!!
先前发布在 https://medium.com/tokopedia-engineering/a-curious-case-of-mach-o-executable-26d5ecadd995
翻译自: https://hackernoon.com/a-curious-case-of-mach-o-executable-oc8l3v18
.o 文件mach-o