iOS 编译与链接二:编译的产物Mach-O
上一篇说到编译产生了目标文件.o,我们知道不同的操作系统,可执行文件是不同的,系统能够理解这个特殊文件,才能加载到内存,创建出进程.
Mach-O是Mach object的缩写,虽然windows,linux,unix,mac os/ios他们的可执行文件虽然有着不同的文件,但是他们都来是来自一种叫做COFF(Common file format)的格式,是它的变种版本,特点是不同的文件有用不同的"段".,是Mac os以及 iOS上用来存储程序的一类文件.Mach-O目标文件是源代码编译得到的文件,包含机器指令,数据,符号表,调试信息,字符串等,然后按照不同的信息,放在不同的“段”(segment)中;比如指令一般放在代码段里,变量一般放在数据段里.
除了.o,还有像可执行文件,framework,.a,.out等文件也都是mach-o.
一.Mach-O的结构
mach-o的结构主要分为三个部分,Mach Header、Load Command、Data.
可以使用MachOView查看mach-o文件
下载地址
下载源码
从EXTERNAL_HEADERS/mach-o/loader.h中的定义可以了解mach-o的一些基本内容.
1.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 */
MH_OBJECT : 经过编译和静态链接(也可以没有这个过程)的.o文件,以及静态链接库.
MH_EXECUTE : 可执行文件
MH_DYLIB : 动态链接库
MH_DYLINKER : 动态链接器
MH_BUNDLE : bundle资源文件
MH_DYLIB_STUB : 静态链接库
MH_DSYM : 符号表和调试信息文件
还是在EXTERNAL_HEADERS/mach-o/loader.h中,可以找到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 */
};
/* Constant for the magic field of the mach_header_64 (64-bit architectures) */
#define MH_MAGIC_64 0xfeedfacf /* the 64-bit mach magic number */
#define MH_CIGAM_64 0xcffaedfe /* NXSwapInt(MH_MAGIC_64) */
magic是mach-o文件的识别符,比如MH_MAGIC,MH_MAGIC_64,MH_CIGAM,MH_CIGAM_64,除此之外还有通用二进制的FAT_MAGIC和FAT_CIGAM后面会说到.
cputype和 cpusubtype是 cpu架构和细分.
filetype是文件类型,也就是上面那些宏定义,MH_OBJECT,MH_EXECUTE等等.
ncmds加载命令的数量
sizeofcmds加载命令的数据大小
flags标识位
reserved保留字段,没有固定的值
可以使用命令查看header
otool -v -h main
输出
main:
Mach header
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
MH_MAGIC_64 ARM64 ALL 0x00 EXECUTE 21 1784 NOUNDEFS DYLDLINK TWOLEVEL PIE
或者用MachOView查看
mach header
可以看到这个main,是64位,是个可执行文件MH_EXECUTE.
2.Load Commands
加载指令,也在EXTERNAL_HEADERS/mach-o/loader.h中定,和Header一样,也有一个结构体
struct load_command {
uint32_t cmd; /* type of load command */
uint32_t cmdsize; /* total size of command in bytes */
};
只有两个属性,命令类型,和命令的字节长度.
cmd是一批宏定义,以LC开头,有五六十个.
主要有这些:
LC_SEGMENT_64 将文件中的段映射到进程地址空间中
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 定义在代码段(__text)内的非指令表
LC_CODE_SIGNATURE 应用的签名信息
不过load_command这个结构体似乎不怎么使用,使用的是其他更具体的定义,每种加载命令都有对应的结构体
比如 dylib_command, symtab_command等等.他们也都有cmd和cmdsize.
struct dylib_command {
uint32_t cmd; /* LC_ID_DYLIB, LC_LOAD_{,WEAK_}DYLIB,
LC_REEXPORT_DYLIB */
uint32_t cmdsize; /* includes pathname string */
struct dylib dylib; /* the library identification */
};
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 */
};
另外还有segment command负责描述segment里的section
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或者LC_SEGMENT_64了.
而segment_command就指定了后面数据段的布局.
vmaddr是内存地址,
vmsize占用内存的大小,
fileoff是这个segment在mach-o文件的数据开始位置.也叫做偏移.
filesize是这个segment包含数据的大小.也叫做段.
也就是从fileoff(也叫做偏移)取filesize字节的数据,放到内存的vmaddr处的vmsize字节.
3.Data
数据段主要由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 */
};
数据段
前面是segment name比如__TEXT, __DATA;
后面是section name比如__text,__cstring等.
__text 机器码
__cstring C 语言字符串
__const 初始化的常量
__objc runtime支持
__data 初始化的变量
__bss 未初始化的静态变量
__stubs 跳转表,重定向到 lazy 和 non-lazy 符号的 section
__stubs_helper lazy 动态绑定符号的辅助函数
__objc_methname OC 方法名
__objc_methtype OC 方法类型
__objc_classname OC 类名
__swift5_proto swift协议
__got 全局偏移表
__la_symbol_ptr lazy binding的指针表,表中的指针一开始都指向__stub_helper
__cfstring 工程中使用的Core Foundation字符串(CFStringRefs)
__objc_classlist OC 类列表
__objc_protolist OC protocol列表
__objc_imginfo OC 镜像信息
__const 没有初始化过的常量
__objc_selfrefs OC 引用的SEL列表
__objc_protorefs OC 引用的protocol列表
__objc_superrefs OC 引用的父类列表
__objc_ivar OC ivar信息
__objc_data class信息
__bss BSS,存放 未初始化的全局变量,就是常说的静态内存分配
__data 初始化的可变数据
...等等
-
Assembly 汇编代码
在这里存储的是编译main.o过程中的生成的汇编指令.
汇编代码
机器码
__TEST__text存储的机器码
-
Symbol Table 符号表
在编译那篇说到过,clang在词法分析这一步中,把代码拆分成一个个token,这其中变量名,方法名,类名等等这些被叫做符号,符号表存储了符号在字符串表中的位置,以及类型,地址,描述等等.
符号表 -
String Table 字符串表
存储变量名,方法名,类名,协议,结构体等等符号
字符串表 -
Dynamic Symbol Table 动态符号表
存储的是动态库函数位于符号表的偏移信息
动态符号表 -
Lazy Symbol Pointers 懒加载符号表
懒加载是指在程序运行时需要访问这些符号的时候再去绑定,一般是动态库里的符号.
懒加载符号表 -
Non Lazy Symbol Pointers 非懒加载符号表
与懒加载符号表相反,这些符号也是来自程序依赖的动态库,不同的是会在程序一加载就绑定好.
非懒加载符号表 -
Symbol Stubs 符号桩
与Lazy Symbol Pointers相对应的,使用外部符号会先在符号桩查找,然后对应到懒加载符号表.
符号桩
二.符号
1.符号
在load command中有两个和符号表相关,LC_SYMTAB和LC_DYSYMTAB,用来描述symbol table和dynamic symbol table的元数据,包括位置,长度等等
LC_SYMTAB
LC_DYSYMTAB
这是前面提到的符号表加载命令
struct symtab_command {
uint32_t cmd; /*固定为 LC_SYMTAB */
uint32_t cmdsize; /* 这个结构体的大小 */
uint32_t symoff; /* 符号表的偏移 */
uint32_t nsyms; /* 符号数量 */
uint32_t stroff; /* 字符串表的便宜 */
uint32_t strsize; /* 字符串表的大小 */
};
这是EXTERNAL_HEADERS/mach-o/nlist.h中符号的定义
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_type字段是一个8位十六进制复合字段,其中bit[0:1]表示是外部符号,bit[5:8]表调试符号,bit[4:5]表示私有 external 符号,bit[1:4]是符号类型,有 N_UNDF 未定义、N_ABS 绝对地址、N_SECT 本地符号、N_PBUD 预绑定符号、N_INDR 同名符号几种类型.
具体定义如下
#define N_UNDF 0x0 // 未定义
#define N_ABS 0x2 // 绝对地址
#define N_SECT 0xe // 本地符号
#define N_PBUD 0xc // 预定义符号
#define N_INDR 0xa // 同名符号
#define N_STAB 0xe0 // 调试符号
#define N_PEXT 0x10 // 私有 external 符号
#define N_TYPE 0x0e // 类型位的掩码
#define N_EXT 0x01 // external 符号
2.符号的类型
分别使用命令和MachOView查看符号表
objdump --macho --syms main.o
输出
main.o:
SYMBOL TABLE:
0000000000000000 l F __TEXT,__text ltmp0
00000000000000a0 l O __DATA,__objc_classrefs _OBJC_CLASSLIST_REFERENCES_$_
00000000000000d0 l O __DATA,__objc_selrefs _OBJC_SELECTOR_REFERENCES_
00000000000000a8 l O __TEXT,__cstring l_.str
00000000000000d8 l O __DATA,__cfstring l__unnamed_cfstring_
00000000000000a0 l O __DATA,__objc_classrefs ltmp1
00000000000000a8 l O __TEXT,__cstring ltmp2
00000000000000bc l O __TEXT,__objc_methname ltmp3
00000000000000bc l O __TEXT,__objc_methname l_OBJC_METH_VAR_NAME_
00000000000000d0 l O __DATA,__objc_selrefs ltmp4
00000000000000b1 l O __TEXT,__cstring l_.str.1
00000000000000d8 l O __DATA,__cfstring ltmp5
00000000000000f8 l O __DATA,__objc_imageinfo ltmp6
0000000000000100 l O __LD,__compact_unwind ltmp7
0000000000000000 g F __TEXT,__text _main
0000000000000000 *UND* _NSLog
0000000000000000 *UND* _OBJC_CLASS_$_NSString
0000000000000000 *UND* ___CFConstantStringClassReference
0000000000000000 *UND* _objc_alloc
0000000000000000 *UND* _objc_autoreleasePoolPop
0000000000000000 *UND* _objc_autoreleasePoolPush
0000000000000000 *UND* _objc_msgSend
其中l表示本地符号,g表示全局符号,下面几个没有l或者的g的,这个命令无法分类,需要其他命令
其中根据功能又分为O(data),F(文件),f(file),d(debug),UDN(未定义)等.
土黄色的是全局符号
绿色的是未定义符号/外部符号
- 查看外部符号
objdump --macho --indirect-symbols main.o
输出是空的,因为下面那几个都是动态链接库里的,而main.o只进行了静态链接
objdump --macho --indirect-symbols main
查看可执行文件main的,就有输出了
main:
Indirect symbols for (__TEXT,__stubs) 5 entries
address index name
0x0000000100003f54 2 _NSLog
0x0000000100003f60 5 _objc_alloc
0x0000000100003f6c 6 _objc_autoreleasePoolPop
0x0000000100003f78 7 _objc_autoreleasePoolPush
0x0000000100003f84 8 _objc_msgSend
Indirect symbols for (__DATA_CONST,__got) 5 entries
address index name
0x0000000100004000 2 _NSLog
0x0000000100004008 5 _objc_alloc
0x0000000100004010 6 _objc_autoreleasePoolPop
0x0000000100004018 7 _objc_autoreleasePoolPush
0x0000000100004020 8 _objc_msgSend
- 查看导出符号
导出不是动词,导出符号就是用来提供给外部访问的符号
objdump --macho --exports-trie main
输出
main:
Exports trie:
0x100000000 __mh_execute_header
0x100003EB4 _main
三.通用二进制
1.Fat Binary
Fat Binary本身是一个mach-o,不过它还包含了多个mach-o.
用MachOView打开一个framework,比如这个Bugly,看起来是这样的
首先它叫做Fat Binary,胖二进制,也就是通用二进制
Fat Header
Fat Header的定义在EXTERNAL_HEADERS/mach-o/fat.h中
#define FAT_MAGIC 0xcafebabe
#define FAT_CIGAM 0xbebafeca /* NXSwapLong(FAT_MAGIC) */
struct fat_header {
uint32_t magic; /* FAT_MAGIC */
uint32_t nfat_arch; /* number of structs that follow */
};
struct fat_arch {
cpu_type_t cputype; /* cpu specifier (int) */
cpu_subtype_t cpusubtype; /* machine specifier (int) */
uint32_t offset; /* file offset to this object file */
uint32_t size; /* size of this object file */
uint32_t align; /* alignment as a power of 2 */
};
magic是FAT_MAGIC或者FAT_CIGAM,nfat_arch表示有几个fat_arch,从上面那张图可以看到,这个header有五个fat_arch,表示了对五种架构的支持,对应了五个Static Library,也就是静态库.
静态库
每个静态库都对应Start,Symtab Header(符号表描述),symbol table(符号表),string table(字符串表),object header(目标文件描述)和一个目标文件.o;
.o.o也是一整个mach-o,magic是MH_MAGIC,并且文件类型是目标文件.
2.拆分和组合
通用二进制文件一般用于库的概念,让一个库能够兼容多个硬件架构,对于开发和测试来说是很方便的,但是实际运行的时候,对于一种硬件架构的设备,就没必要在硬盘里存储其他架构的文件.如果是上传商店,苹果会为我们做这件事.
- 查看
lipo -info Bugly
输出
Architectures in the fat file: Bugly are: armv7 armv7s i386 x86_64 arm64
- 提取
lipo -output 取个名字 -extract 架构 文件
lipo -output Bugly-x86_64 -extract x86_64 Bugly
提取x86-64
- 移除
lipo -output Bugly-noarmv7 -remove armv7 Bugly
输出一个Bugly-noarmv7文件
查看
lipo -info Bugly-noarmv7
Architectures in the fat file: Bugly-noarmv7 are: armv7s i386 x86_64 arm64
- 合并
lipo -output NewBugly -create Bugly-noarmv7 Bugly-armv7
lipo -info NewBugly
Architectures in the fat file: NewBugly are: armv7s i386 armv7 x86_64 arm64
上面的命令都可以使用相对路径
上面这些命令都不会影响源文件,必须添加-output.