较为详细的解析了Mach-O文件格式,并着重阐述了动态连接相关的知识点,开始吧~saonian~html
进程是可执行文件在内存中加载获得的结果,这种文件必须是操做系统理解的格式,这样操做系统才能解析文件,简历所须要的依赖(如库),初始化运行环境并执行。ios
Mach-O(Mach Object File Format)是macOS上的可执行文件,Linux和大部分Unix系统采用的是原生格式 ELF(Extensible Firmware Interface)
,windows支持的格式为PE32/PE32+
,macOS
支持三种可执行文件格式:解释器脚本文件、通用二进制格式和Mach-O
格式,以下图所示:git
可执行格式 | magic | 用途 |
---|---|---|
脚本 | \x7FELF |
主要用于 shell 脚本,可是也经常使用语其余解释器,如 Perl, AWK 等。也就是咱们常见的脚本文件中在 #! 标记后的字符串,即为执行命令的指令方式,以文件的 stdin 来传递命令 |
通用二进制格式 | 0xcafebabe 0xbebafeca |
包含多种架构支持的二进制格式,只在 macOS 上支持 |
Mach-O |
0xfeedface (32 位) 0xfeedfacf (64 位) |
macOS 的原生二进制格式 |
通用二进制格式(
Universal Binary
)也称为“胖二进制格式(Fat Binary)”,主要是解决历史问题,以支持Power PC(PPC)
架构以及Inter
架构,是一种对多架构的二进制文件的打包集合。github
其中常见的包括:可执行文件、动态库文件、动态连接器等都是Mach-O
格式,具体可经过file
命令查看具体的可执行文件格式,以下图:shell
其结构以下图所示,主要包括四部分组成:windows
Header
头部缓存
描述了该文件的CPU
类型、文件类型、加载命令等信息;数据结构
Load commands
加载命令架构
描述了文件中数据的具体组织结构,不一样数据类型如何使用不一样的加载命令表示;app
Data
数据段
存放了包括代码、字符常量、类、方法等代码和数据,而且拥有多个Segment
段,每一个Segment
段都包含零到多个Section
节;
Loader info
连接信息及其余
文件末端包含了一系列连接信息,如动态连接器用来连接可执行文件或者依赖所需使用的符号表、字符串表等,以及签名信息等;
为什么
Segment
段中存在Section
节?分段的目的主要:不一样段可被映射到不一样虚拟存储区域,便于读写权限管理;利用现代CPU缓存体系及程序的局部性原理,将指令和数据缓存分离有利用提高缓存命中率;指令或数据共享,有利于提高内存空间利用率。而分节主要是能够不彻底按照
page
的大小进行内存对齐,提高内存空间利用率。
Mach-O
文件头部具体的数据结构以下(区分32位和64位架构):
//32bit
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 */
};
//64bit
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位架构头部结构没有大的区别,只是64位多了一个保留字段,具体的字段名称以下:
magic
:魔数,用于确认该文件是32位仍是64位
cputype
,CPU类型,如arm
、x86_64
cpusubtype
,CPU具体类型,如arm64
、armv7
filetype
,文件类型,如可执行文件、库文件、动态连接器、符号文件和调试信息等,其中MH_EXECUTE
表明可执行文件,具体的文件类型定义以下:
/* Constants for the filetype field of the mach_header */
#define MH_OBJECT 0x1 /* relocatable object file */
#define MH_EXECUTE 0x2 /* demand paged executable file */
#define MH_FVMLIB 0x3 /* fixed VM shared library file */
#define MH_CORE 0x4 /* core file */
#define MH_PRELOAD 0x5 /* preloaded executable file */
#define MH_DYLIB 0x6 /* dynamically bound shared library */
#define MH_DYLINKER 0x7 /* dynamic link editor */
#define MH_BUNDLE 0x8 /* dynamically bound bundle file */
#define MH_DYLIB_STUB 0x9 /* shared library stub for static */
#define MH_DSYM 0xa /* companion file with only debug */
#define MH_KEXT_BUNDLE 0xb /* x86_64 kexts */
复制代码
ncmd
,加载命令条数
sizeofcmds
,全部加载命令在文件中占用地址空间大小
reserved
,保留字段
flags
,标志位,具体的定义以下:
#define MH_NOUNDEFS 0x1 // 目前没有未定义的符号,不存在连接依赖
#define MH_DYLDLINK 0x4 // 该文件是dyld的输入文件,没法被再次静态连接
#define MH_PIE 0x200000 // 加载程序在随机的地址空间,只在 MH_EXECUTE中使用
#define MH_TWOLEVEL 0x80 // 两级名称空间
复制代码
除了用MachOView能查看MachO文件信息,还能够经过otool命令查看,咱们先来分析Header中的内容:otool -h xxx
来查看。
Load commands
紧跟在头部以后(以下图),这些加载指令清晰地告诉加载器如何处理二进制数据,有些命令是由内核处理的,有些是由动态连接器处理的,常见的加载命令以下:
LC_SEGMENT/LC_SEGMENT_64
: 将该段(32/64位)映射到进程地址空间中,包含了Segment
中全部Section
加载信息;
其中_PAGEZERO
段不具备访问权限,用来处理空指针,其值为0;TEXT
为代码段,_DATA/_DATA_CONST
为可读写的数据段;_LINKEDIT
连接段包含了一些符号表、间接符号表、rebase
操做码、绑定操做码、导出符号、函数启动信息、数据表、代码签名、字符串表等数据,该加载命令下没有Section
,须要配合LC_SYMTAB
来解析symbol table
和string table
。
_LINKEDIT
加载命令信息中的文件偏移为0x4000(十进制16384
)正好对应Dynamic Loader Info
起始地址,文件大小为0x5840(十进制22592)=0x9840(0x9830+10)-0x4000
,正好对应从Dynamic Loader Info
到文件末尾的数据部分;
LC_DYLD_INFO_ONLY
:加载动态连接库信息(重定向地址、弱引用绑定、懒加载绑定、开放函数等的偏移值等信息)
LC_SYMTAB
:载入符号表地址
LC_DYSYMTAB
:载入动态符号表地址
LC_LOAD_DYLINKER
:加载动态加载库
LC_UUID
:肯定文件的惟一标识,crash解析中也会有这个,去检测dysm文件和crash文件是否匹配
LC_VERSION_MIN_MACOSX/LC_VERSION_MIN_IPHONEOS
:肯定二进制文件要求的最低操做系统版本
LC_SOURCE_VERSION
:构建该二进制文件使用的源代码版本
LC_MAIN
:设置程序主线程的入口地址和栈大小
LC_ENCRYPTION_INFO_64
:获取加密信息
LC_LOAD_DYLIB
:加载额外的动态库
LC_FUNCTION_STARTS
:定义一个函数起始地址表,使调试器和其余程序易于看到一个地址是否在函数内
LC_DATA_IN_CODE
:定义在代码段内的非指令的表
LC_CODE_SIGNATURE
:获取应用签名信息
具体的加载命令的数据结构以下(64位格式,与32位格式差异不大):
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
的类型,这里LC_SEGMENT_64
表明将文件中64位的段映射到进程的地址空间;cmdsize:
表明load command
的大小segment name:
段的名称VM Address :
段的虚拟内存地址VM Size :
段的虚拟内存大小file offset:
段在文件中偏移量file size:
段在文件中的大小nsects:
标示了Segment
中有多少secetion
除了使用MachOView
查看,还能够经过otool -l xxx
查看,以下图所示:
对于段的地址大小可经过size -l -m xxx
查看,以下图:
下面重点阐述几个重要的加载命令,便于后续理解整个程序启动、动态加载、逆向等知识点。
该加载命令的内容以下图所示:
其中虚拟地址范围为0x0~0x100000000
正好对应4GB
空间,该文件的起始虚拟地址空间也是从0x100000000
开始,即全部代码和数据都是被加载到4GB
以后的地址。对应的文件内容大小为0,即在该文件中不占用实际空间,且具备不可读写不可执行权限,这样内核就能够识别到空指针或指针截断的错误的范围该地址空间的调用而抛出段异常,如EXC_BAD_ACCESS
异常。
__LINKEDIT
包含了动态连接相关的信息,如虚拟地址空间地址及文件偏移、文件权限等,而LC_DYLD_INFO_ONLY
加载命令,包含了重定位、绑定及导出等偏移/大小信息。
对于LC_SYMTAB
加载命令,其数据结构定义以下:
struct symtab_command {
uint32_t cmd; /* LC_SYMTAB */
uint32_t cmdsize; /* sizeof(struct symtab_command) */
uint32_t symoff; /* symbol table offset */
uint32_t nsyms; /* number of symbol table entries */
uint32_t stroff; /* string table offset */
uint32_t strsize; /* string table size in bytes */
};
复制代码
这个命令告诉了连接器(包括静态连接器或动态连接器)Symbol Table
和String Table
的位置及大小信息。
其中符号的结构由内核定义,以下:
struct nlist_64 {
union {
uint32_t n_strx; /* index into the string table */
} n_un;
uint8_t n_type; /* type flag, see below */
uint8_t n_sect; /* section number or NO_SECT */
uint16_t n_desc; /* see <mach-o/stab.h> */
uint64_t n_value; /* value of this symbol (or stab offset) */
};
复制代码
n_un
,符号的名字在字符串表中的序号(在一个 Mach-O 文件里,具备惟一性)n_sect
,符号所在的 section index(内部符号有效值从 1 开始,最大为 255;外部符号为0)n_value
,符号的地址值(在连接过程当中,会随着其 section 发生变化)n_type
是一个 8 bit 的复合字段:
bit[5:8]
: 若是不为 0,表示这是一个与调试有关的符号,值意义类型详见mach-o/stab.hbit[4:5]
: 若为 1,则表示该符号是私有的(外部符号)bit[1:4]
: 符号类型
N_UNDF
(0x0): 未定义N_ABS
(0x2): 符号地址指向到绝对地址,连接器后期不会再修改N_SECT
(0xe): 本地符号,即符号定义于当前 Mach-ON_PBUD
(0xc): 预绑定符号N_INDR
(0xa): 表示该符号和另外一个符号是同一个,n_value
指向到 string table,即该同名符号的名字bit[0:1]
: 表示这是外部符号,即该符号要么定义在外部,要么定义在本地可是能够被外部使用;对于LC_DYSYMTAB
加载命令,其数据结构以下:
struct dysymtab_command {
uint32_t cmd; /* LC_DYSYMTAB */
uint32_t cmdsize; /* sizeof(struct dysymtab_command) */
uint32_t ilocalsym; /* index to local symbols */
uint32_t nlocalsym; /* number of local symbols */
uint32_t iextdefsym;/* index to externally defined symbols */
uint32_t nextdefsym;/* number of externally defined symbols */
uint32_t iundefsym; /* index to undefined symbols */
uint32_t nundefsym; /* number of undefined symbols */
uint32_t tocoff; /* file offset to table of contents */
uint32_t ntoc; /* number of entries in table of contents */
uint32_t modtaboff; /* file offset to module table */
uint32_t nmodtab; /* number of module table entries */
uint32_t extrefsymoff; /* offset to referenced symbol table */
uint32_t nextrefsyms; /* number of referenced symbol table entries */
uint32_t indirectsymoff; /* file offset to the indirect symbol table */
uint32_t nindirectsyms; /* number of indirect symbol table entries */
uint32_t extreloff; /* offset to external relocation entries */
uint32_t nextrel; /* number of external relocation entries */
uint32_t locreloff; /* offset to local relocation entries */
uint32_t nlocrel; /* number of local relocation entries */
};
复制代码
主要包含了本地、外部符号、未定义外部符号、间接符号表的位置及数目,其中indriectsymoff
指定了Dynamic Symbol Table
的文件偏移位置及数目;
可以使用
otool -I xxx
来获取间接符号表内容;
其中间接符号包含了符号名、符号所处的节及符号间接地址,其所处的Section
处在__stubs
、__got
、及__la_symbol_ptr
等节;
对于后续须要动态连接定位的符号头部,如LC_SEGMENT_64
中的_TEXT.__stubs
、_DATA_CONST.__got
、_DATA.__la_symbol_ptr
,其头部字段中包含了Indirect Sym Index(Reserverd1)
字段,该字段指明在Indirect Symbol Table
间接符号表中的条目序号,以下图:
_la_symbol_ptr
中的符号在间接符号表中的起始条目序号为26。
该加载命令包含了重要的程序启动动态连接器的路径,以下图x86_64
的为/usr/lib/dyld
。
Section
的数据结构
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) */
};
复制代码
sectname:
好比_text
、stubs
segname :
该section
所属的segment
,好比_TEXT
addr :
该section
在内存的起始位置size:
该section
的大小offset:
该section
的文件偏移align :
字节大小对齐reloff :
重定位入口的文件偏移nreloc:
须要重定位的入口数量flags:
包含section
的type
和attributes
常见的 Section
以下表所示:
Section | 用途 |
---|---|
_TEXT.__text |
主程序代码 |
_TEXT.__cstring |
C 语言字符串 |
_TEXT.__const |
const 关键字修饰的常量 |
_TEXT.__stubs |
用于 Stub 的占位代码,不少地方称之为桩代码,用于重定向到 lazy 和 non-lazy 符号的 section ,被标记为 S_SYMBOL_STUBS 。TEXT Segment 里代码和 dylib 外部符号的引用地址对函数符号的引用都指向了 stubs。其中每项都是 jmp 代码间接寻址,可跳到la_symbol_ptr Section 中。 |
_TEXT.__stubs_helper |
当 Stub 没法找到真正的符号地址后的最终指向 |
_TEXT.__objc_methname |
Objective-C 方法名称 |
_TEXT.__objc_methtype |
Objective-C 方法类型 |
_TEXT.__objc_classname |
Objective-C 类名称 |
_TEXT.__eh_frame |
调试辅助信息 |
_TEXT.__unwind_info |
用于存储处理异常状况信息 |
_DATA.__data |
初始化过的可变数据 |
_DATA.__la_symbol_ptr |
lazy binding 的指针表,表中的指针一开始都指向 __stub_helper |
_DATA.nl_symbol_ptr |
非 lazy binding 的指针表,每一个表项中的指针都指向一个在装载过程当中,被动态链机器搜索完成的符号 |
_DATA.__got |
全局偏移表 |
_DATA.__const |
没有初始化过的常量 |
_DATA.__cfstring |
程序中使用的 Core Foundation 字符串(CFStringRefs ) |
_DATA.__bss |
BSS ,存放为初始化的全局变量,即常说的静态内存分配 |
_DATA.__common |
没有初始化过的符号声明 |
_DATA.__mod_init_func |
初始化函数,在main 以前调用 |
_DATA.__mod_term_func |
终止函数,在main 返回以后调用 |
_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 超类引用 |
对于_DATA.__got
节,其内容以下图所示:
其相似一个表,每一个条目是一个地址值,定义的是Non-Lazy Symbol Pointers
即非懒加载符号地址,全部条目的内容都是0。其引入的目的是解决程序在连接阶段存放不能肯定目的地址的符号,当镜像被加载时,动态连接器dyld
会对每一个条目对应的符号进行重定位,将其真正的地址写入,做为条目的内容。对于dyld
如何肯定符号信息的,能够经过上面的Indirect Symbol Table
中的符号看出,包含了符号名称、间接符号地址。
与之对应的是_DATA.__la_symbol_ptr
节,其内容以下图所示:
其实际内容都指向了_TEXT.__stub_helper
节,最终经过jumpq
指令跳转到了dyld_stub_binder
符号,即__got
节中的Non_Lazy Symbol Pointer
中的条目,该符号为一个函数,定义于dyld_stub_binder.S,由 dyld
提供。
dyld_stub_binder
函数其大体逻辑是:内部会寻找锁调用符号的真实地址,并写入_la_symbol_ptr
条目中,而后跳转到真实地址执行;
对于_TEXT.__stubs
节,其内容以下:
该内容也是一个表,每一个条目都是一段数据,称为“符号桩”。经过otool -v xx -s _TEXT __stubs
命令查看内容以下:
其内容都是jmpq
跳转指令,跳转的地址以第一条地址为例计算:
0x100003000 = 0x100001dbc(rip) + 0x1244
复制代码
该地址指向的是__la_symbol_ptr
节,而该节最终都指向了dyld_stub_binder
。
连接加载信息包含了动态加载信息Dynamic Loader Info
(包含了重定向地址、弱引用绑定、懒加载绑定、开放函数等的偏移值等信息,其加载命令为LC_DYLD_INFO_ONLY
),函数起始地址表Function Starts
(其加载命令为LC_FUNCTION_STARTS
),符号表Symbol Table
,动态符号表Dynamic Symbol Table
,代码段非指令表Data in Code Table
,字符串表String Table
(以空值为终止符)及代码签名Code Signature
,以下图所示:
因为地址空间随机化技术(ddress space layout randomization, ASLR
)和地址无关可执行技术(position-indendent excutable, PIE
),使得程序在内存的加载地址是随机的,所以须要程序在动态连接阶段将内部地址进行修正。Rebase
数据描述了哪些是对指向 MachO
内部的引用并将其修正,而 Bind
数据描述哪些是指向外部的引用并进行修正。Lazy Bind
数据描述了哪些符号须要延迟绑定,即仅在第一次使用时才会绑定,不会在启动时进行,提升启动效率;Export
数据描述了对外可见的符号。其内容都是以操做数(Opcodes
)、当即数(immediate)
以及采用uleb128/sleb128
编码的偏移值组成。
PIE(position-independent executable)
是一种生成地址无关可执行程序的技术。若是编译器在生成可执行程序的过程当中使用了PIE,那么当可执行程序被加载到内存中时其加载地址存在不可预知性。PIE还有个孪生兄弟PIC(position-independent code)
。其做用和PIE相同,都是使被编译后的程序可以随机的加载到某个内存地址。区别在于PIC是在生成动态连接库时使用(Linux中的so),PIE是在生成可执行文件时使用。
以Rebase
举例,其协议和操做就是找到地址后将其值加上偏移便可,具体的获取操做数和当即数是经过REBASE_OPCODE_MASK(0xF0)
和REBASE_IMMEDIATE_MASK(0x0F)
对数据进行与&
操做,如0x100004000
的数据字节0x11
,其操做数为0x10=0x11&0xF0
对应的是REBASE_OPCODE_SET_TYPE_IMM
,当即数0x01=0x11&0x0F
为type=1(REBASE_TYPE_POINTER)
,具体的操做数及当即数对应的逻辑可查阅dyld
源码。
注意:MachOView中标注的
Actions
存在误导性,重定位、绑定等操做都是按照字节数据顺序读取并操做直至完整的读取完全部的数据,其标注具体缘由未知,待确认补充!
对于Dynamic Symbol Table
中的Indirect Symbols
其内容为一个表,每一个条目的内容为其在Symbol Table
中的序号,以下图:
其内容为0x3c=60
,对应的就是符号表第60个符号,经过符号表中的起始地址0x4380
,每一个符号占用0x10
,则0x4740=0x4380+0x10*0x3c
,对应的就是_CFRunLoopAddSource
符号地址。
对于字符串表String Table
中内容为全部的符号名称,每一个名称中间经过空字符串间隔,以下图所示:
Symbol Table
中的String Table Index
字段就是字符串表中对应的第index
个字符串。