Mach-O 类型的文件:是一种用于可执行文件、目标代码、动态库、内核转储的文件格式;
使用工具 MachOView 查看 Mach-O 文件结构
分析上图可知,Mach-O 文件主要包含三个区域:
- 1、
mach_header_64
头部: 描述了 Mach-O 的CPU架构、文件类型以及加载命令等信息。 - 2、
Load Commands
加载命令: 描述了文件中数据的具体组织结构,不同的数据类型使用不同的加载命令表示 - 3、
Data
:load_command
中定义的原始数据
1、头部mach_header_64
在 苹果源码 XNU 中的 loader.h
中找到结构mach_header
与mach_header_64
分别描述 32 位与 64 位的 Mach-O 头部信息。
struct mach_header {
uint32_t magic;// Mach-O 文件支持设备的CPU位数, 32位 CPU 取值 oxFEEDFACE
cpu_type_t cputype;// CPU类型
cpu_subtype_t cpusubtype; // CPU 子类型
uint32_t filetype; //文件类型,比如可执行文件、库文件、Dsym文件;
uint32_t ncmds; //加载命令的数量
uint32_t sizeofcmds; //所有加载命令的大小
uint32_t flags; //dyld 加载所需的标记:MH_PIE 表示启动地址空间布局随机化
};
struct mach_header_64 {
uint32_t magic;// Mach-O 文件支持设备的CPU位数,64位取值 xFEEDFACF
cpu_type_t cputype; // CPU类型
cpu_subtype_t cpusubtype;// CPU 子类型
uint32_t filetype; //文件类型,比如可执行文件、库文件、Dsym文件;
uint32_t ncmds; //加载命令的数量
uint32_t sizeofcmds; //所有加载命令的大小
uint32_t flags; //dyld 加载所需的标记:MH_PIE 表示启动地址空间布局随机化
uint32_t reserved; //64 位的保留字段
};
1.1、结构成员 magic
结构成员 magic
表示 Mach-O 文件支持设备的CPU位数, XNU 中有预定义的常量:
// mach_header(32位) 的 magic 字段的常量
#define MH_MAGIC 0xfeedface // 表示32位二进制
#define MH_CIGAM NXSwapInt(MH_MAGIC)
//mach_header_64(64位) 的 magic 字段的常量
#define MH_MAGIC_64 0xfeedfacf //表示64位二进制
#define MH_CIGAM_64 NXSwapInt(MH_MAGIC_64)
上述四个常量很有用处,用于判断当前可执行文件能否支持设备的 CPU:比如 Runtime 库的 objc-os.mm
文件中函数 bad_magic()
bool bad_magic(const headerType *mhdr){
return (mhdr->magic != MH_MAGIC && mhdr->magic != MH_MAGIC_64 &&
mhdr->magic != MH_CIGAM && mhdr->magic != MH_CIGAM_64);
}
该函数判断当前可执行文件,如果既不支持 32 位 CPU,又不支持 64 位 CPU,则返回 YES
。
1.2、结构成员 filetype
结构成员 filetype
表示 Mach-O 文件的类型,关于它的常量有:
#define MH_OBJECT 0x1 //可重定位目标文件:.o文件 .a/.framework静态库
#define MH_EXECUTE 0x2 //请求分页的可执行文件: app/MyApp ; .out
#define MH_FVMLIB 0x3 //固定VM共享库文件
#define MH_CORE 0x4 //核心文件
#define MH_PRELOAD 0x5 //预加载可执行文件
#define MH_DYLIB 0x6 //动态库 .framework .dylib
#define MH_DYLINKER 0x7 //动态链接器 usr/lib/dyld
#define MH_BUNDLE 0x8 //动态绑定 Bundle 文件
在本文,笔者分析的就是 .app
文件,通过运行一个 Demo 得到!从 Mach-O 文件的大致结构 图 可以看到该文件 .app
文件的类型就是 MH_EXECUTE
!
1.2.1、.app
文件
苹果是如何将一个.app
文件加载到 dyld2 呢?笔者在 dyld2 库 的 ImageLoaderMachO.cpp
文件中发现了函数 ImageLoaderMachO()
:
// 为主程序创建镜像
ImageLoaderMachO::ImageLoaderMachO(const struct mach_header* mh, uintptr_t slide, const char* path, const LinkContext& context)
: ImageLoader(path)
{
this->init();
fMachOData = (const uint8_t*)mh;
this->instantiateSegments((const uint8_t*)mh);
this->setSlide(slide);
this->parseLoadCmds();
this->adjustSegments();
#if __i386__
if ( fReadOnlyImportSegment != NULL )
fReadOnlyImportSegment->tempWritable(context, this);
#endif
if ( mh->flags & MH_PIE )
Segment::fgNextPIEDylibAddress = (uintptr_t)this->getEnd();
this->setMapped(context);
if ( context.verboseMapping ) {
dyld::log("dyld: Main executable mapped %s\n", this->getPath());
for (ImageLoader::SegmentIterator it = this->beginSegments(); it != this->endSegments(); ++it ) {
Segment* seg = *it;
if ( (strcmp(seg->getName(), "__PAGEZERO") == 0) || (strcmp(seg->getName(), "__UNIXSTACK") == 0) )
dyld::log("%18s at 0x%08lX->0x%08lX\n", seg->getName(), seg->getPreferredLoadAddress(), seg->getPreferredLoadAddress()+seg->getSize());
else
dyld::log("%18s at 0x%08lX->0x%08lX\n", seg->getName(), seg->getActualLoadAddress(this), seg->getActualLoadAddress(this)+seg->getSize());
}
}
}
此处笔者不对该函数做过多解释,以免跑题! 读者需要知道的就是:本文分析的 Mach-O 文件就是为了在 dyld2 中加载的!
1.2.2、MH_BUNDLE
文件
MH_BUNDLE
类型的文件 是不能加载到共享缓存之中的,所以在加载Mach-O 文件时 Runtime的 objc-runtime-new.mm
文件中函数 mustReadClasses()
,会被调用来检查是否有 Bundle
类:
bool mustReadClasses(header_info *hi){
const char *reason;
if (!hi->isPreoptimized()) {
reason = nil;
goto readthem;
}
#if TARGET_OS_SIMULATOR
reason = "the image is for iOS simulator";
goto readthem;
#endif
assert(!hi->isBundle()); // no MH_BUNDLE in shared cache
if (!noMissingWeakSuperclasses()) {
reason = "the image may contain classes with missing weak superclasses";
goto readthem;
}
if (haveFutureNamedClasses()) {
reason = "there are unresolved future classes pending";
goto readthem;
}
return NO;
readthem:
if (PrintPreopt && reason) {
_objc_inform("PREOPTIMIZATION: reading classes manually from %s because %s", hi->fname(), reason);
}
return YES;
}
我们可以看到该函数的关键代码 assert(!hi->isBundle())
,如果是MH_BUNDLE
类型文件,则无法通过断言!确保 Bundle
类不会被放到共享缓存!
1.3、结构成员 flags
结构成员 flags
有以下几个常量
#define MH_NOUNDEFS 0x1 //目标文件没有未定义的引用,可以执行
#define MH_INCRLINK 0x2 //目标文件是针对基本文件的增量链接的输出,不能再次链接编辑
#define MH_DYLDLINK 0x4 //目标文件是动态链接器的输入,不能再次静态链接编辑
#define MH_BINDATLOAD 0x8 //加载时,目标文件的未定义引用由动态链接器绑定。
#define MH_PREBOUND 0x10 //该文件具有预先绑定的动态未定义引用。
1.4、结构成员 ncmds
与 sizeofcmds
由 Mach-O 文件的大致结构 图 可以看到:
-
load_command
紧跟mach_header_64
之后; - 图中
Size of load Commands
的取值是3496
,该值是结构mach_header_64
的成员sizeofcmds
,表示所有加载命令load_command
所占内存之和; - 图中
Number of load Commands
的取值是21
,该值是结构mach_header_64
的成员ncmds
,表示所有加载命令load_command
的个数;
2、加载命令部分Load commands
Load commands
紧跟mach_header_64
之后;告诉操作系统应当如何加载文件中的数据,对系统内核加载器和动态链接器起指导作用。
一个 Mach-O 文件中有多个加载命令Load commands
,当然这些load_command
也具有不同的类型!
2.1、加载命令load_command
无论查看哪种类型的load_command
,我们都可以发现前两个字段总是cmd
和 cmdsize
,如上图所示:相同的 cmd
下它们的结构成员也是相同的!
struct load_command {
unsigned long cmd;
unsigned long cmdsize;
};
- 结构成员
cmd
表示该条加载命令的类型,每种load_command
类型都有专门针对它的结构; - 结构成员
cmdsize
表示load_command
的总大小:结构体大小 + 加上包含的节、字符串等;
要前进到下一个 load_command
,可以将 cmdsize
添加到当前 load_command
的偏移量或指针: 32位的 cmdsize
必须是4个字节的倍数,64位的 cmdsize
,必须是8个字节的倍数;永远是 load_command
的最大对齐:sizeof(long)
。
填充字节必须为零。
目标文件中的所有表也必须遵循这些规则,以便文件可以进行内存映射;否则,指向这些表的指针在某些 CPU 将无法正常工作或根本无法正常工作。所有填充为 0 的对象将逐个字节进行比较。
2.2.1、加载命令load_command
的类型
在上图可以看到不同的 cmd
下它们的结构成员大不相同,那么都有哪些不同的类型呢?苹果为 load_command
的类型定义了一些常量:
#define LC_SEGMENT 0x1 //该文件被映射的段
#define LC_SYMTAB 0x2 //为文件定义符号表和字符串表,在连接文件时被链接器使用,同时也用于调试器映射符号到源文件。符号表定义的本地符号仅用于本地测试,而已定义和未定义的 external 符号被链接器使用
#define LC_SYMSEG 0x3 //符号表信息,符号表中详细说明了代码中所用符号的信息等(过时)
#define LC_THREAD 0x4 //线程
#define LC_UNIXTHREAD 0x5 //unix线程(包括堆栈)
#define LC_LOADFVMLIB 0x6 //加载指定的固定VM共享库
#define LC_IDFVMLIB 0x7 //固定VM共享库的标识
#define LC_IDENT 0x8 //object 标识信息(已过时)
#define LC_FVMFILE 0x9 /* fixed VM file inclusion (internal use) */
#define LC_PREPAGE 0xa //prepage 命令(内部使用)
#define LC_DYSYMTAB 0xb //将符号表中给出符号的额外符号信息提供给动态链接器
#define LC_LOAD_DYLIB 0xc //依赖的动态库,包括动态库名称、当前版本号、兼容版本号,(可以使用 otool -L xxx 命令查看)
#define LC_ID_DYLIB 0xd //动态链接共享库的标识
#define LC_LOAD_DYLINKER 0xe //默认的加载器路径
#define LC_ID_DYLINKER 0xf //动态链接器识别
#define LC_PREBOUND_DYLIB 0x10 /* modules prebound for a dynamicly */
LC_SEGMENT
:段的映射
LC_SEGMENT
类型的加载命令是最常见的 load_command
了,该命令被映射到段segment_command_64
,关于段的详细讲解,笔者放到下一节再提及!
2.2、段segment_command_64
先在 XNU 中的 loader.h
中找到结构segment_command_64
的实现:
struct segment_command {// 32 位
unsigned long cmd;//load_command结构成员cmd的取值,取值 LC_SEGMENT 将文件中的段映射到进程地址空间
unsigned long cmdsize;//load_command结构大小
char segname[16];// 16字节的段名字
unsigned long vmaddr; //映射到虚拟地址的偏移
unsigned long vmsize; //映射到虚拟地址的大小
unsigned long fileoff;//对应于当前架构文件的偏移(注意:是当前架构,不是整个 FAT 文件)
unsigned long filesize;//文件大小
vm_prot_t maxprot;//段里面的最高内存保护
vm_prot_t initprot;//初始内存保护
unsigned long nsects;//该段包含的节个数
unsigned long flags;//段页面标志
};
struct segment_command_64 {//64 位
uint32_t cmd;//load_command结构成员cmd的取值,取值 LC_SEGMENT 将文件中的段映射到进程地址空间
uint32_t cmdsize;//load_command结构大小
char segname[16];// 16字节的段名字
uint64_t vmaddr; //映射到虚拟地址的偏移
uint64_t vmsize; //映射到虚拟地址的大小
uint64_t fileoff;//对应于当前架构文件的偏移(注意:是当前架构,不是整个 FAT 文件)
uint64_t filesize;//文件大小
vm_prot_t maxprot;//段里面的最高内存保护
vm_prot_t initprot;//初始内存保护
uint32_t nsects;//该段包含的节个数
uint32_t flags;//段页面标志 : 表示节的标志
};
通过对比第2节的 load_command
分析图可以看出,LC_SEGMENT
类型的load_command
中的结构成员对应着段segment_command_64
的成员,其中:
segment_command_64
的结构成员 cmd
取值一定为 LC_SEGMENT
,表示是段命令,该cmd
也对应着load_command
中的cmd
;
cmdsize
也是如此!
2.2.1、结构成员segname
结构成员segname
将段分为不同类型的段,在第2节的 load_command
分析图中,可以看到相同LC_SEGMENT
类型的load_command
有四个,但是这四个load_command
的segname
不同。
结构成员segname
代表该段的名字,是一个 16字节的字符串!苹果为其提供了几个常量
//空指针陷阱段,映射到虚拟内存空间的第 1 页,用于捕捉 MH_EXECUTE 文件对 NULL 指针的引用
#define SEG_PAGEZERO "__PAGEZERO"
#define SEG_TEXT "__TEXT" //传统 UNIX 代码段,只读不可写
#define SEG_DATA "__DATA" //读取和写入数据的程序数据段
#define SEG_OBJC "__OBJC" //objective-C runtime segment
#define SEG_ICON "__ICON" //icon segment
#define SEG_LINKEDIT "__LINKEDIT" //由链接编辑器创建和维护的所有结构的段,仅为 MH_EXECUTE 和 FVMLIB 类型的文件使用 ld(1) 的- seglinkedit 选项创建
#define SEG_UNIXSTACK "__UNIXSTACK" //unix堆段
在不同segname
的段中,还有一堆更细的分类,这些细致分类是一个个的节section_64
!不同的segname
各自存储着它们自己的节section_64
!
a、__TEXT
段
__TEXT
段中常见的节类型:
__text //程序可执行的代码区域
__stubs //间接符号存根,跳转到懒加载指针表
__stub_helper //帮助解决懒加载符号加载的辅助函数
__objc_methname //OC方法名称
__objc_methtype //OC方法签名
__objc_classname //OC类名
__cstring //只读的 C 字符串,包含 OC 的部分字符串和属性名
__const //常量
b、__DATA
段
__DATA
段中常见的节类型
__nl_symbol_ptr //非懒加载指针表,在 dyld 加载时会立即绑定值
__la_symbol_ptr //懒加载指针表,第一次调用时才会绑定值
__got // 非懒加载全局指针表
__mod_init_func // constructor 函数
__mod_term_func // destructor 函数
__cfstring //OC 字符串
__objc_classlist //文件中类的列表
__objc_nlclslist //文件中自己实现了 +load 方法的类
__objc_protolist //协议列表
__objc_classrefs // 被引用的类列表
__objc_imageinfo // OC镜像信息
__objc_protollist //OC原型列表
__objc_const //OC常量
__objc_selfrefs //OC类自引用(self)
__objc_superrefs //OC类超类引用(super)
__objc_protolrefs // OC原型引用
__objc_selrefs //被引用SEL对应的字符串
__objc_msgrefs //
__objc_nlclslist//获取非懒加载的所有的类的列表
__objc_catlist//获取文件中的 category
__objc_nlcatlist //获取非懒加载的所有的分类的列表
__objc_protorefs //OC 协议引用
__objc_init_func //
__bss //没有初始化和初始化为0 的全局变量
__data //真正初始化的数据节没有填充,没有bss重叠
__common //链接编辑器在节中分配公共符号
2.3、节 section_64
说了这么多的节类型,我们还是先看看节 section_64
的具体实现吧:
struct section {// 32 位
char sectname[16];//节的名字
char segname[16];//节所在段的名字
unsigned long addr;//映射到虚拟地址的偏移
unsigned long size;//节的大小
unsigned long offset;//节在当前架构文件中的偏移
unsigned long align;//节的字节对齐大小 n ,计算结果为 2^n
unsigned long reloff;//重定位入口的文件偏移
unsigned long nreloc; //重定位入口的个数
unsigned long flags;//节的类型和属性
unsigned long reserved1; //保留位
unsigned long reserved2; //保留位
};
struct section_64 { // 64 位
char sectname[16];//节的名字
char segname[16];//节所在段的名字
uint64_t addr;//映射到虚拟地址的偏移
uint64_t size;//节的大小
uint32_t offset;//节在当前架构文件中的偏移
uint32_t align; //节的字节对齐大小 n ,计算结果为 2^n
uint32_t reloff;//重定位入口的文件偏移
uint32_t nreloc; //重定位入口的个数
uint32_t flags;//节的类型和属性
uint32_t reserved1; //用于偏移量或索引
uint32_t reserved2; //数量或大小
uint32_t reserved3;//保留位
};
笔者在上节分析的那么多的节类型,就是 section_64
的结构成员sectname
,不同的节名sectname
代表不同的含义!
2.3.1、节 section_64
的 flags
/* section_64 结构的flags字段表示属性部分的常量
*/
#define SECTION_ATTRIBUTES_USR 0xff000000 /* User setable attributes */
#define S_ATTR_PURE_INSTRUCTIONS 0x80000000 /* section contains only true machine instructions */
#define SECTION_ATTRIBUTES_SYS 0x00ffff00 /* system setable attributes */
#define S_ATTR_SOME_INSTRUCTIONS 0x00000400 /* section contains some machine instructions */
#define S_ATTR_EXT_RELOC 0x00000200 /* section has external relocation entries */
#define S_ATTR_LOC_RELOC 0x00000100 /* section has local relocation entries */
2.3.2、虚拟地址
虚拟地址section_64-> addr
,表示该节的内容映射到虚拟内存时相对该模块加载基地址的偏移。
运行程序,获取该模块加载基地址 (该地址每次运行程序,都是不同的),然后加上偏移的虚拟地址:
(lldb) im li -o -f RuntimeUseDemo
[ 0] 0x0000000001584000 /Users/longlong/Library/Developer/Xcode/DerivedData/SourceCode-gbviaxaumpwhkieshjwwgrxrxpva/Build/Products/Debug-iphonesimulator/RuntimeUseDemo.app/RuntimeUseDemo
(lldb) p/x 0x00000001000019C0 + 0x0000000001584000
(long) $0 = 0x00000001015859c0
对 0x00000001015859c0
这个地址反汇编:
(lldb) dis -a 0x00000001015859c0
RuntimeUseDemo`main:
0x1015859c0 <+0>: pushq %rbp
0x1015859c1 <+1>: movq %rsp, %rbp
0x1015859c4 <+4>: subq $0x40, %rsp
0x1015859c8 <+8>: movl $0x0, -0x4(%rbp)
0x1015859cf <+15>: movl %edi, -0x8(%rbp)
0x1015859d2 <+18>: movq %rsi, -0x10(%rbp)
0x1015859d6 <+22>: callq 0x10158a46e ; symbol stub for: objc_autoreleasePoolPush
0x1015859db <+27>: leaq 0x7906(%rip), %rsi ; @"RW_REALIZED|RW_REALIZING -------- %x"
0x1015859e2 <+34>: movl $0x80080000, %edi ; imm = 0x80080000
0x1015859e7 <+39>: movl %edi, -0x14(%rbp)
0x1015859ea <+42>: movq %rsi, %rdi
0x1015859ed <+45>: movl -0x14(%rbp), %esi
0x1015859f0 <+48>: movq %rax, -0x20(%rbp)
0x1015859f4 <+52>: movb $0x0, %al
0x1015859f6 <+54>: callq 0x10158a39c ; symbol stub for: NSLog
0x1015859fb <+59>: leaq 0x7906(%rip), %rdi ; @"RO_META-------- %x"
0x101585a02 <+66>: movl $0x1, %esi
0x101585a07 <+71>: movb $0x0, %al
0x101585a09 <+73>: callq 0x10158a39c ; symbol stub for: NSLog
0x101585a0e <+78>: leaq 0x7913(%rip), %rdi ; @"RO_ROOT-------- %x"
0x101585a15 <+85>: movl $0x2, %esi
0x101585a1a <+90>: movb $0x0, %al
0x101585a1c <+92>: callq 0x10158a39c ; symbol stub for: NSLog
0x101585a21 <+97>: leaq 0x7920(%rip), %rdi ; @"RW_REALIZED-------- %x"
0x101585a28 <+104>: movl $0x80000000, %esi ; imm = 0x80000000
0x101585a2d <+109>: movb $0x0, %al
0x101585a2f <+111>: callq 0x10158a39c ; symbol stub for: NSLog
0x101585a34 <+116>: leaq 0x792d(%rip), %rdi ; @"-------- %lx"
0x101585a3b <+123>: movl $0x1e08, %esi ; imm = 0x1E08
0x101585a40 <+128>: movb $0x0, %al
0x101585a42 <+130>: callq 0x10158a39c ; symbol stub for: NSLog
0x101585a47 <+135>: movl -0x8(%rbp), %edi
0x101585a4a <+138>: movq -0x10(%rbp), %rsi
0x101585a4e <+142>: movq 0x9b63(%rip), %rcx ; (void *)0x000000010158f6a0: AppDelegate
0x101585a55 <+149>: movq 0x989c(%rip), %rdx ; "class"
0x101585a5c <+156>: movl %edi, -0x24(%rbp)
0x101585a5f <+159>: movq %rcx, %rdi
0x101585a62 <+162>: movq %rsi, -0x30(%rbp)
0x101585a66 <+166>: movq %rdx, %rsi
0x101585a69 <+169>: callq *0x7601(%rip) ; (void *)0x0000000101ec6d80: objc_msgSend
0x101585a6f <+175>: movq %rax, %rdi
0x101585a72 <+178>: callq 0x10158a3a8 ; symbol stub for: NSStringFromClass
0x101585a77 <+183>: movq %rax, %rdi
0x101585a7a <+186>: callq 0x10158a4d4 ; symbol stub for: objc_retainAutoreleasedReturnValue
0x101585a7f <+191>: xorl %r8d, %r8d
0x101585a82 <+194>: movl %r8d, %edx
0x101585a85 <+197>: movl -0x24(%rbp), %edi
0x101585a88 <+200>: movq -0x30(%rbp), %rsi
0x101585a8c <+204>: movq %rax, %rcx
0x101585a8f <+207>: movq %rax, -0x38(%rbp)
0x101585a93 <+211>: callq 0x10158a3b4 ; symbol stub for: UIApplicationMain
0x101585a98 <+216>: movl %eax, -0x4(%rbp)
0x101585a9b <+219>: movq -0x38(%rbp), %rcx
0x101585a9f <+223>: movq %rcx, %rdi
0x101585aa2 <+226>: callq *0x75d8(%rip) ; (void *)0x0000000101ec4010: objc_release
0x101585aa8 <+232>: movq -0x20(%rbp), %rdi
0x101585aac <+236>: callq 0x10158a468 ; symbol stub for: objc_autoreleasePoolPop
0x101585ab1 <+241>: movl -0x4(%rbp), %eax
0x101585ab4 <+244>: addq $0x40, %rsp
0x101585ab8 <+248>: popq %rbp
0x101585ab9 <+249>: retq
使用 Xocde 的 Debug -> DebugWorkflow -> ViewMemory ,输入地址0x00000001015859c0
:
而我们使用MachOView 查看文件偏移 0x000019C0
处的数据如下图所示:
可以看到,这两处看到的内容是一样的!
3、数据部分
我们分析了 Mach-O 的头部mach_header_64
头部,知道了 Mach-O 的CPU架构、文件类型以及加载命令等信息;接着又分析了加载命令Load Commands
,知道了 应当如何加载文件中的数据。
我们的目的是为了读取 Mach-O 的信息,这些信息就存储在 Mach-O 的数据部分,接下来,我们尝试去解析一些数据!
文件偏移地址 = 虚拟地址 - 模块在内存中地址 + 模块在文件中的偏移