概览
本次来学习下Mach-O文件的格式,Mach-O(Mach Object File Format) 是针对不同运行时可执行文件的文件类型。
文件类型:
Executable: 应用的主要二进制
Dylib: 动态链接库(又称 DSO 或 DLL)
Bundle: 不能被链接的 Dylib,只能在运行时使用 dlopen() 加载,可当做 macOS 的插件。
Mach-O 文件格式如下:
如何查看文件格式:
我们可以通过file指令查看文件的具体格式:
1 | $ file asdasd |
目前已知的架构分为armv7,armv7s,arm64,i386,x86_64等等,MachO中其实也是这些架构的集合。MachO可以是多架构的二进制文件,称之为「通用二进制文件」
拆分、重组MachO
1 | # 使用lipo -info 可以查看MachO文件包含的架构 |
Mach-O文件格式
新建一个单页面工程 使用 MachOView 来查看.app包内的mach-o文件:
结合可知 Mach-O 文件包含了三部分内容:
- Header(头部),指明了 cpu 架构、大小端序、文件类型、Load Commands 个数等一些基本信息
- Load Commands(加载命令),正如官方的图所示,描述了怎样加载每个 Segment 的信息。在 Mach-O 文件中可以有多个 Segment,每个 Segment 可能包含一个或多个 Section。
- Data(数据区),Segment 的具体数据,包含了代码和数据等。
需要注意的是,不仅仅是可执行文件是Macho-O,目标文件(.o)以及动态库,静态库都是Mach-O格式。
Headers
Mach-O 文件的头部定义如下 loader.h:
1 | // user/include / mach-o / loader.h |
从上图中看出asdasd
这个文件header对应的是 MH_MAGIC_64
、CPU_TYPE_X86_64_ALL
、MH_EXECUTE
…等
filetype的定义有:
1 | // user/include / mach-o / loader.h |
和部分flags定义:
1 |
|
Mach-O 文件头主要目的是为加载命令提供信息。加载命令过程紧跟在头之后,并且 ncmds 和 sizeofcmds 来能个字段将会用在加载命令的过程中。
Load Commands
Headers之后就是Load Commands,其占用的内存和加载命令总数已经在Header中 ncmds/sizeofcmds 中指出。
load commands的定义如下:
1 | // user/include / mach-o / loader.h |
cmd 字段指出了 command 类型,主要有:
1 | // user/include / mach-o / loader.h |
LC_SEGMENT_64和LC_SEGMENT是加载的主要命令,它负责指导内核来设置进程的内存空间。一般Mach-O文件有多个段(Segement),段每个段有不同的功能,一般包括:
- __PAGEZERO: 空指针陷阱段,映射到虚拟内存空间的第一页,用于捕捉对NULL指针的引用;
- __TEXT: 包含了执行代码以及其他只读数据。该段数据的保护级别为:VM_PROT_READ(读)、VM_PROT_EXECUTE(执行),防止在内存中被修改;
- __DATA: 包含了程序数据,该段可写;
- __LINKEDIT: 链接器使用的符号以及其他表
Segment & Section
Segment定义如下:
1 | // user/include / mach-o / loader.h |
其中 segname 在源码中定义的宏,有:
1 |
部分的 Segment (主要指的 __TEXT
和 __DATA
)可以进一步分解为 Section。之所以按照 Segment -> Section 的结构组织方式,是因为在同一个 Segment 下的 Section,可以控制相同的权限,也可以不完全按照 Page 的大小进行内存对其,节省内存的空间。而 Segment 对外整体暴露,在程序载入阶段映射成一个完整的虚拟内存,更好的做到内存对齐(可以继续参考 OS X & iOS Kernel Programming 一书的第一章内容)
Section定义如下
1 | // user/include / mach-o / loader.h |
下面列举一些常见的 Section。
Section | 用途 |
---|---|
__TEXT.__text |
主程序代码 |
__TEXT.__cstring |
C 语言字符串 |
__TEXT.__const |
const 关键字修饰的常量 |
__TEXT.__stubs |
用于 Stub 的占位代码,很多地方称之为桩代码。 |
__TEXT.__stubs_helper |
当 Stub 无法找到真正的符号地址后的最终指向 |
__TEXT.__objc_methname |
Objective-C 方法名称 |
__TEXT.__objc_methtype |
Objective-C 方法类型 |
__TEXT.__objc_classname |
Objective-C 类名称 |
__DATA.__data |
初始化过的可变数据 |
__DATA.__la_symbol_ptr |
lazy binding 的指针表,表中的指针一开始都指向 __stub_helper |
__DATA.__nl_symbol_ptr |
非 lazy binding 的指针表,每个表项中的指针都指向一个在装载过程中,被动态链机器搜索完成的符号 |
__DATA.__const |
没有初始化过的常量 |
__DATA.__cfstring |
程序中使用的 Core Foundation 字符串(CFStringRefs) |
__DATA.__bss |
BSS,存放为初始化的全局变量,即常说的静态内存分配 |
__DATA.__common |
没有初始化过的符号声明 |
__DATA.__objc_classlist |
Objective-C 类列表 |
__DATA.__objc_protolist |
Objective-C 原型 |
__DATA.__objc_imginfo |
Objective-C 镜像信息 |
__DATA.__objc_selfrefs |
Objective-C self 引用 |
__DATA.__objc_protorefs |
Objective-C 原型引用 |
__DATA.__objc_superrefs |
Objective-C 超类引用 |
__TEXT.__text
这里存放的是汇编后的代码,当我们进行编译时,每个.m文件会经过预编译->编译->汇编形成.o文件,称之为目标文件。汇编后,所有的代码会形成汇编指令存储在.o文件的(TEXT,text)区((DATA,data)也是类似)。链接后,所有的.o文件会合并成一个文件,所有.o文件的(TEXT,text)数据都会按链接顺序存放到应用文件的(TEXT,text)中。
__DATA.__data
存储数据的section,static在进行非零赋值后会存储在这里,如果static 变量没有赋值或者赋值为0,那么它会存储在__DATA.__bss
中。
另外还有 Symbol Table
符号表,这个是重点中的重点,符号表是将地址和符号联系起来的桥梁。符号表并不能直接存储符号,而是存储符号位于字符串表的位置。
String Table
字符串表所有的变量名、函数名等,都以字符串的形式存储在字符串表中。
实践:关联类的方法名
上面说了一大堆定义,来查看使用Mach-O文件呢。这里来看一下如何用 MachO 文件关联类的方法名。
先来看看 Load Commands
里的表示类名的 __objc_classname
:
根据 offset字段 0000425F
值可以找到 section中对应的 __TEXT,__objc_classname
信息:
同理可以找到对应方法名的 __objc_methname
和 表示类虚拟地址的 __objc_classlist
:
其中我们需要查看类的方法名信息就在__objc_classlist
中,根据上图中 _OBJC_CLASS_$_ViewController
对应的data : 00000001000060E8
,可以在__DATA,__objc_data
中找到类结构信息:
其中data 是我们感兴趣的,它指向 class_ro_t, class_ro_t 存储了类在编译器就确定的属性、方法、协议等。根据data的值 0x100005370
就找到了__DATA,__objc_const
中:
其中baseMethods 0x100005350
指向了第一个方法。这样我们就就找到了类的方法名。
使用命令查看Mach-O信息
上文是借助 MachOView 这个工具 和 loader.h
来了解Mach-O文件的大概定义,现在我们来使用size
和otool
查看一下Mach-O文件。
如下这样一个 hello.c文件:
1 |
|
使用clang生成Mach-O文件:
1 | #生成 hello.out 的 Mach-O 二进制文件 |
可以通过 file
命令来查看简要的架构信息:
1 | file hello.out |
一、 Section
就像我们上面提到的一样,这里有些东西叫做 section。一个可执行文件包含多个段,也就是多个 section。可执行文件不同的部分将加载进不同的 section,并且每个 section 会转换进某个 segment 里。这个概念对于所有的可执行文件都是成立的。
我们来看看 hello.out 二进制中的 section。我们可以使用 size 工具来观察
1 | size -x -l -m hello.out |
如上代码所示,我们的 hello.out
文件有 4 个 segment。有些 segment 中有多个 section。
当运行一个可执行文件时,虚拟内存 (VM - virtual memory) 系统将 segment 映射到进程的地址空间上。映射完全不同于我们一般的认识,如果你对虚拟内存系统不熟悉,可以简单的想象虚拟内存系统将整个可执行文件加载进内存 – 虽然在实际上不是这样的。VM 使用了一些技巧来避免全部加载。
当虚拟内存系统进行映射时,segment 和 section 会以不同的参数和权限被映射。
上面的代码中,__TEXT segment 包含了被执行的代码。它被以只读和可执行的方式映射。进程被允许执行这些代码,但是不能修改。这些代码也不能对自己做出修改,因此这些被映射的页从来不会被改变。
__DATA segment 以可读写和不可执行的方式映射。它包含了将会被更改的数据。
第一个 segment 是 __PAGEZERO。它的大小为 4GB。这 4GB 并不是文件的真实大小,但是规定了进程地址空间的前 4GB 被映射为 不可执行、不可写和不可读。这就是为什么当读写一个 NULL 指针或更小的值时会得到一个 EXC_BAD_ACCESS 错误。这是操作系统在尝试防止引起系统崩溃。
在 segment中,一般都会有多个 section。它们包含了可执行文件的不同部分。在 TEXT segment 中,text section 包含了编译所得到的机器码。stubs 和 stub_helper 是给动态链接器 (dyld) 使用的。通过这两个 section,在动态链接代码中,可以允许延迟链接。const (在我们的代码中没有) 是常量,不可变的,就像 cstring (包含了可执行文件中的字符串常量 – 在源码中被双引号包含的字符串) 常量一样。
DATA segment 中包含了可读写数据。在我们的程序中只有 nl_symbol_ptr 和 __la_symbol_ptr,它们分别是 non-lazy 和 lazy 符号指针。延迟符号指针用于可执行文件中调用未定义的函数,例如不包含在可执行文件中的函数,它们将会延迟加载。而针对非延迟符号指针,当可执行文件被加载同时,也会被加载。
在 _DATA segment 中的其它常见 section 包括 const,在这里面会包含一些需要重定向的常量数据。例如 char * const p = “foo”; – p 指针指向的数据是可变的。bss section 没有被初始化的静态变量,例如 static int a; – ANSI C 标准规定静态变量必须设置为 0。并且在运行时静态变量的值是可以修改的。common section 包含未初始化的外部全局变量,跟 static 变量类似。例如在函数外面定义的 int a;。最后,dyld 是一个 section 占位符,被用于动态链接器。
二、Section 中的内容
可以通过 otool
来了解section中的内容
1 | otool -s __TEXT __text hello.out |
我们还可以通过添加 -v 来查看反汇编代码 , 由于 -s TEXT text 很常见,otool 对其设置了一个缩写 -t :
1 | otool -v -t hello.out |
上面的内容是一样的,只不过以反汇编形式显示出来。你应该感觉很熟悉,这就是我们在前面编译时候的代码。唯一的不同就是,在这里我们没有任何的汇编指令在里面。这是纯粹的二进制执行文件。