ios深入-MACHO文件解析
发表于 2017-10-26 | 分类于 优化
导读
在分析linkMap文件的时候,遇到一个有趣的问题:获取类名可以用_objc_classname
, 获取方法名可以用_objc_methname
。可是怎么将方法名称和对象名称对应起来,程序是如何对应这两部分数据的。带着这个疑问研究了下macho的文件结构。
MACHO文件说明
macho文件是mac os或ios系统可执行文件的格式,系统通过加载这个格式来执行代码。
相关结构如图:
注:来源于:(http://www.jianshu.com/p/f1a61b53398f)
具体每部分的含义可以参考这个定义:
这里简单讲几个我比较关注的:
注:下面都是以64位做演示说明,cpu结构为arm64。
MachO Header的结构
数据结构为:
/*
* 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 */
};
- 第一个四字节数叫做
magic number
,可以得到使用的是64位还是32位系统 - 第二个字节和第三个字节是CPU类型
- 第四个字节是文件类型。
MH_EXECUTE
表示可执行文件 - 第五个字节和第六个字节表示
load commands
的个数和长度 - 第7个字节是加载的flag信息。具体参考
loader.h
中的文件
MachO load command
程序检索完Header之后就开始加载和解析Load Commands了。
相关代码在mach_loader.c
,通过递归调用加载命令。
load_comand
的数据结构为:
/*
* 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都需要包含
- cmd:加载类型
- cmdsize:加载的大小
相关的最主要的解析源码在mach_loader.c
里的parse_machfile
方法里.最主要的代码如下:
/*
* 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;
}
其中几个比较重要的加载命令:
-
LC_SEGMENT(LC_SEGMENT_64)
,用于加载段(segment)的命令,有下面段用下面加载:__PAGEZERO
、__TEXT
、DATA
、__LINKEDIT
。其中__PAGEZERO
程序保留区,用于处理NULL异常,__TEXT
保存程序代码和字符,DATA
保存程序使用的二进制数据,__LINKEDIT
保存动态库需要原始数据如符号、字符串、重定位条目等。也保留了起始地址信息,后续的LC_SYMTAB
和LC_DYSYMTAB
也是基于起始地址来算出相关偏移的值 -
LC_LOAD_DYLINKER
,用来读取动态加载库路径,通常在usr/lib/dyld
,然后使用这个命令加载后面的动态库(最终还是递归调用parse_machfile
)。 -
LC_MAIN
,用来读取程序入口 -
LC_CODE_SIGNATURE
用来验证程序签名 -
LC_DYSYMTAB
加载Dynamic Symbol Table
,保存了C Function
相关的链接信息,通过数据偏移,可以查询LC_SYMTAB
保存的C Function
相关的信息,比如方法名和实现等。fishhook
,利用这个机制可以找到C对应的方法实现,并动态替换成要hook的函数,具体参考我的fishHooker源码解析。
经过LoadCommand,程序正式被加载到内存中,最终运行起来。
MACHO Section
下面的主要是相关的节数据,主要有:
__TEXT段节名含义
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. __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
来映射的。
在解析的时候需要两个工具:MachOView
和Hopper
,
加载可执行文件
选用真机编译,编译选项选择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
这个数据对应的数据结构为:
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;
-
第一个是64位指针,保存isa指针,指向了
MetaClass
指针,对应的地址为00000001000092A0
,在Hopper
中搜索这个地址,得到的数据为: 第二个指向父类的指针,对应地址为
0000000000000000
-
第5个指向
data
,对应的地址为:00000001000082C8
, 这个数据保存在__objc_const
节,对应的数据结构为__objc_data
,在Hopper
中搜索这个地址,得到的数据为:
对应的具体数据为:
``
解析__objc_data
对应的数据结构为:
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;
}
主要的几个数据结构:
-
name
保存的类名称。这个地址为:00000001000076B6
,对应的数据在__objc_classname
段里,用Hopper
查看这个地址,对应的名称为ViewController
-
baseMethod
,保存了类所有方法,这个地址为:0000000100008278
, 对应数据在__objc_const
,可以在这里找到对应的数据。
对应数据结构为__objc_method_list
,在Hopper
,查看:
解析__objc_method_list
对应的数据结构为:
typedef struct objc_method_list{
uint32_t flags;
uint32_t count;
}
使用到的数据主要是count
,对应数据为00000003
,对应10进制数为3,说明有3个方法。具体方法对应的数据结构为:
typedef struct objc_method{
char* name;
char* signature;
void* implementation;
}
这个数据结构占用24(8*3)字节。objc_method_list
结构体占用8字节,所以从0000000100008278
开始,偏移8个字节,到0000000100008280
就是第一个方法的起始位置,再偏移24个字节到0000000100008298
,就是第二个方法起始地址位置,以此类推,最后一个方法占用地址为00000001000082b0 ~ 00000001000082c7
。
先看第一个方法存储的数据为:
然后分别解析这些地址:
-
000000010000770F
,在__objc_methtype
段里,对应方法签名,这里的值为v16@0:8
,代表含义可以参考这里关于type encodings的理解–runtime programming guide -
0000000100004A20
,在__text
节里,对应的数据为:
最终类需要的数据完全解析完成。
ps:想要知道数据结构是什么,可以在Hopper
的右侧导航栏下,点击Manager type
查看。