Mach-O文件解析
Mach-O是什么?
Mach-O 其实是 Mach Object 文件格式的缩写,在 Mac 和 iOS 上,可执行文件的格式通常都是 Mach-O 格式,但是 Mach-O 格式的文件并非一定是可执行文件。
Mach 不是 Mac,Mac 是苹果电脑 Macintosh(麦金塔电脑) 的简称,而 Mach 是一个由卡内基梅隆大学开辟的用于支持操作体系研究的操作体系内核。史蒂夫·乔布斯在1985年离开苹果后创立了 NeXT 公司,NeXT.Inc 以 Mach内核 和 BSD Unix 操作体系的部分代码为底子,以 Objective-C 作为原生语言,开辟出了 NEXTSTEP 操作体系,在 Mach 上,可执行文件的格式就是 Mach-O(Mach Object file format)。1996年,乔布斯将 NEXTSTEP 带回苹果,Math 也成为了 OS X 的内核底子。所以虽然 Mac OS X 是 Unix 的“子女”,但主要支持的可执行文件格式是 Mach-O。iOS 是从 OS X 演变而来,所以同样支持 Mach-O 格式的可执行文件。
常见属于 Mach-O 格式的文件范例:
- Mach-O Object 目标文件 .o
- Mach-O Ececutable 可执行文件
- Mach-O Dynamically 动态库文件 .a 、 .dylib
- Mach-O dynamic linker 动态链接器文件
- Mach-O dSYM companion 符号表文件
- Bundle 自己创建的动态库,运行在沙盒中,无法被 dyld 链接,只能通过 dlopen() 加载
- Framework 包含Dylib、资源文件和头文件的文件夹
查看Mach-O文件
当我们使用Xcode运行我们的iOS程序的时间,会天生一个 .app 文件,位于 工程目录/Product 文件夹下,.app 文件实际上是一个文件夹,我们可以右键,显示包内容,就可以查看文件夹中的内容了,此中有一个与工程同名的 unix 可执行文件,这就是一个 Mach-O 格式的文件。
使用 file 下令可以查看文件格式:
- ➜ file xxxx
- xxxx: Mach-O universal binary with 2 architectures: [arm_v7:Mach-O executable arm_v7] [arm64]
- xxxx (for architecture armv7): Mach-O executable arm_v7
- xxxx (for architecture arm64): Mach-O 64-bit executable arm64
复制代码 使用 lipo - info 下令可以查看文件所支持的体系架构:
- ➜ lipo -info xxxx
- Architectures in the fat file: xxxx are: armv7 arm64
复制代码
我们可以使用 MachOView 打开该文件,文件结构如下:
可以看到,在该文件中有两种体系架构 armv7 和 arm64,这种包含了支持多架构的 Mach-O Ececutable 可执行文件被称为:通用二进制文件,即多种架构都可读取运行。
在 Xcode 中通过编译设置 Architectures 是可以更改所天生的 Mach-O ececutable 可执行文件的支持架构,想要支持多架构的话,在Valid Architectures 中继承添加就可以了。
通用二进制文件
- 苹果公司提出的一种程序代码。能同时适用多种架构的二进制文件
- 同一个程序包中同时为多种架构提供最抱负的性能。
- 由于必要储存多种代码,通用二进制应用程序通常比单一平台二进制的程序要大。
- 但是由于两种架构有共通的非执行资源,所以并不会达到单一版本的两倍之多。
- 而且由于执行中只调用一部分代码,运行起来也不必要额外的内存。
通用二进制文件通常被称为 Universal binary ,在 MachOView 中叫做 Fat binary ,这种二进制文件是可以完全拆分开来,大概重新组合的,那么接下来我们来试一下。
拆分 Fat binary,拆分后源文件并不会改变:
- [/code] [code]➜ lipo xxxx -thin armv7 -output xxxx_armv7
- ➜ lipo xxxx -thin arm64 -output xxxx_arm64
- ➜ ls
- xxxx
- xxxx_arm64
- xxxx_armv7
复制代码
归并 Fat binary:
- [/code] [code]➜ lipo -create xxxx_armv7 xxxx_arm64 -output NewXXXX
复制代码
归并后我们来看下新天生的和从前的文件的哈希值,千篇同等:
- ➜ Desktop md5 xxxx
- MD5 (xxxx) = 3becf3b28d7bb00035ae2dd85172b303
- ➜ Desktop md5 NewXXXX
- MD5 (NewXXXX) = 3becf3b28d7bb00035ae2dd85172b303
复制代码
多架构二进制文件组合成通用二进制文件时 , 代码部分是不共用的 ( 由于代码的二进制文件不同的组合在不同的 cpu 上可能会是不同的意义 ) 。而公共资源文件是会共用的。
Mach-O文件格式
Header
Header 中生存了一些根本信息,包括了该文件是 32 位还是 64 位、运行该文件对应的处理器架构是什么、文件范例、LoadCommands 的个数等。通过 MachOView 打开可执行文件,可以看到 Header 的结构:
接下来我们联合 Mach-O/loader.h 的源码来分析一下 header 中每一项的寄义,如何找到该源码呢?
- Darwin-XNU 内核中的 Mach-O/loader.h
- Xcode 目录下 mach-o 文件夹的路径
- [/code] [code]/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/mach-o
复制代码 从源码中我们可以找到 mach_header 和 mach_header_64 结构体,mach_header_64 比 mach_header ,多了一个保留字段。
- [/code] [code]struct mach_header_64 {
- uint32_t magic; /* 魔数,快速定位64位/32位 */
- cpu_type_t cputype; /* 支持的 CPU 架构类型 比如 ARM */
- cpu_subtype_t cpusubtype; /* 在支持的CPU架构类型下,所支持的具体机器型号 比如arm64 */
- uint32_t filetype; /* 文件类型 例如可执行文件 .. */
- uint32_t ncmds; /* load commands 加载命令条数 */
- uint32_t sizeofcmds; /* 所有 load commands 加载命令的大小*/
- uint32_t flags; /* 标志位 标识二进制文件支持的功能 , 主要是和系统加载、链接有关*/
- uint32_t reserved; /* reserved , 保留字段 */
- };
复制代码
filetype,Mach-O的文件范例:
- [/code] [code]#define MH_OBJECT 0x1 /* Target 文件:编译器对源码编译后得到的中间结果 */
- #define MH_EXECUTE 0x2 /* 可执行二进制文件 */
- #define MH_FVMLIB 0x3 /* VM 共享库文件(还不清楚是什么东西) */
- #define MH_CORE 0x4 /* Core 文件,一般在 App Crash 产生 */
- #define MH_PRELOAD 0x5 /* preloaded executable file */
- #define MH_DYLIB 0x6 /* 动态库 */
- #define MH_DYLINKER 0x7 /* 动态连接器 /usr/lib/dyld */
- #define MH_BUNDLE 0x8 /* 非独立的二进制文件,往往通过 gcc-bundle 生成 */
- #define MH_DYLIB_STUB 0x9 /* 静态链接文件 */
- #define MH_DSYM 0xa /* 符号文件以及调试信息,在解析堆栈符号中常用 */
- #define MH_KEXT_BUNDLE 0xb /* x86_64 内核扩展 */
复制代码
flags,Mach-O文件的标志位。主要作用是告诉体系该如何加载这个Mach-O文件以及该文件的一些特性。有许多值,我们取常见的几种:
- [/code] [code]#define MH_NOUNDEFS 0x1 /* Target 文件中没有带未定义的符号,常为静态二进制文件 */
- #define MH_SPLIT_SEGS 0x20 /* Target 文件中的只读 Segment 和可读写 Segment 分开 */
- #define MH_TWOLEVEL 0x80 /* 该 Image 使用二级命名空间(two name space binding)绑定方案 */
- #define MH_FORCE_FLAT 0x100 /* 使用扁平命名空间(flat name space binding)绑定(与 MH_TWOLEVEL 互斥) */
- #define MH_WEAK_DEFINES 0x8000 /* 二进制文件使用了弱符号 */
- #define MH_BINDS_TO_WEAK 0x10000 /* 二进制文件链接了弱符号 */
- #define MH_ALLOW_STACK_EXECUTION 0x20000/* 允许 Stack 可执行 */
- #define MH_PIE 0x200000 /* 加载程序在随机的地址空间,只在 MH_EXECUTE中使用 */
- #define MH_NO_HEAP_EXECUTION 0x1000000 /* 将 Heap 标记为不可执行,可防止 heap spray 攻击 */
复制代码
Load Commands
load commands 紧跟在 header 之后,用来告诉内核和 dyld,如何将 APP 运行所需的资源加载入内存中。比如 main 函数的加载地址,动态链接器 dyld 的文件路径,以及相关依靠库的文件路径,另有 Data 中的 Segment 如何加载入内存。
load command 在源码被表示为 struct,有若干种 load command,但是共同的特点是,在其结构的开头处,必须是如下两个属性:
- [/code] [code]struct load_command {
- uint32_t cmd; /* type of load command */
- uint32_t cmdsize; /* total size of command in bytes */
- };
复制代码
苹果为 cmd 定义了若干的宏,用来表示 cmd 的范例,下面列举出几种:
- [/code] [code]#define LC_SEGMENT 0x1 /* 描述该如何将该32位 segment 加载如内存,对应 segment_command 类型 */
- #define LC_SEGMENT_64 0x19 /* 描述该如何将该64位 segment 加载如内存,对应 segment_command_64 类型 */
- #define LC_UUID 0x1b /* 二进制文件的唯一标识符 */
- #define LC_LOAD_DYLINKER 0xe /* 启动动态链接器 dyld */
复制代码 Segment Load Command
在这么多的 load command 中,必要我们重点关注的是 segment load command。segment command 表明了该如何将 Data 中的各个 Segment 加载入内存中。
Segment load command 分为32位和64位,32位和64位的 Segment load command 根本雷同,只不过在64位的结构中,把和寻址相关的数据范例,由32位的 uint32_t 改为了64位的 uint64_t 范例。
- [/code] [code]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 */
- };
- struct segment_command_64 { /* for 64-bit architectures */
- uint32_t cmd; /* LC_SEGMENT_64 */
- uint32_t cmdsize; /* includes sizeof section_64 structs */
- char segname[16]; /* segment name */
- uint64_t vmaddr; /* memory address of this segment */
- uint64_t vmsize; /* memory size of this segment */
- uint64_t fileoff; /* file offset of this segment */
- uint64_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 */
- };
复制代码 我们通过 MachOView 来看看:
可以看到 LC_SEGMENT(__PAGEZERO) 、LG_SEGMENT(__TEXT) 、LG_SEGMENT(__DATA) 、LG_SEGMENT(__LINKEDIT) 都是 Segment load command。
LC_SEGMENT(__PAGEZERO) 是一个特殊的 Segment,这个 Segment 其实是苹果虚拟出来的,只是一个逻辑上的段,而在Data中,根本没有对应的内容,也没有占用任何硬盘空间。LC_SEGMENT(__PAGEZERO) 在VM中被置为 Read only,逻辑上占用APP最开始的一段内存空间,用来处理空指针。由上图可以看到其 vm size 是 16384 字节 = 16KB,但其真正的物理地址 File size 和 File offset 都是0。
当启动一个应用程序的时间,体系内核把应用映射到新的地址空间,且每次起始位置都是随机的(由于使用了ASLR),并将起始位置到某段范围的进程权限都标志为不可读写不可执行,而这个范围便是 LC_SEGMENT(__PAGEZERO) 的 vm size。
- 如果是32位进程,这个范围至少是4KB。
- 如果是64位进程,至少是4GB。
- 可以捕捉任何空指针引用。
- 捕捉任何指针截断。
LC_LOAD_DYLINKER 用来导入 dyld,此中生存着 dyld 的路径,一般为 /usr/lib/dyld 。
LC_LOAD_DYLIB
在 Load Commands 里,有许多 LC_LOAD_DYLIB 下令,用来加载动态库,包括体系动态库和我们自己添加的动态库,比如我们通过 CocoaPods 引入的第三方库。
Section Header
在 Data 中,程序的逻辑和数据是按照 Segment(段)存储,而在 Segment 中,又分为0或多个 Section(节),每个 Section 中存储的才是实际的内容。之所以这样构造数据,是由于同一个段下的节,在内存的权限相同,可以不完全按照页巨细进行内存对齐,节流内存空间。而对外团体暴露段,在装载程序的时间完备映射成一个vma,可以更好的做内存对齐。每一个 Segment 的巨细都是内存页巨细的整数倍。
每一个 Segment load command 下面,都会包含对应 Segment 下所有 Section 的 header。
Section Header 的定义如下:
- [/code] [code]struct section { /* for 32-bit architectures */
- char sectname[16]; /* name of this section */
- char segname[16]; /* segment this section goes in */
- uint32_t addr; /* memory address of this section */
- uint32_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) */
- };
- 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:比如 _text、stubs
- segname : 该 section 所属的 segment,比如 __TEXT
- addr : 该 section 在内存的起始位置
- size : 该 section 的巨细
- offset : 该section的文件偏移
- align :字节巨细对齐
- reloff :重定位入口的文件偏移
- nreloc :必要重定位的入口数量
- flags :包含 section 的 type 和 attributes
加载对应的 Segment 的时间,就是根据 Segment load command 下面的 Section Header 来逐个加载对应的 Section。
Data
Data 是真正存储 APP 二进制数据的地方,前面的 Header 和 Load Command,仅是提供文件的说明以及如何加载信息的功能。Data 也被分为若干的部分,除了包含程序的逻辑和数据的 Segment 外,还包括符号表,代码签名,动态加载器信息等。我们具体来讨论一下 Segment 部分。
Segment 根据内容的不同,分为若干范例,范例名称均是以 “双下划线+大写英文” 表示,有的 Segment 下面还会包含若干的 Section,Section 的定名是以 ”双下划线+小写英文” 表示。
在 Mach-O 中定义了以下5种 Segment:
- [/code] [code]#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" /* 包含需要被动态链接器使用的符号和其他表,包括符号表、字符串表等 */
复制代码
留意这里提到的 SEG_OBJC ,是和 OC 的 Runtime 相关的,但是在 OBJC2.0 中已经废弃掉 __OBJC 段,而是将其放入 __DATA 段中以 __objc 开头的 Section 中。
OC项目标 Mach-O 文件 Data 模块内容:
Swift项目标 Mach-O 文件 Data 模块内容:
__TEXT
__TEXT 是程序的只读段,用于生存我们所写的代码和字符串常量,const 修饰常量等。下面是 __TEXT 段下常见的 Section:
Section用途__TEXT.__text主程序代码__TEXT.__cstringC 语言字符串__TEXT.__constconst 关键字修饰的常量__TEXT.__stubs用于 Stub 的占位代码,许多地方称之为桩代码。__TEXT.__stubs_helper当 Stub 无法找到真正的符号地址后的最终指向__TEXT.__objc_methname记录了当前APP中所定义的所有方法的名称__TEXT.__objc_methtype这个seciton与__objc_methname节对应,记录了method的形貌字符串__TEXT.__objc_classname这里面以字符串常量的形式,记录了我们自定义 Class 以及所引用的体系 Class 的名称,同时也包括 Category,protocol 的名称 值得留意的是,这些都是以明文形式展现的。如果我们将加密key用字符串常量或宏定义的形式存储在程序中,可以想象其安全性是得不到保障的。
__Data
__DATA 段用于存储程序中所定义的数据,可读写。在 __DATA 段下,有许多以 __objc 开头的 Section,而这些 Section,均是和 Runtime 的加载有关的。__DATA 段下常见的 Section 有:
Section用途__DATA.__data初始化过的可变数据__DATA.__la_symbol_ptrlazy binding 的指针表,表中的指针一开始都指向 __TEXT.__stub_helper__DATA.__nl_symbol_ptr非 lazy binding 的指针表,每个表项中的指针都指向一个在装载过程中,被动态链接器搜刮完成的符号__DATA.__const记录在OC内存初始化过程中的不可变内容,比如 method_t 结构体定义__DATA.__cfstring程序中使用的 Core Foundation 字符串(CFStringRefs)__DATA.__objc_ivar存储程序中的 ivar 变量__DATA.__objc_classlist记录了App中所有的class,包括meta class。该节中存储的是一个个的指针,指针指向的地址是class结构体所在的地址__DATA.__objc_protolist记录了App 中所有的 Protocol,存储了一个个指向 protocol_t 的指针__DATA.__objc_catlist记录了App中所有的Catgory,存储了一个个指向 __objc_category 的指针__DATA.__objc_imginfo主要用来区分OC的版本是 1.0 还是 2.0__DATA.__objc_selrefs标志哪些SEL对应的字符串被引用了__DATA.__objc_classrefs标志哪些类被引用了__DATA.__objc_protorefs标志哪些 Protocol 被引用了__DATA.__objc_superrefsObjective-C 超类引用
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |