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 指令页面偏移的重定向,

阅读更多

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

动态链接和加载是怎么做的

  • 特殊的 LC_SEGMENT_64 __LINKEDIT

链接信息段用于存储dyld需要用到的信息包括:符号表、字符串表、重定位项表、 等 是一系列的数据综合
与代码段和数据段在 Data(数据区域)规整化一的存储结构不同的是,因为链接信息段存储着诸多类型不同的用于动态链接的加载命令(LoadCommands)所需要的数据,所以链接信息段在 Data(数据区域)的存储结构,会根据具体加载命令(LoadCommands)的不同而不同
以下加载命令需要额外的空间用于存储数据,其数据存储在__LINKEDIT 区域的数据部分:
01.LC_DYLD_INFO_ONLY
02.LC_SYMTAB
03.LC_DYSYMTAB
04.LC_FUNCTION_STARTS
05.LC_DATA_IN_CODE
06.LC_CODE_SIGNATURE
以下加载命令不需要额外的空间用于存储数据,其将所有信息存储在 LoadCommands 区域的加载命令本身:
01.LC_LOAD_DYLINKER
02.LC_UUID
03.LC_VERSION_MIN_IPHONEOS / LC_VERSION_MIN_MACOSX
04.LC_SOURCE_VERSION
05.LC_LOAD_DYLIB
06.LC_RPATH

iOS 15 以下传统的 LC_DYLD_INFO

主要包含 rebase bind weakbind lazybind,export info 下面分段讲解

 // 所有的 信息按 字节序编码,不需要大小端转换
struct dyld_info_command {
   uint32_t   cmd;      /* LC_DYLD_INFO or LC_DYLD_INFO_ONLY */
   uint32_t   cmdsize;      /* sizeof(struct dyld_info_command) */
   ....
}

这里解释下,rebase 和 bind的区别。
在动态链接过程,
代码访问其他代码分为四种情况

  1. 代码访问同模块的代码,只需要 pc 相对访问即可,无需静态和动态重定位
  2. 代码访问同模块的数据,只需要 pc + 偏移即可, 无需静态和动态重定位
  3. 代码访问外部模块的数据,只需要 got 表,需要动态重定位,也就是绑定
  4. 代码访问外部模块的带阿米,只需要 got 表, 需要动态重定位,也就是绑定
    至于 weak 绑定是绑定的 cxx 特殊case
    lazy 绑定则是 got + plt 表为了优化性能的特殊case

那么数据对数据的访问不是指令,则需要 rebase 。比如如下代码
在静态链接的时候 p 执行 a在data段的绝对地址,但是 由于有了可执行文件加载的 ALSR 机制 再加上动态库加载地址本来就不是固定的加载地址,因此在 load 的时候
进行 rebase , Mach-O 里面最常见的就是 __attribute(constructor) 的数据了 存的是个绝对地址,启动需要rebase。

static int a = 101;
static int *p = &a;

rebase

每当 dyld 将图像加载到与其首选地址不同的地址时 (ASLR),Dyld 都会对 image 进行 rebase 。(换句话说没有 ASLR 就没有rebase)
每次 rebase 其实需要三列元信息
<seg-index, seg-offset, type> (哪个segement,其实偏移,操作类型)
但为了压缩信息,每次rebase 会复用三列数据,那个变化了就更改那个列,
比如 起始给定了(0,1,1) 后面假设 type 需要调整到2就会给一个调整type的指令,后面紧跟2. 那么这个元组就变成了 (0,1,2)
rebase 的定义在 dyld的 template <typename P> void Adjustor<P>::adjustDataPointers() 方法内

struct dyld_info_command {
  ...

    uint32_t   rebase_off;  /* file offset to rebase info  */
    uint32_t   rebase_size; /* size of rebase info   */
    ...
}
1字节指令解码
 低 4 位是**立即数type**
 高 4 位是**rebase指令类型**
 掩码分别是
#define REBASE_OPCODE_MASK                  0xF0
#define REBASE_IMMEDIATE_MASK                   0x0F

/*
 * reabse的数据是什么类型
 */
#define REBASE_TYPE_POINTER                 1 // 指针类型
#define REBASE_TYPE_TEXT_ABSOLUTE32             2 //绝对地址
#define REBASE_TYPE_TEXT_PCREL32                3 // pc相对访问
/*
 * 操作码 类型
 */
// 
#define REBASE_OPCODE_DONE                  0x00 // rebase 指令结束
// 设置type,高位是1,是设置指令,后四位是离立即数,上面也就3种type够用了
#define REBASE_OPCODE_SET_TYPE_IMM              0x10 // 立即数

/* 设置segent列,后面紧跟 1个 uleb 编码位 复制给offset
dyld源码为
case REBASE_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB:
    segIndex = immediate;
    segOffset = read_uleb128(diag, p, end);
*/
#define REBASE_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB       0x20 
/* ,后面紧跟 1个 uleb 编码位 复制给offset
dyld源码为
case REBASE_OPCODE_ADD_ADDR_ULEB:
    segOffset += read_uleb128(p, end);

*/
#define REBASE_OPCODE_ADD_ADDR_ULEB             0x30 // 设置offset立即数,
/* ,后面紧跟 1个 uleb 编码位 * rebase 长度(比如8位指针)复制给 offset
dyld源码为
case REBASE_OPCODE_ADD_ADDR_IMM_SCALED:
    segOffset += immediate*sizeof(pint_t);
    break;

*/
#define REBASE_OPCODE_ADD_ADDR_IMM_SCALED           0x40 //  
/* 真正的rebase 操作 do 立即数 次rebase操作
dyld源码为
case REBASE_OPCODE_DO_REBASE_IMM_TIMES:
    for (int i=0; i < immediate; ++i) {
        slidePointer(segIndex, segOffset, type);
            segOffset += sizeof(pint_t);
        }

template <typename P>
void Adjustor<P>::slidePointer(int segIndex, uint64_t segOffset, uint8_t type)
{
    cache_builder::ASLR_Tracker* aslrTracker = this->_mappingInfo[segIndex].aslrTracker;
    pint_t*   mappedAddrP  = (pint_t*)((uint8_t*)_mappingInfo[segIndex].cacheLocation + segOffset);
    uint32_t* mappedAddr32 = (uint32_t*)mappedAddrP;
    pint_t    valueP;
    uint32_t  value32;
    switch ( type ) {
        case REBASE_TYPE_POINTER:
            valueP = (pint_t)P::getP(*mappedAddrP);
            核心在这里设置为  valueP + slideForOrigAddress(valueP)
            P::setP(*mappedAddrP, valueP + slideForOrigAddress(valueP));
            aslrTracker->add(mappedAddrP);
            break;

        case REBASE_TYPE_TEXT_ABSOLUTE32:
            value32 = P::E::get32(*mappedAddr32);
            P::E::set32(*mappedAddr32, value32 + (uint32_t)slideForOrigAddress(value32));
            break;

        case REBASE_TYPE_TEXT_PCREL32:
            // general text relocs not support
        default:
            _diagnostics.error("unknown rebase type 0x%02X in %s", type, _dylibID);
    }
}
*/
#define REBASE_OPCODE_DO_REBASE_IMM_TIMES           0x50 
/* 同上,只不过立即数编码不够,后面紧跟 1个 uleb 编码位 
然后do ulebnum 次rebase操作
case REBASE_OPCODE_DO_REBASE_ULEB_TIMES:
    count = read_uleb128(p, end);
    for (uint32_t i=0; i < count; ++i) {
        slidePointer(segIndex, segOffset, type);
        segOffset += sizeof(pint_t);
    }
    break;
 */
#define REBASE_OPCODE_DO_REBASE_ULEB_TIMES          0x60 
/** 做一次rebase 并且后面紧跟 uleb 数复制给 segoffset
case REBASE_OPCODE_DO_REBASE_ADD_ADDR_ULEB:
    slidePointer(segIndex, segOffset, type);
    segOffset += read_uleb128(p, end) + sizeof(pint_t);
 */
#define REBASE_OPCODE_DO_REBASE_ADD_ADDR_ULEB           0x70 
/*
// 跳过多少次,后面通常跟 count  和 skip 然后做 count 次 rebase,并且每次 对 segent 做
skip + sizeof(ptr) 个数
case REBASE_OPCODE_DO_REBASE_ULEB_TIMES_SKIPPING_ULEB:
    count = read_uleb128(p, end);
    skip = read_uleb128(p, end);
    for (uint32_t i=0; i < count; ++i) {
        slidePointer(segIndex, segOffset, type);
        segOffset += skip + sizeof(pint_t);
    }

*/
#define REBASE_OPCODE_DO_REBASE_ULEB_TIMES_SKIPPING_ULEB    0x80 和 skip 

这次估计你能看懂rebas代码了

bind

struct dyld_info_command {
  ...
  /*
   同 rebase 信息一样,但是这次需要的是 5元组

` <seg-index, seg-offset, type, symbol-library-ordinal, symbol-name, addend>`
`(seg索引,segoffset,类型,符号来源的动态库以1开头,符号名,加数)`

    uint32_t   bind_off;    /* file offset to binding info   */
    uint32_t   bind_size;   /* size of binding info  */

    ...
}
1字节指令解码
 低 4 位是**立即数type**
 高 4 位是**rebase指令类型**
 掩码分别是
#define BIND_OPCODE_MASK                    0xF0
#define BIND_IMMEDIATE_MASK                 0x0F

/*
 * 绑定的类型
 */
#define BIND_TYPE_POINTER                   1  // 指针类型
#define BIND_TYPE_TEXT_ABSOLUTE32               2 //绝对地址
#define BIND_TYPE_TEXT_PCREL32                  3 // pc相对访问

// 特殊查找序号,分别是自己,主二进制,平坦的动态查找和弱符号冲突
#define BIND_SPECIAL_DYLIB_SELF                  0
#define BIND_SPECIAL_DYLIB_MAIN_EXECUTABLE          -1
#define BIND_SPECIAL_DYLIB_FLAT_LOOKUP              -2
#define BIND_SPECIAL_DYLIB_WEAK_LOOKUP              -3

#define BIND_SYMBOL_FLAGS_WEAK_IMPORT               0x1 weak符号 0b0001
#define BIND_SYMBOL_FLAGS_NON_WEAK_DEFINITION           0x8 非weak 符号 0b1000

#define BIND_OPCODE_DONE                    0x00  // 绑定结束
#define BIND_OPCODE_SET_DYLIB_ORDINAL_IMM           0x10 // 修改 bibary 序号 数存在指令里面
#define BIND_OPCODE_SET_DYLIB_ORDINAL_ULEB          0x20 // 修改 bibary 序号 后面跟一个 uleb 数
#define BIND_OPCODE_SET_DYLIB_SPECIAL_IMM           0x30 // 绑定特殊数字,就是起那么说的负数,或者0 比如 绑定 self符号,主二进制符号,平坦的动态查找,或者weak符号冲突检测
#define BIND_OPCODE_SET_SYMBOL_TRAILING_FLAGS_IMM       0x40 // 修改flag看是否是若符号
#define BIND_OPCODE_SET_TYPE_IMM                0x50// 修改 type 数存在指令里面
#define BIND_OPCODE_SET_ADDEND_SLEB             0x60 // 修改加数,后面跟 uleb 数
#define BIND_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB         0x70 // 修改 segement index 数存在指令里面,后面跟一个uleb 修改 offset
#define BIND_OPCODE_ADD_ADDR_ULEB               0x80 // 修改offset 后面跟一个 uleb 
#define BIND_OPCODE_DO_BIND                 0x90 // 真正的绑定 把 5元组传递下去
#define BIND_OPCODE_DO_BIND_ADD_ADDR_ULEB           0xA0 //绑定完加一个 offset uleb数
#define BIND_OPCODE_DO_BIND_ADD_ADDR_IMM_SCALED         0xB0 // 绑定完添加 offset  segmentOffset += immediate*ptrSize + ptrSize;
#define BIND_OPCODE_DO_BIND_ULEB_TIMES_SKIPPING_ULEB        0xC0 // 后面跟 count 和 skip 做 count 次绑定,每次 offset 做跳过 segmentOffset += skip + ptrSize;
#define BIND_OPCODE_THREADED                    0xD0 // 后面必须跟 后两个 子命令
#define BIND_SUBOPCODE_THREADED_SET_BIND_ORDINAL_TABLE_SIZE_ULEB 0x00 // 修改tablecount 没见过这是干啥的。不太看得懂 感觉像是 iOS 15 优化
#define BIND_SUBOPCODE_THREADED_APPLY                0x00 // 链式绑定 addChainStart(segmentIndex, segIndexSet, segmentOffset, DYLD_CHAINED_PTR_ARM64E, stop);

这次你可以看懂这个了把,例子中第一个还是个weak绑定。后面有元组信息发生变化就单独修改某个数据然后继续绑定。 大部分都是把 got表的值从 0(占位符)改成真正的值。

weak_bind

某些 C++ 程序要求 dyld 具有唯一符号,以便进程中的所有二进制都使用某些代码/数据的相同副本。
weak绑定的操作符 和bind一样,但是他是按符号名字母需排序的。
weak绑定的动作在 bind 之后统一处理
dyld 能够按顺序遍历具有弱绑定信息的所有图像并寻找冲突,如果没有冲突就不更新了,如果有冲突就更新
听起来很拗口那么有啥用?
用处就是比如一开始所有的对象的 "operator new" 绑定到了 libstdc++.dylib
后面有个动态库覆盖了 "operator new" 符号,那么所有的对这个符号的依赖会自动转向这个库,如果后面还有动态库,那么继续覆盖。
感觉是为了实现一些hook的手法。

struct dyld_info_command {
  ...
   uint32_t   weak_bind_off;    /* file offset to weak binding info   */
    uint32_t   weak_bind_size;  /* size of weak binding info  */

}

lazy_bind

有一些符号不是需要立即绑定的可以推迟到首次使用绑定
lazy_bind info 和前面一样也是一堆 BIND Opcodes
但是通常来说 dyld 会忽略绑定这个符号, ld64(静态连接器)这里面套用了模版代码,让他们执行 懒加载桩,并且吧自己的偏移当做参数计算进去

struct dyld_info_command {
  ...
    uint32_t   lazy_bind_off;   /* file offset to lazy binding info */
    uint32_t   lazy_bind_size;  /* size of lazy binding infs */
    ...
}

export Info

  1. 导出的外部符号
    dylib 导出的符号在是一个 trie 树 (或者交字典树)中进行编码。
    导出区域是Trie 节点流。 第一个节点依次是 trie 的起始节点。

符号的节点以 uleb128 开头,它是到目前为止该字符串导出的符号信息的长度。

  • Case1 如果没有导出的符号,则节点以零字节开始。
    后面紧跟一个 childCount,( NodeLable(CString风格),NextNode offset)* childcount

  • case 2 如果有导出信息 后面继续跟

  1. 首先是一个 uleb128,其中包含 flags 。通常情况下,flag后面是一个 uleb128 数的偏移量,。如果flag是 EXPORT_SYMBOL_FLAGS_REEXPORT 则后面的是一个 uleb128 动态库序数,然后跟一个以 0结尾的字符串,如果字符串长度是0,则要去这个动态库重新找这个符号。
    如果flag 是EXPORT_SYMBOL_FLAGS_STUB_AND_RESOLVER 后面跟两个 uleb128 数,stuboffset, 和 resolver offset , stub 是 -nonlazy符号。resolve 是一个必须被调用的函数,调用完填充给lazy符号
  2. 在可选的导出符号信息之后是一个字节,表示离开该节点的边的数量(0-255),然后是每个边。
    每个边都是一个以零结尾的 UTF8 字符串,表示符号的额外字符,然后是一个 uleb128 offset量,指向该边指向的子节点。


    uint32_t   export_off;  /* file offset to lazy binding info */
    uint32_t   export_size; /* size of lazy binding infs */
};
/*
 * 符号标记
 */// 2 位掩码
#define EXPORT_SYMBOL_FLAGS_KIND_MASK               0x03

#define EXPORT_SYMBOL_FLAGS_KIND_REGULAR            0x00
#define EXPORT_SYMBOL_FLAGS_KIND_THREAD_LOCAL           0x01
#define EXPORT_SYMBOL_FLAGS_KIND_ABSOLUTE           0x02

#define EXPORT_SYMBOL_FLAGS_WEAK_DEFINITION         0x04
#define EXPORT_SYMBOL_FLAGS_REEXPORT                0x08
#define EXPORT_SYMBOL_FLAGS_STUB_AND_RESOLVER           0x10
#define EXPORT_SYMBOL_FLAGS_STATIC_RESOLVER         0x20

iOS 15

当你的 iOS 库最低部署版本被升级到 iOS 15 后 静态链接器链接后的动态库和可执行文件不再有 LC_DYLD_INFO_ONLY 而是变成了
LC_DYLD_EXPORTS_TRIELC_DYLD_CHAINED_FIXUPS
其中
LC_DYLD_EXPORTS_TRIE 和之前一样 是一颗 Trie 树,提供外部符号
LC_DYLD_CHAINED_FIXUPS 将以前的 rebase bind(bweakind) 合并为一个链式结构,i
PS lazybind已废弃
并且仅一次 pagefault 就可完成 reabse bind,。
详细的可以参考iOS 15 如何让你的应用启动更快


各个 Section 都是干嘛额

Segement64

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];    /* 段名字 */
    uint64_t    vmaddr;     /* vmaddr 段的虚拟内存起始地址 */
    uint64_t    vmsize;     /* vmsize 段的虚拟内存大小 */
    uint64_t    fileoff;    /* 段在文件中的偏移量 */
    uint64_t    filesize;   /*  段在文件中的大小 */
    vm_prot_t   maxprot;    /* 段页面所需要的最高内存保护 */
    vm_prot_t   initprot;   /* 段页面初始的内存保护 */
    uint32_t    nsects;     /* 段中包含 section 的数量 */
    uint32_t    flags;      /* 标志位 */
};

struct section_64 { /* for 64-bit architectures */
    char        sectname[16];   /* sectname section 名 */
    char        segname[16];    /* segname 该 section 所属的 segment 名 */
    uint64_t    addr;       /* addr 该 section 在内存的启始位置 */
    uint64_t    size;       /* size 该 section 的大小 */
    uint32_t    offset;     /* offset 该 section 的文件偏移*/
    uint32_t    align;      /* align 字节大小对齐 2的align的次方 */
    uint32_t    reloff;     /* 重定位入口的文件偏移 */
    uint32_t    nreloc;     /* 需要重定位的入口数量s */
    uint32_t    flags;      /* 包含 section 的 type 和 attributes*/
    uint32_t    reserved1;  /* reserved (for offset or index) */
    uint32_t    reserved2;  /* reserved (for count or sizeof) */
    uint32_t    reserved3;  /* reserved */
};

Segement 和 section 名称对链接器没啥用,但对人有理解价值
下面是一些常见的


#define SEG_PAGEZERO    "__PAGEZERO"    /* 可执行文件空访问保护段 */


#define SEG_TEXT    "__TEXT"    /* 传统的 unix 代码 segement*/
#define SECT_TEXT   "__text"    /* mach-o 更传统的部分 */

#define SEG_DATA    "__DATA"    /* 传统的 unix 数据 segement */
#define SECT_DATA   "__data"    /* mach-o 更传统的部分 */
                    /* no padding, no bss overlap */
#define SECT_BSS    "__bss"     /* segement 没有文件部分,但是有内存*/
#define SECT_COMMON "__common"  /* 编译器全局符号初始化值*/

#define SEG_OBJC    "__OBJC"    /* objective-C runtime segment */
#define SECT_OBJC_SYMBOLS "__symbol_table"  /*  objc 符号表 */
#define SECT_OBJC_MODULES "__module_info"   /*  objc 模块信息 */
#define SECT_OBJC_STRINGS "__selector_strs" /* objc selector 字符包 */
#define SECT_OBJC_REFS "__selector_refs"    /* objc selector 引用的字符包 */

#define SEG_LINKEDIT    "__LINKEDIT"    /* 静态链接器创建的segement  Created with -seglinkedit option to ld(1) for 仅可执行文件或者动态库有,里面是 rebase bind weakrease 等动态加载库加载使用的信息 */

#define SEG_UNIXSTACK   "__UNIXSTACK"   /* unix stack*/

#define SEG_IMPORT  "__IMPORT"  /* dyld 特有的 segement 具有 读写可执行全局,*/

各个 Section 都是干嘛额

  • __TEXT,__text
    指令 section 这里面放的都是具体的机器码,(机器码可以反汇编,不能说是汇编,因为汇编是程序员编写并不是和指令一一对齐。) r-x 权限

  • __TEXT,__stubs
    函数调用桩,前面解释过 3个指令,用一种固定的模版代码获取got表或者lazy符号表里面供 dyld 填充的外部符号地址

  • __TEXT,__stub_helper
    前面提过,dyld函数桩辅助函数,执行 dyld dyld_stub_binder 绑定函数,目的是为了回填__DATA,__la_symbol_ptr,的函数指针

  • __TEXT,__cstring
    去重后的C 语言字符串

  • __TEXT,__constg_swiftt
    swift 语言类型描述符 ,Swift 源代码里面有,具体干啥不知道

  • __TEXT,__swift5_builtin
    swift5builtin反射描述信息,hopper 可以查看

  • __TEXT,__swift5_typeref
    里是其他sections里引用到的 Swift mangled type name

  • __TEXT,__swift5_reflstr
    swift meta 引用的字符传

  • __TEXT,__swift5_fieldmd
    该部分包含字段描述符数组。字段描述符包含单个类、结构或枚举声明的字段记录的集合。每个字段描述符可以有不同的长度,具体取决于类型包含的字段记录数

  • __TEXT,__swift5_types
    此部分包含 32 位有符号整数数组。每个整数都是一个相对偏移量,指向 TEXT.const 部分中的标称类型描述符。

  • __TEXT,__swift5_assocty
    该部分包含关联类型描述符的数组。关联类型描述符包含一致性的关联类型记录的集合。关联类型记录描述从关联类型到一致性的类型见证的映射

  • __TEXT,__swift5_proto
    此部分包含 32 位有符号整数数组。每个整数都是一个相对偏移量,指向 TEXT.const 部分中的协议一致性 (protocol comformance descripter)描述符。

  • __TEXT,__swift5_protos
    此部分包含 32 位有符号整数数组。每个整数都是一个相对偏移量,指向 TEXT.const 部分中的协议(protocol descriptor)描述符

  • __TEXT,__swift5_capture
    捕获描述符描述闭包上下文对象的布局。与普通类型不同,闭包上下文的泛型替换来自对象,而不是元数据

  • __TEXT, __swift5_mpenum
    实在不知道是啥

  • __TEXT,__swift5_replace
    本节包含动态替换(dynamic 函数)信息。这本质上是 Objective-C 方法调配的 Swift 等价物。

  • __TEXT,__swift5_replac2
    本节包含不透明类型的动态替换信息。目前尚不清楚为什么创建这个附加部分而不是 __swift5_replace。

  • __TEXT,__swift_hooks

  • __TEXT,__swift51_hooks

  • __TEXT,__swift56_hooks

  • __TEXT,__s_async_hooks
    启动的特殊hooks

  • __TEXT,__objc_methname
    objc 的 selector名称,以C字符风格存储

  • __TEXT,__objc_classname
    objc 的类名称,以C字符风格存储

  • __TEXT,__objc_methtype
    objc 的typeencoding名称,以C字符风格存储

  • __TEXT,__objc_stub
    objc 的函数桩,
    +__ETEXT

  • __DATA, __literal16

  • __DATA, __literal8
    16 位 和 8位 浮点数字面量值

  • __DATA, __const
    编译时常量,里面都是具体的数据,由加载指令调用。
    Swift 的各种描述符数据也存在这里面
    Protocol conformance descriptor
    Module descriptor
    Protocol descriptor
    Nominal type descriptors
    Direct field offsets
    Method descriptors

  • __DATA,__got
    non-lazy 间接符号表

  • __TEXT,__unwind_info
    用来存储处理异常情况信息

  • __TEXT,__eh_frame
    用于异常处理

  • __DATA,__mod_init_func
    constructor 函数
    __DATA,__la_symbol_ptr
    lazy 符号指针,

  • __DATA,__objc_classlist
    Objective-C 类列表,以指针形式存储指向 __DATA,__data 数据

  • __DATA,__objc_nlclslist
    OC 的类的 +load 函数列表

  • __DATA,__objc_catlist
    Objective-C 分类列表,以指针形式存储指向 __DATA,__data 数据

  • __DATA,__objc_nlcatlist
    OC 的分类的 +load 函数列表

  • __DATA,__objc_protolist
    Objective-C 协议列表,以指针形式存储指向 __DATA,__data 数据

  • __DATA,__objc_imageinfo
    版本信息,

  • __DATA,__objc_const
    objc 编译时元数据, 包含协议描述信息,

  • __DATA,__objc_selrefs
    有那些 selector 被引用了

  • __DATA,__objc_classrefs
    有那些类被引用了

  • __DATA,__objc_protorefs
    有那些协议被引用了

  • __DATA.__objc_superrefs
    基础了那些父类

  • __DATA,__objc_data

  • objc 编译时元数据, 包含类,方法,描述信息 可以理解为 objc_class 的 ro_t,

  • __DATA,__objc_ivar

  • __DATA_DIRTY,__objc_ivar
    一些字典的优化 objc ivar 信息

  • __DATA,_data
    通用的数据区

没见过的有

  • __DATA,__crash_info

LC_SEGMENT_64(__PAGEZERO):空指针陷阱段

这是一个不可读、不可写、不可执行的空间,能够在空指针访问时抛出异常(用于捕捉对空指针的引用)
在 64 位的操作系统上,这个段的虚拟内存大小是 4GB
4GB 并不是指该段物理文件的真实大小,也不是指该段所占物理内存的真实大小
4GB 是规定了进程地址空间的前 4GB 被映射为:不可读、不可写、不可执行的空间
这就是为什么当读写一个 NULL(0x0) 指针时会得到一个 EXC_BAD_ACCESS 错误
因为 LC_SEGMENT_64(PAGEZERO) 的物理文件大小为 0
所以 Data(数据区域)中没有与 LC_SEGMENT_64(
PAGEZERO) 对应的部分


参考

  1. iOS 研习记—— 谈谈静态库与动态库
  2. iOS 研习记 聊聊 iOS 中的 Mach-O
  3. iOS之深入解析UmbrellaFramework的封装与应用
  4. iOS类加载流程(一):类加载流程的触发
  5. 逆向,插入一个 LC_ROUTINES执行些额外逻辑
  6. LEB128格式的说明
  7. OS Runtime源码解析initialize load attribute总结
  8. Fairplay DRM与混淆实现的研究
  9. iOS 上的自动链接( Auto Linking )
  10. 深入 iOS 静态链接器(一)— ld64
  11. iOS 逆向入门 - 动态库注入原理
  12. Dylib注入&劫持总结
  13. OS X平台的Dylib劫持技术(上)
  14. dyld详解
  15. LLD, THE LLVM Linker
  16. Atom Model Linker
  17. 深入研究了一下mach-o
  18. iOS 15 如何让你的应用启动更快
  19. About the LC_DYLD_INFO[_ONLY] command.
  • Swift 专项
  1. swift-类结构源码探寻(一)
  2. Swift metadata
  3. Mach-O 文件格式探索
阅读更多