ios优化-包大小分析-MACHO文件解析

导读
在分析linkMap文件的时候,遇到一个有趣的问题:获取类名可以用_objc_classname, 获取方法名可以用_objc_methname。可是怎么将方法名称和对象名称对应起来,程序是如何对应这两部分数据的。带着这个疑问研究了下macho的文件结构。

MACHO文件说明

macho文件是mac os或ios系统可执行文件的格式,系统通过加载这个格式来执行代码。

相关结构如图:


注:来源于:(http://www.jianshu.com/p/f1a61b53398f)

具体每部分的含义可以参考这个定义:

mach-0 loader.h

这里简单讲几个我比较关注的:

注:下面都是以64位做演示说明,cpu结构为arm64。

MachO Header的结构

数据结构为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
* 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 */
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 */
};
  1. 第一个四字节数叫做magic number,可以得到使用的是64位还是32位系统
  2. 第二个字节和第三个字节是CPU类型
  3. 第四个字节是文件类型。MH_EXECUTE表示可执行文件
  4. 第五个字节和第六个字节表示load commands的个数和长度
  5. 第7个字节是加载的flag信息。具体参考loader.h中的文件

MachO load command

程序检索完Header之后就开始加载和解析Load Commands了。

相关代码在mach_loader.c,通过递归调用加载命令。


load_comand的数据结构为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*
* The load commands directly follow the mach_header. The total size of all
* of the commands is given by the sizeofcmds field in the mach_header. All
* load commands must have as their first two fields cmd and cmdsize. The cmd
* field is filled in with a constant for that command type. Each command type
* has a structure specifically for it. The cmdsize field is the size in bytes
* of the particular load command structure plus anything that follows it that
* is a part of the load command (i.e. section structures, strings, etc.). To
* advance to the next load command the cmdsize can be added to the offset or
* pointer of the current load command. The cmdsize for 32-bit architectures
* MUST be a multiple of 4 bytes and for 64-bit architectures MUST be a multiple
* of 8 bytes (these are forever the maximum alignment of any load commands).
* The padded bytes must be zero. All tables in the object file must also
* follow these rules so the file can be memory mapped. Otherwise the pointers
* to these tables will not work well or at all on some machines. With all
* padding zeroed like objects will compare byte for byte.
*/
struct load_command {
uint32_t cmd; /* type of load command */
uint32_t cmdsize; /* total size of command in bytes */
};

每一个command都需要包含

  1. cmd:加载类型
  2. cmdsize:加载的大小

相关的最主要的解析源码在mach_loader.c里的parse_machfile方法里.最主要的代码如下:

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
/*
* Act on struct load_command's for which kernel
* intervention is required.
*/
switch(lcp->cmd) {
case LC_SEGMENT:
if (pass != 2)
break;
if (abi64) {
/*
* Having an LC_SEGMENT command for the
* wrong ABI is invalid <rdar://problem/11021230>
*/
ret = LOAD_BADMACHO;
break;
}
ret = load_segment(lcp,
header->filetype,
control,
file_offset,
macho_size,
vp,
map,
slide,
result);
break;
case LC_SEGMENT_64:
if (pass != 2)
break;
if (!abi64) {
/*
* Having an LC_SEGMENT_64 command for the
* wrong ABI is invalid <rdar://problem/11021230>
*/
ret = LOAD_BADMACHO;
break;
}
ret = load_segment(lcp,
header->filetype,
control,
file_offset,
macho_size,
vp,
map,
slide,
result);
break;
case LC_UNIXTHREAD:
if (pass != 1)
break;
ret = load_unixthread(
(struct thread_command *) lcp,
thread,
slide,
result);
break;
case LC_MAIN:
if (pass != 1)
break;
if (depth != 1)
break;
ret = load_main(
(struct entry_point_command *) lcp,
thread,
slide,
result);
break;
case LC_LOAD_DYLINKER:
if (pass != 3)
break;
if ((depth == 1) && (dlp == 0)) {
dlp = (struct dylinker_command *)lcp;
dlarchbits = (header->cputype & CPU_ARCH_MASK);
} else {
ret = LOAD_FAILURE;
}
break;
case LC_UUID:
if (pass == 1 && depth == 1) {
ret = load_uuid((struct uuid_command *) lcp,
(char *)addr + mach_header_sz + header->sizeofcmds,
result);
}
break;
case LC_CODE_SIGNATURE:
/* CODE SIGNING */
if (pass != 1)
break;
/* pager -> uip ->
load signatures & store in uip
set VM object "signed_pages"
*/
ret = load_code_signature(
(struct linkedit_data_command *) lcp,
vp,
file_offset,
macho_size,
header->cputype,
result);
if (ret != LOAD_SUCCESS) {
printf("proc %d: load code signature error %d "
"for file \"%s\"\n",
p->p_pid, ret, vp->v_name);
ret = LOAD_SUCCESS; /* ignore error */
} else {
got_code_signatures = TRUE;
}
break;
#if CONFIG_CODE_DECRYPTION
case LC_ENCRYPTION_INFO:
case LC_ENCRYPTION_INFO_64:
if (pass != 3)
break;
ret = set_code_unprotect(
(struct encryption_info_command *) lcp,
addr, map, slide, vp,
header->cputype, header->cpusubtype);
if (ret != LOAD_SUCCESS) {
printf("proc %d: set_code_unprotect() error %d "
"for file \"%s\"\n",
p->p_pid, ret, vp->v_name);
/*
* Don't let the app run if it's
* encrypted but we failed to set up the
* decrypter. If the keys are missing it will
* return LOAD_DECRYPTFAIL.
*/
if (ret == LOAD_DECRYPTFAIL) {
/* failed to load due to missing FP keys */
proc_lock(p);
p->p_lflag |= P_LTERM_DECRYPTFAIL;
proc_unlock(p);
}
psignal(p, SIGKILL);
}
break;
#endif
default:
/* Other commands are ignored by the kernel */
ret = LOAD_SUCCESS;
break;
}

其中几个比较重要的加载命令:

  1. LC_SEGMENT(LC_SEGMENT_64),用于加载段(segment)的命令,有下面段用下面加载:__PAGEZERO__TEXTDATA__LINKEDIT。其中__PAGEZERO程序保留区,用于处理NULL异常,__TEXT保存程序代码和字符,DATA保存程序使用的二进制数据,__LINKEDIT保存动态库需要原始数据如符号、字符串、重定位条目等。也保留了起始地址信息,后续的LC_SYMTABLC_DYSYMTAB也是基于起始地址来算出相关偏移的值
  2. LC_LOAD_DYLINKER,用来读取动态加载库路径,通常在usr/lib/dyld,然后使用这个命令加载后面的动态库(最终还是递归调用parse_machfile)。
  3. LC_MAIN,用来读取程序入口
  4. LC_CODE_SIGNATURE 用来验证程序签名
  5. LC_DYSYMTAB加载Dynamic Symbol Table,保存了C Function相关的链接信息,通过数据偏移,可以查询LC_SYMTAB保存的C Function相关的信息,比如方法名和实现等。fishhook,利用这个机制可以找到C对应的方法实现,并动态替换成要hook的函数,具体参考我的fishHooker源码解析。

经过LoadCommand,程序正式被加载到内存中,最终运行起来。

MACHO Section

下面的主要是相关的节数据,主要有:

__TEXT段节名含义

1
2
3
4
5
6
7
8
9
10
11
12
13
1. __text: 代码节,存放机器编译后的代码
2. __stubs: 用于辅助做动态链接代码(dyld).
3. __stub_helper:用于辅助做动态链接(dyld).
4. __objc_methname:objc的方法名称
5. __cstring:代码运行中包含的字符串常量,比如代码中定义`#define kGeTuiPushAESKey @"DWE2#@e2!"`,那DWE2#@e2!会存在这个区里。
6. __objc_classname:objc类名
7. __objc_methtype:objc方法类型
8. __ustring:
9. __gcc_except_tab:
10. __const:存储const修饰的常量
11. __dof_RACSignal:
12. __dof_RACCompou:
13. __unwind_info:

__DATA段节名含义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
1. __got:存储引用符号的实际地址,类似于动态符号表,存储了`__nl_symbol_ptr`相关函数指针。
2. __la_symbol_ptr:lazy symbol pointers。懒加载的函数指针地址(C代码实现的函数对应实现的地址)。和__stubs和stub_helper配合使用。具体原理暂留。
3. __mod_init_func:模块初始化的方法。
4. __const:存储constant常量的数据。比如使用extern导出的const修饰的常量。
5. __cfstring:使用Core Foundation字符串
6. __objc_classlist:objc类列表,保存类信息,映射了__objc_data的地址
7. __objc_nlclslist:Objective-C 的 +load 函数列表,比 __mod_init_func 更早执行。
8. __objc_catlist: categories
9. __objc_nlcatlist:Objective-C 的categories的 +load函数列表。
10. __objc_protolist:objc协议列表
11. __objc_imageinfo:objc镜像信息
12. __objc_const:objc常量。保存objc_classdata结构体数据。用于映射类相关数据的地址,比如类名,方法名等。
13. __objc_selrefs:引用到的objc方法
14. __objc_protorefs:引用到的objc协议
15. __objc_classrefs:引用到的objc类
16. __objc_superrefs:objc超类引用
17. __objc_ivar:objc ivar指针,存储属性。
18. __objc_data:objc的数据。用于保存类需要的数据。最主要的内容是映射__objc_const地址,用于找到类的相关数据。
19. __data:暂时没理解,从日志看存放了协议和一些固定了地址(已经初始化)的静态量。
20. __bss:存储未初始化的静态量。比如:`static NSThread *_networkRequestThread = nil;`其中这里面的size表示应用运行占用的内存,不是实际的占用空间。所以计算大小的时候应该去掉这部分数据。
21. __common:存储导出的全局的数据。类似于static,但是没有用static修饰。比如KSCrash里面`NSDictionary* g_registerOrders;`, g_registerOrders就存储在__common里面

这部分数据会在上一步LoadCommand命令时,加载到内存里。

解析__objc_classlist

在看linkMap的时候,很奇怪的是,获取类名可以用_objc_classname, 获取方法名可以用_objc_methname,但是两个数据怎么匹配起来的,根据查相关资料,是通过__objc_classlist来映射的。

在解析的时候需要两个工具:MachOViewHopper

加载可执行文件

选用真机编译,编译选项选择Build Active Architecture Only,这样只生成一个CPU类型的文件,方便后续分析,然后在工程的DerivedData/**/Build/Products/**-iphonesos/**.app中显示包内容,把和工程同名的文件copy到自己的目录下。

打开`MachOview,打开刚才的可执行文件。

解析__objc_class结构

直接看__objc_classlist节,

然后看下__objc_classlist数据结构,这个是个内存地址占用64位,
经过分析,__objc_classlist,保存的地址,映射的是__objc_data的地址,在MachOView中,对应的数据为:

使用Hopper打开可执行文件,按G,在搜索框里输入这个地址,比如输入0000000100009278

之后显示了一个数据结构。

这个数据对应的数据结构为:

1
2
3
4
5
6
7
8
typedef struct objc_class{
struct __objc_class* isa;
struct __objc_class* wuperclass;
struct __objc_cache* cache;
struct __objc_vtable* vtable;
struct __objc_ data* data;
}objc_class;
  1. 第一个是64位指针,保存isa指针,指向了MetaClass指针,对应的地址为00000001000092A0,在Hopper中搜索这个地址,得到的数据为:

  2. 第二个指向父类的指针,对应地址为0000000000000000

  3. 第5个指向data,对应的地址为:00000001000082C8, 这个数据保存在__objc_const节,对应的数据结构为__objc_data
    ,在Hopper中搜索这个地址,得到的数据为:


    对应的具体数据为:

``

解析__objc_data

对应的数据结构为:

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct objc_data{
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
uint32_t reserved;
void* ivarlayout;
char* name;
struct __objc_method_list* baseMethod;
struct __objc_protos* baseProtocol;
struct __objc_ivars* ivars;
struct __objc_ivars weakIvarLayout;
struct __objc_ivars baseProperties;
}

主要的几个数据结构:

  1. name 保存的类名称。这个地址为:00000001000076B6,对应的数据在__objc_classname段里,用Hopper查看这个地址,对应的名称为ViewController

  2. baseMethod,保存了类所有方法,这个地址为:0000000100008278 , 对应数据在__objc_const,可以在这里找到对应的数据。

    对应数据结构为__objc_method_list,在Hopper,查看:

解析__objc_method_list

对应的数据结构为:

1
2
3
4
typedef struct objc_method_list{
uint32_t flags;
uint32_t count;
}

使用到的数据主要是count,对应数据为00000003,对应10进制数为3,说明有3个方法。具体方法对应的数据结构为:

1
2
3
4
5
6
typedef struct objc_method{
char* name;
char* signature;
void* implementation;
}

这个数据结构占用24(8*3)字节。objc_method_list结构体占用8字节,所以从0000000100008278开始,偏移8个字节,到0000000100008280就是第一个方法的起始位置,再偏移24个字节到0000000100008298,就是第二个方法起始地址位置,以此类推,最后一个方法占用地址为00000001000082b0 ~ 00000001000082c7

先看第一个方法存储的数据为:

然后分别解析这些地址:

  1. 0000000100006924,在__objc_methname段里,对应方法名称。

  2. 000000010000770F,在__objc_methtype段里,对应方法签名,这里的值为v16@0:8,代表含义可以参考这里关于type encodings的理解–runtime programming guide

  1. 0000000100004A20,在__text节里,对应的数据为:

最终类需要的数据完全解析完成。

ps:想要知道数据结构是什么,可以在Hopper的右侧导航栏下,点击Manager type查看。

参考

  1. iOS安全–从Mach-o文件结构分析类名和方法名
  2. 从macho中解析类名
  3. 深入理解Macho文件(二)- 消失的OBJC段与新生的DATA段
  4. mach-o文件格式分析
  5. Macho kern
  6. main.m 方法之前的优化
  7. OSX内核加载mach-o流程分析
  8. iOS程序启动->dyld加载->runtime初始化(初识)
坚持原创技术分享,您的支持将鼓励我继续创作!