Objective-C runtime机制前传1 - Mach-O格式

Mach-O

Mach-O是Mach Object文件格式的缩写。它是用于可执行文件,动态库,目标代码的文件格式。作为a.out格式的替代,Mach-O格式提供了更强的扩展性,以及更快的符号表信息访问速度。
Mach-O格式为大部分基于Mach内核的操作系统所使用的,包括NeXTSTEP, Mac OS X和iOS,它们都以Mach-O格式作为其可执行文件,动态库,目标代码的文件格式。
具体到我们的iOS程序,当用XCode打包后,会生成一个.app为扩展名的文件(位于工程目录/Products文件夹下),其实.app是一个文件夹,我们用鼠标右键选择‘Show Package contents’,就可以查看文件夹的内容,其中会发现有一个和我们工程同名的unix 可执行文件,这个就是iOS可执行文件,它是符合Mach-O格式的
这里写图片描述

Mach-O文件结构

关于Mach-O的文件格式,在苹果官网已经找不到相关说明了,但是你可以通过下面链接获取PDF版说明:

Mach-O File Format Reference

Mach-O格式如下图所示,它被分为headerload commandsdata三大部分:

这里写图片描述

header:对Mach-O文件的一个概要说明,包括Magic Number, 支持的CUP类型等。

load commands: 当系统加载Mach-O文件时,load command会指导苹果的动态加载器(dyld)h或内核,该如何加载文件的Data数据。

data: Mach-O文件的数据区,包含代码和数据。其中包含若干Segment块(注意,除了Segment块之外,还有别的内容,包括code signature,符号表之类,不要被苹果的图所误导!),每个Segment块中包含0个或多个seciton。Segment根据对应的load command被dyld加载入内存中。

我们可以使用MachOView(一个查看MachO 格式文件信息的开源工具)工具来查看一个具体的文件的Mach-O格式。

header

我们以一个普通的iOS APP为例,看看Mach-O文件header部分的具体内容。通过MachOView打开可执行文件,可以看到header的结构:
这里写图片描述

是不是有些懵?下面我们就结合Darwin内核源码,来了解下Mach header的定义。

Mach header的定义位于Darwin源码中的 EXTERNAL_HEADERS/mach-o/loader.h 中:

32位:

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 */
};

64位:

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 */
};

可以看到,32位和64位的Mach header基本相同,只不过64位header中多了一个保留参数reserved。

  • magic:魔数,用来标识这是一个Mach-O文件,有32位和64位两个版本:
#define	MH_MAGIC	0xfeedface	/* the mach magic number */
#define MH_MAGIC_64 0xfeedfacf /* the 64-bit mach magic number */
  • cputype:支持的CUP架构类型,如arm。
  • cpusubtype:在支持的CUP架构类型下,所支持的具体机器型号。在我们的例子中,APP是支持所有arm64的机型的:CUP_SUBTYPE_ARM64_ALL
  • filetype: Mach-O的文件类型。包括:
#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 内核扩展 */
  • ncmds:load command的数量
  • sizeofcmds: 所有load command的大小
  • flags: Mach-O文件的标志位。主要作用是告诉系统该如何加载这个Mach-O文件以及该文件的一些特性。有很多值,我们取常见的几种:
#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 攻击 */





MH_PIE 随机地址空间

每次系统加载进程后,都会为其随机分配一个虚拟内存空间。

在传统系统中,进程每次加载的虚拟内存是相同的。这就让黑客有可能篡改内存来破解软件。

dyld

dyld是苹果公司的动态链接库,用来把Mach-O文件加载入内存。

二级命名空间

表示其符号空间中还会包含所在库的信息。这样可以使得不同的库导出通用的符号。与其相对的是扁平命名空间。

Load commands

load commands 紧跟在header之后,用来告诉内核和dyld,如何将各个Segment加载入内存中。

load command被源码表示为struct,有若干种load command,但是共同的特点是,在其结构的开头处,必须是如下两个属性:

struct load_command {
	uint32_t cmd;		/* type of load command */
	uint32_t cmdsize;	/* total size of command in bytes */
};

苹果为cmd定义了若干的宏,用来表示cmd的类型,下面列举出几种:

// 描述该如何将32或64位的segment 加载入内存,对应segment command类型
#define	LC_SEGMENT	0x1
#define	LC_SEGMENT_64	0x19	
// UUID, 2进制文件的唯一标识符
#define LC_UUID		0x1b
// 启动动态加载器dyld
#define LC_LOAD_DYLINKER 0xe

Segment load command

在这么多的load command中,需要我们重点关注的是segment load command。segment command解释了该如何将Data中的各个Segment加载入内存中,而和我们APP相关的逻辑及数据,则大部分位于各个Segment中。

而和我们的Run time相关的Segment,则位于__DATA类型Segment下。

Segment load command分为32位和64位:

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 */
};

32位和64位的Segment load command基本类似,只不过在64位的结构中,把和寻址相关的数据类型,由32位的uint32_t改为了64位的uint64_t类型。

结构体的定义,看注释基本能够看懂,就是maxprot, initprot不太明白啥意思。

这里介绍一个特殊的‘Segment’,叫做__PAGEZERO Segment。 这里说它特殊,是因为这个Segment其实是苹果虚拟出来的,只是一个逻辑上的段,而在Data中,根本没有对应的内容,也没有占用任何硬盘空间。

__PAGEZERO Segment在VM中被置为Read only,逻辑上占用APP最开始的4GB空间,用来处理空指针。

我们用MachOV点开__PAGEZERO Segment所对应的Segment load command,LC_SEGMENT_64(__PAGEZERO):
这里写图片描述

可以看到其vm size是4GB,但其真正的物理地址File size和offset都是0。

Section header

在Data中,程序的逻辑和数据是按照Segment(段)存储,在Segment中,又分为0或多个section,每个section中在存储实际的内容。而之所以这么做的原因在于,在section中,可以不用内存对齐达到节约内存的作用,而所有的section作为整体的Segment,又可以整体的内存对齐。

在Mach-O文件中,每一个Segment load command下面,都会包含对应Segment 下所有section的header。

section header的定义如下:

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 */
};

这样,关于load commonds部分,其真正的结构其实和苹果提供的图片有些许的差异:
这里写图片描述

Data

Mach-O的Data部分,其实是真正存储APP 二进制数据的地方,前面的header和load command,仅是提供文件的说明以及加载信息的功能。

Data部分也被分为若干的部分,除了我们前面提到的Segment外,还包括符号表,代码签名,动态加载器信息等。

而程序的逻辑和数据,则是放在以Segment分割的Data部分中的。我们在这里,仅关心Data中的Segment的部分。

Segment根据内容的不同,分为若干类型,类型名称均是以“双下划线+大写英文”表示,有的Segment下面还会包含若干的section,section的命名是以"双下划线+小写英文"表示。

先来看Segment,Mach-O中有如下几种Segment:

#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相关的。但是根据这篇文章中说所,在OC 2.0中已经废弃掉__OBJC段,而是将其放入到了__DATA段中以__objc开头的section中。这些和runtime相关的sections是本文的重点,我们稍后再分析。我们先看看其他的段。

__TEXT段

__TEXT是程序的只读段,用于保存我们所写的代码和字符串常量,const修饰常量等。

下面是__TEXT段下常见的section:

Section用途
__TEXT.__text主程序代码
__TEXT.__cstringC 语言字符串
__TEXT.__constconst 关键字修饰的常量
__TEXT.__stubs用于 Stub 的占位代码,很多地方称之为桩代码。
__TEXT.__stubs_helper当 Stub 无法找到真正的符号地址后的最终指向
__TEXT.__objc_methnameObjective-C 方法名称
__TEXT.__objc_methtypeObjective-C 方法类型
__TEXT.__objc_classnameObjective-C 类名称

例如,我们点击__TEXT.__objc_classname, 会看到我们程序中所使用到的类的名称:
这里写图片描述

而在__TEXT.__cstring section中,则看到我们定义的字符串常量(如@“I’m a cat!! miao miao”):
这里写图片描述

值得注意的是,这些都是以明文形式展现的。如果我们将加密key用字符串常量或宏定义的形式存储在程序中,可以想象其安全性是得不到保障的。

__DATA段

__DATA段用于存储程序中所定义的数据,可读写。__DATA段下常见的section有:

Section用途
__DATA.__data初始化过的可变数据
__DATA.__la_symbol_ptrlazy binding 的指针表,表中的指针一开始都指向 __stub_helper
__DATA.nl_symbol_ptr非 lazy binding 的指针表,每个表项中的指针都指向一个在装载过程中,被动态链机器搜索完成的符号
__DATA.__const没有初始化过的常量
__DATA.__cfstring程序中使用的 Core Foundation 字符串(CFStringRefs)
__DATA.__bssBSS,存放为初始化的全局变量,即常说的静态内存分配
__DATA.__common没有初始化过的符号声明
__DATA.__objc_classlistObjective-C 类列表
__DATA.__objc_protolistObjective-C 原型
__DATA.__objc_imginfoObjective-C 镜像信息
__DATA.__objc_selfrefsObjective-C self 引用
__DATA.__objc_protorefsObjective-C 原型引用
__DATA.__objc_superrefsObjective-C 超类引用

可见,在__DATA段下,有许多以__objc开头的section,而这些section,均是和runtime的加载有关的。
这里写图片描述

我们将在后续的文章中,继续探讨这些section和runtime的关系。

总结

这次我们一起了解了XNU内核下的二进制文件格式Mach-O。它由header,load command以及data三部分组成:
这里写图片描述

我们重点应该了解的应该是data部分,因为这里存储着我们程序真正的数据和代码。

在data部分中,又区分为以Segment划分的部分以及代码签名等其他部分。

在Segment下,有区分有若干的section。

常用的Segment有__PAGE_ZERO, __TEXT, __DATA(注意区分Mach-O的data和这里的__DATA段名称)。

参考资料

趣探 Mach-O:文件格式分析
深入理解Macho文件(二)- 消失的__OBJC段与新生的__DATA段
mach-o格式分析
Mach-O 文件格式探索
Mach-O 维基百科

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值