Mach-O详解

本文将会结合苹果官方文档,来详细解读Mach-O
本文会说明Mach-O的定义,及设计原理,其中与链接相关的部分原理不会在本文说明,后面会有专门的链接文章,结合demo操作和Mach-O来详细说明。
那么通过本文可以收获什么?

带着问题学习

  1. Mach-O是什么(定义)?
  2. Mach-O的内容是什么?
  3. 为什么要有“段”?

    定义

    Mach-O又称Mach object,是不同运行时规则下的 可执行文件的文件类型。
    关于编译和链接会有另一篇文章来结合Mach object具体说明

    文件类型

    1
    2
    3
    4
    5
    MH_OBJECT	        0x1	    目标文件	
    MH_EXECUTE 0x2 可执行文件
    MH_DYLIB 0x6 动态库
    MH_DYLINKER 0x7 动态连接器
    MH_DSYM 0xa 存储二进制文件符号信息,用于Debug分析

目标文件和可执行文件的区别

假设现在有a.m,b.m两个文件,经过编译之后,会分别产生a.o,和b.o两个文件,此时的a.o和b.o就是目标文件,当经过链接之后,或产生新的文件c.out,此时的c文件就是可执行文件。

内容

已mac 的应用 计算器为例,找到计算器app,右键显示包内容,contents->MacOS->Calculator,使用MachOView打开,如图
注意看第一条,Executable,说明是可执行文件。
对于Mach-O的定义可查看mach-o/loader.h的定义

Mach-O 文件包含三个区域

Mach-O Header:包含字节顺序,magic,cpu 类型,加载指令的数量等
先来看下定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*
* The 64-bit mach header appears at the very beginning of object files for
* 64-bit architectures.
*/
struct mach_header_64 {
uint32_t magic; /* mach magic number identifier */
1. cafebade :跨处理器架构的通用格式
2. feedface :32
3. feadfacf :64

cpu_type_t cputype; /* cpu specifier */CPU类型,比如 arm
cpu_subtype_t cpusubtype; /* machine specifier */对应的具体类型,比如arm64、armv7
uint32_t filetype; /* type of file */ 文件类型,比如可执行文件、库文件、Dsym文件,见<mach-o/loader.h>
uint32_t ncmds; /* number of load commands */加载命令条数
uint32_t sizeofcmds; /* the size of all the load commands */ 命令总size
uint32_t flags; /* flags */
uint32_t reserved; /* reserved */
};

再来看图看图

Load Commands

Load Commands:包含很多内容的表,包括区域的位置,符号表,动态符号表等。每个加载指令包含一个元信息,比如指令类型,名称,在二进制中的位置等。

图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

LINKEDIT:链接器使用段,这里记录了链接器(通常是dyld)需要的信息的位置.

LC_DYLD_INFO_ONLY:记录具体的链接器需要的信息,比如重定向,懒加载,绑定等.

LC_SYMTAB:符号表的信息,记录符号表的位置,偏移量,数据个数等,便于dyld使用

LC_DYSYMTAB:符号表的额外信息,这些信息也会提供给dyld.

LC_LOAD_DYLINKER:该Mach-O使用的链接器信息,记录了具体使用哪个链接器接管内核后续的加载工作,以及链接器的位置信息,通常是dyld.

LC_UUID:Mach-O唯一标识符.

LC_VERSION_MIN_IPHONES:该Mach-O运行的最低系统版本.

LC_SOURCE_VERSION:源代码版本信息.

LC_MAIN:入口地址.dyld会通过这个段去跳转程序的主入口.

LC_ENCRYPTION_INFO_64:加密标识,标识了是否被加密,加密内容的偏移及大小等.

LC_LOAD_DYLIB:依赖库信息,dyld会通过这个段去加载动态库,这个段标注了库的位置以及版本等信息.

LC_RPATH:@rpath的路径信息.

LC_FUNCTION_STARTS:函数起始地址表.

LC_DATA_IN_CODE:代码段非指令的表.

LC_CODE_SIGNATURE:代码签名信息.

其中__TEXT segment 包含被执行的代码以只读和可执行的方式映射。

1
2
3
4
__text section 包含编译后的机器码。
stubs 和 stub_helper 是给动态链接器 dyld 使用,可以允许延迟链接。
__cstring 可执行文件中的字符串。
__const 不可变的常量。

__DATA segment 以可读写和不可执行的方式映射,里面是会被更改的数据。

1
2
3
4
5
6
__nl_symbol_ptr 非延迟指针。可执行文件加载同时加载。
__la_symbol_ptr 延迟符号指针。延迟用于可执行文件中调用未定义的函数,可执行文件里没有包含的函数会延迟加载。
__const 需要重定向的常量,例如 char * const c = “foo”; c指针指向可变的数据。
__bss 不用初始化的静态变量,例如 static int i; ANSI C 标准规定静态变量必须设置为0。运行时静态变量的值是可修改的。
__common 包含外部全局变量。例如在函数外定义 int i;
__dyld 是section占位符,用于动态链接器。

section 的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
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 */

其中 reloffnreloc会表明重定向入口信息

更多 section 类型介绍可以查看苹果文档: OS X Assembler Reference

Data

最大的部分,包含了代码,数据,比如符号表,动态符号表等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

__text: 主程序代码

__stubs, __stub_helper: 用于动态链接的桩

__cstring: 程序中c语言字符串

__const: 常量

__TEXT,__objc_methname:OC方法名称

__TEXT__objc_methtype:OC方法类型

__TEXT__objc_classname:OC类名

__DATA,__objc_classlist:OC类列表

__DATA,__objc_protollist:OC原型列表

__DATA,__objc_imageinfo:OC镜像信息

__DATA,__objc_const:OC常量

__DATA,__objc_selfrefs:OC类自引用(self)

__DATA,__objc_superrefs:OC类超类引用(super)

__DATA,__objc_protolrefs:OC原型引用

__DATA, __bss: 没有初始化和初始化为0 的全局变量

Dynamic Loader Info:动态链接器所需要使用的信息(重定向,符号绑定,懒加载绑定等..)

后续的信息就是函数起始位置,符号表,字符表,代码签名等.

为什么要分段

  1. 数据和指令可以被映射到两个不同的虚拟内存区域。数据区域是可读写的,指令区域是只读可执行。那就可以方便分别设置这两个区域的操作权限。
  2. 两个区域分离,有助于提高缓存的命中率。(提高了程序的局部性)
  3. 最主要是,系统运行多个该程序的副本时,它们指令是一样的,那内存只需要保存一份指令部分,可读写的数据区域进程私有。是不是节约了内存,动态链接那篇也是讲这样的方式来节约内存。