现在做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 文件(对应的 官方文档 )包含三个区域:
- 头-包含该二进制文件的一般信息:字节顺序、(魔数)、cpu 类型、加载指令的数量等等。
- 加载指令-它是一张包含很多内容的表,内容包括区域的位置、符号表、动态符号表等。每个加载指令都包含一个元信息,比如指令类型、名称、在二进制文件中的位置等等。
- 数据-通常是对象文件中最大的部分。主要包含代码、数据,例如符号表,动态符号表等等。
这里是一个简化的图形表示︰
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 文件(而不是胖文件),它只包含两个段类型的数据条目。
仅需下面的结构体我们就可以描述该文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
struct mach_header { uint32_t magic; cpu_type_t cputype; cpu_subtype_t cpusubtype; uint32_t filetype; uint32_t ncmds; uint32_t sizeofcmds; uint32_t flags; }; struct segment_command { uint32_t cmd; uint32_t cmdsize; char segname[16]; uint32_t vmaddr; uint32_t vmsize; uint32_t fileoff; uint32_t filesize; vm_prot_t maxprot; vm_prot_t initprot; uint32_t nsects; uint32_t flags; }; |
下面是内存映射的情况:
如果你想要从文件中读取特定的信息,你只需要一个正确的数据结构和偏移量。
解析
让我们来编写一个程序,它能读取 Mach-O 或 胖文件 并打印每个段的名称以及它编译的目标架构。
结束时,我们可能会有类似这样的东西︰
1 2 3 4 5 6 |
$ ./segname_dumper some_binary i386 segname __PAGEZERO segname __TEXT segname __LINKEDIT |
驱动
让我们从一个简单的“驱动”开始。
至少有两种可用的方式来解析此类文件︰ 加载文件内容到内存中并直接处理缓冲区 或打开一个文件在其中来回跳转。两种方法都有自己的优点和缺点,但这里我会选用第二种。此外,我假定没有人会用错误的方式使用该程序,因此我没有添加错误处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#include <stdio.h> #include <stdlib.h> #include <mach-o/loader.h> #include <mach-o/swap.h> void dump_segments(FILE *obj_file); int main(int argc, char *argv[]) { const char *filename = argv[1]; FILE *obj_file = fopen(filename, "rb"); dump_segments(obj_file); fclose(obj_file); return 0; } void dump_segments(FILE *obj_file) { // Driver } |
魔数、CPU、字节序
为了至少阅读对象文件的头,我们需要得到我们需要的所有信息︰ CPU 架构(32 位或 64 位) 和字节顺序。但首先我们需要取出一个魔数︰
1 2 3 4 5 6 7 8 9 10 |
uint32_t read_magic(FILE *obj_file, int offset) { uint32_t magic; fseek(obj_file, offset, SEEK_SET); fread(&magic, sizeof(uint32_t), 1, obj_file); return magic; } void dump_segments(FILE *obj_file) { uint32_t magic = read_magic(obj_file, 0); } |
函数 read_magic 是非常直截了当的,但有一件事可能看起来很怪︰ fseek。问题是 多,了解一下在苹果平台上程序运行的原理
解析 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 文件(对应的 官方文档 )包含三个区域:
- 头-包含该二进制文件的一般信息:字节顺序、(魔数)、cpu 类型、加载指令的数量等等。
- 加载指令-它是一张包含很多内容的表,内容包括区域的位置、符号表、动态符号表等。每个加载指令都包含一个元信息,比如指令类型、名称、在二进制文件中的位置等等。
- 数据-通常是对象文件中最大的部分。主要包含代码、数据,例如符号表,动态符号表等等。
这里是一个简化的图形表示︰
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 文件(而不是胖文件),它只包含两个段类型的数据条目。
仅需下面的结构体我们就可以描述该文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
struct mach_header { uint32_t magic; cpu_type_t cputype; cpu_subtype_t cpusubtype; uint32_t filetype; uint32_t ncmds; uint32_t sizeofcmds; uint32_t flags; }; struct segment_command { uint32_t cmd; uint32_t cmdsize; char segname[16]; uint32_t vmaddr; uint32_t vmsize; uint32_t fileoff; uint32_t filesize; vm_prot_t maxprot; vm_prot_t initprot; uint32_t nsects; uint32_t flags; }; |
下面是内存映射的情况:
如果你想要从文件中读取特定的信息,你只需要一个正确的数据结构和偏移量。
解析
让我们来编写一个程序,它能读取 Mach-O 或 胖文件 并打印每个段的名称以及它编译的目标架构。
结束时,我们可能会有类似这样的东西︰
1 2 3 4 5 6 |
$ ./segname_dumper some_binary i386 segname __PAGEZERO segname __TEXT segname __LINKEDIT |
驱动
让我们从一个简单的“驱动”开始。
至少有两种可用的方式来解析此类文件︰ 加载文件内容到内存中并直接处理缓冲区 或打开一个文件在其中来回跳转。两种方法都有自己的优点和缺点,但这里我会选用第二种。此外,我假定没有人会用错误的方式使用该程序,因此我没有添加错误处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#include <stdio.h> #include <stdlib.h> #include <mach-o/loader.h> #include <mach-o/swap.h> void dump_segments(FILE *obj_file); int main(int argc, char *argv[]) { const char *filename = argv[1]; FILE *obj_file = fopen(filename, "rb"); dump_segments(obj_file); fclose(obj_file); return 0; } void dump_segments(FILE *obj_file) { // Driver } |
魔数、CPU、字节序
为了至少阅读对象文件的头,我们需要得到我们需要的所有信息︰ CPU 架构(32 位或 64 位) 和字节顺序。但首先我们需要取出一个魔数︰
1 2 3 4 5 6 7 8 9 10 |
uint32_t read_magic(FILE *obj_file, int offset) { uint32_t magic; fseek(obj_file, offset, SEEK_SET); fread(&magic, sizeof(uint32_t), 1, obj_file); return magic; } void |