目录
Data(数据)
-
简介
通过前面对 MachO 文件 Header 和 LoadCommands 的介绍,可知:
Header 区域主要用于存储 MachO 文件的一般信息,并且描述了 LoadCommands 区域
而 LoadCommands 区域则详细描述了 Data 区域
如果说 Header 区域和 LoadCommands 区域的主要作用是:
① 让系统内核加载器知道如何读取 MachO 文件
② 并指定动态链接器来完成 MachO 文件后续的动态库加载
③ 然后设置好程序入口等一些列程序启动前的信息
那么,Data 区域的作用,就是当程序运行起来后,为每一个映射到虚拟内存中的指令操作提供真实的物理存储支持Data 区域通常是 MachO 文件中最大的部分,主要包含:代码段、数据段,链接信息等
注意:不要把 Data 区域与数据段搞混掉了,Data 区域指的是广义上的数据,而不是特指数据段的数据在 MachO 文件中,Data 为第三个区域
与 LoadCommands 区域紧接着 Header 区域不同的是,Data 区域并没有紧接着 LoadCommands 区域。LoadCommands 与 Data 之间还留有不少的空间,为代码注入提供了很大的便利
以下我们通过几个小示例,来简单地探索一下 MachO 文件的 Data 区域 -
MachOView 中的 Segment 与 Section
我们要分清楚:位于
LoadCommands
区域的段加载指令 && 位于Data
区域的段数据
在 MachO 文件中,通常约定:
segment 的名称为双下划线加全大写字母(如 __TEXT)
section 的名称为双下划线加全小写字母(如 __text) -
MachO 文件的 Header 区域和 LoadCommands 区域也会被映射到进程的虚拟地址空间中
LoadCommands
区域的LC_SEGMENT_64
命令用于将 MachO 文件中 64 位的Segment
(段)映射到进程的虚拟地址空间中(即加载命令)
特别地,在 MachO 文件中,并不是只有Data
区域会被映射到进程的虚拟地址空间中Header
区域和LoadCommands
区域也会被映射到进程的虚拟地址空间中
请看以下分析:①
LoadCommands
区域的第一条加载命令LC_SEGMENT_64(__PAGEZERO)
:加载空指针陷阱段(不可读,不可写,不可执行)
用于将 MachO 文件中,起始地址为0x0
,大小为0x0
的区域 映射到
进程的虚拟地址空间[0x0, 0x1 0000 0000 (4GB)]
中
即规定了进程地址空间的前4GB
:不可读、不可写、不可执行
②LoadCommands
区域的第二条加载命令LC_SEGMENT_64(__TEXT)
:加载代码段(可读,可执行,不可写)
用于将 MachO 文件中,起始地址为0x0
,大小为0x8000
的区域 映射到
进程的虚拟地址空间[0x1 0000 0000 (4GB), 0x1 0000 8000]
中
③ 我们注意到:
在 MachO 文件中,Header
区域并上LoadCommands
区域的起始地址(0x0
)和结束地址(0x0B77
),包含在LC_SEGMENT_64(__TEXT)
对 MachO 文件的映射范围[0x0, 0x8000]
内
也就是说,Header
区域和LoadCommands
区域会被当成代码段的一部分,从而映射到进程的虚拟地址空间中
④ 特别地,Data
区域中的第一个节Section64(__TEXT, __text)
,在 MachO 文件中的起始地址为0x6158
,大小为0x2A4
,将会被映射到进程的虚拟地址空间0x1 0000 6158
处
也就是说,进程的可用虚拟空间起始处(0x1 0000 0000 (4GB)
),最先存储的是 MachO 文件的Header
区域和LoadCommands
区域的数据([0x1 0000 0000 (4GB), 0x1 0000 0B77]
),然后接着是一段留白的区域([0x1 0000 0B78, 0x1 0000 6157]
),接着才是Data
区域中代码段的第一个节Section64(__TEXT, __text)
(0x1 0000 6158
)
其内存分布如下图所示:
⑤ 这里还有一点需要注意:
既然在内存中 MachO 文件的 Header 区域和 LoadCommands 区域被映射成代码段的一部分
那么在内存中 Header 和 LoadCommands 的数据的访问权限就跟代码段是一样的(可读,可执行,不可写) -
查看 Dynamic Loader Info
LoadCommands 区域的 LC_DYLD_INFO_ONLY 命令中记录着动态链接器加载动态库所需的必要信息的位置和大小
Data 区域的 Dynamic Loader Info 则实际存储着动态链接器加载动态库所需的必要信息 -
查看主线程的入口
主线程的入口即
main
函数的入口 -
一个简单的 Demo
在项目
MachODemo
的ViewController.m
里面,有以下代码:- 静态 C 字符串 和 静态 OC 字符串
- 无参数和带参数的 C 函数
- 无参数和带参数的 OC 方法
- 一个动态库 HcgServices
如下图所示:
根据以上代码,对项目的 MachO 文件进行探索:① 静态的 C 字符串 和 静态的 OC 字符串,都被存储在
Section64(__TEXT, __cstring)
里面
不仅如此,函数和方法里面用到的字符串(哪怕是像%d
、%s
这样的占位符),也都被存储在Section64(__TEXT, __cstring)
里面。并且所有相同的字符串,只会被保存一次
② 由于 C 语言是静态语言,在程序构建时,所有的 C 函数都被编译成汇编代码了,所以在 MachO 文件中,找不到 无参数和带参数的 C 函数③ 所有的 OC 方法名都被存储在
Section64(__TEXT, __objc_methname)
中
④ 在LoadCommands
区域,有加载动态库libHcgServices.dylib
的命令 -
查看 Symbol Table
①
LoadCommands
区域的LC_SYMTAB
命令中记录了:Symbol Table
的偏移量为50272(0x0000C460)
String Table
的偏移量为54004(0x0000D2F4)
②Data
区域的Symbol Table
的起始地址正好为0x0000C460
Symbol Table
的每一个元素都是一个struct nlist_64
结构体#import <mach-o/nlist.h> struct nlist_64 { union { uint32_t n_strx; /* 符号的名称在 string table 中的索引 */ } n_un; uint8_t n_type; /* 符号的类型标识,可选的值有:N_STAB、N_PEXT、N_TYPE、N_EXT */ uint8_t n_sect; /* 符号所在的 section 的索引。如果没有对应的 section,则为 NO_SECT */ uint16_t n_desc; /* 符号的描述,see <mach-o/stab.h> */ uint64_t n_value; /* 符号所在的地址 或 stab 的偏移量 */ };
Data
区域的String Table
的起始地址正好为0x0000D2F4
String Table
存储着所有符号的名字,以.
作为分隔符
③ 这里以类AppDelegate
为例进行演示(注意:这里说的AppDelegate
是一个类)AppDelegate
的符号在Symbol Table
中所对应的struct nlist_64
结构体如下图所示
④ 由步骤 ③ 可知:
符号AppDelegate
所对应的字符串在String Table
中的偏移量为0x00000ABF
又因为String Table
的起始地址为0x0000D2F4
所以符号AppDelegate
所对应的字符串在String Table
中的位置为0x0000D2F4 + 0x00000ABF = 0x0000DDB3
String Table
的0x0000DDB3
处,确实存储着类AppDelegate
的符号名称
这与步骤 ③ 中 MachOView(Value 列)的显示结果不谋而合⑤ 同样由步骤 ③ 可知:
符号AppDelegate
位于第0x15(21)
个 Section 里面,并且符号的地址为0x00009568
MachOView 已经帮我们解析出第 21 个 Section 就是Section64(__DATA, __objct_data)
Section64(__DATA, __objct_data)
的第二个条目中(地址为0x00009568
),记录着类AppDelegate
的基本信息:- 类
AppDelegate
的isa 指针
指向了_OBJC_METACLASS_$_AppDelegate
- 类
AppDelegate
的父类是_OBJC_CLASS_$_UIResponder
(也就是我们所熟知的UIResponder
) - 此时类
AppDelegate
的缓存为空 - 类
AppDelegate
的 VTable 数量为 0 - 类
AppDelegate
的详细信息在地址0x100008CF0
处
到这里,是不是觉得特别熟悉呢?我们通过clang
把AppDelegate.m
重写为AppDelegate.cpp
,可以看到:~/Desktop/AppDelegate > ls AppDelegate.h AppDelegate.m ~/Desktop/AppDelegate > clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk AppDelegate.m ~/Desktop/AppDelegate > ls AppDelegate.h AppDelegate.m AppDelegate.cpp
[AppDelegate.cpp...] extern "C" __declspec(dllexport) struct _class_t OBJC_CLASS_$_AppDelegate __attribute__ ((used, section ("__DATA,__objc_data"))) = { 0, // &OBJC_METACLASS_$_AppDelegate, 0, // &OBJC_CLASS_$_UIResponder, 0, // (void *)&_objc_empty_cache, 0, // unused, was (void *)&_objc_empty_vtable, &_OBJC_CLASS_RO_$_AppDelegate, }; static void OBJC_CLASS_SETUP_$_AppDelegate(void ) { OBJC_METACLASS_$_AppDelegate.isa = &OBJC_METACLASS_$_NSObject; OBJC_METACLASS_$_AppDelegate.superclass = &OBJC_METACLASS_$_UIResponder; OBJC_METACLASS_$_AppDelegate.cache = &_objc_empty_cache; OBJC_CLASS_$_AppDelegate.isa = &OBJC_METACLASS_$_AppDelegate; OBJC_CLASS_$_AppDelegate.superclass = &OBJC_CLASS_$_UIResponder; OBJC_CLASS_$_AppDelegate.cache = &_objc_empty_cache; } [AppDelegate.cpp...]
这与我们通过 MachOView 看到的结果不谋而合
⑥ 根据步骤 ⑤ 中 MachOView 的显示结果可知,类
AppDelegate
的详细信息在地址0x100008CF0
处
同样地,根据步骤 ⑤ 中AppDelegate.cpp
的代码可知,0x100008CF0
就是_OBJC_CLASS_RO_$_AppDelegate
的地址_OBJC_CLASS_RO_$_AppDelegate
实际上是一个struct class_ro_t
结构体struct class_ro_t
结构体的原始定义位于runtime
的源码中AppDelegate.cpp
对struct class_ro_t
结构体的描述为[AppDelegate.cpp...] struct _class_ro_t { unsigned int flags; unsigned int instanceStart; unsigned int instanceSize; const unsigned char *ivarLayout; const char *name; const struct _method_list_t *baseMethods; const struct _objc_protocol_list *baseProtocols; const struct _ivar_list_t *ivars; const unsigned char *weakIvarLayout; const struct _prop_list_t *properties; }; [AppDelegate.cpp...]
到这里,我们已经可以获取到类
AppDelegate
的所有信息了⑦ 比如,我们想看看类
AppDelegate
的方法列表
由步骤 ⑥ 中 MachOView 的显示结果可知:Base Methods = 0x100008C48
这里的每一个条目(item)都对应一个struct objc_method
结构体
同样地,struct objc_method
结构体的原始定义也位于runtime
的源码中AppDelegate.cpp
对struct objc_method
结构体的描述为[AppDelegate.cpp...] struct _objc_method { struct objc_selector * _cmd; const char *method_type; void *_imp; }; [AppDelegate.cpp...]
⑧ 比如,我们想看看类
AppDelegate
的属性列表
由步骤 ⑥ 中 MachOView 的显示结果可知:Base Properties = 0x100008C98
⑨ 我们来梳理一下:
一开始的时候,我们根据LoadCommands
区域的LC_SYMTAB
命令知道了Symbol Table
和String Table
的位置和大小,并在 Data 区域实际找到了Symbol Table
和String Table
接下来,我们以类AppDelegate
为例,说明Symbol Table
是如何工作的
首先,我们根据类AppDelegate
的Symbol Table
找到了AppDelegate
的符号名称
其次,我们根据类AppDelegate
的Symbol Table
知道了AppDelegate
位于Section64(__DATA, __objct_data)
在
Section64(__DATA, __objct_data)
中,我们获取到了类AppDelegate
的一般信息(比如,isa 指针、父类、类缓存)
并且知道了类AppDelegate
详细的信息存储在Section64(__DATA, __objct_constant)
中
之后我们根据struct class_ro_t
结构体的描述,找到类AppDelegate
的详细信息(比如,类名,方法列表,属性列表)这里有一点需要注意:
如果 Project 通过 Release 配置构建,那么 MachO 中Section64(__DATA, __objct_data)
里面的内容会被抹去 - 类
-
查看 Dynamic Symbol Table
①
LoadCommands
区域的LC_DYSYMTAB
命令中记录了所有动态链接时需要用到的符号的信息:
② 由LC_DYSYMTAB
命令可知,MachO 文件内部的符号在符号表(Symbol Table
)中的起始索引为 0 ,数量为 201 个,如下图所示:
③ 由LC_DYSYMTAB
命令可知,MachO 文件导出给外部使用的符号在符号表(Symbol Table
)中的起始索引为 201 ,数量为 1 个,如下图所示:
④ 由LC_DYSYMTAB
命令可知,MachO 文件用于懒绑定的符号在符号表(Symbol Table
)中的起始索引为 202 ,数量为 24 个,如下图所示:
⑤ 由LC_DYSYMTAB
命令可知,间接符号表在 MachO 文件中的偏移量为0x0000D280(53888)
,共有0x1D(29)
个元素,如下图所示:乍一看,MachOView 中的
Indirect Symbols
里面似乎存储了大量的信息
但实际上,Indirect Symbols
里面所存储的每一个元素,都只是一个 8Byte 的数据
这个 8Byte 的数据,代表的是 和动态库相关的符号 在符号表(Symbol Table
)的索引值而 MachOView Value 列的数据,都是 MachOView 根据这个 8Byte 的索引值,一步一步解析出来的
通过这个 8Byte 的索引值,可以解析出来的数据有:- Symbol:符号的名称
- Section:符号所处在的 section,一般有
section64(__TEXT, __stubs)
、section64(__DATA, __got)
、section64(__DATA, __la_symbol_ptr)
- Indirect Address:可以通过 Indirect Address 找到符号在
section64(__TEXT, __stubs)
、section64(__DATA, __got)
、section64(__DATA, __la_symbol_ptr)
中的地址
fishhook 的原理就用到了这个,后面会单独写一篇文章来讲
-
查看 Function Starts
LoadCommands
区域的LC_FUNCTION_STARTS
命令用于描述函数的起始地址信息,指向了Data
区域的链接信息段(__LINKEDIT
)中Function Starts
的首地址Function Starts
定义了一个函数起始地址表,调试器和其他程序通过该表可以很容易地判断出一个地址是否在函数内
通过 Hopper Disassembler 能解析出同样的结果
iOS 系统的懒绑定机制
-
iOS 的懒绑定流程 && MachO 相关的数据结构
虽然 iOS 系统的懒绑定思路和 Linux 系统的懒绑定思路基本相同
但是在懒绑定的具体流程以及所使用的数据结构上却略有不同在 XCode 中新建一个 iOS Project:LazyBindingDemo
并使用如下代码来探究 iOS 系统动态库Foundation.framwork
的NSLog
函数是如何被懒绑定的
使用 Release 配置 Build 项目 LazyBindingDemo,并使用 MachOView 打开主程序的 MachO 文件
① 在 iOS 系统中,当程序调用动态库的函数时,它实际上是执行Section64(__TEXT, __stubs)
处的代码(你也可以将Section64(__TEXT, __stubs)
理解成 ELF 文件中从plt[1]
开始的部分)
下图红框标出的部分便是用来调用NSLog
函数的Symbol Stub
它的地址是0x100006524
(先记住这个地址,后面通过调试验证的时候会用到)
② 外部函数的地址被放在Section64(__DATA, __la_symbol_ptr)
中,而Symbol Stub
的作用便是找到相应的Lazy Symbol Pointer
并跳转到它所包含的地址
此处NSLog
函数的Lazy Symbol Pointer
所记录的地址为0x00000001 000065E4
③ 当我们第一次调用NSLog
函数时,Lazy Symbol Pointer
尚未记录NSLog
函数的地址,而是指向Section64(__TEXT, __stub_helper)
中相关的内容。在Section64(__TEXT, __stub_helper)
中,它将懒绑定函数dyld_stub_binder
所需的参数放到寄存器 w16
中,之后跳转到地址0x000065CC
处,也就是Section64(__TEXT, __stub_helper)
的头部,然后调用懒绑定函数dyld_stub_binder
进行符号绑定,最后会将NSLog
函数的真实地址回写到Section64(__DATA, __la_symbol_ptr)
中对应的Lazy Symbol Pointer
④ 仔细观察后发现,寄存器 w16
实际上是存放一个int
类型的值,那么这个int
类型的值究竟代表什么呢?为什么懒绑定函数dyld_stub_binder
可以利用它来绑定符号?实际上,它是相对于Lazy Binding Info
的偏移量(在LINKEDIT
段的Dynamic Loader Info
中)
懒绑定函数dyld_stub_binder
根据这个偏移量便可从Lazy Binding Info
中找到绑定过程所需的信息(比如到系统的Foundation
动态库中寻找NSLog
函数) -
通过 LLDB 的调试,验证懒绑定流程
上面介绍了 iOS 的懒绑定流程 && MachO 文件中用于支持懒绑定的数据结构
接下来通过 LLDB 调试项目:LazyBindingDemo 来验证以上的内容
在两个NSLog
输出语句中都下断点
并在调试的时候显示汇编代码(XCode - Debug - Debug Wrokflow - Always Show Disassembly
)
① 程序运行到NSLog(@"First");
处,我们可以看到程序实际上是跳转到地址0x104116524
处的Symbol Stub
代码。但是等等,我们之前在 MachOView 中观察到程序此时应该跳转到地址0x100006524
处,但是为什么这里的地址却是0x104116524
呢?
这是因为 iOS 系统加载 MachO 文件的时候,使用了 ASLR 技术(地址空间布局随机化)。通过计算0x104116524 - 0x100006524
可以得到程序此次加载的偏移量为0x04110000
② 根据上一小节的介绍:
当程序调用动态库的函数时,它实际上是执行Section64(__TEXT, __stubs)
处的代码
那么地址0x104116524
处对应的汇编代码,应该就是NSLog
函数的Symbol Stub
我们通过 LLDB 打印地址0x104116524
处对应的汇编代码
LLDB 的输出结果,与前面用 MachOView 显示的NSLog
函数的Symbol Stub
是一致的
③ 因为是首次调用 NSLog 函数,所以地址0x104116524
处记录的,应该是Section64(__TEXT, __stub_helper)
中 NSLog 函数执行懒绑定前,用于准备懒绑定的参数的代码
我们通过 LLDB 打印地址0x00000001 04116524
处对应的汇编代码
LLDB 的输出结果,与前面用 MachOView 显示的NSLog
函数执行懒绑定前,准备参数的代码是一致的
④ 那么可想而知,地址0x1041165cc
应该就是Section64(__TEXT, __stub_helper)
的首地址,其记录的应该就是对懒绑定函数dyld_stub_binder
的调用
我们通过 LLDB 打印地址0x1041165cc
处对应的汇编代码
果不其然,地址0x1041165cc
记录的就是对懒绑定函数dyld_stub_binder
的调用
出于好奇,我们通过地址0x00000001 a986 e08c
进去看一下懒绑定函数长什么样子
⑤ iOS 系统首次调用NSLog
函数进行懒绑定的流程,我们已经验证完了
接来下清空 LLDB 的输出并过掉第一个断点,程序运行到NSLog(@"Secondt");
处
我们接着探索 iOS 系统第二次调用NSLog
函数的流程
可想而知,地址0x104116524
记录的应该还是NSLog
函数在Section64(__TEXT, __stubs)
的Symbol Stub
我们通过 LLDB 打印地址0x104116524
处对应的汇编代码,LLDB 的输出结果与预期相符
⑥ 我们注意到, 此时Section64(__TEXT, __stubs)
中NSLog
函数的Symbol Stub
记录的不再是指向Section64(__TEXT, __stub_helper)
的调用,而是NSLog
函数的真是地址
这也证实了,懒绑定只会在函数首次调用的时候执行一次
最后,出于严谨,我们还是要验证一下地址0x00000001 a9e6253c
出是不是存储着NSLog
函数对应的汇编代码
⑦ 懒绑定函数dyld_stub_binder
位于动态链接器 dyld 中,大致的绑定过程如下:(libdyld.dylib) dyld_stub_binder --> (libdyld.dylib) _dyld_fast_stub_entry --> dyld::fastBindLazySymbol --> ImageLoaderMachO::bindLocation
如何获取到 Lazy Symbol Pointers 对应的函数名
-
思考
先回忆起上节在讲 iOS 系统的懒绑定机制 时的这张图
这里是通过 MachOView 解析的,项目LazyBindingDemo
的 MachO 文件中的Lazy Symbol Pointers
在调用NSLog
函数时,会到这里取NSLog
函数的地址,然后跳转执行根据上节对 iOS 系统懒绑定机制的介绍:
第一次从这里取到的地址值会指向stub_helper
第二次从这里取到的地址值会指向NSLog
函数的入口
留意到上图最右边的 Value 列,这里 MachOView 已经帮我们解析出:地址0x10000C000
是指向NSLog
函数的调用
这里需要注意一点:MachOView 仅仅是帮我们解析出了地址0x10000C000
调用的函数名是NSLog
,而不是解析出了NSLog
函数的真实地址。因为 MachOView 打开的仅仅是存储在磁盘上的静态 MachO 文件,而不是装载到内存中的动态进程,所以 MachOView 是无法获取到位于动态库中的NSLog
函数的真实地址的言归正传:在
Lazy Symbol Pointers
中,MachOView 是如何解析出地址0x10000C000
对应的函数名就是NSLog
的呢? -
Section64 Header 中的 Indirect Sym Index
在开始介绍函数名获取的流程之前,这里先补充一个知识点
我们知道,MachO 文件的
Data
区域是分段(Segment
)管理的,每个段(Segment
)会有 0 到 多个节(Section
)
其中,用于描述节(Section
)的数据结构如下所示:// (64 位的)节 struct section_64 { char sectname[16]; /* 16 Byte 的节名 */ char segname[16]; /* 该节所属的段,16 Byte 的段名 */ uint64_t addr; /* 节的虚拟内存起始地址 */ uint64_t size; /* 节所占内存空间的大小(Byte) */ uint32_t offset; /* 节数据在文件中的偏移 */ uint32_t align; /* 节的内存对其边界(2 的次方) */ uint32_t reloff; /* 重定位信息在文件中的偏移 */ uint32_t nreloc; /* 重定位信息的条数 */ uint32_t flags; /* 标志信息(节的类型与属性。一个节只能有一个类型,但是可以有多个属性,可以通过位运算分别获取节的类型和属性) */ uint32_t reserved1; /* 保留字段 1(可以用来表示偏移量或者索引,一般用来表示 Indirect Symbol Index,也就是首元素在间接索引表的位置) */ uint32_t reserved2; /* 保留字段 2(可以用来表示数量或者大小,比如,在 Section64(__TEXT, __sutbs) 中就用来表示 stub 的个数 */ uint32_t reserved3; /* 保留字段 3(无任何用处,真正的保留字段)*/ };
留意到
uint32_t reserved1
字段:保留字段 1(可以用来表示偏移量或者索引,一般用来表示 Indirect Symbol Index,也就是首元素在间接索引表的位置)。什么意思呢?uint32_t reserved1
字段其实是一个索引偏移量,指的是Section
的第 0 个元素对应Indirect Symbols
表中的第几个元素。以Section64 Header(__stubs)
为例进行说明: -
由 Lazy Symbol Pointers 获取函数名的过程
① 由
LoadCommands
区域的LC_SEGMENT_64(__DATA)
.Section64 Header(__la_symbol_ptr)
.Indirect Sym Index
=0xF(15)
可知,Lazy Symbol Pointers
的第0
个元素对应Indirect Symbols
的第15
个元素。因为NSLog
函数正好为Lazy Symbol Pointers
的第0
个元素,所以NSLog
函数对应Indirect Symbols
的第15
个元素,如下图所示:
② 留意上图NSLog
函数在Indirect Symbols
中对应的条目,其 Data 列的值为0x00000C0(192)
,说明NSLog
函数的符号在符号表Symbol Table
中的索引为 192,找到Symbol Table
的第 192 个元素,如下图所示:
③ 留意上图NSLog
函数在Symbol Table
中对应的条目,其String Table Index
为 16,说明NSLog
函数在符号表String Table
中的起始位置为 16,找到String Table
的第 16 个位置,正好为_NSLog
,如下图所示:
④ 整体的解析顺序为:Section64 Header(__la_symbol_ptr)
->Lazy Symbol Pointers
->Indirect Symbols
->Symbols
->String Table
Section64 Header(__stubs)
->Symbol Stubs
->Indirect Symbols
->Symbols
->String Table
通用二进制文件(多层 MachO 文件)
-
通用二进制文件简介
思考一个问题:
不同的 iOS 设备,可能具有不同的 CPU 架构(armv7、armv7s、arm64)
那么,通过 XCode 存档(Archive)出来的同一个 IPA 包为什么可以运行在具有不同 CPU 架构的所有 iOS 设备上呢?
因为,通过 XCode 存档(Archive)出来的 IPA 包中,包含了不同 CPU 架构的二进制代码可以在
XCode - Target - Build Setttings - Architectures
中,设置项目所支持的 CPU 架构
各个选项的含义如下:-
Architectures
选项,指示项目将被编译成支持哪些 CPU 指令集的二进制代码。Standard architectures 表示标准架构,里面包含 armv7 和 arm64 -
Valid Architectures
选项:指示项目可支持的 CPU 指令集。Architectures 选项 和 Valid Architectures 选项的交集,将是 XCode 最终生成的 IPA 包所支持的指令集 -
Build Active Architecture Only
选项:指示是否只编译出当前真机调试的设备所对应的指令集,该选项 Debug 模式下默认为 YES,Release 模式下默认为 NO
开发调试时,为了加快编译速度,一般只需编译出调试设备的 CPU 型号所对应的二进制代码即可
测试发布时,为了涵盖大部分机型,一般需要编译出所有主流机型的 CPU 型号所对应的二进制代码
通用二进制文件(Universal Binary)也叫胖二进制文件(Fat Binary),是由苹果公司提出的 能同时适用多种 CPU 架构的二进制文件,具有以下特点:
- 使用 通用二进制文件 的同一个程序包能同时为多种 CPU 架构提供最理想的性能
- 因为 通用二进制文件 需要储存多种 CPU 架构的二进制代码,所以(通用二进制应用程序)通常比(单一平台二进制应用程序)要大
- 因为两种 CPU 架构的二进制代码有共同的非可执行资源,所以(通用二进制应用程序)的大小并不会达到(单一平台二进制应用程序)的两倍之多
- 因为一个 iOS 设备只会有一种 CPU 架构,所以(通用二进制应用程序)在执行时只会调用一种 CPU 架构的代码。因此,(通用二进制应用程序)运行起来不会比(单一平台二进制应用程序)耗费额外的内存
可以通过 file 指令,查看 MachO 文件支持哪些 CPU 指令集
~/Desktop/lipo_demo > file MachODemo MachODemo: Mach-O universal binary with 3 architectures: [arm_v7:Mach-O executable arm_v7] [arm_v7s:Mach-O executable arm_v7s] [arm64:Mach-O 64-bit executable arm64] MachODemo (for architecture armv7): Mach-O executable arm_v7 MachODemo (for architecture armv7s): Mach-O executable arm_v7s MachODemo (for architecture arm64): Mach-O 64-bit executable arm64
-
-
通用二进制文件的结构
接着再思考一个问题:
由上面的介绍可知,IPA 包中含有不同 CPU 架构的二进制代码
那么,这些不同 CPU 架构的二进制代码存放在 IPA 包中的哪里呢?
这些不同 CPU 架构的二进制代码存放在 IPA 包中的主可执行文件中那么,通用二进制文件的结构和普通 MachO 文件的结构,有哪些区别,又有哪些联系呢?
接下来,通过 MachOView 查看通用二进制文件的内部结构:
由上图可知,通用二进制文件由:
① Fat Header
② 存储不同 CPU 架构代码的 MachO 文件
按次序组成。因此,有时候,通用二进制文件也被叫做多层 MachO 文件通用二进制文件与单一架构的 MachO 文件的关系,如下图所示:
在 fat.h 中用于描述Fat Header
的数据结构,如下所示:struct fat_header { uint32_t magic; /* 魔数,用于描述通用二进制文件的字节顺序 */ uint32_t nfat_arch; /* 通用二进制文件所包含的架构数量 */ }; struct fat_arch { cpu_type_t cputype; /* cpu 类型 */ cpu_subtype_t cpusubtype; /* cpu 子类型 */ uint32_t offset; /* 当前架构在通用二进制文件中的偏移量 */ uint32_t size; /* 当前架构的大小 */ uint32_t align; /* 内存对齐边界(2 的次方) */ }; #define FAT_MAGIC 0xcafebabe /* 小端模式(arm cpu 默认工作在小端模式) */ #define FAT_CIGAM 0xbebafeca /* 大端模式(需要转换成小端模式) */
-
lipo 命令
① 使用 lipo 命令查看 MachO 文件的信息
# lipo -info 命令只能查看 MachO 文件包含哪些 CPU 架构 # 如果要查看 MachO 文件的详细信息,可以使用 OTool 或者 MachOView ~/Desktop/lipo_demo > lipo -info MachODemo Architectures in the fat file: MachODemo are: armv7 armv7s arm64
② 使用 lipo 命令拆分多层 MachO 文件(通用二进制文件)
~/Desktop/lipo_demo > lipo MachODemo -thin arm64 -output MachO_arm64
③ 使用 lipo 命令合并 MachO 文件
~/Desktop/lipo_demo > lipo -create MachO_armv7 MachO_arm64 -output MachO_standard