Mach-O文件
- Mach-O是Mach Object文件格式的缩写,是mac以及iOS上可执行文件的格式,例如当Xcode App工程编译完成之后就会生成一个可执行文件,其格式就是Mach-O文件;
Mach-O的相关名词
- Executable 可执行文件;
- Dylib 动态库;
- Bundle 无法被连接的动态库,只能通过dlopen()加载;
- Image 指的是Executable,Dylib或者Bundle的一种,文中会多次使用Image这个名词;
- Framework 动态库(可以是静态库)和对应的头文件和资源文件的集合;
Mach-O文件的常见类型
#define MH_OBJECT 0x1 /* relocatable object file */
#define MH_EXECUTE 0x2 /* demand paged executable file */
#define MH_FVMLIB 0x3 /* fixed VM shared library file */
#define MH_CORE 0x4 /* core file */
#define MH_PRELOAD 0x5 /* preloaded executable file */
#define MH_DYLIB 0x6 /* dynamically bound shared library */
#define MH_DYLINKER 0x7 /* dynamic link editor */
#define MH_BUNDLE 0x8 /* dynamically bound bundle file */
#define MH_DYLIB_STUB 0x9 /* shared library stub for static */
/* linking only, no section contents */
#define MH_DSYM 0xa /* companion file with only debug */
/* sections */
#define MH_KEXT_BUNDLE 0xb /* x86_64 kexts */
-
MH_OBJECT
:目标文件即 .o 文件 以及静态库文件即 .a 文件(多个.o文件合并在一起); -
MH_EXECUTE
:可执行文件,即App编译运行后生成的可执行文件,在/Products路径下; -
MH_DYLIB
:动态库文件,即.dylib文件 或者 .framework文件; -
MH_DYLINKER
:/usr/lib/dyld路径下的dyld文件; -
MH_DSYM
:Xcode打包后生成的符号表文件,即.dSYM文件;
查看文件的格式类型
使用命令行
file 文件名
-
查看自定义的目标.o文件
- 终端输入:
file YYPerson.o
- 终端输出:
YYPerson.o: Mach-O 64-bit object x86_64
- 终端输入:
-
查看Xcode编译运行后生成的可执行文件
- 终端输入:
file SuningWeiDian
- 终端输出:
Mach-O 64-bit executable x86_64
- 终端输入:
-
终端cd /usr/lib 然后 ls 列出所有lib文件;然后查看ACIPCBTLib.dylib文件
- 终端输入:
file ACIPCBTLib.dylib
- 终端输出:
ACIPCBTLib.dylib: Mach-O 64-bit dynamically linked shared library x86_64
- 终端输入:
-
同上 查看 dyld文件
- 终端输入:
file dyld
- 终端输出:
dyld: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit dynamic linker x86_64] [i386:Mach-O dynamic linker i386]
dyld (for architecture x86_64): Mach-O 64-bit dynamic linker x86_64
dyld (for architecture i386):Mach-O dynamic linker i386
- 可以看出 dyld 这个文件有点特殊;能同时支持x86_64与i386两种架构;
- 终端输入:
-
查看打包之后生成的dSYM文件
- 终端输入:
file SuningWeiDian
- 终端输出:
Mach-O universal binary with 2 architectures: [arm_v7:Mach-O dSYM companion file arm_v7] [arm64] SuningWeiDian (for architecture armv7): Mach-O dSYM companion file arm_v7 SuningWeiDian (for architecture arm64): Mach-O 64-bit dSYM companion file arm64
- 终端输入:
通用二进制文件(Universal binary)
- 在iOS中不同手机对应着可能不同的架构,如arm64、armv7、armv7s,为了兼容不同架构的手机,苹果推出了通用二进制文件,其能同时支持多个不同架构,因此通用二进制文件,比单一架构二进制文件要大很多,因此也称之为
胖二进制文件
; - 当一个文件同时支持多个架构平台,比如同时支持 ARMV7、ARM64,就相当是两个 Mach-O 文件,编译器会编译两个Mach-O文件,然后合成一个Fat文件;
- 例如上面的dSYM文件,就是通用二进制文件,支持两种架构;
- 在Xcode工程中有配置支持不同架构的选项,如下图所示:
Mach-O文件的基本结构
- 先上一个官方截图,如下所示:
- 可以看出Mach-O文件主要包含三个部分:
-
Header
:包含Mach-O文件的基本信息,例如文件类型,支持的CPU架构类型,加载指令的数量,所占内存大小等等; -
Load Commands
:不同数据段segment的加载命令,指导加载器加载数据; -
Data
:指数据段Segment,其有不同的Section组成;
-
otool工具
-
otool是Mac系统自带的
,可以查看Mach-O文件特定部分和段的内容的工具; - 下面使用的资源是自己本地工程
Mach-O文件结构
生成一个Mach-O文件结构.app文件,其包内容中有一个Mach-O目标文件:Mach-O文件结构
,下面利用otool的常见命令行操作Mach-O文件结构
: -
otool -h Mach-O文件结构
:获取Mach-O文件的Header头信息
,输出结果如下:
Mach header
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
0xfeedfacf 16777223 3 0x00 2 75 8304 0x00218085
-
otool -L Mach-O文件结构
:查看Mach-O文件所使用的动态库
,会打印出App中所有的动态库
如下所示:
-
objdump --macho --private-headers Mach-O文件结构
,输出结果如下:
MachOView图形化界面工具
- otool是通过命令行来查看Mach-O文件的结构,但是不够直观,而MachOView是一款图形化的查看Mach-O文件结构的工具软件,更加直观;
- 点击这里 进行下载;
- 将
Mach-O文件结构
这个Mach-O文件直接拖入MachOView中
,如下所示:
Mach-O文件三部分的详细分析
- 源码查看 在Xcode中按下
Command+Shift+O
然后输入loader.h
可以定位到系统关于Mach-O文件的定义;
第一部分:Mach_Header
- 定义如下所示:
struct mach_header {
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 */
};
-
magic
:提供给系统内核,用来判断文件是否是Mach-O的文件格式; -
cputype
:表示支持的CPU类型,一般有armv7, armv64, x86, x86_64 这几种类型; -
filetype
:表示Mach-O的具体文件类型,如果是可执行文件就是 MH_EXECUTE,如果是动态库就是 MH_DYLIB,详情见文章顶部; -
ncmds
:表示Mach-O文件中所有Load Commands(加载命令)的总个数; -
sizeofcmds
:表示Load Commands所有(加载命令)占用的字节总大小; -
flags
:表示文件的标志信息;
第二部分:Load Commands
-
Load Commands
紧跟在Mach_Header之后,这些加载指令告诉loader加载器如何加载二进制数据,本质就是确定如何加载段segment数据
,其定义如下所示:
struct load_command {
uint32_t cmd; /* type of load command */
uint32_t cmdsize; /* total size of command in bytes */
};
-
属性cmd
:表示Load Commands(加载命令)的类型; -
属性cmdsize
:表示当前的加载命令所占内存大小; - 使用MachOView工具查看Mach-O文件的Load Commands部分可以看到:
- 常见的加载命令的简介如下所示:
-
LC_SEGMENT_64
:将该段(64位)映射到进程地址空间中; -
LC_DYLD_INF0_0NLY
:加载动态链接库信息(重定向地址、弱引用绑定、懒加载绑定、开放函数等的偏移值等信息); -
LC_SYMTAB
:加载符号表; -
LC_DYSYMTAB
:加载动态符号表; -
LC_LOAD_DYLINKER
:加载动态加载库,可以看出示例使用的是/usr/lib/dyld; -
LC.UUID
: 确定文件的唯一标识,crash解析中也会有这个,去检测dysm文件
和crash文件
是否匹配; -
LC_VERSION_MIN_IPHONEOS
:确定二进制文件要求的最低操作系统版本; -
LC.SOURCE.VERSION
:构建二进制文件的源代码版本号; -
LC.MAIN
:主程序的入口,dyld获取该地址,然后跳转到该处执行; -
LC_ENCRYPTION_INFO_64
:加载加密信息; -
LC_LOADJDYLIB
:加载额外的动态库; -
LC_FUNCTION_STARTS
:定义一个函数起始地址表,使调试器和其他程序易于看到一个地址是否在函数内; -
LC_DATA_IN_CODE
:定义在代码段内的非指令的表; -
LC_CODE_SIGNATURE
:获取应用签名信息;
-
- 下面以加载指令
LC_SEGMENT_64
为例,此加载指令的结构如下所示:
-
LC_SEGMENT_64
此加载指令属于segment段加载指令
,其结构体源码如下所示:
struct segment_command { /* for 32-bit architectures */
uint32_t cmd; /* LC_SEGMENT */
uint32_t cmdsize; /* includes sizeof section structs */
char segname[16]; /* segment name */
uint32_t vmaddr; /* memory address of this segment */
uint32_t vmsize; /* memory size of this segment */
uint32_t fileoff; /* file offset of this segment */
uint32_t filesize; /* amount to map from the file */
vm_prot_t maxprot; /* maximum VM protection */
vm_prot_t initprot; /* initial VM protection */
uint32_t nsects; /* number of sections in segment */
uint32_t flags; /* flags */
}
-
cmd
:加载命令的类型; -
cmdsize
:加载命令的所占内存大小; -
segname
: 加载目标段Segment的名称,常见的段segment有__PAGEZERO
、__LINKEDIT
、__TEXT
、__DATA
;-
__PAGEZERO
在可执行文件有的,动态库里没有,这个段开始地址为0(NULL指针指向的位置),是一个不可读、不可写、不可执行的空间,能够在空指针访问时抛出异常; -
__TEXT
:代码段
,里面主要是存放代码的,该段是可读可执行,但是不可写; -
__DATA
:数据段
,里面主要是存放数据,该段是可读可写,但不可执行; -
__LINKEDIT
:用于存放签名信息,该段是只可读,不可写不可执行;
-
- 段Segment类型的截图如下:
-
vmaddr
:段Segment的虚拟内存地址; -
vmsize
:段Segment的虚拟内存大小; -
fileoff
:段Segment的在文件中的偏移量; -
filesize
:段Segment在文件中所占的内存大小; -
nsects
:段Segment包含节区sections的数量; -
maxprot
:表示页面所需要的最高内存保护; -
initprot
:表示页面初始的内存保护; -
flags
:表示段的标志信息;
第三部分:Data数据部分
- Data数据部分,就是指段Segment的数据,而Segment段是由多个
Section
组成的,所以其主体部分为Section
,而Section的头部信息Section Header
是存放在段的加载命令中,其结构如下所示:
- 首先来介绍一下
Section Header
,当一个段segmemt包含多个节区Section,节区头Section Header
会以数组
的形式存储在段加载命令
中,如上截图所示,毋庸置疑其是描述Section
的结构信息的; -
Section
的源码如下所示:
struct section_64 { /* for 64-bit architectures */
char sectname[16]; /* name of this section */
char segname[16]; /* segment this section goes in */
uint64_t addr; /* memory address of this section */
uint64_t size; /* size in bytes of this section */
uint32_t offset; /* file offset of this section */
uint32_t align; /* section alignment (power of 2) */
uint32_t reloff; /* file offset of relocation entries */
uint32_t nreloc; /* number of relocation entries */
uint32_t flags; /* flags (section type and attributes)*/
uint32_t reserved1; /* reserved (for offset or index) */
uint32_t reserved2; /* reserved (for count or sizeof) */
uint32_t reserved3; /* reserved */
};
-
sectname
:section的名称,常见的section有_text、stubs等等; -
segname
:当前section所隶属的Segment,例如__TEXT(代码段); -
addr
: section在内存的起始位置; -
size
: section所占内存大小; -
offset
: section在文件中的偏移量; -
align
:字节大小对齐,2的align次方; -
reloff
:重定位入口的文件偏移; -
nreloc
: 需要重定位的入口数量; -
flags
:包含section的type和attributes;
__TEXT段中的Section组成如下所示:
-
__text
:代码节,存放机器编译后的代码; -
__stubs
:用于辅助做动态链接代码(dyld); -
__stub_helper
:用于辅助做动态链接(dyld); -
__objc_methname
:objc的方法名称; -
__cstring
:代码运行中包含的字符串常量,比如代码中定义#define kGeTuiPushAESKey "DWE2#@e2!"
,那DWE2#@e2!会存在这个区里; -
__objc_classname
: objc类名; -
__objc_methtype
: objc方法类型; -
__ustring
: -
__gcc_except_tab
: -
__const
:存储const修饰的常量; -
__dof_RACSignal
: -
__dof_RACCompou
: -
__unwind_info
:
__DATA段中的Section组成如下所示:
-
__got
:存储引用符号的实际地址,类似于动态符号表; -
__la_symbol_ptr
:lazy symbol pointers,懒加载的函数指针地址,和__stubs和stub_helper配合使用,具体原理暂留; -
__mod_init_func
:模块初始化的方法; -
__const
:存储constant常量的数据,比如使用extern导出的const修饰的常量; -
__cfstring
:使用Core Foundation字符串; -
__objc_classlist
:objc类列表,保存类信息,映射了__objc_data的地址; -
__objc_nlclslist
:Objective-C 的 +load 函数列表,比 __mod_init_func 更早执行; -
__objc_catlist
:categories分类; -
__objc_nlcatlist
:Objective-C 的categories的 +load函数列表; -
__objc_protolist
:objc协议列表; -
__objc_imageinfo
:objc镜像信息; -
__objc_const
:objc常量,保存objc_classdata结构体数据,用于映射类相关数据的地址,比如类名,方法名等; -
__objc_selrefs
:引用到的objc方法; -
__objc_protorefs
:引用到的objc协议; -
__objc_classrefs
:引用到的objc类; -
__objc_superrefs
:objc超类引用; -
__objc_ivar
:objc ivar指针,存储属性; -
__objc_data
:objc的数据,用于保存类需要的数据,最主要的内容是映射,__objc_const地址,用于找到类的相关数据; -
__data
:暂时没理解,从日志看存放了协议和一些固定了地址已经初始化的静态量; -
__bss
:存储未初始化的静态量,比如:static NSThread *_networkRequestThread = nil,
其中这里面的size表示应用运行占用的内存,不是实际的占用空间,所以计算大小的时候应该去掉这部分数据; -
__common
:存储导出的全局的数据,类似于static,但是没有用static修饰,比如KSCrash里面NSDictionary* g_registerOrders
,g_registerOrders就存储在__common里面;
Mach-O文件的结构分析
- 首先创建两个.c文件分别为
a.c
与b.c
,代码如下:
//a.c文件
#include <stdio.h>
//显式的说明了a的存储空间是在程序的其他地方分配的,在文件中其他位置或者其他文件中寻找a这个变量
extern int global_var;
void func(int a);
int main(int argc, const char * argv[]) {
int a = 100;
func(a + global_var);
return 0;
}
//b.c文件
#include <stdio.h>
int global_var = 1;
void func(int a) {
global_var = a;
}
在进行代码分析之前,首先介绍两个概念
模块
和符号
;模块
:我们可以理解一个源代码文件为一个模块。比如上面a模块和b模块。我们现在写一个程序,不可能所有代码都在一个源代码文件上,都是分模块的,一般一个类在一个源文件上,就成为一个模块,模块化好处就是复用、维护,还有编译时候,未改动的模块,不用重新编译,直接用之前编译好的缓存;符号
:简单理解就是函数名和变量名,比如上面总共有三个符号:global_var
、main
、func
;将a.c与b.c文件分别编译生成目标文件
a.o
与b.o
文件,可通过终端命令来实现,输入:xcrun -sdk iphoneos clang -c a.c b.c -target arm64-apple-ios12.2
-
将生成的a.o与b.o目标文件,进行静态链接,生成一个最终的目标文件,命名为
ab
,可通过终端命令来实现,输入:xcrun -sdk iphoneos clang a.o b.o -o ab -target arm64-apple-ios12.2
,最终的所有文件如下所示:
-
可使用
file 文件名
,查看文件类型,如下所示:
a.o
,b.o
和ab
均属于Mach-O文件,可使用MachOView打开进行查看;-
使用MachOView打开
a.o
文件,内容如下所示:
adrp x10 #0
:其中#0
是全局变量global_var
的临时内存地址,是编译器暂时用#0
代替的;ldr w11 [x10]
:将x10寄存器中的内存地址中的数值,也就是全局变量global_var
,写入w11寄存器中;add w0 w9 w11
:将w9与w11中的数值相加,即100+1计算结果赋值给w0,w0寄存器中存储着func函数的参数;bl #0x3c
:其中#0x3c
是func函数的临时地址,是编译器暂时用#0x3c
代替的;-
使用MachOView打开
ab
文件,内容如下所示:
a.o
与b.o
在经过链接器
进行静态链接之后,生成ab文件,在ab
文件中的全局变量global_var
与函数func
的内存地址是真正的内存地址,那么链接器是怎么进行调整的;全局变量
global_var
与函数func
的内存地址从a.o
到ab
经过了链接器
的静态链接,这两个符号的内存地址在前后发生了变化,现在我们来探索其中的工作原理;首先在
a.o
文件中包含了一个重定位表
,其专门保存了所有需要进行重定位的符号,根据符号信息可以在当前文件的符号表
中查看符号的详细信息;-
在进行
a.o
与b.o
文件链接时,会将a.o里面有这两符号的引用,然后b.o里面有这两符号的定义,一起合并到全局符号表里,最后在全局符号表中,对符号进行重定位,修正符号的正确地址;