学习Mach-O文件类型

1. Mach-O定义

Mach-O(Mach Object File Format)是macOS上的可执行文件格式,它是一种用于可执行文件,目标代码,动态库,内核转储的文件格式。

2. Mach-O 文件格式

根据官网的描述,Mach-O文件的结构如下图:

主要分为三个部分:

  • Header:记录了Mach-O文件的基本信息,包括CPU架构、文件类和Load Commands等信息。
  • Load Commands:描述了怎样加载每个 Segment 的信息,
  • Data:Data 中每一个Segment的数据都保存在此,每个Segment拥有一个或多个 Section ,用来存放数据和代码

这里我们借助MachOView来观察文件结构,先写一段简单的cpp代码:

#import <stdio.h>

int main() {
    printf("Mach-O Test");
    return 0;
}

使用 clang -g main.cpp -o main 生成执行文件,随后通过MachOView观察:

根据<mach-o/loader.h>中的源码,我们可以一起看看这三个部分的结构体

2.1 Header

Mach-O 文件头主要目的是为加载命令提供信息。加载命令过程紧跟在头之后,并且 ncmds 和 sizeofcmds 来能个字段将会用在加载命令的过程中。

/*
 * The 64-bit mach header appears at the very beginning of object files for
 * 64-bit architectures.
 */
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 */
}
  • magic:魔术,根据宏定义,标识当前Mach-O位32位(0xfeedface)/ 64位 (0xfeedfacf)
#define MH_MAGIC 0xfeedface /* the mach magic number */`
#define MH_MAGIC_64 0xfeedfacf /* the 64-bit mach magic number */
  • cputype / cpusubtype:CPU类型/CPU子类型
  • filetype:文件类型,常用的如下
 * Constants for the filetype field of the mach_header
 */
#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 Commands数量
  • sizeofcmds:Load Commands的总大小
  • flag:标识位,记录文件的详细信息
#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  /* 对可执行的文件类型启用地址空间 layout 随机化 */
#define MH_NO_HEAP_EXECUTION 0x1000000 /* 将 Heap 标记为不可执行,可防止 heap spray 攻击 */
  • reserved:64位文件特有的保留字段
    对于刚才生成的可执行文件,其Header信息如下:

2.2. Load Commands

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

一起来看看Load Commands在MachOView中的构成,以LC_SEGMENT_64这个cmd为例(cmd类型为:LC_SEGMENT_64,size为72):

2.3 Data

从整体架构图中可以看到,Data又分为SegmentSection两个部分

2.3.1 Segment

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 */
};
  • cmd :Load Commands部分中提到的cmd类型
  • cmdsize :同上
  • segname[16] :段名称
  • vmaddr :段虚拟地址(未偏移),真实虚拟地址要加上ASLR的偏移量
  • vmsize :段的虚拟地址大小
  • fileoff :段在文件内的地址偏移
  • filesize :段在文件内的大小
    加载segment的过程,就是从文件偏移 fileoff 处,将大小为 filesize 的段,加载到虚拟机 vmaddr 处。
  • nsects :段内section数量
  • flags :标志位,用于描述详细信息

大家看到后面几个地址和偏移肯定会头晕,其实这几个变量主要作用在加载segment的时候。
加载segment的过程,就是从文件偏移 fileoff 处,将大小为 filesize 的段,加载到虚拟机 vmaddr 处。

segment[16]其实我们刚才在MachOView中就有见到:

可以看到,LC_SEGMENT_64中包含了五种类型,分别是: __PAGEZERO, __TEXT, __DATA, __DATA_CONST, __LINKEDIT:

  • PAGEZERO:可执行文件捕获空指针的段
  • TEXT:代码段和只读数据
  • DATA_CONST:常态变量
  • DATA:全局变量和静态变量
  • LINKEDIT:包含动态链接器所需的符号字符串表等数据

而对于__TEXT__DATA这两个Segment,则可以继续分解为Section,从而形成Segment->Section的结构。之所以要这样设计,是因为在同一个Segment下的Section可以拥有相同的控制权限,并且可以不完全按照Page的大小进行内存对齐,从而达到节约内存的效果。

2.3.2 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名称
  • segname :所属的segment名称
  • addr :section在内存中的地址
  • size :section大小
  • offset :section在文件中的偏移
  • align :内存对齐边界
  • reloff :重定位入口在文件中的偏移
  • nreloc :重定位入口数量

以LC_SEGMENT_64为例,其中的Section64 Header(__text),大写的 __TEXT 代表 segment ,小写的 __text 代表 section ,其中的不同的Section代表着不同的含义,列举一下常见的Section:

Section 用途
__TEXT.__text 主程序代码
__TEXT.__cstring C 语言字符串
__TEXT.__const const 关键字修饰的常量
__TEXT.__stubs 用于 Stub 的占位代码,很多地方称之为桩代码。
__TEXT.__stubs_helper 当 Stub 无法找到真正的符号地址后的最终指向
__TEXT.__objc_methname Objective-C 方法名称
__TEXT.__objc_methtype Objective-C 方法类型
__TEXT.__objc_classname Objective-C 类名称
__DATA.__data 初始化过的可变数据
__DATA.__la_symbol_ptr lazy binding 的指针表,表中的指针一开始都指向 __stub_helper
__DATA.nl_symbol_ptr 非 lazy binding 的指针表,每个表项中的指针都指向一个在装载过程中,被动态链机器搜索完成的符号
__DATA.__const 没有初始化过的常量
__DATA.__cfstring 程序中使用的 Core Foundation 字符串( CFStringRefs
__DATA.__bss BSS,存放为初始化的全局变量,即常说的静态内存分配
__DATA.__common 没有初始化过的符号声明
__DATA.__objc_classlist Objective-C 类列表
__DATA.__objc_protolist Objective-C 原型
__DATA.__objc_imginfo Objective-C 镜像信息
__DATA.__objc_selfrefs Objective-C self 引用
__DATA.__objc_protorefs Objective-C 原型引用
__DATA.__objc_superrefs Objective-C 超类引用

3. Mach-O实验

3.1 验证__TEXT.__text的加载

上文提到,__TEXT.__text的含义是主程序代码,更值得一提的是这个section的加载过程可以观察得到。

首先通过MachOView来看Load Commonds中__TEXT.__text的数据,为什么要先看Load Comands呢,因为Load Commands记录了Data是如何加载的,即作为Data加载结果的预期值,所以当结果=预期时我们就达到了验证效果。

从图中可以看到,此时__TEXT.__text的address(section在内存中的地址)为0000000100003F60,这是预期值。

随即通过vtool命令来查看汇编之后的代码起始地址:

otool -vt MachOTest

可见加载同样起始于0000000100003F60,验证完毕。

3.2 探索__DATA.__la_symbol_ptr和__TEXT.__stubs的关系

在上文中,我们提到了常见section的用途。其中
__DATA.__la_symbol_ptr的用途为: lazy binding 的指针表,表中的指针一开始都指向 __stub_helper。因为这些用途都是从资料上搜集的,有些不知道其具体含义。但是对于这条尤其困惑,借此例子理解一下。

首先,我们从MachOView上点开__TEXT.__stubs,选择其中一个stub,取其Data。由于我们demo比较简单,只存在一个stub,所以选择这个就OK了。

接下来在Hopper Disassembler中打开这个之前demo的Mach-O文件,在其中搜索刚才的地址FF2570400000,得到:

找到了对应代码的汇编表示,我们双击进入:

可以看出,这个stub的含义就是跳转到以__la_symbol_ptr 对应表项数据所指向地址的代码,我们再取地址100008000回到MachOView中看一下:

得到这个地址中的Data为0000000100003FA0,在通过Hopper查看这个Data:

果然落在了__stub_helper这个section!

回想一下我们刚才走过的链路

  • __TEXT.__stubs中取出一个stub,取其Data(FF2570400000),在Hopper中打开
  • 通过Hopper中找到Data并继续进入后,发现最终指向__la_symbol_ptr的某一项
  • 取这一项的地址(100008000),在MachOView中找到对应的Data(0000000100003FA0)
  • 发现这个Data最终落在__TEXT.__stubs_helper

也就是说, __DATA.__la_symbol_ptr 里面的所有表项的数据在开始时都会被 binding 成 __stub_helper 。而一旦被首次调用,找到地址后,就会将 __DATA.__la_symbol_ptr内的占位符binding为真实的地址,便可以直接执行函数,后续无需再走binding的流程。这就如其名一样完成了lazy binding的过程。

posted @ 2021-08-21 12:54  图袋鼠  阅读(372)  评论(0编辑  收藏  举报