作者:字节跳动终端技术——李翔
前言
静态链接(static linking)是程序构建中的一个重要环节,它负责分析 compiler 等模块输出的 .o
、.a
、.dylib
、经过对 symbol 的解析、重定向、聚合,组装出 executable 供运行时 loader 和 dynamic linker 来执行,有着承上启下的作用。
对于 iOS 工程而言,目前负责静态链接的主要是 ld64。苹果对 ld64 加持了一些功能,以适配 iOS 项目的构建,比如:
- 现在在 Xcode 中即使不主动管理依赖的系统动态库(如 UIKit),你的工程也可以正常链接成功
- 提供“强制加载静态库中 ObjC class 和 category” 的开关(默认开启),让 ObjC 的信息在输出中完整不丢失
大量特性的实现也在静态链接这一步完成,如:
- 基于二进制重排的启动速度优化,利用 ld64 的
-order_file
让 linker 按照指定顺序生成 Mach-O - 用
-exported_symbols_list
优化构建产物中 export info 占用的空间,减少包大小
借助组件二进制化、自定义构建系统等优化手段,当前大型工程中增量构建的效率已经显著提升,但静态链接作为每次必须执行的环节依然“贡献”了大部分耗时。了解 ld64 的工作原理能辅助我们加深对构建过程的理解、寻找提升链接速度的方法、以及探索更多品质和体验优化的可能性。
目录
- 历史背景
- 概念铺垫
- ld64 命令参数
- ld64 执行流程
- ld64 on iOS
- 其他
一、历史背景
- GNU ld:GNU ld,或者说 GNU linker,是 GNU 项目对 Unix ld 命令的实现。它是 GNU binary utils 的一部分,有两个版本:传统的基于 BFD & 只支持 ELF 的 gold。(gold 由 Google 团队研发,2008 年被纳入 GNU binary utils。目前随着 Google 重心放到 llvm 的 lld 上,gold 几乎不怎么维护了)。 ld 的命名据说是来自
LoaDer
、Link eDitor
。 - ld64:ld64 是苹果为 Darwin 系统重新设计的 ld。和 ld 的最大区别在于,ld64 是 atom-based 而不是 section-based(关于 atom 的介绍后面会展开)。在 macOS 上执行
ld
(/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/ld
)默认就是 ld64。系统和 Xcode 自带的版本可以通过ld -version_details
查询,如 650.9。苹果在这里 https://opensource.apple.com/tarballs/ld64/ 开放了 ld64 的源码,但更新不那么及时,始终落后于正式版(如 2021.8 为止开源最新是 609 版本,Xcode 12.5.1 是 650.9) 。zld 等基于 ld64 的项目都是 fork 自开源版的 ld64。
二、概念铺垫
在介绍 ld64 的执行流程之前,需要先了解几个概念。
输入 — .o
、.a
、.dylib
ld64 主要处理 Mach kernel 上的 Mach-O 输入,包括:
- Object File (
.o
)- 由 compiler 生成,包含元数据(header、LoadCommand 等)、segments & sections(代码、数据 等)、symbol table & relocation entries。
- object file 之间可能相互依赖(如 A 引用了 B 定义的函数),static linker 做的事情本质上就是把这些信息关联起来输出成一个总的有效的 Mach-O 。
- 静态库 (
.a
)- 可以视为
.o
的集合,让工程代码能模块化地被组织和复用。 - 其头部还存储了 symbol name ->
.o
offset 的映射表,便于 link 时快速查询某个 symbol 的归属。 - 一个静态库可能包含多个架构(universal / fat Mach-O),static linker 在处理时会按需选择目标架构。可以通过
lipo
等工具查看其架构信息。
- 可以视为
- 动态库 (
.dylib
、.tbd
)- 不同于静态库,动态库由 dyld 在运行时经过 rebase、binding 等过程后加载。static linker 在 link 时仅在处理 undefined symbol 时会尝试从输入的动态库列表中查询每个动态库 export 的 symbol。
- iOS 工程中使用的大部分是系统动态库(UIKit 等),工程也可以以 framework 等形式提供自己的动态库(需要指定对 rpath 以让自定义动态库能被 dyld 正常加载)
.tbd
(text-based dylib stub) 是苹果在 Xcode 7 后引入的一种描述 dylib 的文件格式,包含支持的架构、导出哪些 symbol 等信息。通过解析.tbd
ld64 可以快速地知道该 dylib 提供了哪些 symbol 可被用于链接 & 有哪些其他动态库依赖,而不用去解析整个解析一遍 dylib。目前大多数系统的 dylib 都采用这种方式。- 如 Foundation:
--- !tapi-tbd
tbd-version: 4
targets: [ i386-ios-simulator, x86_64-ios-simulator, arm64-ios-simulator ]
uuids:
- target: i386-ios-simulator
value: A4A5325F-E813-3493-BAC8-76379097756A
- target: x86_64-ios-simulator
value: C2A18288-4AA2-3189-A1C6-5963E370DE4C
- target: arm64-ios-simulator
value: 81DE1BE5-83FA-310A-9FB3-CF39C14CA977
install-name: '/System/Library/Frameworks/Foundation.framework/Foundation'
current-version: 1775.118.101
compatibility-version: 300
reexported-libraries:
- targets: [ i386-ios-simulator, x86_64-ios-simulator, arm64-ios-simulator ]
libraries: [ '/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation',
'/usr/lib/libobjc.A.dylib' ]
exports:
- targets: [ arm64-ios-simulator, x86_64-ios-simulator, i386-ios-simulator ]
symbols: [ '$ld$hide$os10.0$_OBJC_CLASS_$_NSURLSessionStreamTask', '$ld$hide$os10.0$_OBJC_CLASS_$_NSURLSessionTaskMetrics',
....
_NSLog, _NSLogPageSize, _NSLogv, _NSMachErrorDomain, _NSMallocZone,
....]
Symbol & Symbol Table
对 static linker 来说,symbol 是 Mach-O 提供的、link 时需要参考的一个个基本元素。
Mach-O 有一块专门的区域用于存储所有的 symbol,即 symbol table。
global function、global variable、class 等都会作为一条条 entry 被放入 symbol table 中。
Symbol 包含以下属性:
- 名称:具体生成规则由 compiler 决定。如 C variable
_someGlolbalVar
、C function_someGlobalFunction
、 ObjC class__OBJC_CLASS_$_SomeClass
、 ObjC method-[SomeClass foo]
等。不同的 compiler 有不同的 name mangling 策略。 - 是“定义”还是“引用”:对应函数、变量的“定义”和“引用”。
- visibility:如果是“定义”,还有 visibility 的概念来控制对其他文件的可见性(具体说明见后文「visibility」)、
- strong / weak:如果是“定义”,还有 strong / weak 的概念来控制多个“定义” 存在时的合并策略(具体说明见后文「strong / weak definition」。
Mach-O symbol table entry 具体的数据结构可以参考文档或源码
Visibility
Mach-O 中将 symbol 分为三组:
- global / defined external symbol :外部可用的 symbol 定义
- local symbol:该文件定义和引用的 symbol,仅该文件可用(比如被
static
标记) - undefined external symbol:依赖外部的 symbol 引用
属性 | 说明 | 举例 |
---|---|---|
global / defined external symbol | 由该文件定义,对外部可见 | int i = 1; |
local symbol | 由该文件定义,对外部不可见 | static int i = 1; |
undefined external symbol | 引用了外部的定义 | extern int i; |
可以通过查看该 Mach-O LoadCommand 中的 LC_DYSYMTAB
来获取三组 symbol 的偏移和大小
visibility 决定了 symbol definition 在 link 时对其他文件是否可见。上面说的 local symbol 对外不可见,global symbol 对外可见。
global symbol 里又分为两类:normal & private external。如果是 private external(对应 Mach-O 中 N_PEXT
字段) ,static linker 会在输出中把该 symbol 转为 local symbol。可以理解为该 symbol definition 只在这一次 link 过程中对外可见,后续 link 的产物如果要被二次 link,就对外不可见了(体现了 private 的性质)
一个 symbol 是否是 「private external」可以在源码和编译期用 __attribute__((visibility("xxx")))
来标识,可选值为 default
(normal)、hidden
(private external)
- 不指定
__attribute__((visibility("xxx")))
的,默认为default
-fvisibility
可以修改默认 visibility (gcc、clang 都支持)
- 指定
__attribute__((visibility("xxx")))
的,visibility 为xxx
举例:
// test.c
__attribute__((visibility("default"))) int i1Default = 101;
__attribute__((visibility("hidden"))) int i1Hidden = 102;
int i1Normal = 103;
不指定 -fvisibility
:
-fvisibility=hidden
:
Strong / Weak definition
symbol definition 中还有 strong / weak 之分:当 static linker 发现多个 name 相同的 symbol definition 时,会根据 strong/weak 类型执行以下合并策略:
- 有多个 strong => 非法输入,abort
- 有且仅有一个 strong => 取