Mach-O(二)内存分布

一、引子:一个实例

加载命令内存地址空间分析中,使用一个简单的程序,来说明整个流程。如下程序a:

编辑一个源文件:a.c

1
2
3
4
5
6
void main(int argc, char const *argv[])
{
printf("Save, Munde!\n");
printf("Vale\n");
exit(0);
}

编译:

1
2
3
4
5
//新Xcode版本支持clang编译
$ clang a.c -o a

//或者在老的Xcode版本上
//$ gcc -Wall -o a a.c

整个编译流程,参考iOS 编译与链接

上一节,我们分析了Mach-O的大致结构,现在我们需要更进一步,来观察Mach-O的详细内容。

我们先从Command分类说起,在<EXTERNAL_HEADERS/mach-o/loader.h>定义了下面几个段,这里忽略了一些其他段:

1
2
3
4
5
#define	SEG_PAGEZERO	"__PAGEZERO" /* 当时 MH_EXECUTE 文件时,无保护且用于捕获到空指针 */
#define SEG_TEXT "__TEXT" /* 代码/只读数据段 */
#define SEG_DATA "__DATA" /* 数据段 */
#define SEG_OBJC "__OBJC" /* Objective-C runtime 段 */
#define SEG_LINKEDIT "__LINKEDIT" /* 包含需要被动态链接器使用的符号和其他表,包括符号表、字符串表等 */

当代码运行后,可执行文件会将代码载入内存,那么内存中的地址是怎样的?

源码在这儿,通过Hopper以及MachOExplore分析,得到如下部分:

二、常见的段

__PAGEZERO

空指针陷阱,不占用任何磁盘空间,filesize为0。

在32位系统上,这是内存中单独的一个页面(4KB),而且该页面所有的访问权限都被撤销。

在64位上,这个段对应了一个完整的32位地址空间——即4GB。

这个段有助于捕捉空指针引用(因为空指针实际上就是0),或捕获整数当做指针引用(因为32位下4095以下的值,以及64位4GB以下的值都在这个范围内)。也用来捕获指针截断。

由于该段任何权限都被取消了(不可执行、不可写和不可读),所有任何解引用操作都会引发来自MMU的硬件页错误,进而产生一个内核可以捕捉的陷阱。内核将这个陷阱转换为C++异常或表示总线错误的POSIX信号(SIGBUGS)。

1
2
3
4
5
6
7
8
9
10
11
12
Load command 0
cmd LC_SEGMENT_64
cmdsize 72
segname __PAGEZERO
vmaddr 0x0000000000000000
vmsize 0x0000000100000000
fileoff 0
filesize 0
maxprot ---
initprot ---
nsects 0
flags (none)

__TEXT

包含了被执行的代码。它被以只读和可执行的方式映射。进程被允许执行这些代码,但是不能修改。这些代码也不能对自己做出修改,因此这些被映射的页从来不会被改变。

还可以通过共享该只读段来优化内存,代码存放在__text区。

另外,该段还包括:常量、硬编码的字符串。

下面逐步观察这些区:

1
2
3
4
5
6
LC 01: LC_SEGMENT_64          Mem: 0x100000000-0x100001000	__TEXT
Mem: 0x100000f30-0x100000f6b __TEXT.__text (Normal)
Mem: 0x100000f6c-0x100000f78 __TEXT.__stubs (Symbol Stubs)
Mem: 0x100000f78-0x100000f9c __TEXT.__stub_helper (Normal)
Mem: 0x100000f9c-0x100000fb0 __TEXT.__cstring (C-String Literals)
Mem: 0x100000fb0-0x100000ff8 __TEXT.__unwind_info

或许直接通过size来观察大小:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ size -x -l -m a                                                                      
Segment __PAGEZERO: 0x100000000 (vmaddr 0x0 fileoff 0)
Segment __TEXT: 0x1000 (vmaddr 0x100000000 fileoff 0)
Section __text: 0x3b (addr 0x100000f30 offset 3888)
Section __stubs: 0xc (addr 0x100000f6c offset 3948)
Section __stub_helper: 0x24 (addr 0x100000f78 offset 3960)
Section __cstring: 0x14 (addr 0x100000f9c offset 3996)
Section __unwind_info: 0x48 (addr 0x100000fb0 offset 4016)
total 0xc7
Segment __DATA: 0x1000 (vmaddr 0x100001000 fileoff 4096)
Section __nl_symbol_ptr: 0x10 (addr 0x100001000 offset 4096)
Section __la_symbol_ptr: 0x10 (addr 0x100001010 offset 4112)
total 0x20
Segment __LINKEDIT: 0x1000 (vmaddr 0x100002000 fileoff 8192)
total 0x100003000

继续观察每一个段内部的区:

1
2
3
4
5
$ otool -v -s __TEXT __cstring a                                        
a.o:
Contents of (__TEXT,__cstring) section
000000000000003b Save, Munde!\n
0000000000000049 Vale\n

__DATA

包含了程序数据,以可读写和不可执行的方式映射,可读/可写数据存放段。

1
2
3
LC 02: LC_SEGMENT_64          Mem: 0x100001000-0x100002000	__DATA
Mem: 0x100001000-0x100001010 __DATA.__nl_symbol_ptr (Non-Lazy Symbol Ptrs)
Mem: 0x100001010-0x100001020 __DATA.__la_symbol_ptr (Lazy Symbol Ptrs)

在程序中只有 __nl_symbol_ptr__la_symbol_ptr

  • __nl_symbol_ptr: lazy 符号指针,延迟符号指针用于可执行文件中调用未定义的函数,例如不包含在可执行文件中的函数,它们将会延迟加载。其第一次被程序使用时绑定,是将与动态链接相关的信息映射到虚拟地址空间。
  • __la_symbol_ptr:non-lazy符号指针,非延迟符号指针,需要在加载时绑定。

__LINKEDIT

由dyld使用的符号以及其他表,链接的部分,支持dyld,包含了一些符号表等数据。

1
LC 03: LC_SEGMENT_64          Mem: 0x100002000-0x100003000	__LINKEDIT

其他段

  • LC_SYMTAB : 符号表信息
  • LC_DYSYMTAB :动态符号表信息
  • LC_LOAD_DYLINKER : 加载动态链接器(/usr/lib/dyld),使用Mach-O文件的时候链接器,可以看到name为 /usr/lib/dyld 的链接器来加载Mach-O文件。
  • LC_UUID : 对每个Mach-O文件都是唯一标识,crash解析中也会有,去匹配dysm文件和crash文件。
  • LC_VERSION_MIN_IPHONEOS : 二进制文件要求的最低操作系统版本(iOS Deployment Target) ,打开xcode ,输入iOS Development Target 可以查看版本
  • LC_MAIN : 设置程序主线程的入口地址和栈大小
  • LC_ENCRYPTION_INFO : 加密信息,查看文件是否加密(在终端输入otool -l 文件名 | grep cryptid
  • LC_LOAD_DYLIB : 加载的动态库,包括动态库地址和名称,当前版本号,兼容版本号(终端输入`otool -l 文件名)
  • LC_RPATH : 环境变量路径
  • LC_FUNCTION_STARTS : 函数起始地址表
  • LC_CODE_SIGNATURE : 记录可执行文件的代码是否签名

三、常见的区

3.1 汇总表

SECTION USE 实例
__TEXT.__text 包含了主程序代码编译所得到的机器码 lea rax, qword [rbp+var_20]
__TEXT.__stubs Stubs used in dynamic linking。是给动态链接器 (dyld) 使用的,符号桩。本质上是一小段会直接跳入lazybinding的表对应项指针指向的地址的代码。 详见下面截图
__TEXT. __stub_helper 是给动态链接器 (dyld) 使用的辅助函数。上述提到的lazybinding的表中对应项的指针在没有找到真正的符号地址的时候,都指向这。
__TEXT.__cstring C hard-coded strings in the program。程序中硬编码的C字符串 1. “Hello.”(C字符串)
2. “Greeting with %s\n”(C格式符字符串)
__TEXT.__const const keyworded variables and hard coded constants。const的常量,未初始化将会初始化为默认值。 const double weight;
static int a;
__TEXT. __objc_classname Objective-C Class names “MachOViewController”
__TEXT. __objc_methname Objective-C method names “viewDidLoad”
__TEXT.__objc_methtype Objective-C method types “v24@0:8@16”
“@\”UIView\””
__TEXT.unwind_info 用于存储处理异常情况信息
__TEXT.eh_frame 调试辅助信息
__DATA.__data 初始化过的可变数据 详见下面截图
__DATA.__la_symbol_ptr lazy binding 的指针表,表中的指针一开始都指向 __stub_helper。lazy 符号指针,延迟符号指针用于可执行文件中调用未定义的函数,例如不包含在可执行文件中的函数,它们将会延迟加载。其第一次被程序使用时绑定,是将与动态链接相关的信息映射到虚拟地址空间。 _NSLog_ptr:<br/ >0000000100003010 dq _NSLog
__DATA.nl_symbol_ptr 非 lazy binding 的指针表,每个表项中的指针都指向一个在装载过程中,被动态链机器搜索完成的符号。non-lazy符号指针,非延迟符号指针,需要在加载时绑定。 dyld_stub_binder_100003000
0000000100003000 dq dyld_stub_binder
__DATA.__const 常量,不管是否初始化,假如未初始化,默认初始化为0。 //常量const
const int age = 10;
const double weight;
__DATA.__common 包含未初始化的外部全局变量,跟 static 变量类似。 例如在函数外面定义的 int a;
__DATA.__bss BSS,没有被初始化的静态变量。ANSI C 标准规定静态变量必须设置为 0。并且在运行时静态变量的值是可以修改的。 static int a;
__DATA.__cfstring Core Foundation strings (CFStringRefs) in the program cfstring_weng:
0000000100003078 dq __\_CFConstantStringClassReference, 0x7c8, 0x100001a01, 0x4
cfstring_My_first_name_is____:
0000000100003098 dq ___CFConstantStringClassReference, 0x7c8, 0x100001a06, 0x14 ; “My first name is %@\n”, DATA XREF=-[MachOViewController machoAddressTest]+66
__TEXT.__objc_classname Objective-C class names 0000000100003118 dq _OBJC_CLASS_$_MachOViewController
__DATA.__objc_classlist Objective-C class list
__DATA.__objc_protolist Objective-C prototypes 0000000100003130 dq __objc_proto_UIApplicationDelegate_protocol
__DATA.__objc_imginfo Objective-C image information
__DATA.__objc_const Objective-C constants 详见下面截图
__DATA.__objc_selfrefs Objective-C Self (this) references
__DATA.__objc_protorefs Objective-C prototype references
__DATA.__objc_superrefs Objective-C superclass references

段还可以设置一些在<mach/loader.h>中制定的标志。如PROTECTED_VERSION_1表示这个段的页面是“受保护的”,及加密的。苹果通过这种技术加密一些二进制文件。如Finder。

Xcode中包括一个segedit工具,可以提取或替换Mach-O文件中的段,这个工具用于提取内嵌的文本信息,如内核的PRELINK_INFO区。

jtool也可以实现该功能,jtool还提供了另一个Xcode 工具size的功能,用于打印出每一个段的大小和地址。

3.2 示例

3.2.1 __TEXT.__stubs

屏幕快照 2018-10-29 下午12.16.12

3.2.2 __DATA.__data

屏幕快照 2018-10-29 下午2.53.35

3.2.3 __DATA.__objc_const

屏幕快照 2018-10-29 下午2.50.30

下面我们通过两个实验来,一探深浅。

四、实验

4.1 实验:查看程序的地址空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdlib.h>
int global_j;
const int ci = 24;
void main (int argc, char **argv) {
int local_stack = 0;
char *const_data = "This data is constant";
char *tiny = malloc (32); /* allocate 32 bytes */ char *small = malloc (2*1024); /* Allocate 2K */
char *large = malloc (1*1024*1024); /* Allocate 1MB */
printf ("Text is %p\n", main);
printf ("Global Data is %p\n", &global_j);
printf ("Local (Stack) is %p\n", &local_stack);
printf ("Constant data is %p\n",&ci );
printf ("Hardcoded string (also constant) are at %p\n",const_data ); printf ("Tiny allocations from %p\n",tiny );
printf ("Small allocations from %p\n",small );
printf ("Large allocations from %p\n",large );
printf ("Malloc (i.e. libSystem) is at %p\n",malloc );
sleep(100); /* so we can use vmmap on this process before it exits */
}

编译该程序后,运行该程序。

  1. 运行该程序后,通过活动监视器,获取到该进程的pid。
  2. /usr/bin/vmmap pid 查看内存地址分布。
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
$ /usr/bin/vmmap 28171                                                                               
Process: address [28171]
Path: /Users/wenghengcong/Desktop/mach-o/address
Load Address: 0x105b1d000
Identifier: address
Version: ???
Code Type: X86-64
Parent Process: zsh [24039]

Date/Time: 2018-09-10 17:41:47.915 +0800
Launch Time: 2018-09-10 17:41:25.388 +0800
OS Version: Mac OS X 10.13.5 (17F77)
Report Version: 7
Analysis Tool: /Applications/Xcode.app/Contents/Developer/usr/bin/vmmap
Analysis Tool Version: Xcode 9.4.1 (9F2000)

Physical footprint: 348K
Physical footprint (peak): 348K
----

Virtual Memory Map of process 28171 (address)
Output report format: 2.4 -- 64-bit process
VM page size: 4096 bytes

==== Non-writable regions for process 28171
REGION TYPE START - END [ VSIZE RSDNT DIRTY SWAP] PRT/MAX SHRMOD PURGE REGION DETAIL
__TEXT 0000000105b1d000-0000000105b1e000 [ 4K 4K 0K 0K] r-x/rwx SM=COW ...ong/Desktop/mach-o/address
__LINKEDIT 0000000105b1f000-0000000105b20000 [ 4K 4K 0K 0K] r--/rwx SM=COW ...ong/Desktop/mach-o/address
MALLOC metadata 0000000105b22000-0000000105b23000 [ 4K 4K 4K 0K]

此外,可以通过gdb附加到进程。通过:

  • info mach-regions
  • maintenance info section
  • show files

也可以获取上面信息。

更多段与区,参考OS X Assembler Reference

4.2 性能上需要注意的事项

从侧面来讲,__DATA 和 __TEXT segment对性能会有所影响。如果你有一个很大的二进制文件,你可能得去看看苹果的文档:关于代码大小性能指南。将数据移至 __TEXT 是个不错的选择,因为这些页从来不会被改变。

4.3 实验:显示dyldinfo的绑定操作码

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
$ dyldinfo -opcodes a | more                                                          
rebase opcodes:
0x0000 REBASE_OPCODE_SET_TYPE_IMM(1)
0x0001 REBASE_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB(2, 0x00000010)
0x0003 REBASE_OPCODE_DO_REBASE_IMM_TIMES(2)
0x0004 REBASE_OPCODE_DONE()
binding opcodes:
0x0000 BIND_OPCODE_SET_DYLIB_ORDINAL_IMM(1)
0x0001 BIND_OPCODE_SET_SYMBOL_TRAILING_FLAGS_IMM(0x00, dyld_stub_binder)
0x0013 BIND_OPCODE_SET_TYPE_IMM(1)
0x0014 BIND_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB(0x02, 0x00000000)
0x0016 BIND_OPCODE_DO_BIND()
0x0017 BIND_OPCODE_DONE
no compressed weak binding info
lazy binding opcodes:
0x0000 BIND_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB(0x02, 0x00000010)
0x0002 BIND_OPCODE_SET_DYLIB_ORDINAL_IMM(1)
0x0003 BIND_OPCODE_SET_SYMBOL_TRAILING_FLAGS_IMM(0x00, _exit)
0x000A BIND_OPCODE_DO_BIND()
0x000B BIND_OPCODE_DONE
0x000C BIND_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB(0x02, 0x00000018)
0x000E BIND_OPCODE_SET_DYLIB_ORDINAL_IMM(1)
0x000F BIND_OPCODE_SET_SYMBOL_TRAILING_FLAGS_IMM(0x00, _printf)
0x0018 BIND_OPCODE_DO_BIND()
0x0019 BIND_OPCODE_DONE
0x001A BIND_OPCODE_DONE
0x001B BIND_OPCODE_DONE
0x001C BIND_OPCODE_DONE
0x001D BIND_OPCODE_DONE
0x001E BIND_OPCODE_DONE
0x001F BIND_OPCODE_DONE

4.4 符号表

在前面的实例中,printf以及exit,都是外部函数,需要导入相应的库——libSystem。

所有的这些东西都被形象的称之为符号。我们可以把符号看成是一些在运行时将会变成指针的东西。虽然实际上并不是这样的。

每个函数、全局变量和类等都是通过符号的形式来定义和使用的。当我们将目标文件链接为一个可执行文件时,链接器 (ld(1)) 在目标文件和动态库之间对符号做了解析处理。

LC_SYMTAB加载命令制定的symoff找到。对应的符号名称在stroff,总共有nsyms条符号消息。

1
2
3
4
5
6
7
8
9
10
11
12
//otool
Load command 5
cmd LC_SYMTAB
cmdsize 24
symoff 8312
nsyms 5
stroff 8416
strsize 64
//jtool
LC 05: LC_SYMTAB
Symbol table is at offset 0x2078 (8312), 5 entries
String table is at offset 0x20e0 (8416), 64 bytes

如上,有5个符号:

导出具体的符号表:

1
2
3
4
5
6
$ nm -nm a                                                                        
(undefined) external _exit (from libSystem)
(undefined) external _printf (from libSystem)
(undefined) external dyld_stub_binder (from libSystem)
0000000100000000 (__TEXT,__text) [referenced dynamically] external __mh_execute_header
0000000100000f30 (__TEXT,__text) external _main

_exit 该符号是 undefined, externalexternal 的意思是指该符号并不是这个目标文件私有的,相反,non-external 的符号则表示对于目标文件是私有的。

接下来是 _main 符号,它是表示 main() 函数,同样为 external,这是因为该函数需要被调用,所以应该为可见的。

4.5 查看符号和观察加载过程

解析绑定信息,查看二进制文件导出了哪些符号以及链接了哪些库:

1
2
3
4
5
$ dyldinfo -lazy_bind a                                                    
lazy binding information (from lazy_bind part of dyld info):
segment section address index dylib symbol
__DATA __la_symbol_ptr 0x100001010 0x0000 libSystem _exit
__DATA __la_symbol_ptr 0x100001018 0x000C libSystem _printf

对二进制文件反汇编

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ otool -p _main -tV a                                                             
a:
(__TEXT,__text) section
_main:
0000000100000f30 pushq %rbp
0000000100000f31 movq %rsp, %rbp
0000000100000f34 subq $0x20, %rsp
0000000100000f38 leaq 0x5d(%rip), %rax ## literal pool for: "Save, Munde!\n"
0000000100000f3f movl %edi, -0x4(%rbp)
0000000100000f42 movq %rsi, -0x10(%rbp)
0000000100000f46 movq %rax, %rdi
0000000100000f49 movb $0x0, %al
0000000100000f4b callq 0x100000f72 ## symbol stub for: _printf
0000000100000f50 leaq 0x53(%rip), %rdi ## literal pool for: "Vale\n"
0000000100000f57 movl %eax, -0x14(%rbp)
0000000100000f5a movb $0x0, %al
0000000100000f5c callq 0x100000f72 ## symbol stub for: _printf
0000000100000f61 xorl %edi, %edi
0000000100000f63 movl %eax, -0x18(%rbp)
0000000100000f66 callq 0x100000f6c ## symbol stub for: _exit

显示未决的符号:

1
2
3
4
$ nm a | grep "U "                                                                   
U _exit
U _printf
U dyld_stub_binder

表中一共有多少个符号:

1
2
$ nm a | wc -l                                                                       
5

4.6 GDB调试

下面的命令说明下,更多工具:GDB

x/2i:将指定地址的内存导出并反汇编为2条指令。

x/2g:将制定地址的内存能导出为2个64位地址。

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
$ gdb ./a
(gdb) x/2i 0x0000000100000f6c //__TEXT段__stub起始地址
0x100000f6c: jmpq *0x9e(%rip) # 0x100001010
0x100000f72: jmpq *0xa0(%rip) # 0x100001018 //0x100000f72指向printf函数,后面jmpq是该地址的指令
(gdb) x/2g 0x100001010
0x100001010: 0x0000000100000f88 0x0000000100000f92 //**f88、**f92地址都在__stub_helper中:起始地址0x0000000100000f78,size:0x0000000100000024

(gdb) x/2i 0x0000000100000f88
0x100000f88: pushq $0x0
0x100000f8d: jmpq 0x100000f78

(gdb) x/2i 0x0000000100000f92
0x100000f92: pushq $0xc
0x100000f97: jmpq 0x100000f78

(gdb) x/3i 0x100000f78 //以上两个均跳转到0x100000f78
0x100000f78: lea 0x89(%rip),%r11 # 0x100001008
0x100000f7f: push %r11
0x100000f81: jmpq *0x79(%rip) # 0x100001000

(gdb) x/2g 0x100001000 //跳转的地方内容为空
0x100001000: 0x0000000000000000 0x0000000000000000

(gdb) b main //设置埋点
Breakpoint 1 at 0x100000f34

(gdb) r //其实并不是真正运行程序,只是想要看dyld连接完成的结果
Starting program: /Users/wenghengcong/Desktop/mach-o/a
[New Thread 0x2603 of process 13383]
warning: unhandled dyld version (15)

Thread 2 hit Breakpoint 1, 0x0000000100000f34 in main ()

(gdb) x/2g 0x100001000 //再看一下该跳转地址
0x100001000: 0x00007fff754c7178 0x0000000000000000

(gdb) disass 0x0000000100000f34
Dump of assembler code for function main:
0x0000000100000f30 <+0>: push %rbp
0x0000000100000f31 <+1>: mov %rsp,%rbp
=> 0x0000000100000f34 <+4>: sub $0x20,%rsp
0x0000000100000f38 <+8>: lea 0x5d(%rip),%rax # 0x100000f9c
0x0000000100000f3f <+15>: mov %edi,-0x4(%rbp)
0x0000000100000f42 <+18>: mov %rsi,-0x10(%rbp)
0x0000000100000f46 <+22>: mov %rax,%rdi
0x0000000100000f49 <+25>: mov $0x0,%al
0x0000000100000f4b <+27>: callq 0x100000f72 //printf函数地址
0x0000000100000f50 <+32>: lea 0x53(%rip),%rdi # 0x100000faa
0x0000000100000f57 <+39>: mov %eax,-0x14(%rbp)
0x0000000100000f5a <+42>: mov $0x0,%al
0x0000000100000f5c <+44>: callq 0x100000f72
0x0000000100000f61 <+49>: xor %edi,%edi
0x0000000100000f63 <+51>: mov %eax,-0x18(%rbp)
0x0000000100000f66 <+54>: callq 0x100000f6c
End of assembler dump.

参考

链接

Mach-O 文件格式探索

OS X Assembler Reference