解析Mach-o文件

现在做iOS开发的挺多,了解一下在苹果平台上程序运行的原理

解析 MACH_O 文件

这篇文章描述了如何解析 Mach-O 文件并稍微解释了一下它的格式。这不是一份权威指南,不过当你不知从何开始时,它可能有些帮助。想了解更多信息,请考虑阅读官方文档和操作系统提供的头文件。

Macho-O 是什么

维基百科 的简单描述:

Mach-O 是 Mach object 文件格式的缩写,它是一种用于记录可执行文件、对象代码、共享库、动态加载代码和内存转储的文件格式。作为 a.out 格式的替代品,Mach-O 提供了更好的扩展性,并提升了符号表中信息的访问速度。

大多数基于 Mach 内核的操作系统都使用 Mach-O。NeXTSTEP、OS X 和 iOS 是使用这种格式作为本地可执行文件、库和对象代码的例子。

Mach-O 格式

Mach-O 没有类似于 XML、YAML、JSON 等诸如此类的特殊格式,它只是一个二进制字节流,被划分为了有意义的数据块。这些块包含元信息,比如,字节顺序、cpu 类型、块的大小,等等。

典型的 Mach-O 文件(对应的 官方文档 )包含三个区域:

  1. 头-包含该二进制文件的一般信息:字节顺序、(魔数)、cpu 类型、加载指令的数量等等。
  2. 加载指令-它是一张包含很多内容的表,内容包括区域的位置、符号表、动态符号表等。每个加载指令都包含一个元信息,比如指令类型、名称、在二进制文件中的位置等等。
  3. 数据-通常是对象文件中最大的部分。主要包含代码、数据,例如符号表,动态符号表等等。

这里是一个简化的图形表示︰

OS X 有两种类型的目标文件:Mach-O 文件和通用二进制文件,也叫作胖文件。它们之间的区别是:Mach-O 文件包含一种架构(i386、x86_64、arm64 等等)的对象代码,而胖文件可能包含若干包含不同架构(i386、x86_64、arm、arm64 等等)对象代码的对象文件。

胖文件的结构相当简单︰ 胖文件头以及后面的 Mach-O 文件:

解析 Mach-O 文件

OS X 没有提供 libmacho 或任何类似的工具,我们唯一拥有的是一组定义在 /usr/include/mach-o/* 中的 C 结构体,因此我们需要自己实现解析。它可能非常棘手,但也并不是非常困难。

内存描述

在我们开始解析前,让我们看看一个 Mach-O 文件的详细描述。简单起见,下面的对象文件是单个 i386 Mach-O 文件(而不是胖文件),它只包含两个段类型的数据条目。

仅需下面的结构体我们就可以描述该文件:

下面是内存映射的情况:

如果你想要从文件中读取特定的信息,你只需要一个正确的数据结构和偏移量。

解析

让我们来编写一个程序,它能读取 Mach-O 或 胖文件 并打印每个段的名称以及它编译的目标架构。

结束时,我们可能会有类似这样的东西︰

驱动

让我们从一个简单的“驱动”开始。

至少有两种可用的方式来解析此类文件︰ 加载文件内容到内存中并直接处理缓冲区 或打开一个文件在其中来回跳转。两种方法都有自己的优点和缺点,但这里我会选用第二种。此外,我假定没有人会用错误的方式使用该程序,因此我没有添加错误处理。

魔数、CPU、字节序

为了至少阅读对象文件的头,我们需要得到我们需要的所有信息︰ CPU 架构(32 位或 64 位) 和字节顺序。但首先我们需要取出一个魔数︰

函数 read_magic 是非常直截了当的,但有一件事可能看起来很怪︰ fseek。问题是,每当有人读取文件,文件内部的偏移量都会发生改变。最好显式指定偏移量,以确保我们阅读到我们实际上想要读取的内容。此外,这个小技巧稍后也会有用。

描述 32 位和 64 位对象文件的结构体是不同的(例如︰ mach_header 和 mach_header_64),我们需要检查文件的体系结构来选择需要的结构体:

MH_MAGIC_64 和 MH_CIGAM_64 是系统提供的魔数。第二个看起来比第一个更多 magicly(译者注:原文如此,magic 对应的形容词是 magical,作者使用了magicly。)。解释如下。

由于历史的原因,不同的计算机可能使用不同的 字节顺序︰ 大端字节序 (从左到右) 和小端字节序 (从右至左)。魔数同样存储了这一信息︰ MH_CIGAMMH_CIGAM_64 表示字节顺序不同于主机操作系统,因此所有字节都应颠倒︰

Mach-O 头

终于我们能够读取 mach_header 了。我们先来介绍几个用于从文件中读取数据的常用函数。

注意︰ 数据应当在使用后释放 !

为了不搞砸驱动函数,我们在这里引入另一个函数 dump_mach_header。下一步是读取所有段指令并打印它们的名字。问题是,mach-o 文件通常也包含其他指令。如果你还记得 segment_command 结构的第一个字段的是 uint32_t cmd;,此字段表示指令的类型。下面是我们将使用的由系统提供的另一种结构体︰

除了以上的所有信息 mach_header 还有许许多多加载命令,所以我们可以只是遍历并跳过我们不感兴趣的指令。此外,我们需要计算标头结束位置的偏移量。这里是 dump_mach_header 的最终版本︰

段指令

是时候去打印所有的段名了:

这个函数不需要 is_64 参数,因为我们可以从 cmd 类型本身 (LC_SEGMENT/LC_SEGMENT_64)推断它。如果它不是段,我们只需跳过该命令并向前移动到下一个。

CPU 名

我想要展示的最后一件事是如何基于 mach_headercputype 获得处理器的名称。我相信这不是最好的选择,但对于本文的示例来说,它是可以接受的︰

OS X 为大量的 CPU 提供了 CPU_TYPE_ *,所以我们可以 “容易”地为特定的魔数关联一个字符串。为了打印 CPU 的名称,我们需要稍微修改 dump_mach_header

胖对象

这篇文章已经包含大量的内容,所以我不会描述如何处理胖对象,但你可以在这里找到如何实现它︰ segment_dumper

接下来是什么

大概就是以上这些。

这里是一组可能有用的链接,如果你想要更深入地挖掘和了解更多关于 mach-o 的内容:

打赏支持我翻译更多好文章,谢谢!

打赏译者

打赏支持我翻译更多好文章,谢谢!

任选一种支付方式

1 3 收藏 评论

关于作者:酷酷的哀殿

常年混迹于创业公司的 iOS 开发者。 个人主页 · 我的文章 · 11 ·    

相关文章

可能感兴趣的话题



直接登录
跳到底部
返回顶部