整体结构

Mach-O的整体结构如下:

整体结构

2 Header

Mach-O最顶部是头信息。

头信息定义了这个Mach-O的基本信息,比如这个Mach-O使用的CPU架构、文件类型等。

头信息定义在XNU源码macho/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 */
};
  • magic

magic使用的常量为MH_MAGIC_64MH_CIGAM_64

对这两个值常见的一个误解是,以为MH_MAGIC_64代表Mach-O使用大端字节序,MH_CIGAM_64使用小端字节序

而实际上是,如果Mach-O使用的字节序和后面cputype指定的CPU使用的字节序一样,那么就是MH_MAGIC_64

反之,如果Mach-O使用的字节序和后面cputype指定的CPU使用的字节序相反,那么就是MH_CIGAM_64

  • cputype

cputype表示这个Mach-O文件要运行的CPU类型,比如是ARM64还是X86_64

注意到这个字段的类型是cpu_type_t,它的定义如下:


// mach/machine.h
typedef integer_t cpu_type_t;
typedef integer_t cpu_subtype_t;
typedef integer_t cpu_threadtype_t;
// mach/arm/vm_types.h
typedef int integer_t;

从上面代码可以看到,cpu_type_t实际就是一个int类型。

  • cpusubtype

定义了CPU的子类型,比如CPU_SUBTYPE_ARM64_ALL

  • filetype

常见的值如下:

MH_OBJECT表明当前是一个.o文件。

MH_EXECUTE表明当前是一个可执行文件。

MH_DYLIB表明当前是一个动态链接库。

MH_PRELOAD这个类型已经废弃了。

MH_CORE表明当前是一个core文件。

程序崩溃后产生core文件,后续可以直接调试core文件定位问题,但是iOS应用不会产生这个文件类型。

MH_DYLINKER表明当前是一个动态链接器,dyld就是这个类型。

MH_DSYM表明当前是一个.dsym符号文件。

  • ncmds

文件后后面LC_Command的个数

  • sizeofcmds

所有LC_Command占用的字节数大小。

  • flags

常见的flags值如下:

MH_NOUNDEFS表明当前Mach-O内没有未定义的引用。

MH_DYLDLINK表明当前Mach-O只能作为动态连接器的输入,不能再进行静态链接,可执行文件有这个标志。

MH_TOWLEVEL表明当前Mach-O使用二级命名空间,也就是说每一个外部符号都会记录它来自哪个库,避免名称冲突。

MH_PIE表明当前Mach-O会使用ASLR

3 Load Command

紧接着Mach-O头信息的是一系列Load Command

Load Command的种类很多,所有的Load Command开头的结构都是一样的:


// mach-o/loader.h
struct load_command {
uint32_t cmd; /* type of load command */
uint32_t cmdsize; /* total size of command in bytes */
};
  • cmd

表明当前Load Command的类型。

  • cmdsize

不同的Load Command大小的计算方式不一样。

但是无论如何,对于64bit机器上的Load Command,需要8bytes对齐

下面就来看下常见的Load Command

3.1 segment_command_64

segment_command_64定义了一个段SegmentMach-O中的偏移,以及这个段加载到虚拟内存后的地址:


// mach-o/loader.h
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

设置为LC_SEGMENT_64

  • cmdsize

当前这个Load Command的大小,计算方式为:


cmdsize = sizeof(segment_command_64) + sizeof(section_64) * nsects

从计算公式上可以看到,LC_SEGMENT_64的大小除了包含自身结构体,还包含它下面的节section_64结构体的大小。

  • segname

段名,按照约定,段名都得是大写,比如__TEXT

  • vmaddr

当前段加载到虚拟内存后的地址,由于ASLR的存在,实际地址为vmaddr + ASLR

  • vmsize

Segment所占用的虚拟内存的大小,必须对齐内存页

对于iOS,内存页的大小为16KB

也就是说,段Segmentvmaddr最低4bit必须是0

  • fileoff

LC_SEGMENT_64对应的段SegmentMach-O文件中的偏移量。

  • filesize

LC_SEGMENT_64对应的段SegmentMach-O文件中占用的磁盘大小。

  • maxprot

LC_SEGMENT_64对应的段Segment在内存中最大的保护设置,比如只读VM_PROT_READ

  • initprot

LC_SEGMENT_64对应的段Segment在内存中的初始保护设置。

  • nesects

LC_SEGMENT_64对应的段Segment中包含的节section的数量。

  • flags

通常为0

一个Mach-O文件中,包含的常见Segment如下:

__TEXTSegment包含程序代码或者一些只读数据,比如C字符串。

__DATASegment包含程序数据。

__LINKDITSegment包含符号表、字符串表、间表等。

在一个.o文件中,所有的section_64都位于一个匿名LC_SEGMENT_64下面:

image

静态连接器最后会将不同的section放到对应的Segment

3.1.1 section_64

一个LC_SEGMENT_64结构后面,可能会跟着0个或者多个section_64结构体:


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 */
};
  • sectname

节名,按照约定,节名都是小写字母,比如__text

  • segname

节所在的段名。

  • addr

节被加载到虚拟内存后的地址,实际地址为addr + ASLR

  • size

节所占用的虚拟内存大小。

对于一些特殊的节比如__bss,可能磁盘占用大小为0,但是内存大小size不为0

  • offset

当前节在Mach-O文件中的偏移量。

  • align

节的内存对齐要求,如果值为3,代表是2^3,也就是8字节对齐。

  • reloff

与重定位Relocation有关。

.o中会有一个重定位表,refloff表示重定向表中,第一个属于这个节需要定位的项在Mach-O中的偏移。

也就是说,通过这个字段,可以在重定位表中找到这个节所有需要重定位的项。

我们可能会经常遇到重定位(Relocation)、重基址(Rebase)和绑定(Bind)。

这三者的区别是:

重定位(Relocation)发生在静态链接期间,由静态连接器ld完成。

一个.o文件会调用另一个.o文件中函数,编译期间前者并不知道后者的正确地址,只会使用一个占位地址。

静态链接器ld在合并多个.o文件成为可执行文件时,将上面的占位地址替换成正确的地址。

重基址(Rebase)发生在可执行文件加载时,由动态链接器dyld完成。

其实就是因为ASLRdyld需要对可执行文件中的地址进行重新修复。

绑定(Bind)由动态链接器dyld完成,它将可执行文件中指向外部动态库中函数的占位地址替换成真正的地址。

  • nreloc

表示这个节需要重定向的个数。

  • flags

这个字段被分成了2部分。

8bit定义了这个节的类型,高24bit定义了这个节的属性。

常见 Section 类型

  • S_REGULAR

表示这节是一个普通的节,比如__TEXT,__text

  • S_ZEROFILL

表示这个节初始时会被填入0,比如__DATA,__bss

  • S_CSTRING_LITERAL

表示这个节只包含C字符串。

  • S_LITERAL_POINTERS

表示这个节只包含常量指针,比如__DATA,__objc_selrefs

  • S_LAZY_SYMBOL_POINTERS

表示这个节包含的都是延迟绑定的指针,也就是只有第一次访问时才由动态连接器dyld绑定真正的地址。

之所以需要动态连接器dyld绑定,是因为动态库的存在。

如果一个可执行文件调用了某个动态库中的外部函数,比如系统C库中的print函数,这个外部函数print的地址在可执行文件编译链接期间是无法知道的。

这是因为动态库在每次操作系统加载它时,位于虚拟内存中的地址是不固定的。

因此,静态连接器ld在链接期间只能给print函数一个占位地址,并且标记它需要在运行时由动态连接器dyld绑定。

由于动态连接器dyld绑定的过程需要进行符号查找,为了加快可执行App启动速度,就产生了延迟绑定技术。

但是在iOS >= 15上由于dyld使用Chained Fixup技术,已经取消了延迟绑定,都是非延迟绑定。

非延迟绑定,就是在App启动时,所有外部地址都已经由dyld绑定好了,不用等到第一次访问这个外部地址。

  • S_NON_LAZY_SYMBOL_POINTERS

表示这个节包含的都是非延迟绑定的指针,这些指针的地址在启动时就已经由动态链接器dyld绑定好了。

  • S_SYMBOL_STUBS

表示这个节只包含Stubs函数,比如__TEXT,__stubs

每一个Stubs函数都是一段很简单的汇编代码,与延迟绑定有关。

每个Stubs函数都会获取一个S_LAZY_SYMBOL_POINTERS节中需要延迟绑定的指针,然后跳转到__TEXT,__stub_helper节中的汇编函数,调用dyld来进行符号绑定。

但是,在iOS >= 15上由于已经取消了延迟绑定,已经没有__TEXT,__stub_helper节了。

因此,在iOS >= 15上,Stubs函数都是非延迟绑定的,直接跳转到对应的外部函数。

  • S_COALESED

用于处理重复符号的定义,确保在静态链接后,多个.o合并之后,只有一份代码。

举个例子,在C++中不同源文件可能对同一个模版进行相同的实例化,这样导致编译后不同.o包含多个重复的代码。

将它们标记为S_COALESED后,静态连接器就会进行合并(Coalesed),只会保留一份代码。

常见 Section 属性

  • S_ATTR_PRUE_INSTRUCTIONS

表示本节包含的只有可执行代码,比如__TEXT,__text

  • S_ATTR_SOME_INSTRUCTIONS

表示本节包含有可执行代码。

S_ATTR_NO_DEAD_STRIP告诉静态链接器ld,不管本节内容有没有被引用到,都不能被删除。

  • S_ATTR_LIVE_SUPPORT

告诉静态链接器ld,只有当本节引用的某些代码是"活"的,它自己才存活,否则就会被删除掉。

  • S_ATTR_STRIP_STATIC_SYMS

告诉静态连接器ld,移除由static定义的静态符号。因为它们只在当前文件中可见,移除后可以减少符号表体积。

  • reserved1

这个字段通常是0,只在某些特别的节有用:

S_SYMBOL_STUBS节,比如__TEXT,__stubs

S_LAZY_SYMBOL_POINTERS节,比如__DATA,__la_symbol_ptr

S_NON_LAZY_SYMBOL_POINTERS节,比如__DATA_CONST,__got

在这些节中,reserve1表示当前节中的第一个stub或者pointer在间接符号表中的索引。

间接符号表存储的是符号表中的索引。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐