小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
上一篇我们介绍了编译的流程,本篇我们主要看编译产物Mach-O
文件的内容。
目标文件
编译器编译源代码后生成的文件就叫目标文件,Windows平台下PE
(Portable Executable)和Linux下是ELF
(Executalbe Linkable Format),它们都是COFF格式的变种,目标文件就是源代码编译后但未进行链接
的中间文件(Windows的.obj和Linux下的.o),它和可执行文件的结构很相似,从广义上看,目标文件和可执行文件的格式几乎是一样的,所以我们可以广义的将目标文件和可执行文件看成是一种类型的文件,本文我们主要看iOS/macOS平台的目标文件Mach-O
的内容。
Mach-O文件内容概况
使用MachOView
工具,我们查看一个Mach-O
文件的内容 \
可以看到主要分为三部分:
- Header:它描述了整个文件的基本属性,比如文件版本、目标机器型号、程序入口地址等。
- Load Commands:它包含了很多表,描述了文件中数据的组织结构,不同的数据类型使用不同的加载命令标识。
- Data:目标文件最大的部分,包含
Segment
的具体数据。
Header
我们先看Header
部分,测试工程的字段值如下:
我们也可以在iOS的sdk的loader.h
文件中找到定义:
struct mach_header_64 {
uint32_t magic; /* mach magic number identifier */
cpu_type_t cputype; /* cpu specifier */
cpu_subtype_t cpusubtype; /* machine specifier */
uint32_t filetype; /* type of file */
uint32_t ncmds; /* number of load commands */
uint32_t sizeofcmds; /* the size of all the load commands */
uint32_t flags; /* flags */
uint32_t reserved; /* reserved */
};
复制代码
可以看到Header
主要包括:
-
Magic Number:魔数,它是文件的标识码,用于标识当前设备是大端序还是小端序,有
MH_MAGIC_64
和MH_CIGAM
两种形式 。 -
CPU Type:表示cpu的架构,包括
ARM64
、X86_64
、i386
等。 -
CPU SubType:表示具体cpu的类型,区分不同版本的处理器。
-
File Type:表示Mach-O文件类型,有
MH_EXECUTE
(可执行文件)、MH_OBJECT
(可重定向的目标文件)、MH_DYLIB
(动态绑定的共享库文件)等。 -
Number of Load Commands:表示文件中
Load Commads
(加载命令)的个数。 -
Size of Load Commands:表示文件中
Load Commads
(加载命令)所占的总字节大小。 -
Flags:不同的文件标签的组合,每个标签占一个位,可以用位或来进行组合,常见的标签有
- MH_NOUNDEFS:该文件没有未定义的引用。
- MH_DYLDLINK:该文件将要作为动态链接器的输入,不能再被静态链接器修改。
- MH_TWOLEVEL: 该文件使用两级名字空间绑定
- MH_PIE: 可执行文件会被加载到随机地址,只对
MH_EXECUTE
有效,意思是启用ALSR
。
-
Reserved:保留字段。
Mach-O文件被系统装载的时候,会先读取Header部分,通过Header找到Load Commands加载指令部分,读取到加载指令就可以加载到我们编写的代码。
Load Commands
Load Commands描述了Data中的各个段信息,比如每个段的段名、段的长度、在文件中的偏移、读取权限以及段的其他属性,它的起始位置要由Header的大小决定。
由上图可以看到LoadCommad由很多段构成:
- LCSEGMEMT64(_PAGEZERO):该段在虚拟内存中的位置及大小都为0,不可读、不可写、不可执行,用来处理空指针,如果试图访问该段,那么将会引起系统的崩溃。
- LCSEGMEMT64(_TEXT):可执行的代码和其他一些只读数据,映射到内存,紧跟随_PAGEZERO端之后。
- LCSEGMEMT64(_DATA):可以更改的数据,映射到内存,紧跟随_TEXT端之后。
- LCSEGMEMT64(_LINKEDIT):动态链接库的原始数据,如符号、字符串和重定位表条目等。
- LC_DYLD_INFO_ONLY:保存动态链接的重要信息,根据它来进行地址重定向.根据它所记录的偏移量,我们便可以找到在 Dynamic Loader Info 中的相关信息。
- LC_SYMTAB:文件所使用的符号表,找到后分别获取符号表偏移量,符号数,字符串标偏移量,字符串表大小。
- LC_DYSYMTAB:动态链接器所使用的符号表,找到后获取间接符号表偏移符申表偏移量。
- LC_LOAD DYLINKER:默认的加载器路径(/usr/bin/dyld)
- LCUD: Mach-O文件的唯一标识(UUID)
- LC_VERSION MIN IPHONEOS: Mach-O文件要求的最低系统版本,和Xcod配置的 target有关。
- LC_SOURCE VERSION:构建二进制文件的源代码版本。
- LC_MAIN:程序的入口,包括入口的
Offset
和Point
。 - LC_ENCRYPTION_INFO64:文件加密信息,包括加密标记、加密数据的偏移和大小。
Crypt Id
为1表示加密,0表示未加密。 - LC_LOAD_DYLIB:依赖的动态库,包括动态库路径、当前版本、兼容版本,对应我们依赖framework选择
Required
。 - LC_LOAD_WEAK_DYLIB:依赖的弱引用库,如果加载路径下有这个库,就引入,否则可以不引入,不影响使用,但是一旦使用的时候没有这个库,也会报异常,对应我们依赖framework是选择
Optional
。 - LC_RPATH:@math的路径,指定动态链接器搜索路径列表
- LC_FUNCTION_STARTS:记录函数起始地址表
- LC_DATA_IN_CODE:定义在代码段内的非指令表。
- LC_CODE_SIGNATURE:代码签名信息
Load Command 由多个 Segment 构成,同样的我们也可以从loader.h
文件中找到Segment
的数据结构:
struct segment_command_64 { /* for 64-bit architectures */
uint32_t cmd; /* LC_SEGMENT_64 */
uint32_t cmdsize; /* includes sizeof section_64 structs */
char segname[16]; /* segment name */
uint64_t vmaddr; /* memory address of this segment */
uint64_t vmsize; /* memory size of this segment */
uint64_t fileoff; /* file offset of this segment */
uint64_t filesize; /* amount to map from the file */
vm_prot_t maxprot; /* maximum VM protection */
vm_prot_t initprot; /* initial VM protection */
uint32_t nsects; /* number of sections in segment */
uint32_t flags; /* flags */
};
复制代码
- cmd:segment 的类型,和下面的 flags 标记位决定着这个段如何被装载。
- cmdsize:section_64 结构体所需要的空间。
- segname[16]:segment的名字。
- vmaddr:所描述段的虚拟内存地址。
- vmsize:为当前段分配的虚拟内存大小。
- fileoff:当前段在文件中的偏移量。
- filesize:当前段在文件中占用的字节。
- maxprot:segment所在页需要的最高内存保护,八进制表示。
- initprot:segment所在页面原始内存保护。
- nsects:segment中section的数量
- flags:标识符
Data
Data 中存放的所有的 Section,例如机器指令,全局变量和局部静态变量,符号表,调试信息等都会被存储到对应的 Section 中。在loader.h
文件中可以查看Section
结构体的定义:
struct section_64 { /* for 64-bit architectures */
char sectname[16]; /* name of this section */
char segname[16]; /* segment this section goes in */
uint64_t addr; /* memory address of this section */
uint64_t size; /* size in bytes of this section */
uint32_t offset; /* file offset of this section */
uint32_t align; /* section alignment (power of 2) */
uint32_t reloff; /* file offset of relocation entries */
uint32_t nreloc; /* number of relocation entries */
uint32_t flags; /* flags (section type and attributes)*/
uint32_t reserved1; /* reserved (for offset or index) */
uint32_t reserved2; /* reserved (for count or sizeof) */
uint32_t reserved3; /* reserved */
};
复制代码
几个重要的section如下
__TEXT(代码段)部分:
- __text:主程序代码。
- __objc_methname:OC方法名
- __csstring:只读C语言字符串
- __objc_methtype:OC方法类型(方法签名),方法签名包含了一个方法的方法名、参数类型、它所在的类等信息。
__Data(数据段)部分:
- __const:常量
- __data:存储已经初始化的可变数据
- __bss:存储未初始化的全局变量和局部静态变量
- __objc_classname:存储OC的类名。
- __objc_clalsslist:OC方法列表
- __objc_protocollist:协议列表
- _la_symbol_ptr:懒绑定符号指针表 (对应还有一个nl_symbol_pt:非 懒绑定 的指针表)
- _objc_nlclslist:实现了+load方法的 Objective-C类列表。
- _ objc_catalist:OC分类( Category)列表。
- objc classes:OC类引用列表
- _objc_protore:OC协议引用列表
总结
本文我们主要看了Mach-O文件的结构主要包括:
- Header:用于快速确定该文件的CPU类型、文件类型
- Load Commands:指示加载器如何设置并加载二进制数据
- Data:存放数据,例如代码、数据、字符串常量、类、方法等。
我们研究Mach-O文件有一些应用(后面有机会再详细写),包括但不限于以下几点:
- 研究fishhook原理,它利用了
_la_symbol_ptr
等方面原理,在加载时实现C语言方法的替换。 - 包体积优化,查找未使用的类(objc_classlist和_objc classes比较)
- load方法使用的检测(_objc_nlclslist)。