一个iOS程序员的自我修养(二)Mach-O里面有什么

Mach-O

Mach-O 文件网上介绍的比较多,可是大多数都只是介绍了文件内的结构,并无说明为何会以这样的结构排布。经过阅读《程序员的自我修养》一书,结合 MachOView 工具,从新梳理一下 Mach-O 文件。程序员

除了 iOS 系统的 Mach-O,与之对应的还有 Windows 下的 PE 和 Linux 下的 ELF。它们都是基于一种叫作 COFF 文件的变种,它的主要贡献是引入了“段”的机制,咱们编写的应用程序正是被以这种段的形式存储在 Mach-O 中。Mach-O 中除了包含机器代码指令和数据,还包括符号表、调试信息、字符串表等等,它们都被以段的形式存储。苹果官方描述 Mach-O 的结构以下:缓存

下面会经过这个图逐步展开分析 Mach-O 的内部细节,Mach-O 总体分为三部分:markdown

  • Header:最前面的部分是 Mach-O 文件头,用来描述文件版本、目标机器型号、程序入口等信息。
  • Load commands: 多个 Segment 组成,每一个 Segment 又包含了多个相同类型的 Section。为什么叫加载命令,由于它是用来被系统加载使用的。
  • Data: 被 Load commands 描述的各个 Section,包括编写的指令代码,定义的常量变量等,还包括符号表,字符串表等等其余咱们比较熟悉的段,也就是说咱们所写的应用程序会被拆分红一个个的 Section 存储在 Mach-O 文件中。

那么 Mach-O 为何要以这种“段”的形式存储呢?其实这种分段的好处有不少:架构

  1. 每一个段能够根据它们的读写权限被映射到不一样的内存区域,例如程序的指令是可读的,因此会被映射到可读区域,这样能够防止程序的指令被有意或无心的改写。
  2. 对于现代的 cpu 来讲,它们有着强大的缓存体系,按段的形式存放对缓存命中提升有好处。
  3. 当系统中运行着多个该程序的副本时,它们的指令都是同样的,因此内存中的只读数据只保存一份,能够节省大量的内存。

Header

下面以蚂蚁财富的可执行文件为例,经过 MachOView 来看下内部具体细节: ide

因为 Data 中段比较多这里只截取了部分。工具

Mach-O 文件头结构及相关常数被定义在 “/usr/include/mach-o/loader.h” 文件中,由于 5s 以后的版本 cpu 架构都是 64 位,这里以 64 位版本为例来看一下它的结构体定义:post

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:一个叫作 “魔数” 的字段,标识了 Mach-O 文件的格式。
  • cputype:cpu 类型。
  • cpusubtype:机器标识符。
  • filetype:文件类型。
  • ncmds:Load Commands 的数量。
  • sizeofcmds:Load Commands 大小。
  • flags:动态连接器标识。
  • reserved:保留字段。

单纯的看这个结构体不太好理解,下面再经过 MachOView 看一下蚂蚁财富 App 的 Header 构成: ui

Header 部分主要是对 Mach-O 文件的描述,从上图中能够看出,Header 的最后一个字段相对于文件开始的偏移量为 0x1c,reserved 字段虽然没有值,但它仍然占用了 4 个字节,这样整个 Header 一共占用了 32 个字节。Mach-O 文件被系统装载的时候,会先读出 Header 部分,经过 Header 就能够找到 Load Commands 加载指令部分,读取到加载指令就能够加载到咱们编写的代码了。在 Header 中有个重要的字段 sizeofcmds 用来表示 Load Commands 的大小,经过它就能够找到 Load Commands 的位置。this

Load Command

Load Command 是 Mach-O 文件中除了文件头之外最重要的结构,它描述了 Data 中的各个段信息,好比每一个段的段名、段的长度、在文件中的偏移、读写权限以及段的其余属性,它的位置要由 Header 的大小决定,下面是 Load Command 的起始位置和结束位置:spa

Load Command 的起始偏移和结束偏移分别为 0x200x2380。0x20 换算成 10 进制是 32,正好是 Header 的大小,0x23800x20 差值正好是 Header 中 Size of Load Commands 的值 0x2368,由此也验证了 Load Command 的位置是紧随 Header 后面的。

在 MachOView 中 Load Command 的主要结构以下:

Load Command 由多个 Segment 构成,一个 Segment 包含一个或多个属性相似的 Section。关于 Segment 结构的定义也能够在 “/usr/include/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:Segment 的类型,和下面的 flags 标记位决定着这个段如何被装载。
  • segname:段名。
  • vmaddr:当前段在虚拟内存起始地址。
  • vmsize:当前段在虚拟内存地址占用的长度。
  • fileoff:在文件中的偏移。
  • filesize:在文件中的长度。
  • nsects:包含section 的个数。
  • flags:标记位,表示在进程虚拟地址空间中的属性,好比是否可写、是否可执行等。

vmaddrvmsize 是在应用程序被加载进虚拟内存用到的,在将 Mach-O 加载到虚拟内存的时候,会在虚拟内存上的 vmaddr 位置开始,取出 vmsize 大小的空间来存放这个段。然而在 Section 内就不存在这两个字段,由于这两个字段是给装载用的,这也是 Segment 和 Section 最主要的区别。

Data

Data 中存放的全部的 Section,例如机器指令,全局变量和局部静态变量,符号表,调试信息等都会被存储到对应的 Section 中:

Mach-O 被装载的时候会经过 Segment 寻找对应的 Section,在 Load Commands 中 经过 Segment 能够直接找到每一个 Section 的位置和大小,例如上图 _text 段在文件中的偏移为 0x40E0,这与它在 Segment 中的 offset 是一致的,以下图红框内所示:

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 */
};
复制代码

关于 sectname 特别说明一下,它表示了这个 Section 存放了哪些信息,下面列举一些:

  • __text:可执行的机器码。
  • __cstring:一些C字符串。
  • __const:常量。
  • __data:存储初始化的可变数据。
  • __bss:存储未初始化的全局变量和局部静态变量。
  • __objc_clasname:存储 OC 类名。
  • __objc_classlist:方法列表。
  • __objc_protocollist:协议列表。

Section 和 Segument

上面关于 Section 和 Segment 的主要区别没有细说,既然有了 Section,为何还要有 Segment ?

这块涉及到内存分页加载的概念,以前很火的抖音的二进制重排也是利用这种分页加载的机制,Mach-O 被虚拟内存加载的时候是以页为单位,在 iOS 上,一页的大小被划定为 16kb,每一个 Section 在映射时都是系统页长度的整数倍,不足一个页的部分也会占用一个页,这样在 Section 增多后会带来大量内存碎片。Segment 是在装载的角度从新划分了 Mach-O 的各个段,对于相同权限的 Section,把它们合并到一块儿做为一个 Segment 进行映射。从目标文件连接的角度看,Mach-O 文件是按照 Section 存储的,但从装载的角度看,它是按照 Segment 划分的,到这里就应该很容易的理解上面苹果官方给出的 Mach-O 结构图了。

引用

《程序员的自我修养》

相关文章
相关标签/搜索