对Mach-O文件的初步探索

2,539 阅读13分钟

Mach-O是Mac/iOS上的可执行文件格式,类似于UNIX中的ELF(Extensible Firmware InInterface)。Mach-O文件的的Magic Number为0xfeedface(32位)和0xfeedfacf(64位)。

macOS上的可执行文件有三类:

可执行文件 Magic Number 用途
shell脚本 \x7FELF shell脚本,perl,awk等。
通用二进制格式 0xcafebabe,0xbebafeca 包含多种架构支持,也叫Fat Binary。
Mach-O 0xfeedface(32位), 0xfeedfacf(64位) iOS和macOS上使用。

通用二进制格式

lipo是管理Fat Binary的工具,可查看平台列表,提取特定平台的Binary等。一般用于macOS平台上多架构通用二进制文件的处理。

关于lipo命令的示例请看下文。

Mach-O文件类型

iOS, macOS平台上有以下几种类型:

  • 二进制文件 .o
  • 静态链接库 .a文件,即多个.o合并在一起
  • 动态链接库 .dylib文件
  • Bundle,不能被链接的dylib,只能在运行时使用dlopen()来加载
  • 可重定向文件类型
  • nib 本质就是dylib文件
  • dSYM release版本
  • 存储二进制文件符号信息的文件 .dSYM/Contents/Resources/DWARF/xx, 常用于分析APP的崩溃信息。

注意:可执行文件的格式与mach-o文件不能等同。

在iOS 12.2/usr/include/mach-o/header.h中有相关定义。

使用MachOView工具来查看Mach-O文件,如查看该main文件:

在该工具中,可以清晰地看到Mach-O的内部格式,包括Magic Number、CPU Type等,以及各个段:Load Command、__TEXT, __DATA等。

Mach-O的结构

  • Header 文件类型,目标架构类型,加载命令等一般信息。由系统内核负责读取。
  • Load commands 描述文件在虚拟内存中的逻辑结构,布局等。内容包含了符号表等,数据的具体组织结构。加载命令,告诉loader如何设置并加载二进制数据。
  • Data 存放数据:代码、字符常量、类、方法等。可拥有多个segment,每个segment有多个section,每个段都有一段虚拟内存映射到进程的地址空间。
  • Loader Info 链接信息,一个完整的用户级MachO文件的末端是一系列链接信息,博阿憨了dyld用来链接可执行文件或依赖所需使用的符号表、字符串表等。
  • Raw segment data 原始数据区。文件中最大的部分,包含了Segement的具体数据。(在Load commands中定义的Segment的原始数据)。每一个segment中有一个或多个Section,用来存放数据和代码。

Load Commands

Load Commands展示了Mach-O文件的具体结构,不同的结构会采用不同的加载命令。

  • LC_SEGMENT_64, 将文件中的段映射到进程地址空间中,即加载命令
  • LC_DYLD_INFO_ONLY,动态链接库相关(重定向地址、弱引用绑定、懒绑定、开放函数等的偏移等信息)
  • LC_SYMTAB,符号表
  • LC_DYSYMTAB,动态符号表
  • LC_LOAD_DYLNKER,加载器/usr/bin/dyld
  • LC_UUID,文件的唯一UUID
  • LC_MAIN, 程序主线程的入口地址和栈大小
  • LC_LOAD_DYLIB(Foundation) 动态库的地址;只有标记了,才能被dyld加载。
  • LC_FUNCTION_STARTS 函数起始地址表
  • LC_DATA_IN_CODE 数据段
  • LC_CODE_SIGNATURE 代码签名

这些命令都是load_command结构,cmd字段标记了命令类型,而cmdsize则是命令的长度,即命令二进制的长度。

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

几种常见的Segment或Section

section是相同或相似信息的集合,如.text、.data、.bss section都是不同的section。而segment是由多个属性相同的section组成的。我们通常说的代码段和数据段指的其实就是segment。

__PAGEZERO

#define	SEG_PAGEZERO	"__PAGEZERO"	/* the pagezero segment which has no */
					/* protections and catches NULL */
					/* references for MH_EXECUTE files */

__PAGEZERO是空指针陷阱段,映射到虚拟内存空间的第一页,用于捕捉对NULL指针的引用。

__PAGEZERO的大小为 4GB。这 4GB 并不是文件的真实大小,但是规定了进程地址空间的前 4GB 被映射为 不可执行、不可写和不可读。这就是为什么当读写一个 NULL 指针或更小的值时会得到一个 EXC_BAD_ACCESS 错误。这是操作系统在尝试防止引起系统崩溃。

__TEXT

代码区,这一部分是App中的不可变部分,即Read only的。

#define	SEG_TEXT	"__TEXT"	/* the tradition UNIX text segment */
#define	SECT_TEXT	"__text"	/* the real text part of the text */
					/* section no headers, and no padding */

#define SECT_FVMLIB_INIT0 "__fvmlib_init0"	/* the fvmlib initialization */
						/*  section */
#define SECT_FVMLIB_INIT1 "__fvmlib_init1"	/* the section following the */
					        /*  fvmlib initialization */
						/*  section */                    

__TEXT包含了很多其他的Section:

Section 解释
__TEXT, __text 主程序代码段。The compiled machine code for the executable
__TEXT, __stubs Stub可以理解为一段占位空间,placeholder,用于符号的lazy binding。
__TEXT, __stubs_helper Stub helper
__TEXT, __cstring C语言字符串。Literal string constants (quoted strings in source code)
__TEXT, __entitlements __entitlements
__TEXT, __unwind_info C语言字符串
__TEXT, __const 常量段(const修饰)。The general constant data for the executable
__TEXT, __objc_classname OC的类名
__TEXT, __objc_methname OC方法名称
__TEXT, __objc_methtype OC方法类型,即方法签名

__stubs和__stubs_helper是给dyld使用的,使得符号可以被懒绑定(lazy binding),即在首次使用而非加载时就去绑定其符号地址。

注意,objc相关的有__objc_classname,__objc_methname,__objc_methtype,这三部分均为不可变的。

__DATA

#define	SEG_DATA	"__DATA"	/* the tradition UNIX data segment */
#define	SECT_DATA	"__data"	/* the real initialized data section */
					/* no padding, no bss overlap */
#define	SECT_BSS	"__bss"		/* the real uninitialized data section*/
					/* no padding */
#define SECT_COMMON	"__common"	/* the section common symbols are */
					/* allocated in by the link editor */
Section 解释
__DATA_CONST, __got __got
__DATA, __got __got
__DATA, __data 已初始化的全局变量。Initialized global variables (for example int a = 1; or static int a = 1;).
__DATA, __bss 未初始化的静态变量。Uninitialized static variables (for example, static int a;).
__DATA, __const Constant data needing relocation (for example, char * const p = "foo";).
__DATA, __cfstring Core Foundation字符串(CFStringRefs),没有ARC
__DATA, __common 未初始化的外部全局变量。Uninitialized external globals (for example, int a; outside function blocks).
__DATA, __la_symbol_ptr 懒绑定的符号指针表。“Lazy” symbol pointers. Symbol pointers for each undefined function called by the executable.
__DATA, __nl_symbol_ptr 非懒绑定的符号指针表。“Non lazy” symbol pointers. Symbol pointers for each undefined data symbol referenced by the executable.
__DATA, __objc_classlist OC的类列表,存储一个个指向objc_class结构体的指针
__DATA, __objc_nlclslist OC的类列表,+load相关?
__DATA, __objc_catlist OC的category列表,存储一个个指向__objc_category结构体的指针
__DATA, __objc_protolist OC的协议列表,存储一个个指向protocol_t结构体的指针
__DATA, __objc_imginfo OC的image信息
__DATA, __objc_selrefs 哪些SEL对应的字符串被引用了
__DATA, __objc_classrefs 类的引用,即msg_objSend相关
__DATA, __objc_superrefs super引用,记录了super方法调用的类。如ViewController中的viewDidLoad中调用了[super viewDidLoad],则ViewController class即被记录。也可以理解为objc_msgSendSuper相关。
__DATA, __objc_protorefs 协议引用
__DATA, __objc_ivar 成员变量
__DATA, __objc_const 这里的const跟__TEXT的const完全不同。__objc_const指的是OC内存布局中的不可变部分,即class_ro_t类型。
__DATA, __objc_data 保存类所需的数据?

__la_symbol_ptr是懒绑定(lazy binding)的符号指针,在加载的时候,并未直接确定符号地址,而是在第一次调用该函数的时候,通过PLT(Procedure Linkage Table)进行一次懒绑定。而__nl_symbol_ptr则不会进行懒绑定。

__OBJC

OC及runtime相关的

#define	SEG_OBJC	"__OBJC"	/* objective-C runtime segment */
#define SECT_OBJC_SYMBOLS "__symbol_table"	/* symbol table */
#define SECT_OBJC_MODULES "__module_info"	/* module information */
#define SECT_OBJC_STRINGS "__selector_strs"	/* string table */
#define SECT_OBJC_REFS "__selector_refs"	/* string table */

__ICON

icon,tiff资源等。

#define	SEG_ICON	 "__ICON"	/* the icon segment */
#define	SECT_ICON_HEADER "__header"	/* the icon headers */
#define	SECT_ICON_TIFF   "__tiff"	/* the icons in tiff format */

__LINKEDIT

包含需要被动态链接器dyld使用的符号和其他表,包括符号表、字符串表等。

#define	SEG_LINKEDIT	"__LINKEDIT"	/* the segment containing all structs */
					/* created and maintained by the link */
					/* editor.  Created with -seglinkedit */
					/* option to ld(1) for MH_EXECUTE and */
					/* FVMLIB file types only */

__UNIXSTACK

#define SEG_UNIXSTACK	"__UNIXSTACK"	/* the unix stack segment */

__IMPORT

#define SEG_IMPORT	"__IMPORT"	/* the segment for the self (dyld) */
					/* modifing code stubs that has read, */
					/* write and execute permissions */

Dynamic Loader Info

Symbol Table

符号表是Mach-O中的符号映射。

Dynamic Symbol Table

动态符号表是加载动态库时的函数表,是符号表的子集。动态符号表的符号 = 符号在原所属符号表中的offset + 原所属符号表在动态符号表中的offset + 动态符号表的基地址base。在动态符号表中查找到的这个符号的值,又等于该符号在符号表中的offset。

参考:Hook 原理之 fishhook 源码解析

对比汇编代码来查看mach-o

使用otool来查看对main文件进行反汇编后的汇编代码:

main:
(__TEXT,__text) section
_main:
0000000000000000	pushq	%rbp
0000000000000001	movq	%rsp, %rbp
0000000000000004	subq	$0x20, %rsp
0000000000000008	movl	$0x0, -0x4(%rbp)
000000000000000f	movl	%edi, -0x8(%rbp)
0000000000000012	movq	%rsi, -0x10(%rbp)
0000000000000016	leaq	0x2e(%rip), %rdi
000000000000001d	movb	$0x0, %al
000000000000001f	callq	0x24
0000000000000024	xorl	%ecx, %ecx
0000000000000026	movl	$0x1, -0x14(%rbp)
000000000000002d	movl	$0x2, -0x18(%rbp)
0000000000000034	movl	-0x14(%rbp), %edx
0000000000000037	addl	-0x18(%rbp), %edx
000000000000003a	addl	$0x3, %edx
000000000000003d	movl	%edx, -0x1c(%rbp)
0000000000000040	movl	%eax, -0x20(%rbp)
0000000000000043	movl	%ecx, %eax
0000000000000045	addq	$0x20, %rsp
0000000000000049	popq	%rbp
000000000000004a	retq

相关数据结构

在iOS 12.2/usr/include/mach-o/fat.h中:

struct fat_header {
	uint32_t	magic;		/* FAT_MAGIC or FAT_MAGIC_64 */
	uint32_t	nfat_arch;	/* number of structs that follow */
};

struct fat_arch_64 {
	cpu_type_t	cputype;	/* cpu specifier (int) */
	cpu_subtype_t	cpusubtype;	/* machine specifier (int) */
	uint64_t	offset;		/* file offset to this object file */
	uint64_t	size;		/* size of this object file */
	uint32_t	align;		/* alignment as a power of 2 */
	uint32_t	reserved;	/* reserved */
};

在iOS 12.2/usr/include/mach-o/loader.h

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 */
    //表示二进制文件所支持的一些功能,和系统加载有关系。描述文件在编译、链接等过程中的信息,示例中的 MH_NOUNDEFS 表示文件中不存在未定义的符号,MH_DYLDLINK 表示文件要交由 DYLD 进一步处理,MH_TWOLEVEL 表示文件使用两级命名空间,MH_PIE 表示启用地址空间布局随机化。
	uint32_t	flags;		/* flags */
	uint32_t	reserved;	/* reserved */
};

header描述了Mach-O文件的基本信息,注意其中的CPU类型cputype,文件类型filetype,加载命令个数ncmds,所有加载命令的大小sizeofcmds。

不同的segment,会使用不同的加载命令。

struct dyld_info_command {
    uint32_t   cmd;     /* LC_DYLD_INFO or LC_DYLD_INFO_ONLY */
    uint32_t   cmdsize;     /* sizeof(struct dyld_info_command) */
    uint32_t   rebase_off;  //重定向的偏移值。(ASLR)
    uint32_t   rebase_size; //重定向的大小。
    uint32_t   bind_off;    //绑定。可执行文件读到内存中会绑定一些数据。weak绑定,lazy绑定。
    uint32_t   bind_size;   /* size of binding info  */
    uint32_t   weak_bind_off; /* file offset to weak binding info   */
    uint32_t   weak_bind_size;  /* size of weak binding info  */
    uint32_t   lazy_bind_off; /* file offset to lazy binding info */
    uint32_t   lazy_bind_size;  /* size of lazy binding infs */
    uint32_t   export_off;  //对外开发的函数。
    uint32_t   export_size; /* size of lazy binding infs */
};
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 */
};
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 */
};
  1. Mach-O 了解一下
  2. Mach-O 文件格式探索

一些相关工具

class-dump

在.app包内,执行命令查看可执行文件:

plutil -p Info.plist | grep CFBundleExecutable
# 结果 "CFBundleExecutable" => "XXXX"

class-dump即利用OC的runtime特性,将mach-o文件中的@interface和@prototol信息提取出来,生成对应的.h文件。

class-dump -H XXXX -o xxxx_headers

-H表示要生成头文件,-o指定头文件的存放目录。

如果是Swift混编,则会出错:

Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid parameter not satisfying: [cursor offset] != 0'
libc++abi.dylib: terminating with uncaught exception of type NSException

从AppStore下载的App都是经过签名加密的,class-dump会失败。因此需要先砸壳,否则dump出来的头文件是空的。 使用AppCrackr。

lipo

lipo是管理Fat File的工具,可查看平台列表,提取特定平台,重新打包。一般用于多架构mach-o文件的处理。

查看架构信息:

# lipo -info main
Non-fat file: main is architecture: x86_64
# lipo -info XXXX
Non-fat file: XXXX is architecture: arm64

查看到了该mach-o文件指定的平台信息。如果有指定了多个平台,则可以提取单个平台的mach-o文件。

提取特定架构:

lipo -thin armv7 MAMapKit -output MAMapKit.armv7

如单独提取armv7结果的mach-o文件,体积要小很多。而多个mach-o之间会有共享的一些资源。

合并多个架构:

lipo -create WeChat_arm64 WeChat_armV7 -output WeChat_64_V7
lipo -detailed_info LuckyClient

等同于

otool -f -V LuckyClient

nm

nm用来显示一个程序包的符号表,默认会显示入口地址、符号类型、符号名。

nm -j MAMapKit.armv7 | grep png > symbols

可以获得所有的libpng导出符号,存入的symbols文件。-j只输出符号名。

strip

strip用来删除程序里的符号表。-R指定要删除的符号列表。-S保留其他符号。

strip -S -R symbols MAMapKit.armv7 -o MAMapKit.armv7.strip

otool

查看mach-o特定部分(segment或者section)的内容。

otool -L XXXX

可查看当前App需要连接的所有framework。主要都是一些Apple的库,以及swift相关。

otool可以查看mach-o的信息:

-f fat headers
-a archive header
-h mach header
-l load commands
-L shared libraries used
-D shared library id name
-t text section (disassemble with -v)
-p <routine name> start disassemble from routine name
-s <segname> <sectname> contents of se

如:

# 查看头文件
otool -v -h main
# 查看__TEXT __text
otool -s __TEXT __text main
# 查看__TEXT __cstring
otool -s __TEXT __cstring main

MachOView

使用MachOView工具来查看Mach-O文件的具体细节,这里就不多说了。

MachO-Kit

MachO-Kit是一个解析Mach-O文件的C语言库。

其中使用了libMachO来进行Mach-O文件解析。

动态库dylib

查看系统的所有动态库:

find /usr/lib -name "*.dylib"

iOS系统常见的dylib有:

  • libobjc objc和runtime
  • libdispatch GCD
  • libsystem_c C语言库
  • libsystem_blocks Block
  • libcommonCrypto 加密库

dyld

dyld是Apple的动态链接库加载器,内核做好App的初始准备后,交个dyld负责。作用如下:

  • 从内核留下的原始调用栈引导和启动自己
  • 将程序以来的dylib递归加载进内存,考虑缓存机制
  • non-lazy符号立即link到可执行文件,lazy的存表里
  • Runs static initializers for the executable
  • 找到可执行文件的main函数,准备参数并调用
  • 程序执行中负责绑定lazy符号,提供runtime dynamic loading services,提供调试器接口
  • 程序main函数return后执行static terminator
  • 某些场景下main函数结束后调用libsystem的_exit函数

iOS 程序 main 函数之前发生了什么

使用/usr/lib/dyld来加载动态库,可加载dylib,bundle,execute等类型的文件。dyld会通过一个共享缓存来加速dylib的加载。

ImageLoader

image即mach-o,里边是被编译过的符号、代码等,作用是将这些文件递归加载进内存,且每个文件对应一个ImageLoader实例来负责加载。

由于runtime向dyld绑定了回调,当image加载到内存后,dyld会通知runtime进行处理。 runtime接手后调用map_images做解析和处理,接下来load_images中调用call_load_methods方法,遍历所有加载进来的Class,按继承层级依次调用Class的+load方法和其Category的+load方法。

iOS 程序 main 函数之前发生了什么

dlopen

加载dylib文件

void *_revealLib;
_revealLib = dlopen([filePath cStringUsingEncoding:NSUTF8StringEncoding], RTLD_NOW);
// xxx
dlclose(_revealLib);

dyld

如果在+load方法中断点,即可在Xcode中进行调试。查看调用堆栈:

+ (void)load方法
call_load_methods
_dyld_start
dyldbootstrap::start(macho_header const*, int, char const*, long, macho_header const*, unsigned long*)

call_load_methods中就能看到objc_autoreleasePoolPush与objc_autoreleasePoolPop。

参考资料

  1. Mach-O develop.apple.com
  2. Mach-O 可执行文件
  3. mach-o/loader
  4. Macho文件浏览器---MachOView
  5. 深入剖析 iOS 编译 Clang LLVM
  6. Mach-O文件格式
  7. iOS逆向(5)-不知MachO怎敢说自己懂DYLD
  8. Objective-C runtime机制(前传2)——Mach-O格式和runtime