1、MachO文件的概念
Mach-O
其实是Mach Object
文件格式的缩写,是 mac 以及 iOS 上可执行文件的格式, 类似于 windows
上的 PE
格式 (Portable Executable
), linux
上的 elf
格式 (Executable and Linking Format
) 。常⻅的 .o
,.a
.dylib
Framework
,dyld
.dsym
。
2、整体结构如下图
2.1、Header(头部)
Header
表明该文件是 Mach-O 格式,指定目标架构,还有一些其他的文件属性信 息,文件头信息影响后续的文件结构安排。
与 Mach-O 文件格式有关的结构体定义都可以从xnu源码的loader.h
中找到。
32位和64位的mach_header的数据结构如下
/*
* 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 */
};
/* Constant for the magic field of the mach_header (32-bit architectures) */
#define MH_MAGIC 0xfeedface /* the mach magic number */
#define MH_CIGAM 0xcefaedfe /* NXSwapInt(MH_MAGIC) */
/*
* 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
魔数(特征数字),用来标记当前设备是大端序还是小端序。 -
cputype
标识 CPU 的架构,例如 ARM、ARM64、X86_64 等。 -
cpusubtype
标识 CPU 的具体类型,区分不同版本的处理器。 -
filetype
由于 Mach-O 支持多种类型文件,所以此处引入了 filetype 字段来标明(.o
,.a
.dylib
Framework
,dyld
.dsym
等) -
ncmds
Mach-O 文件中加载命令(load commands)的条数。 -
sizeofcmds
Mach-O文件中加载命令(load commands)的总大小。 -
flags
标识着 Mach-O 文件的一些重要信息(可以loader.h
中查看结构,其中MH_PIE
启用ASLR) -
reserved
64位预留字段
2.2、Load commands
Load commands
是一张包含很多内容的表。内容包括区域的位置、符号表、动态符号表 等。用于告诉loader
如何设置并加载二进制数据,对系统内核加载器和动态链接器(dyld
)起指导作用。
load_command
的数据结构
struct load_command {
uint32_t cmd; /* type of load command */
uint32_t cmdsize; /* total size of command in bytes */
};
前 4 个字节表示类型,不同类型的 load command 作用不一样,紧跟其后的 4 个字节表示该 load command 的大小.
可以通过MachOView
软件来查看类容(类型)
-
LC_SEGMENT_64
将文件中(32位或64位)的段映射到进程地 址空间中 -
LC_DYLD_INFO_ONLY
动态链接相关信息 -
LC_SYMTAB
符号地址 -
LC_DYSYMTAB
动态符号表地址 -
LC_LOAD_DYLINKER
dyld加载 -
LC_UUID
文件的UUID -
LC_VERSION_MIN_MACOSX
支持最低的操作系统版本 -
LC_SOURCE_VERSION
源代码版本 -
LC_MAIN
设置程序主线程的入口地址和栈大小 -
LC_LOAD_DYLIB
依赖库的路径,包含三方库 -
LC_FUNCTION_STARTS
函数起始地址表 -
LC_CODE_SIGNATURE
代码签名 -
LC_ENCRYPTION_INFO
和LC_ENCRYPTION_INFO_64
:加密信息,如果是从App Store上下载的应用,外面被加了一层壳,对应的加密标记(Crypt ID)不为0,如果不是App Store上下载的应用(例如PP助手上),或这个已经被脱过壳的,加密标记(Crypt ID)便为0
LC_SEGMENT(段)
LC_SEGMENT_64
和LC_SEGMENT
(32位系统)是加载的主要命令,翻译成中文叫做“段”,它负责指导内核来设置进程的内存空间,说白了,只要是这个类型的 load command,系统会将其指示的内容全部加载到指定的虚拟内存地址上来。
MachOView看到的内容
通过
mach-o/loader.h
找到数据结构
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 */
};
-
cmd
该加载命令类型,为LC_SEGMENT_64(64位)或LC_SEGMENT(32位) -
cmdsize
该加载命令大小,包括segement下session结构所占大小 -
segname[16]
segment 名字 -
vmaddr
为当前segment分配的虚拟内存地址 -
vmsize
为当前segment分配的虚拟内存大小 -
fileoff
当前segment在 Mach-O 文件中的偏移量 -
filesize
当前segment在 Mach-O 文件中占用的字节 -
maxprot
segment所在页所需要的最高内存保护 -
initprot
segment所在页原始内存保护 -
nsects
segment中section数量 -
flags
标识符
大致可以这么理解:系统 Mach-O 从fileoff
处加载filesie
大小的内容到虚拟内存vmaddr
处,大小为vmsize
,segment
页权限initport
进行初始化,这些权限可以被修改,但是不能超过maxprot
的值。通俗来说就是,fileoff
和filesie
指导和说明了内容从哪里来,vmaddr
和vmsize
指导和说明了文件到哪里去。需要留意,对某些segment
来说,vmsize
可能会大于 filesize
,如__DATA、__LINKEDIT。
segname
分类(用下划线和大写字母组成)
-
__PAGEZERO
:静态链接器创建了__PAGEZERO命名的段作为可执行文件的第一个段,该段在文件中所占大小为0,在 32 位系统上,加载到虚拟内存中是 0x4000,也就是 16kb,在 64 位系统上,加载到虚拟未存中是 0x100000000,也就是 4GB。是一个不可读、不可写、不可执行的空间,能够在空指针访问时抛出异常。 -
__TEXT
:代码段,里面包含了可执行代码和其他一些只读数据,该段是可读、可执行,但是不可写。 -
__DATA
:数据段,里面主要是存放将会被更改的数据,该段是可读、可写,但不可执行。 -
__LINKEDIT
:包含需要被动态链接器使用的符号和其他表,包括符号表、字符串表等,可读,但不可写不可执行。
segname
下的section
segname
为__TEXT
和__DATA
两个segment
可以进一步分解为section
。之所以按照segment
-> section
的结构组织方式,是因为在同一个segment
下的section
,可以控制相同的权限,也可以不完全按照Page的大小进行内存对其,节省内存的空间。而segment
对外整体暴露,在程序载入阶段映射成一个完整的虚拟内存,更好的做到内存对齐。
从mach-o/loader.h
文件中可以找到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) */
};
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
:section所在的segment名称 -
addr
:section所在的内存地址 -
size
:section的大小 -
offset
:section所在的文件偏移 -
align
:section的内存对齐边界 (2 的次幂) -
reloff
:重定位信息的文件偏移 -
nreloc
:重定位条目的数目 -
flags
:标志属性 -
reserved
:保留字段
使用 MachOView 进行查看OC工程的macho文件
各个section作用如下:
-
__TEXT.__text
:主程序代码 -
__TEXT.__stubs
、__TEXT.__stub_helper
:用于帮助动态链接器绑定符号 -
__TEXT.__const
:const关键字修饰的常量 -
__TEXT.__objc_methodname
:OC方法名 -
__TEXT.__cstring
:只读的C语言字符串 -
__TEXT.__objc_classname
:OC类名 -
__TEXT.__objc_methtype
:OC方法类型(方法签名) -
__TEXT.__gcc_except_tab
、__ustring
、__unwind_info
:GCC编译器自动生成,用于确定异常发生是栈所对应的信息(包括栈指针、返回地址及寄存器信息等) -
__DATA.__got
:全局非懒绑定符号指针表 -
__DATA.__la_symbol_ptr
:懒绑定符号指针表 -
__DATA.__mod_init_func
:C++类的构造函数 -
__DATA.__const
:未初始化过的常量 -
__DATA.__cfstring
:Core Foundation字符串 -
__DATA.__objc_classlist
:OC类列表 -
__DATA.__objc_nlclslist
:实现+load方法的 OC 类列表 -
__DATA.__catlist
:OC 分类(Category)列表 -
__DATA.__protolist
:OC 协议(Protocol)列表 -
__DATA.__imageinfo
:镜像信息,可用它区别 OC 1.0与2.0 -
__DATA.__const
:OC 初始化过的常量 -
__DATA.__selrefs
:OC 选择器(SEL)引用列表 -
__DATA.__protorefs
:OC 协议引用列表 -
__DATA.__classrefs
:OC 类引用列表 -
__DATA.__superrefs
:OC 超类(即父类)引用列表 -
__DATA.__ivar
:OC 类的实例变量 -
__DATA.__objc_data
:OC 初始化过的变量 -
__DATA.__data
:实际初始化数据段 -
__DATA.__common
:未初始化过的符号申明 -
__DATA.__bss
:未初始化的全局变量
使用 MachOView 进行查看Swift工程的macho文件
在Swift的macho文件中,types是4字节存储信息,存储的是相对地址
注意:swift5的macho会多出几个section(目前知道的就是下面几个,后面了解再补充)
__swift5_types
:存储的是Class、Struct、Enum的描述(Descriptor)偏移信息
__swift5_fieldmd
:存储的是属性描述(属性信息)的偏移信息
__swift5_reflstr
:存储的是属性名称
__swift5_protos
:存储的是代理的描述
2.3、Data
Data
区主要就是负责代码和数据记录的。Mach-O 是以 Segment
这种结构来组织数据 的,一个 Segment 可以包含 0 个或多个 Section。根据 Segment 是映射的哪一个 Load Command,Segment 中 section 就可以被解读为是是代码,常量或者一些其他的数据类 型。在装载在内存中时,也是根据 Segment 做内存映射的。