Mach-O
PE32/PE32+
是 Windows
和 Intel
的 EFI
的二进制文件的可执行文件格式,ELF
是 Linux
和 Unix
的可执行文件格式,通用二进制格式(胖二进制格式)只在 OS X
上支持,Mach-O
是 OS X
的原生可执行文件格式。在 Unix
中,任何文件都可以通过简单的 chmod +x
命令标记为可执行文件,但是不一定能保证这个文件可以执行。OS X
目前只支持三种可执行文件格式:解释器脚本格式、通用二进制格式以及 Mach-O 格式。
可执行文件格式 | magic (魔数) |
用途 |
---|---|---|
脚本 | #! | UNIX 脚本和一些解释器脚本的使用格式:主要用于 shell 脚本,但是也常用于其他解释器,例如 Perl 、AWK 、PHP 等。内核寻找 #! 后面跟着的字符串,然后执行这个字符串表示的命令。文件剩下的部分通过标准输入(stdin )传递这个命令 |
通用二进制格式(胖二进制格式) | 0xcafebabe (小尾顺序) 0xbebafeca (大尾顺序) |
包含多种架构支持的二进制格式,只在 OS X 上支持 |
Mach-O |
0xfeedface (32位) 0xfeedfacf (64位) |
OS X 的原生二进制格式 |
Mach-O 文件格式
据说了解 Mach-O
最佳方法是查看下面的图片:
通过上图可以看出 Mach-O
由三部分构成:
Header
(mach_header
) : 包含mach_header
的CPU
类型、文件类型、加载命令、动态连接器的标志等- 加载命令(
Load command
) : 加载命令紧跟头文件之后,这些命令在调用时清晰的告诉加载器如何设置并加载二进制数据。有一些命令是有内核加载器(定义在bsd/kern/mach_loader.c
文件中)直接使用的,其他命令是由动态链接器处理的。 Data
: 每一个segment
的具体数据都保存在这里,这里包含了具体的代码、数据等
mach_header
Mach-O
具有一个固定的 mach_header
,这个文件头的详细信息在 <mach-o/loader.h>
头文件中。
文件头一开始有一个魔数值(magic
),加载器可以通过这个魔数值快读判断这个二进制文件用于32位还是64位。在魔数值之后跟着的是 CPU 类型及子类型字段,这两个字段和通用二进制文件的相同字段作用是一样的(用于确保二进制文件适用并且可以在当前架构下运行)。除此之外32位架构和64位架构的文件头结构没有实质差别:除了64位的头文件还包含了一个额外的预留字段,这个字段暂时没有使用。
32位结构:
/*
* The 32-bit mach header appears at the very beginning of the object file for
* 32-bit architectures.
*/
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位结构:
/*
* 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 |
#define MH_MAGIC 0xfeedface 表示32位二进制 , #define MH_MAGIC_64 0xfeedfacf 表示64位二进制 |
cputype |
CPU 类型,如arm |
cpusubtype |
CPU 子类型,也是对应的具体类型,如 arm64 ,armv7 |
filetype |
文件类型,如可执行文件、库文件、核心转储文件、内核扩展等 |
ncmds |
用于加载器的“加载命令”的条数 |
sizeofcmds |
所有加载命令的大小 |
flags |
动态连接器(dylib )加载时所需要的标志位 |
reserved |
仅限64位:保留给未来使用 |
下面是 MachOView
分析的图
filetype
filetype
用于表示目标文件的类型,这个字段的可取值以宏的形式定义在 <mach-o/loader.h>
中。
- 文件的布局取决于文件的类型,除了
MH_OBJECT
以外的所有文件类型段被填充并在segment
上对齐。 MH_EXECUTE
,MH_FVMLIB
,MH_DYLIB
,MH_DYLINKER
和MH_BUNDLE
文件类型的头文件也包含在他们的第一segment
中。- 文件类型
MH_OBJECT
是一个紧凑的格式,用于输出链接编辑器(.o)的汇编程序和输入(可能还有输出)格式。所有部分都在一个没有段填充的未命名段中。当文件很小的时候,这种格式被用作可执行格式段填充大大增加了它的大小。 - 文件类型
MH_PRELOAD
是一种可执行格式,不在内核下执行(proms
、standalones
、kernel
等)操作。可执行文件可以在内核下执行,但可能需要分页并且不需要预加载。 - 核心文件是
MH_CORE
格式的,可以是arbritray
中的任何合法的Mach-O
文件。
*
* 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 */
/* linking only, no section contents */
#define MH_DSYM 0xa /* companion file with only debug */
/* sections */
#define MH_KEXT_BUNDLE 0xb /* x86_64 kexts */
解释下文件类型:
File Type |
用处 | 示例 |
---|---|---|
MH_OBJECT |
编译器对源码编译后得到的中间结果,文件的后缀是 .o ,还有静态库文件(.a ),.a 文件是有 N 个 .o 合并在一起的文件 |
gcc -c xx.c 生成的 xx.o 文件 |
MH_EXECUTE |
可执行二进制文件 | /usr/bin 目录下的那些二进制文件,以及应用程序的二进制文件 |
MH_FVMLIB |
VM 共享库文件(还不清楚是什么东西) |
|
MH_CORE |
core 文件,一般在 App Crash 产生 |
崩溃时的 Dump 文件 |
MH_PRELOAD |
预加载可执行文件 | |
MH_DYLIB |
动态库,文件的后缀是 .dylib |
/usr/lib/ 里面的那些库文件,还有 framework 文件的 xx 也是动态库文件 |
MH_DYLINKER |
动态连接器 | /usr/lib/dyld 文件 |
MH_BUNDLE |
插件:非独立的二进制文件,要加载至其他的二进制文件才能发挥作用。和 DYLIB 类型的文件的不同之处在于,这些二进制文件是可执行文件显式地加载的,通常是调用 NSBundle(Objective-C) 或者 CFBundle(C) |
往往是通过 gcc-bundle 生成,/System/Library/QuickLokk 目录下的 QuickLook 插件,System/Library/Spotlight 目录下的 Spotlight Importer 插件,System/Library/Automator 目录下的 Automator action 插件 |
MH_DYLIB_STUB |
静态链接文件,只链接,没有 section 内容 |
不清楚是个什么东西 |
MH_DSYM |
存储着二进制文件符号信息和调试信息的文件 | 在解析堆栈符号中常用,常用于分享 App 的崩溃信息,位置一般在 xx.dsym/Contents/Resources/DWARF/xx ,通过 gcc -g 生成 |
MH_KEXT_BUNDLE |
x86_64 内核扩展文件 |
64位的内核扩展 |
flags
在 <mach-o/loader.h>
中包含了重要的 flags
/* Constants for the flags field of the mach_header */
#define MH_NOUNDEFS 0x1 /* the object file has no undefined
references */
#define MH_INCRLINK 0x2 /* the object file is the output of an
incremental link against a base file
and can't be link edited again */
#define MH_DYLDLINK 0x4 /* the object file is input for the
dynamic linker and can't be staticly
link edited again */
#define MH_BINDATLOAD 0x8 /* the object file's undefined
references are bound by the dynamic
linker when loaded. */
#define MH_PREBOUND 0x10 /* the file has its dynamic undefined
references prebound. */
#define MH_SPLIT_SEGS 0x20 /* the file has its read-only and
read-write segments split */
#define MH_LAZY_INIT 0x40 /* the shared library init routine is
to be run lazily via catching memory
faults to its writeable segments
(obsolete) */
#define MH_TWOLEVEL 0x80 /* the image is using two-level name
space bindings */
#define MH_FORCE_FLAT 0x100 /* the executable is forcing all images
to use flat name space bindings */
#define MH_NOMULTIDEFS 0x200 /* this umbrella guarantees no multiple
defintions of symbols in its
sub-images so the two-level namespace
hints can always be used. */
#define MH_NOFIXPREBINDING 0x400 /* do not have dyld notify the
prebinding agent about this
executable */
#define MH_PREBINDABLE 0x800 /* the binary is not prebound but can
have its prebinding redone. only used
when MH_PREBOUND is not set. */
#define MH_ALLMODSBOUND 0x1000 /* indicates that this binary binds to
all two-level namespace modules of
its dependent libraries. only used
when MH_PREBINDABLE and MH_TWOLEVEL
are both set. */
#define MH_SUBSECTIONS_VIA_SYMBOLS 0x2000/* safe to divide up the sections into
sub-sections via symbols for dead
code stripping */
#define MH_CANONICAL 0x4000 /* the binary has been canonicalized
via the unprebind operation */
#define MH_WEAK_DEFINES 0x8000 /* the final linked image contains
external weak symbols */
#define MH_BINDS_TO_WEAK 0x10000 /* the final linked image uses
weak symbols */
#define MH_ALLOW_STACK_EXECUTION 0x20000/* When this bit is set, all stacks
in the task will be given stack
execution privilege. Only used in
MH_EXECUTE filetypes. */
#define MH_DEAD_STRIPPABLE_DYLIB 0x400000 /* Only for use on dylibs. When
linking against a dylib that
has this bit set, the static linker
will automatically not create a
LC_LOAD_DYLIB load command to the
dylib if no symbols are being
referenced from the dylib. */
#define MH_ROOT_SAFE 0x40000 /* When this bit is set, the binary
declares it is safe for use in
processes with uid zero */
#define MH_SETUID_SAFE 0x80000 /* When this bit is set, the binary
declares it is safe for use in
processes when issetugid() is true */
#define MH_NO_REEXPORTED_DYLIBS 0x100000 /* When this bit is set on a dylib,
the static linker does not need to
examine dependent dylibs to see
if any are re-exported */
#define MH_PIE 0x200000 /* When this bit is set, the OS will
load the main executable at a
random address. Only used in
MH_EXECUTE filetypes. */
解释如下
标志 | 用处 |
---|---|
MH_NOUNDEFS |
表示目标文件没有带有未定义的符号,这些目标文件大部分都是静态二进制文件,没有进一步的链接依赖关系 |
MH_INCRLINK |
目标文件是针对基本文件的增量链接输出,不能再次链接 |
MH_DYLDLINK |
该文件是动态链接器的输入,不能再次静态链接 |
MH_BINDATLOAD |
加载文件时,动态链接器应绑定未定义的引用 |
MH_PREBOUND |
该文件的动态未定义引用已预先绑定 |
MH_SPLIT_SEGS |
目标文件中的只读 segment 和可读写的 segment 是分开的 |
MH_LAZY_INIT |
共享库的初始化例程是通过将内存错误捕捉到其可读写segments(懒散)来延迟运行。–已过时 |
MH_TWOLEVEL |
该 Image 使用两级名称空间绑定 |
MH_FORCE_FLAT |
使用扁平命名空间(不能和 MH_TWOLEVEL 同时出现) |
MH_NOMULTIDEFS |
这把伞保证在其子图像符号没有多重定义,所以这两个级别的命名空间的提示总是可以使用 |
MH_NOFIXPREBINDING |
没有 dyld 通知预绑定代理有关此可执行文件 |
MH_PREBINDABLE |
二进制文件不是预先绑定的,但可以重新预绑定。 仅在未设置MH_PREBOUND 时使用 |
MH_ALLMODSBOUND |
表示此二进制文件绑定到其依赖库的所有两级命名空间模块。 仅在MH_PREBINDABLE 和 MH_TWOLEVEL 都设置时使用 |
MH_SUBSECTIONS_VIA_SYMBOLS |
通过符号进行死代码剥离,可以安全地将这些部分分成子部分 |
MH_CANONICAL |
二进制文件已经通过非预处理操作进行了规范化 |
MH_WEAK_DEFINES |
二进制文件使用了弱符号(weak ) |
MH_BINDS_TO_WEAK |
二进制文件链接了弱符号(weak ) |
MH_ALLOW_STACK_EXECUTION |
允许栈可执行,只有可执行文件可以用这个标志,但是通常不建议使用。当发生缓冲区溢出时,可执行的栈会给代码注入带来方便,一般是默认关闭的 |
MH_DEAD_STRIPPABLE_DYLIB |
仅适用于 dylib 。 当链接到具有此位设置的dylib 时,如果没有从 dylib 引用符号,静态链接器将自动不为 dylib 创建 LC_LOAD_DYLIB 加载命令 |
MH_ROOT_SAFE |
设置此位时,二进制文件声明在 uid 为零的进程中使用它是安全的 |
MH_SETUID_SAFE |
设置此位时,二进制文件声明当 issetugid() 为 true 时,在进程中使用它是安全的 |
MH_NO_REEXPORTED_DYLIBS |
在 dylib 上设置此位时,静态链接器不需要检查依赖的dylib 是否有任何重新导出 |
MH_PIE |
对可执行的文件类型启用地址空间布局随机化 |
从表中可以看出有个标志和执行有关:
MH_ALLOW_STACK_EXECUTION
,这个标志用于防止某些数据的执行,通常称为NX(Non-eXecutable)
。通过将数据所在的内存页面标记为不可执行,一般情况下可以防止黑客进行代码注入,因为黑客不能方便地执行数据段中的代码。如果试图执行数据段中的代码,会引发一个硬件异常,进程会被终止,让进程崩溃,从而避免执行注入的代码
Mach-O
头文件的主要功能在于加载命令(load command
)。加载命令紧跟在文件头之后,文件头中的两个字段 ncmds
和 sizeofncmds
用于解析加载命令。
随机地址空间
进程每一次启动,地址空间都会简单地随机化。对于大多数应用程序来说,地址空间随机化是一个和他们完全不相关的实现细节,但是对于黑客来说,它具有重大的意义。
如果采用传统的方式,程序的每一次启动的虚拟内存镜像都是一致的,黑客很容易采取重写内存的方式来破解程序。采用 ASLR
可以有效的避免黑客的中间人攻击等
dyld
动态链接库,这是苹果的一个开源的项目,可以在这里下载,当内核执行LC_DYLINK
时,链接器会启动,查找进程所依赖的动态库,并加载到内存中。
二级名称空间
这是 dyld
的一个独有特性,说是符号空间中还包括所在库的信息,这样子就可以让两个不同的库导出相同的符号,与其对应的是平坦名称空间
Load Commands
Mach-O
文件头中包含了非常详细的指令,这些指令在被调用时清晰地指导了如何设置并加载二进制数据。这些指令或者成为“加载指令”,紧跟在 mach_header
之后,所有加载命令的总大小由 mach_header
中的 sizeofcmds
字段给出。所有加载命令必须具有 cmd
和 cmdsize
作为前两个字段。cmd
字段用该命令类型的常亮填充。每种命令类型都有专门针对它的结构。cmdsize
字段是特定加载命令结构的大小(以字节为单位)加上它后面的任何加载命令的一部分(即部分结构,字符串等)。要前进到下一个加载命令,可以将 cmdsize
添加到当前加载命令的偏移量或者指针。32位体系结构的 cmdsize
必须是4字节的倍数,64位体系结构,必须是8字节的倍数(这些永远是任何加载命令的最大对齐)。填充字节必须为0。目标文件中的所有表也必须遵循这些规则,以便文件进行内存映射。否则,指向这些表的指针在某些机器上将无法正常工作或者根本无法正常工作,所有填充都归0,对象将逐字节比较。有一些命令是由内核加载器直接使用的,其他命令由动态链接器处理。
load command
的结构如下:
struct load_command {
uint32_t cmd; /* type of load command */
uint32_t cmdsize; /* total size of command in bytes */
};
内核处理的 Mach-O
命令
#1 | 命令 | 内核中处理的函数(定义在 bsd/kern/mach_loader.c 文件中) |
用途 |
---|---|---|---|
0x01 |
LC_SEGMENT |
load_segment |
将文件中的(32位)的 segment 映射到进程地址空间中 |
0x19 |
LC_SEGMENT_64 |
load_segment |
将文件中的(64位)的 segment 映射到进程地址空间中 |
0x0E |
LC_LOAD_DYLINKER |
load_dylinker |
调用 dyld(/usr/lib/dyld) |
0x1B |
LC_UUID |
内核将 UUID 复制到内部表示 mach 目标的数据中 |
一个唯一的128位 ID 。这个 ID 匹配一个二进制文件及其对应的符号 |
0x04 |
LC_THREAD |
load_thread |
开启一个 Mach 线程,但是不分配栈(很少在核心转储文件之外使用) |
0x05 |
LC_UNIXTHREAD |
load_unixthread |
开启一个 UNIX 线程(初始化栈布局和寄存器)。通常情况下,除了指令指针、程序计数器之外,所有的寄存器值都为0。从 Mountain Lion 开始,这条命令被废弃了,被 dyld 的 LC_MAIN 替换 |
0x1D |
LC_CODE_SIGNATURE |
load_code_signature |
代码前面(在 OS X 中偶尔使用。在 iOS 中强制使用) |
0x21 |
LC_ENCRYPTION |
set_code_unprotect() |
加密的二进制文件。同样在 OS X 中几乎不用,在 iOS 中普通使用 |
加载过程在内核的部分负责新进程的基本设置————分配虚拟内存、创建主线程,以及处理任何可能的代码签名、加密的工作。然后对于动态链接的可执行文件(大部分可执行文件都是动态链接的)来说,真正的库加载和符号解析的工作都通过 LC_LOAD_DYLINKER
命令指定的动态链接器在用户态完成。控制权会转交给连接器,连接器进而接着处理文件头的其他加载指令。
下面详细介绍这些加载指令:
LC_SEGMENT 以及进程虚拟内存设置
LC_SEGMENT
(LC_SEGMENT_64
)命令是主要的加载命令,这条命令指导内核如何设置新运行的进程的内存空间。这些“段” 直接从 Mach-O
二进制文件加载到内存中。
每一条LC_SEGMENT
(LC_SEGMENT_64
)命令都提供了段布局的所有必要细节信息:
LC_SEGMENT
(LC_SEGMENT_64
) 提供了段布局所有必要的参数信息:
参数 | 用途 |
---|---|
cmd |
LC_SEGMENT (LC_SEGMENT_64 ) |
segment |
load_segment |
cmdsize |
load command 的大小 |
segname |
段的名称 如 _PAGEZERO |
vmaddr |
所描述的段的虚拟物理地址 |
vmsize |
为这个段分配的虚拟内存大小 |
fileoff |
表示这个段在文件中的偏移量 |
filesize |
表示这个段在文件中占用的字节数 |
maxprot |
段的页面所需要的最高内存保护,用八进制表示(4=r,2=w,1=x ) |
initprot |
段的页面最初始的内存保护 |
nsects |
段中的区(section )数量(如果存在的话) |
flags |
杂项标志位 |
有了 LC_SEGMENT
命令,设置进程虚拟内存的过程就变成遵循 LC_SEGMENT
命令的简单操作。对于每一个段,将文件中相应的内容加载到内存中:从偏移量为 fileoff
处加载 filesize
字节到虚拟内存地址 vmadder
处的 vmsize
字节。每一个 segment
的页面都根据 initprot
进行初始化,initprot
指定了如何通过读/写/执行位初始化页面的保护级别。段的保护设置可以动态改变,但是不能超过 maxprot
中指定的值(在 iOS
中,+x
和 +w
是互斥的)。
_PAGEZERO
段(空指针陷阱)、_TEXT
段(程序代码)、_DATA
段(程序数据,可读/科协的数据)和 _LINKEDID
(链接器使用的符号和其他表)段提供了 LC_SEGMENT
命令。段有时候也可以进一步分解为区(section
)。
下面列出一些常见的区
Mach-O
可执行文件常见的段和区:
_TEXT section |
用途 |
---|---|
__text |
主程序代码 |
__stubs |
用于动态链接的存根 |
__stub_helper |
用于动态链接的存根 |
__cstring |
程序中硬编码的 C 语言字符串 |
__const |
用 const 修饰的常量变量以及硬编码的常量 |
__objc_methname |
Objective-C 方法名称 |
__objc_methtype |
Objective-C 方法类型 |
__objc_classname |
Objective-C 类名称 |
__objc_classlist |
Objective-C 类列表 |
__objc_protolist |
Objective-C 原型 |
__objc_imginfo |
Objective-C 镜像信息 |
__objc_const |
Objective-C 常量 |
__objc_selfrefs |
Objective-C 自引用(this ) |
__objc_protorefs |
Objective-C 原型引用 |
__ustring |
unicode 字符串 |
__gcc_except_tab |
异常处理相关 |
__unwind_info |
异常处理 |
__eh_frame |
异常处理 |
| __DATA section
| 用途 |
| ——————- | —————————————— |
| __nl_symbol_ptr
| 动态符号链接相关,指针数组 |
| __got
| 全局偏移表, Global Offset Table
|
| __la_symbol_ptr
| 动态符号链接相关,也是指针数组,通过 dyld_stub_binder
辅助链接 |
| __mod_init_func
| 初始化的全局函数地址,会在 main
之前被调用 |
| __const
| const
修饰的常量 |
| __cstring
| 程序中硬编码的 ANSI
的字符串 |
| __cfstring |
CF 用到的字符串 |
|
__objc_classlist |
Objective-C 类列表 |
|
__objc_nlclslist |
Objective-C
load 方法列表 |
|
__objc_catlist |
Objective-C
category 列表 |
|
__objc_protolist |
Objective-C
protocol 列表 |
|
__objc_imageinfo |
Objective-C 镜像洗洗 |
|
__objc_const |
Objective-C 常量 |
|
__objc_selrefs |
Objective-C 引用的
SEL 列表 |
|
__objc_protorefs |
Objective-C 引用的
protocol 列表 |
|
__objc_classrefs |
Objective-C 引用的
class 列表 |
|
__objc_supperrefs |
Objective-C 父类的引用列表 |
|
__objc_ivar |
Objective-C
ivar 信息 |
|
__objc_data |
Objective-C
class 信息 |
|
__bss | 未初始化的静态变量区 |
|
__data` | 初始化的可变变量 |
段也可以设置一些 <mach/loader,h>
头文件中定义的标志。苹果使用的一个标志是 SD_PROTECTED_VERSION_1(0X08)
,表示这个段的页面是“受保护的”,即加密的。苹果通过这种技术加密一些二进制文件。
通过 otool -v -l xxx
或者 otool -lv xxx
可以查看所有的 Load Commands
/System/Library/CoreServices/Finder.app/Contents/MacOS
➜ MacOS otool -lV Finder
Finder:
Mach header
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
MH_MAGIC_64 X86_64 ALL LIB64 EXECUTE 80 9928 NOUNDEFS DYLDLINK TWOLEVEL BINDS_TO_WEAK PIE MH_HAS_TLV_DESCRIPTORS
Load command 0
cmd LC_SEGMENT_64
cmdsize 72
segname __PAGEZERO
vmaddr 0x0000000000000000
vmsize 0x0000000100000000
fileoff 0
filesize 0
maxprot ---
initprot ---
nsects 0
flags (none)
Load command 1
cmd LC_SEGMENT_64
cmdsize 1112
segname __TEXT
vmaddr 0x0000000100000000
vmsize 0x0000000000802000
fileoff 0
filesize 8396800
maxprot rwx
initprot r-x
nsects 13
flags PROTECTED_VERSION_1
在创建 Mach-O
对象时,可以通过 -segcreate
开关让 Xcode
的 ld
创建段。Xcode
还包含一个名为 segedit
的特殊工具,可以用于提取或者替换 Mach-O
文件中的段。这个工具用于提取内嵌的文本信息.
2.LC_UNIXTHREAD
当所有的库都完成加载之后,dyld
的工作也完成了,之后由 LC_UNIXTHREAD
命令负责启动二进制程序的主线程(因此主线程总是在可执行文件中,而不会再其他二进制文件中,例如库文件)。根据架构的不同,这条命令会列出所有初始化寄存器的状态,不同架构的寄存器状态不同,这个不同的架构包括 i386_THREAD_STATE
、x86_THREAD_STATE64
以及 iOS
中的 ARM_THREAD_STATE
。在任何一种架构中,大部分的寄存器应该都会初始化为0
,其中指令指针(Intel
的 IP
)或者程序计数器(ARM
的 r15
)是例外,这些寄存器保存了程序入口点的地址。
3.LC_THREAD
和 LC_UNIXTHREAD
的功能类似,LC_THREAD
用于核心转储文件,Mach-O
核心转储文件实际上市一组 LC_SEGMENT
(LC_SEGMENT_64
)命令的合集,这些命令负责建立起进程的内存镜像(只不过现在进程已经无效了),然后就是最后一条 LC_THREAD
命令。LC_THREAD
命令也包含几种不同的类型,每一种类型对应不同的机器状态(即线程、浮点和异常)。只要创建了一个核心转储并通过 otool -l
查看这个核心转储文件就可以找到这个命令。
LC_MAIN
LC_MAIN
这条命令的作用是设置程序主线程的入口点地址和栈大小。这条命令比 LC_UNIXTHREAD
命令更实用一些,因为无论如何除了程序计数器之外所有的寄存器都设置为0
了,
LC_CODE_SIGNATURE
Mach-O
二进制文件有一个重要特性就是可以进行数字签名。尽管在 OS X
中仍然没怎么使用数字签名,不过由于代码签名和新改进的沙盒机制绑定在一起,所有签名的使用率也越来越高。在 iOS
中,代码签名是强制要求的,这也是苹果尽可能对系统封锁的另一种尝试:在 iOS
中只有苹果的签名才会被认可。在 OS X
中 codesign
工具可以用于操纵和显示代码签名。
LC_CODE_SINGATURE
包含了 Mach-O
二进制文件的代码签名,如果这个签名和代码本身不匹配(或者如果在 iOS
上这条命令不存在),那么内核会立即给进程发送一个 SIGKILL
信号将进程杀掉。untethered
越狱(完美越狱)因为利用了一个内核漏洞所以可以修改这些变量。由于这些变量的默认值都是启用签名检查,所以不完美越狱会导致非苹果签名的应用程序崩溃——除非设备以完美越狱的方式引导。
此外通过 Saurik
的 ldid
这类工具可以在 Mach-O
中嵌入伪代码签名。这个工具可以代替 OS X
的 codesign
,允许生成自我签署认证的伪签名。这在 iOS
中尤为重要,因为签名和沙盒模型的应用程序“entitlement
”绑定在一起,而后者在 iOS
中是强制要求的。entitlement
是声明式的许可(以 plist
的形式保存),必须内嵌在 Mach-O
中并通过签名,从而允许执行安全敏感的操作时具有运行权限。
动态库
可执行文件很少是独立的,除了少数的一些静态链接的可执行文件。大部分可执行文件都是动态链接的,这些可执行文件依赖一些预先存在的库,这些苦即可能是操作系统提供的,也可能是第三方库
启动时库的加载
在 iOS
中,少量的进程只需要内核加载器就可以完成加载,在 OS X
和 iOS
上几乎所有的程序都是动态链接的。Mach-O
镜像中有很多的“空洞”———即对外部的库和符号的引用————这些空洞在程序启动时填补。这些工作是由动态链接器来完成,这个过程叫符号绑定。
动态链接库在内核执行 LC_DYLINKER
加载命令时启动。一般来说使用的是 /usr/lib/dyld
作用动态链接器,不过这条加载命令可以指定任何程序作为参数。链接器接管刚创建进程的控制器,因为内核将进程的入口点设置为链接器的入口点。
链接器所要完成的工作,就是查找进程中所有的符号和库的依赖关系,然后解决这些关系。这个过程必须是递归完成,通常情况下库还会依赖于其他的库。
dyld
是一个用户态的进程。dyld
不属于内核的一部分,而是作为一个单独的开源项目由苹果维护的(当然也属于Darwin
的一部分),地址在http://www.opensource.apple.com/source/dyld
。从内核的角度看,dyld
是一个可插入的组件,可以替换为第三方的链接器。
链接器在 mach_header
中的相关加载命令:
# | 加载命令 | 用途 |
---|---|---|
0x02 |
LC_SYMTAB |
符号表。符号表和字符串表是由这些命令中指定的偏移量分别提供的 |
0X0B |
LC_DSYMTAB |
符号表。符号表和字符串表是由这些命令中指定的偏移量分别提供的 |
0x0C |
LC_LOAD_DYLIB |
加载额外的动态库。这条命令取代了 NeXTSTEP 中使用的 LC_LOAD_FVMLIB |
0x20 |
LC_LAZY_LOAD_DYLIB |
和 LC_LOAD_DYLIB 功能一样,但是将实际的加载工作延迟到第一次使用这个库中的符号时。 |
0x0D |
LC_ID_DYLIB |
只在 dylib 中寻找,指定了 dylib 的 ID 、时间戳、版本和兼容版本 |
0x1F |
LC_REEXPORT_DYLIB |
只在动态库中寻找,允许一个库将其他库的符号作为自己的符号重新导出。这是 Cocoa 和 Carbon 作为很多其他框架的保护伞框架采用的方法 |
0x24 、0x25 |
LC_VERSION_MIN_IPHONEOS 、LC_VERSION_MIN_MACOSX |
这个二进制文件要求的最低操作系统的版本。 |
0x26 |
LC_FUNCTION_STARTS |
压缩的函数起始地址表 |
0X2A |
LC_SOURCE_VERSION |
构建这个二进制文件使用的源代码的版本。非正式场合使用,不会对实际的链接产生影响 |
0x2B |
?? (常量名称未知) | 来自 dylibs 的代码签名区 |
通过 otool -L
可以显示库的依赖关系。nm
命令可以显示 Mach-O
二进制文件的符号表。OS X
的 nm
还支持 -m
开关,这个开关不仅能够显示符号,还能跟踪符号的解析过程。此外,Xcode
的 dyldinfo
命令也能完成统一的功能。使用这个命令,可以显示链接器在加载库时使用的操作码。
显示 dyld
绑定操作码
➜ WeXin xcrun dyldinfo -opcodes /bin/ls | more
rebase opcodes:
0x0000 REBASE_OPCODE_SET_TYPE_IMM(1)
0x0001 REBASE_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB(2, 0x00000038)
0x0003 REBASE_OPCODE_DO_REBASE_ULEB_TIMES(76)
0x0005 REBASE_OPCODE_ADD_ADDR_IMM_SCALED(0x10)
0x0006 REBASE_OPCODE_DO_REBASE_ULEB_TIMES_SKIPPING_ULEB(16, 16)
0x0009 REBASE_OPCODE_DO_REBASE_ADD_ADDR_ULEB(48)
0x000B REBASE_OPCODE_DO_REBASE_ULEB_TIMES_SKIPPING_ULEB(3, 16)
0x000E REBASE_OPCODE_DO_REBASE_ADD_ADDR_ULEB(64)
0x0010 REBASE_OPCODE_DO_REBASE_IMM_TIMES(1)
0x0011 REBASE_OPCODE_DONE()
binding opcodes:
0x0000 BIND_OPCODE_SET_DYLIB_ORDINAL_IMM(3)
0x0001 BIND_OPCODE_SET_SYMBOL_TRAILING_FLAGS_IMM(0x00, __DefaultRuneLocale)
0x0016 BIND_OPCODE_SET_TYPE_IMM(1)
0x0017 BIND_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB(0x02, 0x00000010)
0x0019 BIND_OPCODE_DO_BIND()
0x001A BIND_OPCODE_SET_SYMBOL_TRAILING_FLAGS_IMM(0x00, ___stack_chk_guard)
0x002E BIND_OPCODE_DO_BIND()
0x002F BIND_OPCODE_SET_SYMBOL_TRAILING_FLAGS_IMM(0x00, ___stderrp)
0x003B BIND_OPCODE_DO_BIND()
0x003C BIND_OPCODE_SET_SYMBOL_TRAILING_FLAGS_IMM(0x00, ___stdoutp)
0x0048 BIND_OPCODE_DO_BIND()
0x0049 BIND_OPCODE_SET_SYMBOL_TRAILING_FLAGS_IMM(0x00, _optind)
0x0052 BIND_OPCODE_DO_BIND()
0x0053 BIND_OPCODE_SET_SYMBOL_TRAILING_FLAGS_IMM(0x00, dyld_stub_binder)
0x0065 BIND_OPCODE_ADD_ADDR_ULEB(0xFFFFFFC8)
:0x0009 REBASE_OPCODE_DO_REBASE_ADD_ADDR_ULEB(48)
0x000B REBASE_OPCODE_DO_REBASE_ULEB_TIMES_SKIPPING_ULEB(3, 16)
0x000E REBASE_OPCODE_DO_REBASE_ADD_ADDR_ULEB(64)
0x0010 REBASE_OPCODE_DO_REBASE_IMM_TIMES(1)
0x0011 REBASE_OPCODE_DONE()
binding opcodes:
0x0000 BIND_OPCODE_SET_DYLIB_ORDINAL_IMM(3)
0x0001 BIND_OPCODE_SET_SYMBOL_TRAILING_FLAGS_IMM(0x00, __DefaultRuneLocale)
0x0016 BIND_OPCODE_SET_TYPE_IMM(1)
0x0017 BIND_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB(0x02, 0x00000010)
0x0019 BIND_OPCODE_DO_BIND()
如果二进制文件中使用了外部定义的函数和符号,那么在他们的文本段中会有一个名为__stubs
(桩)的区,在这个区存放的是这些本地未定义符号的占位符。编译器生成代码时会创建对符号桩区的调用,链接器在运行时会解决对桩的这些调用。链接器解决的方式是在被调用的地址处放置一条 JMP
指令。JMP
指令将控制权转交给真实的函数体,但是不会以任何方式修改栈。因此真是函数可以正常返回。
LC_LOAD_DYLIB
命令告诉链接器在哪里可以找到这些符号。链接器要加载每一个指定的库,并且搜索匹配的符号。被链接的库有一个符号表,符号表将符号名称和地址关联起来。符号表在 Mach-O
目标文件的地址可以通过 LC_SYMTAB
加载命令指定的 symoff
找到。对应的符号名称在 stroff
,总共有 nsyms
条符号信息。
和其他所有的 UNIX
一样,Mach-O
格式的库可以在 /usr/lib
目录下找到(在 OS X
和 iOS
中没有 /lib
目录)。然后,和其他 UNIX
有两点不同:
OS X
的库不是“共享目标文件(.so
)”,因为OS X
和ELF
不兼容,所有文件是“动态库”文件,带有dylib
后缀- 没有
libc
。可能有熟悉其他UNIX
上C
运行时库。但是在OS X
上对应的库/usr/lib/libc.dylib
只不过是指向libSystem.b.dylib
的符号链接。libSystem
提供了LibC
的功能,还提供了其他额外的功能,而在其他UNIX
上这些额外的功能是提供在不同的库中
libSystem
库是系统上所有二进制代码的绝对先决条件,不论是 C
、C++
还是 Objcetive-C
程序。这是因为这个库是对底层系统调用和内核服务的接口,如果没有这些接口就什么事也干不了。这个库在还是 /usr/lib/system
目录下一些库的保护伞库(通过 LC_REEXPORT_LIB
加载命令重新导出了符号)。在 iOS
中,重新导出了多大20
多个库
查看符号和观察加载过程
考虑下面这个简单的“hello world”程序,这个程序调用了两次 printf()
,然后退出
➜ Desktop touch a.c
➜ Desktop cat a.c
➜ Desktop cat a.c
void main (int argc,char **argv) {
printf("Saleve ,Munde!\n");
printf("Value\n");
exit(0);
}
➜ Desktop
利用 Xcode
的 dyldinfo
和 nm
,可以解析绑定信息,并且看出二进制文件导出了哪些符号以及链接了哪些库。
➜ Debug xcrun dyldinfo -lazy_bind a
lazy binding information (from lazy_bind part of dyld info):
segment section address index dylib symbol
__DATA __la_symbol_ptr 0x100001010 0x0000 libSystem _exit
__DATA __la_symbol_ptr 0x100001018 0x000C libobjc _objc_autoreleasePoolPush
__DATA __la_symbol_ptr 0x100001020 0x002C libSystem _printf
➜ Debug
符号表
➜ Desktop nm a.out
0000000100000000 T __mh_execute_header
U _exit
0000000100000f30 T _main
U _printf
U dyld_stub_binder
➜ Desktop
利用 Xcode
的 otool
工具,可以进步深入底层,查看汇编层次的代码:
/// otool 对一个简单二进制文件的反汇编
➜ Debug otool -p _main -tV a
a:
(__TEXT,__text) section
_main:
0000000100000f10 pushq %rbp
0000000100000f11 movq %rsp, %rbp
0000000100000f14 subq $0x20, %rsp
0000000100000f18 movl $0x0, -0x4(%rbp)
0000000100000f1f movl %edi, -0x8(%rbp)
0000000100000f22 movq %rsi, -0x10(%rbp)
0000000100000f26 callq 0x100000f5e ## symbol stub for: _objc_autoreleasePoolPush
0000000100000f2b leaq 0x68(%rip), %rdi ## literal pool for: "Hellow dsf\n"
0000000100000f32 movq %rax, -0x18(%rbp)
0000000100000f36 movb $0x0, %al
0000000100000f38 callq 0x100000f64 ## symbol stub for: _printf
0000000100000f3d leaq 0x62(%rip), %rdi ## literal pool for: "fdsfsfd\n"
0000000100000f44 movl %eax, -0x1c(%rbp)
0000000100000f47 movb $0x0, %al
0000000100000f49 callq 0x100000f64 ## symbol stub for: _printf
0000000100000f4e xorl %edi, %edi
0000000100000f50 movl %eax, -0x20(%rbp)
0000000100000f53 callq 0x100000f58 ## symbol stub for: _exit
➜ Debug
接下来,通过 otool -l
展示加载命令,这一次特别关注 stubs
区:
➜ Debug otool -l -V a
Section
sectname __stubs
segname __TEXT
addr 0x0000000100000f58
size 0x0000000000000012
offset 3928
align 2^1 (2)
reloff 0
nreloc 0
type S_SYMBOL_STUBS
attributes PURE_INSTRUCTIONS SOME_INSTRUCTIONS
reserved1 0 (index into indirect symbol table)
reserved2 6 (size of stubs)
Section
sectname __stub_helper
segname __TEXT
addr 0x0000000100000f6c
size 0x000000000000002e
offset 3948
align 2^2 (4)
reloff 0
nreloc 0
type S_REGULAR
attributes PURE_INSTRUCTIONS SOME_INSTRUCTIONS
reserved1 0
reserved2 0
Section
➜ Debug
通过 nm
显示未决的符号:
➜ Debug nm a | grep "U"
U _exit
U _objc_autoreleasePoolPush
U _printf
U dyld_stub_binder
➜ Debug nm a | wc -l # 一共有多少符号
6 在 arm 上有7个,还有一个__dyld_func_lookup
➜ Debug
共享库缓存
命令总结
otool -hv xxx
查看xx
的header
otool -v -l xxx
查看xx
当前所有的load command
otool -vl xx
查看xx
当前所有的load command
otool -L xxx
查看xx
当前所依赖的所有动态库otool -p _main -tV xx
利用otool
从_main
开始进行反汇编xcrun dyldinfo -bind xx
查看符号的绑定信息xcrun dyldinfo -opcodes /bin/ls | more
查看dyld
的绑定的操作码xcrun dyldinfo -bind xx | grep UITableView
查看绑定信息中,包含UITableView
的部分nm
命令可以显示Mach-O
二进制文件的符号表。nm xx | grep "U"
显示未决的符号nm xx | wc -l xx
中一共有多少符号