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文件
下载地址

一个可执行文件main

下载源码
从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  */
    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,看起来是这样的

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.

你可能感兴趣的:(iOS 编译与链接二:编译的产物Mach-O)