深入 iOS 静态链接器(一)— ld64

本文详细介绍了 iOS 中静态链接器 ld64 的工作原理,包括历史背景、概念解析如输入类型、symbol、visibility、strong/weak 定义等,以及解析输入文件、解决未定义符号、死代码优化等关键步骤。ld64 的执行流程涵盖从命令行参数处理到输出最终文件,深入解析了静态链接过程中涉及的 symbol table、atom 和 fixup 等核心概念。文章还讨论了 ld64 在 iOS 上的特定功能,如自动链接和静态库中的 ObjC 类和 category 处理。
摘要由CSDN通过智能技术生成


作者:字节跳动终端技术——李翔

前言

静态链接(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 的命名据说是来自 LoaDerLink 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 类型执行以下合并策略:

  1. 有多个 strong => 非法输入,abort
  2. 有且仅有一个 strong => 取
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值