iOS技术积累

不管生活有多不容易,都要守住自己的那一份优雅。

深入理解Mach-O文件格式_2.md

接前文

距离上篇文章已经过去 6 年之久了,最近又在学习汇编,链接,动态链接,就顺便重新复习了下 Mach-O 格式,这次把之前的坑填上。

前面讲到了 Mach-O 文件格式只不过是一种设计到的二进制结构,今天我们就来剖析下他。
Mach-O 除了区分最重要的 32/64位机器和cpu类型,最重要的就是文件类型了,Mach-O 支持重定向目标文件,可执行文件,动态库文件,Debug信息文件(没错 DWAF 也是Mach-O 类型)及其他(PS: 读者没见过也不做分析了,大同小异,理解这个也能理解其他)

#define MH_OBJECT   0x1     /* 可重定位目标文件*/
#define MH_EXECUTE  0x2     /* 可执行文件 */
#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     /* 动态了解库 */
#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     /* 仅包含调试信息的 DWAF 实际上是从 其他Mach-O 剥离 debug Section */
#define MH_KEXT_BUNDLE  0xb     /* x86_64 kexts */
#define MH_FILESET  0xc     
/* a file composed of other Mach-Os to be run in the same userspace sharing a single linkedit. */
#define MH_GPU_EXECUTE  0xd     /* gpu program */
#define MH_GPU_DYLIB    0xe     /* gpu support functions */

名词科普

  1. 源码文件-> 编译-> 可重定位目标文件,换句话说 静态库仅经过编译,没有经过链接
  2. 一堆目标文件->压缩归档->静态库,换句话说,静态库就是一堆目标文件的压缩包 可以使用 ar 命令来转
  3. 目标文件/静态库 + 依赖的动态库,静态库 ->静态链接(Rebase/binding)-> 动态库/可执行文件, 经过 ld 静态链接产生动态库或者可执行文件,一个不准确的比喻,动态库就是没有 _main 函数的可执行文件。
    # 静态链接生成动态库
    ld 14-foo1.o 14-foo2.o -lSystem -L `xcrun --show-sdk-path -sdk macosx`/usr/lib -dylib -o lib14-foo-dynamic.dylib 
    # 静态链接 动态库 生成可执行文件
    ld -L. 14-main.o -l14-foo-dynamic -lSystem -L `xcrun --show-sdk-path -sdk macosx`/usr/lib -o 14-main-dynamic
    # 静态链接 静态库 生成可执行文件
    ld -L. 14-main.o -l14-foo-static -lSystem -L `xcrun --show-sdk-path -sdk macosx`/usr/lib -o 14-main-static 
  4. 动态库/可执行文件->dyld(rebase/bingding/loader) -> 被加载到内存中启动。
    Segment 和 sction 的区别,一般 Linux ELF 不怎么区分,但 Mach-O 区分的更细一点, Segment 是把一堆具有 相同权限 的 section 组合起来。section 才是真正的数据内容。
    一般 C/CXX 的 sectio 相对简单,只有常见的 data, text ,bss, 等 section ,Apple 支持 Swift 和 OC 语言,因此这里面有挺多的跟语言相关的 Section,PS: Linux 平台上的 swift 也有很多对应的 section,毕竟 Swift 和 OC 语言需要编译器,运行时共同配合才能工作,而 C 语言基本没有RT。
  5. 以前的Mach-O 粗暴的吧不太架构的二进制粘在一起,就叫做Fat-Binary,前文提到过,可以使用 file 命令查看,使用 lipo 合并或者剔除。
  6. 现在 Apple Siclion 芯片也是arm 64的 cpu结构不一样,但是指令集一样,都是Arm的,fat结构不能再合并了,因此现在使用 xcframeowork来解决相同指令集,不太CPU的二进制文件,本质上就是个特殊格式的文件夹。

Mach-O 文件主要内容

Mach-O 文件格式支持多种不同的 CPU 架构(例如 x86、ARM、PPC 等),以及不同的文件类型(例如可执行文件、动态链接库、静态库等)。Mach-O 文件包含了程序代码、数据、符号表、重定位信息等,使得操作系统能够正确加载和执行程序。

Mach-O 文件格式具有以下几个主要组成部分:

  1. Header(文件头): 文件头包含了文件的一些基本信息,如文件类型、CPU 架构、加载偏移等。

  2. Load Commands(加载命令): 这些命令指示了如何加载和链接文件。它们包括加载可执行段、动态链接信息、符号表等。

  3. Segments 和 Sections(段和节): 段描述了文件的逻辑组织,节包含了实际的代码和数据。不同的段和节有不同的用途,如代码段、数据段、符号表节等。

  4. Symbol Table(符号表): 符号表包含了程序中定义和引用的符号(如函数、变量)信息,帮助链接器解析模块间的引用关系。如果被 Strip 掉就消失了本地符号,无法被外部访问(因为不知道符号名可函数起始指令地址)

  5. Dynamic Symbol Table(动态符号表): 一般仅存在 动态库/可执行文件中,用于给 dyld binding 外部符号使用。

  6. String Table(字符表) 以CString格式存储的字符,因此给的一个起始位置,读到\0就是一个完整的字符,一般用于 符号表中的符号名。

  7. Relocation Information(重定位信息): 一般仅存在可重定位目标文件* 重定位信息指示链接器如何修改程序代码和数据,以便正确地在内存中加载。所有需要重定位信息的 section 都会产生一个对应的重定向的 section。

  8. Dynamic Loader Information(动态链接信息): 用于支持动态链接的信息,如共享库的引用和导出。

9 Function Starts: 一般仅存在 动态库/可执行文件中 。 __TEXT,__text 或者 `__text等地方的机器码是按顺序放置,因此需要定位哪一行地址是哪一个函数的首地址 这些信息对于调试、符号解析,动态分析,性能分析以及符号解析等方面都非常有用。


Mach-O format Overview


PS 左边可重定位文件,右边,动态库/可执行文件
代码定义为

struct load_command {
  uint32_t cmd;    
  uint32_t cmdsize;  
};

#define LC_REQ_DYLD 0x80000000
在MacOS X 10.1之后,当添加新的load命令时,可以或上这个掩码,如果是dyld的看到这样的命令,但是不支持,将报错 "unknown load command required for execution"错误,并且拒绝启动,但如果不加这个掩码的链接命令,将会被忽略
// 将文件中的段(32位)映射到进程地址空间中
#define  LC_SEGMENT  0x1
// 将文件中的段(64位)映射到进程地址空间中
#define  LC_SEGMENT_64  0x19
// 链接编辑stab符号表信息
#define  LC_SYMTAB  0x2
// 链接编辑gdb符号表信息 (已经被废弃了)
#define  LC_SYMSEG  0x3
// 开启一个Mach线程,不开辟栈
#define  LC_THREAD  0x4
// 开启一个UNIX线程
#define  LC_UNIXTHREAD  0x5
// 加载指定的固定VM共享库
#define  LC_LOADFVMLIB  0x6  
// 修复了VM共享库标识
#define  LC_IDFVMLIB  0x7
// 对象标识信息
#define  LC_IDENT  0x8
// 固定VM文件包含
#define LC_FVMFILE 0x9
// prepage命令
#define LC_PREPAGE      0xa
// 动态链接编辑符号表信息 
#define  LC_DYSYMTAB  0xb
// 加载动态链接的共享库
#define  LC_LOAD_DYLIB  0xc
// 动态链接共享库标识
#define  LC_ID_DYLIB  0xd
// 加载动态链接器
#define LC_LOAD_DYLINKER 0xe
// 动态链接器标识
#define LC_ID_DYLINKER  0xf
// 动态预绑定的模块
#define  LC_PREBOUND_DYLIB 0x10

// 下面的命令与链接共享库有关
// image routines
#define  LC_ROUTINES  0x11  /* image routines */
#define  LC_ROUTINES_64  0x1a
#define  LC_SUB_FRAMEWORK 0x12  /* sub framework */
#define  LC_SUB_UMBRELLA 0x13  /* sub umbrella */
#define  LC_SUB_CLIENT  0x14  /* sub client */
#define  LC_SUB_LIBRARY  0x15  /* sub library */
#define  LC_TWOLEVEL_HINTS 0x16  /* two-level namespace lookup hints */
#define  LC_PREBIND_CKSUM  0x17  /* prebind checksum */
// 加载允许丢失的动态链接共享库
#define  LC_LOAD_WEAK_DYLIB (0x18 | LC_REQ_DYLD)

// 唯一的UUID,表示当前二进制文件
#define LC_UUID    0x1b
// 进行本地代码签名
#define LC_CODE_SIGNATURE 0x1d
// 本地拆分段
#define LC_SEGMENT_SPLIT_INFO 0x1e 
// 加载并重新导出dylib
#define LC_REEXPORT_DYLIB (0x1f | LC_REQ_DYLD)
// 将dylib加载延迟至首次使用
#define  LC_LAZY_LOAD_DYLIB 0x20
// 加密的段信息
#define  LC_ENCRYPTION_INFO 0x21
#define  LC_ENCRYPTION_INFO_64 0x2C
// 压缩的dyld信息
#define  LC_DYLD_INFO   0x22
// 仅限压缩的dyld信息
#define  LC_DYLD_INFO_ONLY (0x22|LC_REQ_DYLD)
// 向上加载dylib
#define  LC_LOAD_UPWARD_DYLIB (0x23 | LC_REQ_DYLD)
// 为MacOSX最小操作系统版本构建
#define LC_VERSION_MIN_MACOSX 0x24
// 为iOS最小操作系统版本构建
#define LC_VERSION_MIN_IPHONEOS 0x25
// 为TVOS最小操作系统版本构建
#define LC_VERSION_MIN_TVOS 0x2F
// 为WATCHOS最小操作系统版本构建
#define LC_VERSION_MIN_WATCHOS 0x30
// 压缩的函数起始地址表
#define LC_FUNCTION_STARTS 0x26
// dyld要像环境变量一样处理的字符串
#define LC_DYLD_ENVIRONMENT 0x27
// 同LC_UNIXTHREAD
#define LC_MAIN (0x28|LC_REQ_DYLD)
// __text段中的非说明表
#define LC_DATA_IN_CODE 0x29
// 用于生成二进制文件的源代码版本
#define LC_SOURCE_VERSION 0x2A
// 从链接的dylibs复制的代码签名DR
#define LC_DYLIB_CODE_SIGN_DRS 0x2B
// MH_OBJECT文件中的链接器选项
#define LC_LINKER_OPTION 0x2D 
// MH_OBJECT文件中的优化提示
#define LC_LINKER_OPTIMIZATION_HINT 0x2E
// Mach-O文件中包含的任意数据
#define LC_NOTE 0x31
// 为平台最小操作系统版本构建
#define LC_BUILD_VERSION 0x32
// 与linkedit_data_command一起使用
#define LC_DYLD_EXPORTS_TRIE (0x33 | LC_REQ_DYLD)
#define LC_DYLD_CHAINED_FIXUPS (0x34 | LC_REQ_DYLD)
// 与fileset_entry_command一起使用
#define LC_FILESET_ENTRY (0x35 | LC_REQ_DYLD)

通过 Type 确定类型后可以再讲loadCommand强砖为对应的类型,进行解析

Load commands 里面都有什么?


load Command是主要描述各个 数据 Section 的元信息,比如加载大小,权限,文件偏移等。
在可重定位目标文件中,因为没有加载运行能力,因此没有细分权限,所以 所有的 Sction 都合在一个 Segement 里面。
这里仅介绍非 bss/data/text Section的信息,其他的由后面各 Section 不分一起介绍。

通用描述信息

LC_BUILDE_VERSION

无对应 Section, 直接表示构建版本信息,

LC_SOURCE_VRESION

无对应 Section, 直接表示源代码版本

LC_VERSION_MIN_IPHONEOS/LC_VERSION_MIN_MACOSX/LC_VERSION_MIN_TVOS/LC_VERSION_MIN_WATCHOS

无对应 Section,在展示最低支持版本

LC_MAIN / LC_UNIXTHREAD

无对应 Section,LC_MAIN 加载命令在 Mach-O 可执行文件中记录了程序的主函数(或启动代码)所在的偏移量或虚拟地址。当操作系统加载程序到内存并启动它时,它会从主入口点开始执行程序的代码。
c/C++ 常见是 main 函数,非这两个语言可以是自定义入口

LC_CODE_SIGNATURE 签名

Mach-O 包含的数字签名,如果此签名与代码不匹配,则进程会被强制关闭. 重签名主要也是把签名信息替换掉这部分。

struct linkedit_data_command {
    uint32_t    cmd;        /* LC_CODE_SIGNATURE, LC_SEGMENT_SPLIT_INFO,
                   LC_FUNCTION_STARTS, LC_DATA_IN_CODE,
                   LC_DYLIB_CODE_SIGN_DRS,
                   LC_LINKER_OPTIMIZATION_HINT,
                   LC_DYLD_EXPORTS_TRIE, or
                   LC_DYLD_CHAINED_FIXUPS. */
    uint32_t    cmdsize;    /* sizeof(struct linkedit_data_command) */
    uint32_t    dataoff;    /* file offset of data in __LINKEDIT segment */
    uint32_t    datasize;   /* file size of data in __LINKEDIT segment  */
};

LC_ENCRYPTION_INFO/LC_ENCRYPTION_INFO_64 加密信息

苹果用来做 DRM(,Digital Rights Management,即数字版权保护)
一些逆向或者黑客大佬可能感兴趣:
Fairplay DRM与混淆实现的研究

/*
 * The encryption_info_command contains the file offset and size of an
 * of an encrypted segment.
 */
struct encryption_info_command {
   uint32_t cmd;        /* LC_ENCRYPTION_INFO */
   uint32_t cmdsize;    /* sizeof(struct encryption_info_command) */
   uint32_t cryptoff;   /* file offset of encrypted range */
   uint32_t cryptsize;  /* file size of encrypted range */
   uint32_t cryptid;    /* which enryption system,
                   0 means not-encrypted yet */
};

/*
 * The encryption_info_command_64 contains the file offset and size of an
 * of an encrypted segment (for use in x86_64 targets).
 */
struct encryption_info_command_64 {
   uint32_t cmd;        /* LC_ENCRYPTION_INFO_64 */
   uint32_t cmdsize;    /* sizeof(struct encryption_info_command_64) */
   uint32_t cryptoff;   /* file offset of encrypted range */
   uint32_t cryptsize;  /* file size of encrypted range */
   uint32_t cryptid;    /* which enryption system,
                   0 means not-encrypted yet */
   uint32_t pad;        /* padding to make this structs size a multiple
                   of 8 bytes */
};

LC_DYLIB_CODE_SIGN_DRS

不知道啥。

LC_LOAD_DYLINKER/ LC_ID_DYLINKER

标示动态链接加载库的路径,理论上你如果能自行实现一个,并且改掉这个 ldcommand 可以转交到自己的动态链接加载库,一般仅存在 动态库或者可执行文件中。
而 LC_ID_DYLINKER 标示自己就是一个动态连接器

LC_UUID

image 文件 UUID,用于标识。

依赖信息

LC_LOAD_DYLIB

LC_ID_DYLIB

LC_LOAD_WEAK_DYLIB

LC_REEXPORT_DYLIB

这几个命令用来加载被依赖的动态库,几乎所有的程序都有依赖,一般会有多条 LC_LOAD_DYLIB 命令,每条命令指定一个要加载的动态库。此命令的值结构定义如下:

其中 LC_ID_DYLIB 标示自己的名字。
LC_LOAD_WEAK_DYLIB 标示弱依赖,无需启动就加载
LC_REEXPORT_DYLIB 标示加载重新导出的动态库。

Otool -L someBinary 就查看一个二进制所依赖的所有二进制。

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; // 动态库结构体
};

struct dylib {
    union lc_str  name;      // 动态库所在路径字符串的起始位置(相对与当前命令起始的偏移字节)
    uint32_t timestamp;      // 库编译时的时间戳
    uint32_t current_version;   // 动态库版本号
    uint32_t compatibility_version;  // 兼容版本号
};

LC_REEXPORT_DYLIB

笔者没有见过,以下是 ChatGPT的答案
LC_REEXPORT_DYLIB 是 Mach-O 文件格式中的一种加载命令(Load Command),用于描述一个共享库(动态链接库)重新导出另一个共享库的符号的情况。在动态链接中,重新导出允许一个共享库将其依赖的其他共享库中的符号重新暴露出来,以便其他程序可以通过该共享库访问这些符号,而不需要直接链接到原始的依赖库。

具体来说,LC_REEXPORT_DYLIB 加载命令包含以下信息:

  1. 动态库标识(Dynamic Library Identifier): 指定被重新导出的共享库的标识符(通常是库的路径或名称)。

  2. 重新导出符号列表(Reexported Symbol List): 列出了需要从被重新导出的共享库中重新导出的符号列表。这些符号可以在当前库中被其他程序访问。

使用 LC_REEXPORT_DYLIB 加载命令可以在一个中间库中创建一个符号的桥接,使得其他程序可以通过这个中间库来访问某些符号,而不需要直接链接到原始的共享库。这在解决版本问题、避免符号冲突等方面都非常有用。

LC_RPATH

此命令用来进行 @rpath 外部动态库的路径

struct rpath_command {
    uint32_t   cmd;    /* LC_RPATH */
    uint32_t   cmdsize;  /* includes string */
    union lc_str path;    
};

LC_DYLD_ENVIRONMENT

数据结构同 LC_ID_DYLINKER/LC_LOAD_DYLINKER ,传递给 dyld 一些环境变量控制一些内容。

LC_LOAD_UPWARD_DYLIB

LC_LOAD_UPWARD_DYLIB 是 Mach-O 文件中的加载命令之一,用于指示要加载的上游动态库。 很类似 LC_LOAD_DYLIB, 上游动态库是指在当前可执行文件之前加载的动态库,而不是像通常的动态库那样在之后加载。这种方式可以在解决依赖性和加载顺序方面提供更多的灵活性。

做逆向的经常用到,可以插入hook逻辑
iOS 逆向入门 - 动态库注入原理
Dylib注入&劫持总结
OS X平台的Dylib劫持技术(上)
dyld详解

LC_LAZY_LOAD_DYLIB

据说可以延迟加载动态库到首次使用。没有额外的信息了。 已经废弃了

LC_DYLD_INFO/

LC_LOADFVMLIB/LC_IDFVMLIB

二进制文件类型为 MH_FVMLIB 的文件可能存在这几个加载命令, PS: 已经废弃了
其中:

  1. LC_IDFVMLIB 表名自己的名字
  2. LC_LOADFVMLIB 依赖别人的信息
/*
 * 固定虚拟内存共享库通过两个要素进行标识。目标路径名(在执行时找到的库的名称)和次版本号。头部加载的地址在header_addr中。
 * (这是过时的,并不再支持。)
 */
struct fvmlib {
    union lc_str name;        /* 库的目标路径名 */
    uint32_t minor_version;    /* 库的次版本号 */
    uint32_t header_addr;      /* 库的头部地址 */
};

/*
 * 固定虚拟共享库(文件类型为MH_FVMLIB在Mach头部中)
 * 包含一个fvmlib_command(cmd == LC_IDFVMLIB)来标识库。
 * 使用固定虚拟共享库的对象还包含每个使用的库的fvmlib_command(cmd == LC_LOADFVMLIB)。
 * (这是过时的,并不再支持。)
 */
struct fvmlib_command {
    uint32_t cmd;             /* LC_IDFVMLIB或LC_LOADFVMLIB */
    uint32_t cmdsize;         /* 包括路径名字符串 */
    struct fvmlib fvmlib;     /* 库的标识信息 */
};

链接信息

LC_LINKER_OPTIMIZATION_HINT

C_LINKER_OPTIMIZATION_HINT 是 Mach-O 文件格式中的一种加载命令(Load Command),用于向链接器提供有关优化建议的信息。这个加载命令在 Mach-O 文件中记录了一些链接器优化的提示,以帮助链接器进行更好的优化决策。

一般存在可重定位目标文件中。

LC_LINKER_OPTION Auto_Linking 自动链接

仅存在可重定位目标文件,当目标文件依赖其他Framework,其他人员一般需要指定我们依赖什么framework,这对二进制分发很繁琐,因此 ld 更新后自动连接帮我们省去这步骤,
比如

  1. 某个源文件声明依赖了 #import <AppKit/AppKit.h>
  2. link 时不指定 -framework AppKit
  3. 编译生成的 .o 的 LC_LINKER_OPTION 中带有 -framework AppKit

或者 enable Clang Module 后 可以自动依赖
比如 /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/usr/include/module.moduleMap

module zlib [system] [extern_c] {
 header "zlib.h"
 export *
 link "z"
}

参考自如下文章
iOS 上的自动链接( Auto Linking )
深入 iOS 静态链接器(一)— ld64

具体来说,LC_PREBOUND_DYLIB

拒 ChatGPT 来说
具体来说,LC_PREBOUND_DYLIB 加载命令包含了预绑定过的动态库的信息,其中的数据结构记录了与该程序预绑定的动态库的路径名、已绑定的模块信息等。预绑定的目的是在程序启动时减少符号解析和重定位的开销,因为程序已经在编译和链接阶段与动态库进行了绑定。

这个加载命令的存在可以帮助系统在加载程序时更快地完成符号解析和链接步骤,从而加速程序的加载过程。需要注意的是,预绑定是一种早期的优化技术,在现代操作系统中可能已经被更高级的优化替代或不再推荐使用


/*
 * 对于预绑定到其动态库的程序(文件类型为MH_EXECUTE),每个被静态链接器用于预绑定的库都有一个这样的结构。
 * 它包含一个位向量,表示库中的模块。位指示哪些模块已经绑定(1),哪些尚未绑定(0)。模块0的位是第一个字节的低位。
 * 因此,第N个模块的位为:
 * (linked_modules[N/8] >> N%8) & 1
 */
struct prebound_dylib_command {
    uint32_t cmd;               /* LC_PREBOUND_DYLIB */
    uint32_t cmdsize;           /* 包括字符串 */
    union lc_str name;          /* 库的路径名 */
    uint32_t nmodules;          /* 库中的模块数 */
    union lc_str linked_modules; /* 链接模块的位向量 */
};

LC_FUNCTION_STARTS 函数分割

Function Starts 段信息是编译器和调试工具用于确定可执行程序中函数的起始位置的一种元数据。它在程序的可执行文件中存储了每个函数的起始地址。这些信息对于调试、性能分析以及符号解析异常处理等方面都非常有用。
其中这个段里面直接存储了 uleb128 编码正数,用于标示每个函数的起始地址,关于 uleb128 可以参考
LEB128格式的说明.
简单来讲,这里面的每个数据代表了与前一个数据的偏移长度,类似一种前缀和数组的技巧,可以极小占用空间来表示所有函数的起始位置。


杂七杂八

LC_NOTE

可以理解为可变数据端,自己放一些自己想理解的信息

/*
 * LC_NOTE commands describe a region of arbitrary data included in a Mach-O
 * file.  Its initial use is to record extra data in MH_CORE files.
 */
struct note_command {
    uint32_t    cmd;        /* LC_NOTE */
    uint32_t    cmdsize;    /* sizeof(struct note_command) */
    char    data_owner[16]; /* owner name for this LC_NOTE */
    uint64_t    offset;     /* file offset of this data */
    uint64_t    size;       /* length of data region */
};

LC_DATA_IN_CODE

该命令使用一个 struct linkedit_data_command 指向一个 data_in_code_entry 数组
data_in_code_entry 数组中的每一个元素,用于描述代码段中一个存储数据的区域
在 loader.h 中用于描述 LC_DATA_IN_CODE 命令的数据结构,如下所示:

struct linkedit_data_command {
    uint32_t    cmd;    
    uint32_t    cmdsize;    /* sizeof(struct linkedit_data_command) */
    uint32_t    dataoff;    /* file offset of data in __LINKEDIT segment */
    uint32_t    datasize;   /* file size of data in __LINKEDIT segment  */
};

struct data_in_code_entry {
    uint32_t    offset;     /* from mach_header to start of data range*/
    uint16_t    length;     /* number of bytes in data range */
    uint16_t    kind;       /* a DICE_KIND_* value  */
};
#define DICE_KIND_DATA              0x0001
#define DICE_KIND_JUMP_TABLE8       0x0002
#define DICE_KIND_JUMP_TABLE16      0x0003
#define DICE_KIND_JUMP_TABLE32      0x0004
#define DICE_KIND_ABS_JUMP_TABLE32  0x0005

一般程序可能为了把一些立即初始化的数据放在代码段里面,便于访问,比如c数组的初始值
不过我暂时没见过这样的二进制

int main() {
    int array[] = {10, 20, 30, 40, 50};
    int value = 30;
    int result = findValue(value, array, sizeof(array) / sizeof(int));
    if (result != -1) {
        printf("Value %d found at index %d\n", value, result);
    } else {
        printf("Value %d not found\n", value);
    }
    return 0;
}

LC_FILESET_ENTRY

有些Mach-O 文件可能是由多个文件组成的,这个命令描述了一堆文件集合。

/*
 * LC_FILESET_ENTRY 命令描述了作为文件集一部分的组成 Mach-O 文件。在某个实现中,条目是具有独立 mach 头文件和可重新定位的文本和数据段的 dylibs。每个条目还由其自己的 mach 头文件进一步描述。
 */
struct fileset_entry_command {
    uint32_t cmd;        /* LC_FILESET_ENTRY */
    uint32_t cmdsize;    /* 包括 entry_id 字符串的大小 */
    uint64_t vmaddr;     /* 条目的内存地址 */
    uint64_t fileoff;    /* 条目的文件偏移 */
    union lc_str entry_id;   /* 包含的条目 ID */
    uint32_t reserved;   /* 保留字段 */
};

待详细了解

LC_SUB_FRAMEWORK

代码定义: 猜测类似 SubPod 这种结构吧。不常见,不细究,
看这个参考吧 iOS之深入解析UmbrellaFramework的封装与应用

动态链接的共享库可能是 公开框架的子框架。如果是,则使用 “-umbrella umbrella_name”链接,其中,“umbrella_name”为空开框架的名称。子框架只能通过其总括框架或属于同一总括框架的其他子框架进行链接。否则,静态链接编辑器会产生一个错误,并声明要针对umbrella框架进行链接。子框架的伞形框架的名称记录在以下结构中。
struct sub_framework_command {
    uint32_t    cmd;        /* LC_SUB_FRAMEWORK */
    uint32_t    cmdsize;    /* includes umbrella string */
    union lc_str    umbrella;   /* the umbrella framework name */
};

LC_SUB_UMBRELLA

配合 LC_SUB_FRAMEWORK ,这个是看注释是指定公开头文件的。

LC_SUB_CLIENT/LC_SUB_LIBRARY

看注释和 LC_SUB_FRAMEWORK 很像,从来没见过,将来有机会再说。

LC_ROUTINES/LC_ROUTINES_64

看这个参考吧
iOS类加载流程(一):类加载流程的触发
逆向,插入一个 LC_ROUTINES执行些额外逻辑

例程(Routines ) 命令包含动态共享库初始化例程的地址,以及定义该例程的模块的模块表索引。在任意模块被使用前,动态链接器将**完全绑定**定义初始化例程的模块,然后调用它。
这个要早于 constructor 函数
 The routines command contains the address of the dynamic shared library 
 initialization routine and an index into the module table for the module
 that defines the routine.  Before any modules are used from the library the
 dynamic linker fully binds the module that defines the initialization routine
 and then calls it.  This gets called before any module initialization
 routines (used for C++ static constructors) in the library.
struct routines_command { /* for 32-bit architectures */
    uint32_t    cmd;        /* LC_ROUTINES */
    uint32_t    cmdsize;    /* total size of this command */
    uint32_t    init_address;   /* address of initialization routine */
    uint32_t    init_module;    /* index into the module table that */
                        /*  the init routine is defined in */
    uint32_t    reserved1;
    uint32_t    reserved2;
    uint32_t    reserved3;
    uint32_t    reserved4;
    uint32_t    reserved5;
    uint32_t    reserved6;
};

/*
 * The 64-bit routines command.  Same use as above.
 */
struct routines_command_64 { /* for 64-bit architectures */
    uint32_t    cmd;        /* LC_ROUTINES_64 */
    uint32_t    cmdsize;    /* total size of this command */
    uint64_t    init_address;   /* address of initialization routine */
    uint64_t    init_module;    /* index into the module table that */
                    /*  the init routine is defined in */
    uint64_t    reserved1;
    uint64_t    reserved2;
    uint64_t    reserved3;
    uint64_t    reserved4;
    uint64_t    reserved5;
    uint64_t    reserved6;
};

LC_THREAD

看注释是跟特定机器有关的。

LC_TWOLEVEL_HINTS

不认识。

prebound

不认识。

LC_SEGMENT_SPLIT_INFO

不认识


已废弃

  1. LC_IDENT 注释写的是 对象标识信息,不知道啥意思。

私有

  1. LC_FVMFILE 内核使用,引用一个加载(mmap)到内存中的文件的虚拟内存首地址。不对外公开
  2. LC_PREPAGE 内核使用,

跟链接与动态链接器相关的 load Commands

String Table


代码中出现的符号名,比如常量名,常量字符串,函数名都可能出现在这里,以 C String 风格拼接以\0 为结束,因此只需要给出一个起始位置,按需遍历到结束符既是一个字符。
PS: 注意 OC 和 Swift 语言中很多数据,类名,方法名/Selector 不存在这里,放在单独的 section 里面

Symbol Table LC_SYMTAB

符号是什么?

上古时期开发程序的时候,可能是一个文件,大家都手写机器码,数据和指令都是二进制表示,想读取某个数据的区域,或者跳转到某个函数的入口都是绝对地址,但是程序一旦修改,程序员就要手工修正(fixup)绝对地址, 之后出现了汇编助记符标签语法,那绝对地址的修正留给了汇编器。
再后来一个文件写不下了,大家要分工开发,因此会互相引用文件和变量,单独的汇编器只能处理一个文件,遇见不认识的标签但是可能再别的文件的标签,那暂时就预留位0 留个一个软件叫链接器做,链接器链接的时候发现互相之间的引用就帮助他们绑定(bind)到一起,当然没有的也会报错,那么这个标签需要知道它所在文件中的地址是什么,它的名字是什么,它的类型是什么,等等,逐步发展到现在得 Symbol。
因此每个二进制文件都有自己的符号表供链接器或者动态链接器使用。

在 Mach-O 中的数据结构如下

/*
关于符号表的数据结构,和一些基本常量定义在如下文件中
 * <nlist.h> and <stab.h>.
 */
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 */
};
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_list 常见字段

其中 n_types 占8位,代表4个数据

/*
 * The n_type field really contains four fields:
 *  unsigned char N_STAB:3,
 *            N_PEXT:1,
 *            N_TYPE:3,
 *            N_EXT:1;
 * which are used via the following masks.
 */
#define N_STAB  0xe0  /* 0b11100000 当这个type的高3位被设置了,这个代表是一个调试符号,具体是啥可以去 stab 看看 */
#define N_PEXT  0x10  /* 0b00010000 如果这个type的第5位 被设置了,代表这是一个私有外部符号,比如 static int i = 1;  */
#define N_TYPE  0x0e  /* mask for the type bits */
#define N_EXT   0x01  /* 当这个标记被标记了证明是外部符号,也可能是外部定义的,也可以是自己定义外部的。 */
  • N_STAB
    不解释了 看注释都是调试字段,整不明白。

  • N_PEXT
    __attribute__((visibility("hidden"))) int i = 1 当一个这样的变量存在时,在可重定位目标文件中,他是一个 Private extern symbol 私有外部符号,但是当静态链接过后,他就变成了 local symbol
    举例

    __attribute__((visibility("hidden")))
    int abcdefg = 100  ;
    int main(void) {
      printf("%d", abcdefg);
      return 0;
    }


    可以看到经过静态链接后,最后1位的extern已经被去掉了

  • N_EXT
    可以看到如下示例中 N_EXT字段都被设置了,但是不能表面自己是自己定义的还是别人定义的,还是要看其他信息。

    int extCanAccess = 13;
    extern int FromExtern;
    int main(void) {
      printf("%d, %d ", extCanAccess, FromExtern);
      return 0;
    }

  • N_TYPE

/*
 * Values for N_TYPE bits of the n_type field.
 */
#define N_UNDF  0x0     /* 0b0000 当整个字段为0 是代表 undefined, n_sect 位必须为0,证明没定义 */
#define N_ABS   0x2     /* 0b0010 绝对符号,, n_sect 必须为0 */
#define N_SECT  0xe     /* 0b1110 证明是本文件中定义的符号,n_section代表了第几个section  section 以1索引。最大2^8=255个 */
#define N_PBUD  0xc     /* 0b1100  在其他动态库中 prebound undefined (defined in a dylib) */
#define N_INDR  0xa     /* 0b1010 间接符号 */

#define NO_SECT     0   /* symbol is not in any section */
#define MAX_SECT    255 /* 1 thru 255 inclusive */

其中

  1. N_TYPE = N_UNDF 代表是外部引用,需要填充值
  2. 当N_TYPE = N_SECT 代表是本文件定义的。
  3. 当N_TYPE = N_INDR 间接符号时,证明这个符号和另外一个符号相同, 这时候 n_value 字段代表了 字符表里面的索引,当遇到第二个相同名的符号时,他们的属性值是一模一样的。

静态链接器链接时会根据 N_SECT 所引用的section来进行重定向。

n_list 中 n_desc 字段

  • 前8位

外部定义的符号分为 lazy 惰性 符号和 非惰性 non-Lazy 符号,非惰性符号就是静态连接器一定要链接的,比如来自静态库的符号,惰性符号一般来自其他动态库,只有 dyld 动态链接并加载的时候才填充,这写信息记录在 n_desc字段中,共 16为信息。
PS: 当对某个外部符号的引用全部是 lazy 的最终才是lazy的,否则是 non-lazy的。

#define REFERENCE_TYPE 0x7 这个数据有16为,但是只有最后三位代表了这些信息,计算的时候用这个掩码计算 暂时有6中值

#define REFERENCE_FLAG_UNDEFINED_NON_LAZY       0   // undefine non-lazy
#define REFERENCE_FLAG_UNDEFINED_LAZY           1 // undefine lazy 
#define REFERENCE_FLAG_DEFINED              2  // defined 
#define REFERENCE_FLAG_PRIVATE_DEFINED          3 // private define 
#define REFERENCE_FLAG_PRIVATE_UNDEFINED_NON_LAZY   4 // private undefine non-lazy
#define REFERENCE_FLAG_PRIVATE_UNDEFINED_LAZY       5 // private undefine lazy
  • 两级命名空间 MH_TWOLEVEL
    这是 ld64的特有功能,GNULD 没有
    当一个二进制启用两级符号时,(iOS动态库 基本都是MH_TWOLEVEL的)两个二进制库可以暴露相同的符号,但是静态连接器可以把库来源也作为命名空间,当使用两级符号绑定时,n_desc字段的前8位代表了 LC_LOAD_DYLD,LC_LOADWEAK_DYLD,LD_REEXPORT_DYLD,LD_LOAD_UPWARD_DYLIB,LC_LAZY_LOAD_DYLD 等加载指令的顺序
    注意,顺序从1开始。

当一个动态库是利用两级命名空间静态链接得来时,它的 undefine 符号存在三种情况

  1. 它自己定义的符号的的 n_desc 前八位 必须是 SELF_LIBRARY_ORDINAL=0 也就是说来自自己 (这个很奇怪,我不是特别理解,但是找到了这些的case)。
  2. 如果来自别人就要定义这个值,
  3. 如果自己是个特殊的插件动态库,比如单测工程,那么你依赖的符号其实来自可执行文件,那么设置为 EXECUTABLE_ORDINAL=0xff
    这是个新增功能,虽然可能新增至2010年,因此最多支持0~DYNAMIC_LOOKUP_ORDINAL(oxfe)
#define GET_LIBRARY_ORDINAL(n_desc) (((n_desc) >> 8) & 0xff)
#define SET_LIBRARY_ORDINAL(n_desc,ordinal) \
    (n_desc) = (((n_desc) & 0x00ff) | (((ordinal) & 0xff) << 8))
#define SELF_LIBRARY_ORDINAL 0x0
#define MAX_LIBRARY_ORDINAL 0xfd
#define DYNAMIC_LOOKUP_ORDINAL 0xfe
#define EXECUTABLE_ORDINAL 0xff

系统好多库都是这样的,比如 libsqlite3 这也是为什么大家可以同时链接自定义的sqlite3是coredata用的是系统的 libsqlite 而不出现符号冲突.


  • 其他8位 (当然有可能复用前8位)

仅出现在可重位目标文件中,目的高速静态连接器不要strip

#define N_NO_DEAD_STRIP 0x0020 // 0b 0000 0000 0010 0000
#define N_DESC_DISCARDED 0x0020 /* 已废弃*/

指示动态链接器允许这个符号不存在,地址填充为0
一般配合 静态连接器 参数 -undefine dynamic-look 指示导出的产物其实是不完全链接的,需要动态链接器填充,不过这玩意好像只有苹果的 ld 支持,GNU 不支持

#define N_WEAK_REF  0x0040    // 0b 0000 0000 0100 0000

这个比较特殊,这个说的是 把这个符号定义为弱符号,
当 static linker 发现多个相同的符号定义时,会根据 strong/weak 类型执行以下合并策略:

  1. 有多个 strong ,非法输入,链接报错。
  2. 有且仅有一个 strong 用这个。
  3. 有多个 weak,没有 strong 取第一个 weak。

可以用 __attribute__((weak)) 来更改符号强弱性,其实Swift语言中定义的 Codable 属性都是weak的,如果你自行实现了 那么系统的weak就会被忽略,而不是说 系统根据是否定义来进行生成代码。

#define N_WEAK_DEF  0x0080    // 0b 0000 0000 1000 0000
__attribute__((weak)) 
void test() {
}

同时这个标记还指示动态链接器查找的时候使用 flat namespace 规则
注意前面说的是静态链接器,这时候是动态链接器,是不同的阶段

#define N_REF_TO_WEAK   0x0080 /* reference to a weak symbol */

The N_ARM_THUMB_DEF bit of the n_desc field indicates that the symbol is a defintion of a Thumb function.

  • 其他常见标记
    指示这是一个thumb指令,具体没见过
    #define N_ARM_THUMB_DEF 0x0008 /* symbol is a Thumb function (ARM) */

说明这个符号是个 解析器函数指令,你必须调用后才能获取真正的函数地址,具体没见过

#define N_SYMBOL_RESOLVER  0x0100 

没见过

/*
 * symbol is pinned to the previous content.
 */
#define N_ALT_ENTRY 0x0200

非热区函数,可以排到section的最后面。

#define N_COLD_FUNC 0x0400

这个符号存在动态调用 静态链接期间不要 strip 掉

#define REFERENCED_DYNAMICALLY  0x0010

[toc]

LC_DYSMYTAB 动态符号表

符号表分类

这部分的数据结构是为了支持 动态链接器(dyld) 所设计的,也可以快速区分符号的类型
这个 load command 必须和 符号表 (LC_SYMTAB)部分与字符表部分一起工作,缺少就会报错。
如果这个动态符号表存在时,则真正的符号表数据必须是经过设计的。包含三部分

  1. 本地符号(比如 static 或者调试符号)并且按模块分组

  2. 定义的可被外部访问的公开符号,按模块分组,如果不是 lib 开头的,则按名称排序

  3. 引用的外部符号
    这三部分由一个符号表起始偏移和数量描述。数据结构如下

    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 */
    * The last two groups are used by the dynamic binding process to do the
      * binding (indirectly through the module table and the reference symbol table when this is a dynamically linked shared library file).
      */
     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 */
    }

    额外信息

    在 ld64 中除了这三个重要的信息,还有一些额外的信息

  4. TOC 表

  5. module 表

  6. 引用符号 表

  • 当文件是 动态库是(dynamiclly linked shared library)时,有前面三个表。
  • 当文件是 可执行文件,或者可重定位目标文件是 这三个表有特殊的含义
    • TOC 表被任务是定义的公开符号,等同于 iextdefsym table。
    • module 表,可执行文件与可重定位目标文件仅有一个module,因此没有额外的意义。
    • 引用符号 表,定义和引用的外部符号表,等同于 iextdefsym 和 iundefsym 加起来。

数据结构为

struct dysymtab_command {
    ... 前面已经出现的
    uint32_t tocoff;    /* file offset to table of contents */
    uint32_t ntoc;  /* number of entries in table of contents */

    为了支持动态绑定过程中的 module(整个目标文件)绑定,符号表必须能表达来自哪个文件创建的, 只有动态库有这个内容
    但是具体哪个动态库没有找到例子 ,这个指出了 dylib_module_64 数据架构在mach-O文件中的偏移和数量
    uint32_t modtaboff; /* file offset to module table */
    uint32_t nmodtab;   /* number of module table entries */
   为了支持动态绑定过程中的 module(整个目标文件)绑定,module 数据结构(dylib_module_64)定义了 模块引用内容
    uint32_t extrefsymoff;  /* offset to referenced symbol table */
    uint32_t nextrefsyms;   /* number of referenced symbol table entries */
}

搜了下 好像toc是在 powerPC 中使用,我没见过具体的文件有这个数据。
module和 extrefsymoff 没在任何系统库中见过也没啥用。


间接符号表 Dynamic Symbol Tab

  1. 间接符号表文件地址 (Dynamic Symbol Tab)
    在 CPU 的指令集中,以 Arm64 体系结构中 A 64 结构中,指令一般中有些是操作寄存器,有些是操作离立即数,有一些是操作pc相关的指针偏移。
    有一些是操作绝对地址,
    比如 ldr 绝对地址, str 绝对地址, adrp 后面的绝对地址,这种指令在编写代码时候的绝对地址在文件经过编译链接和加载时都一样
    为了有效支持这些绝对地址的引用,静态连接器和动态链接器都会配合使用间接跳转表和函数桩来进行实现,
    Dynamic Symbol Tab 就是一个这样的实现,indirectsymoff 指向了 Mach-O 文件中的偏移 其中每个条目 4个字节
    这里面有两类数据
  2. routine stub
  3. symbol pointers 供ld 或者 dyld 填充。 具体还分为 lazy 和 non-lazy

具体来说 routine stub 就是一段模板桩函数, 一般由三条指令组成比如大家调试的时候见到的

adrp x16, #138492  ; 其中这个立即数就是为了计算 got表或者 lazy
ldr x16, [x16, #1234l] #1234l ; 这个立即数位是这个地址按4k为大小的偏移的页,页内偏移
br x16 ; 跳转到具体某个地址

2.1 non-lazy就是存在__DATA,__got 里面的函数指针。 由 dyld 负责填充
2.2 lazy 就是存在 __DATA,__la_symbol_ptr,的函数指针,这里面的指针dyld启动的时候填充为__TEXT,__stub_helper 的值为 dyld_stub_binder(...)
然后再首次调用的时候 dyld_stub_binder 这个函数获取真正的函数地址,然后再填充回去,以后就是这个函数真正的值

例子某个动态库的依赖信息

struct dysymtab_command {
    ... 前面已经出现的
    其中 indirectsymoff 指出 Dynamic Symbol Tab的内容,
    nindirectsyms 指出有多少条目的,其中包含 `__TEXT,__stubs`, `__DATA,__got` 和 `__TEXT,__stub_helper` 的个数。
    其中 `__TEXT,__stubs`和 `__TEXT,__stub_helper` 是一一匹配的。
     `__DATA,__got` 和 `__TEXT,__stub_helper` 的起始位置保留在 section header  的 reverse1字段
    uint32_t indirectsymoff; /* file offset to the indirect symbol table */
    uint32_t nindirectsyms;  /* number of indirect symbol table entries */
}

下面是三份实例

  • __TEXT,__stubs 起始位置固定是 0

  • __DATA,__got

  • __DATA,__la_symbol_ptr

静态链接是怎么做的

静态链接主要做什么?

  1. 符号解析,目标文件中定义和引用符号,每个符号对应一个 全局变量,函数地址,或者静态变量(这里还是以C语言为例子,当然 OC/Swift 等Native 语言要自己设置自己的符号和加载信息,还需要ld和dyld来配合,不过概念逻辑一致。)。符号解析的目的是为了将一个符号定义与每个相同的符号引用绑定起来
  2. 重定位,编译器输出的目标文件中包含很多 Section。链接器将一一关联符号定义特定的内存位置,并修改所有对这些符号的引用,使他们执行这个内存地址
    一般所有的 section 只要有需要重定位的都有一个与之匹配的 重定位 section,放置
    重定位条目。

符号粗糙的分为三类

  1. 定义的全局符号
  2. 定义的本地符号
  3. 引用的全局符号

当然ld64还有前面提到的私有外部符号,但是最终又转换为定义的本地符号。

符号解析

这一步比较简单,就是把所有输入文件的符号引用与定义关联起来,但有一些简单的规则,

  1. 只能有一个强符号
  2. 如果有强弱之分,选择强的
  3. 如果多个弱的选择第一个

当然 ld64有一些私货 比如 推迟到运行时解析的 -undefined dynamic_loopup

注意静态链接阶段的符号解析有个很隐晦的规则,
链接参数中的输入顺序会对产物有影响,原因在于链接器不会循环搜索,因此如果定义在静态库中的目标文件没有被引用到,那么这些目标文件直接会被丢弃,不会使用,
一般可以把静态库丢在最后解析或者重复输入静态库参数解决

不过好像 ld64 没有这问题。
CMake 这种工具也可以自动解决。不纠结 Demo 了。

重定位

这里面分两种

GNULD

GNU ld 是传统的链接器工具,在这种工具中,GCC 的源码编译会得到汇编文件,as 汇编器在进行汇编时,任何代码section 和数据section中对
符号的引用,在汇编时都是不确认的,因此汇编器都会为这些不确定的地址生成一个可重定位条目 供链接器使用

  • 产出可执行文件

GUN ld是一种叫 section based 的链接器
这种链接器做的第一件事就是 section merge

  1. 把 一组输入文件的相同 section 进行合并,并且把合并后的运行时(链接地址)赋给输入模块中的 section
  2. 把输入模块中定义的 每一个 symbol 都计算好合并后的地址。
    这一步后 每个 symbol 都包含一个正确的地址。

第二步叫重定位
汇编文件中产生的重定位一般是类似下面的数据结构

struct rela_entr {
    long offset; // 定义 section 中需要重定向的偏移,比如 __text, 第0x128这个位置以前是一个访问了某个符号,现在链接后要填充上真正的地址
     int type; // 重定位指令的类型,可能跟机器指令相关,比如要支持 arm intel 指令中具体是那个指令编码怎么改,
     int symbol; // 执行了符号表的地址
     long addend; //偏移调整
}

举个伪代码的例子

假设ADDR(_) 是计算某个符号的运行时地址
for each secion s:
    for each rela_entr r:
        refptr = s+r.offset# 找到文件中需要重定向的位置, 比如 __text 中第 0x128偏移文件以前是符号a的地址,连接后肯定要变
        section_addr = ADDR(s) + r.offset # 计算这个w位置的运行时地址,
        ref_addr = ADDR(r.symbol) + r.addend -  section_addr # 计算这个位置需要填充上的真正地址,
        *preftr = ref_addr # 给这个地址写值,可能就是修改汇编指令了
  • 产出动态链接共享库

上面讲述的是确定符号之间的重定向关系,静态链接器除了可以产出可执行文件,还可以产出动态库,那么动态库怎么做?
众所周知,经过重定位的可执行文件中指令section中的指令访问引用(符号)的地址已经确定,通常在本地指令section 和 data section里面
但是动态库访问的外部符号(变量和函数)通常是在别的动态库里面,且代码 __TEXT, Section加载到内存中 仅具有 r-x 权限,那么怎么做比较合适? 这时候就要把地址有关的代码想办法设计成地址无关(PIC)的代码。怎么做?

链接器的设计者们发现通常一个可执行文件被加载到内存开始执行的时候,整个ELF文件是被完整的 mmap 进内存中的,那TEXT, DATA Section之间的位置就是一个偏移位置,并且在文件中和在内存中是完全相等的。
于是链接器就涉及了 GOT(全局偏移表)数据结构,放在DATA section 里面, 本文件中的TEXT 对外部符号的访问变成了对 DATA 中的 GOT 表偏移,因此也就不需要重定向了,同时在 动态库/可执行文件, 预留个绑定信息,由加载器加载的时候加载,并且读取这个信息,然后查找外部符号的真实地址,然后填充到 GOT (DATA section 拥有 rw-权限)于是就完成了动态外部符号访问。

不过除了这些直接程序执行就要访问的数据,链接器发现很多外部引用不一定需要立即访问,如果全部启动就要绑定,那么这个工作量一定非常大会拖慢程序的执行时间,于是又把外部符号设计成了 lazy 和 non-lazy 的 lazy的借用 PLT (过程链接表)初始的时候,GOT表中的地址指向 PLT 表特定偏移, PLT表里面包含一段模版代码,通过是把GOT 表中某个函数的地址传递过来,然后调用 动态链接加载器的符号查找函数,当首次访问 lazy 符号时,就会调用这个函数,查找完成后,GOT 表中的 lazy 访问地址被填充上真正的外部符号地址,之后就再也不会跳转回 PLT 表了。于是就完成了 延迟符号加载和绑定。

前文提到的 Mach-O 文件中的 lazy 符号绑定的原理是一样的,只不过 苹果分配了单独的section。

LLD ( The LLVM linker )

LLD 在Mach-O 文件结构体系中使用的是 Atom 模型,而不是 section merge 模型,这种模型以更细粒度的模型处理,并且可以很好的完成 dead code stipping , reordering functions for locality(没错二进制重排仅 LLD 这个链接器支持), C++ coalescing

  • Atom
    Atom 是不可分割的代码或数据块。
    通常具有如下属性:name, scope, content-type, alignment
    Atom 的引用关系具有如下属性: a kind, an optional offset, an optional addend, and an optional target atom.

Atom 模型的链接是一种图论的实现, Atom 代表一个 Node 引用关系代表一条 Edge。
举个例子: dead code stripping 就是删除无边引用的游离原子

当然除了标准 Atom 编译器也会创建一些 Atom,比如

  • c 字符串常量
  • 浮点数常量
  • drawf 信息

  • Atome 的类型有4种
  1. Defined Atom 代表一段代码或者数据
  2. Undefined Atom 引用占位符,,在链接期间,被合并或者替换为真正的Atom
  3. Shared Libary Atom 如果所需的符号不在其他静态库或者目标文件存在,则是这种类型, 也是一种占位符
  4. Absolute Atom 绝对定位,不需重定向,仅需 fix up

  • 文件模型

LLD 的文件模型仅支持三种 目标文件,静态库,动态库,这三种文件是 atom 和 引用边 的容器。
由于每种文件格式不一样,因此有特定的文件读取器,然后转化为统一模型。

  1. 目标文件是最基本的文件,lld读取这种文件,比较简单,直接读取所有的数据并构建 Atom 模型。
  2. 静态库是压缩文件,但是带有一个 TOC 表,默认情况下 lld不是直接全部解析,毕竟很大,而且不是所有文件都用,当解析目标文件的引用时,如果有引用关系则会通过TOC表读取所在的目标文件,然后解析这个目标文件,并且递归的完成这个动作,可以理解为懒加载的遍历。
  3. 动态库,动态库的不会添加 Atom 到输出集合中,而是供 ld64 检查剩余的 undefine atom 是否在这些库里面,并提供依赖源,比如 rpath,LD_LOAD_DYLIB 信息(当然ld64还支持-undefined dynamic_lookup 忽略这种符号),然后使用 Shared Libary Atom 替换 Undefined Atom。

  • 链接步骤
  1. 命令行参数解析
  2. 解析文件
  3. resolve
  4. passes/Optimization
  5. 生成文件

PS:由于 LLVM 支持多平台,因此 resolve 和 pass 阶段是一个抽象概念,与平台无关逻辑,解析文件和生成文件利用抽象工厂实现的,特定平台有对应的实现

最重要的是 resolve 阶段,这个阶段 维护一个全局的链接状态,还有一个解析后的 符号表,。使用这些数据结构,链接器遍历所有输入文件中的所有Atom。 对于每个 Atom ,检查该 Atom 是否已全局或者hidden的作用域。如果是,则将该原子添加到符号表里面。如果该表中已经存在匹配的 Atom ,则意味着当前原子与已经存在的 Atom 合并,否则就是 Duplicate Symbol。
当解析器处理完所有初始输入文件Atom后,将进行扫描以查看图中是否存在任何 Undefined Atom 。如果有,链接器会扫描所有库(静态和动态),里面的 Define Atom/ sharead Library Atom 来替换 Undefined Atom。当然如果还有 Undefined Atom,就会报常见的错误 undefined Symbol。

优化阶段,比如 DeadCodeStripping 就是可达性遍历,比如从 _main 函数开头,不可达的会删除,不过这个主要还是说的 c 和cpp oc由于动态性,都不会被删除,不过分类的 可已用 -Objc -all_load来控制

Passes 阶段类似于一个 pipline。针对前面生成好的 lld::file (抽象)模型,进行操作,
比如

  1. stub (PLT 链接过程表)生成 (Proxy Atom)
  2. GOT 生成
  3. 重排优化
  4. 分层生成
  5. oc优化
  6. thread local var 生成
  7. Dtrace 探针生成
  8. 或者其他平台的特性
  • 生成文件阶段
    也是利用抽象工厂,特定文件有自己的特定子类来实现

  • 一些 lld 的特性

为了方便调试和阅读,lld 跟llvm 一样会有一种抽象的中间结构标示中间描述,这样做的好处是可以解决不同的输入和不同的输出后端,lld 的 Atom 中间表示 就是一个 YAML
文本格式的好处就是编译观察和编写单测。

一个具体的例子来说
可重定位目标文件的需要重定位的 section 都会有 reloc 的个数和偏移,数据结构定义在 <mach-o/reloc.h>

struct relocation_info {
   int32_t  r_address;  /* 对应section中的多少偏移的位置需要重定向 */
   uint32_t     r_symbolnum:24, /* 
   r_extern 为 1 表示从 symbol table 的第 r_symbolnum 个 symbol 读取信息
   r_extern 为 0 表示从第 r_symbolnum 个 section 读取信息
                   ordinal if r_extern == 0 */
        r_pcrel:1,  /* was relocated pc relative already */
        r_length:2, /* 0=byte, 1=word, 2=long, 3=quad */
        r_extern:1, /* does not include value of sym referenced */
        r_type:4;   /*当是0是,代表绝对地址,不需要重定位,不是 0的时候是机器相关的特定的重定向类型  比如 arm 64 定义在 <mach-o/arm64/reloc.h> */
};

由于 iPhone 平台都是 arm 64 这里举例为 arm 64
arm64 中
所有的重定位条目都是 extern 的 所以这一位固定位 1,那 r_symbolnum 代表了 符号表的地址
当汇编器在生成重定位项时,如果目标是个本地标签(比如L开头的),同一个 section的第一个非本地标签
将会生成一条 extern 重定位项,后面的标签将会使用一个 Addend (加数)计算两个label之间的相对位置,不过如果它前面没有外部标签也会使用 internal 重定位条目代替。

这个加数有时候编码到指令里面,或者当一个重定向的条目是 ARM64_RELOC_ADDEND 编码到 r_symbolnum 数里面
比如
ARM64_RELOC_UNSIGNED 和 ARM64_RELOC_AUTHENTICATED_POINTER 直接存在指令里面,
ARM64_RELOC_PAGE21 ARM64_RELOC_PAGEOFF12 ARM64_RELOC_BRANCH26 这三个指令需要加数是必须前面跟一个ARM64_RELOC_ADDEND重定位项。
其他的指令不支持这个

上面啰嗦了一大堆的意思,其实就分两种

  1. 有些 arm64 的机器码需要1条重定位项即可重定位
  2. 有些 arm64 的机器码需要2条重定项才能重定位
    2.1 其中那还有2个 ARM64_RELOC_UNSIGNED ARM64_RELOC_AUTHENTICATED_POINTER 特殊的需逻辑上要2个重定项但是实际上加数直接存在指令里面,需要提取起来
    2.2 ARM64_RELOC_PAGE21 ARM64_RELOC_PAGEOFF12 ARM64_RELOC_BRANCH26 的加数存在前面一个重定位项,
    2.3 ARM64_RELOC_BRANCH26 前面如果没跟重定位项 那么固定加上 0x14000000

举个例子,代码中有一块关于 b 指令的重定向。

这个是 ldr 指令页面偏移的重定向,

评论卡