编译器|Swift编译器Crash—Segmentation fault解决方案
背景 抖音上线 Swift 后,编译时偶现Segmentation fault: 11
和Illegal instruction: 4
的错误,CI/CD 和本地均有出现,且重新编译后均可恢复正常。
由于属于编译器层抛出的 Crash,加之提示的错误代码不固定且非必现,一时较为棘手。网上类似错误较多,但Segmentation fault
属于访问了错误内存的通用报错,参考意义较小。和公司内外的团队交流过,也有遇到类似错误,但原因各不相同,难以借鉴。
虽然 Swift 库二进制化后,相关代码不会参与编译,本地出现的概率大大减少,但在 CI/CD/仓库二进制化任务中依旧使用源码,出现问题需要手动重试,影响效率且繁琐,故深入编译器寻求解决方案。
Crash 堆栈
文章图片
结论 简而言之,是 Swift 代码中将在 OC 中声明为类属性的NSDictionary
变量,当成 Swift 的Dictionary
使用。即一个 immutable 变量当作 mutable 变量使用了。编译器在校验SILInstruction
时出错,主动调用abort()
结束进程或出现EXC_BAD_ACCESS
的 Crash。
准备工作
编译 Swift
由于本地重现过错误,故拉取和本地一致的 swift-5.3.2-RELEASE 版本,同时推荐使用 VSCode 进行调试,Ninja 进行构建。
Ninja 是专注于速度的小型构建系统。注意事项
- 提前预留 50G 磁盘空间
- 首次编译时长在一小时左右,CPU 基本打满
brew install cmake ninja
mkdir swift-source
cd swift-source
git clone git@github.com:apple/swift.git
cd swift/utils
./update-checkout --tag swift-5.3.2-RELEASE --clone
./build-script
主要目录
![编译器|Swift编译器Crash—Segmentation fault解决方案](https://img.it610.com/image/info8/377d78ecb3474334846e90127542b184.jpg)
文章图片
提取编译参数
笔者将相关代码抽离抖音工程, 本地复现编译报错问题后,从 Xcode 中提取编译参数:
![编译器|Swift编译器Crash—Segmentation fault解决方案](https://img.it610.com/image/info8/d9d70647fa7f40dda9dd09b7d50eab6c.jpg)
文章图片
VSCode 调试
选择合适的 LLDB 插件,以 CodeLLDB 为例配置如下的 launch.json。
其中
args
内容为获取前一步提取的编译参数,批量将其中每个参数用双引号包裹,再用逗号隔开所得。{
"version": "0.2.0",
"configurations": [
{
"type":"lldb",
"request": "launch",
"name": "Debug",
"program": "${workspaceFolder}/build/Ninja-DebugAssert/swift-macosx-x86_64/bin/swift",
"args": ["-frontend","-c","-primary-file"/*and other params*/],
"cwd": "${workspaceFolder}",
}
]
}
SIL LLVM
在深入 SIL 之前,先简单介绍 LLVM,经典的 LLVM 三段式架构如下图所示,分为前端(Frontend),优化器(Optimizer)和后端(Backend)。当需要支持新语言时只需实现前端部分,需要支持新的架构只需实现后端部分,而前后端的连接枢纽就是 IR(Intermediate Representation),IR 独立于编程语言和机器架构,故 IR 阶段的优化可以做到抽象而通用。
![编译器|Swift编译器Crash—Segmentation fault解决方案](https://img.it610.com/image/info8/3c34ea5f8b864b67a807ca72eaae2150.png)
文章图片
Frontend 前端经过词法分析(Lexical Analysis),语法分析(Syntactic Analysis)生成 AST,语义分析(Semantic Analysis),中间代码生成(Intermediate Code Generation)等步骤,生成 IR。
IR 格式
IR 是 LLVM 前后端的桥接语言,其主要有三种格式:
- 可读的格式,以.ll 结尾
- Bitcode 格式,以.bc 结尾
- 运行时在内存中的格式
SSA
LLVM IR 和 SIL 都是 SSA(Static Single Assignment)形式,SSA 形式中的所有变量使用前必须声明且只能被赋值一次,如此实现的好处是能够进行更高效,更深入和更具定制化的优化。
如下图所示,代码改造为 SSA 形式后,变量只能被赋值一次,就能很容易判断出 y1=1 是可被优化移除的赋值语句。
![编译器|Swift编译器Crash—Segmentation fault解决方案](https://img.it610.com/image/info8/b2487078f7d0415daa76160583e88595.jpg)
文章图片
结构
基础结构由 Module 组成,每个 Module 大概相当于一个源文件。Module 包含全局变量和 Function 等。Function 对应着函数,包括方法的声实现,参数和返回值等。Function 最重要的部分就是各类 Basic Block。
Basic Block(BB) 对应着函数的控制流图,是 Instruction 的集合,且一定以 Terminator Instructions 结尾,其代表着 Basic Block 执行结束,进行分支跳转或函数返回。
Instruction 对应着指令,是程序执行的基本单元。
![编译器|Swift编译器Crash—Segmentation fault解决方案](https://img.it610.com/image/info8/a75068dce7a34cca964885557dcacf08.jpg)
文章图片
Optimizer IR 经过优化器进行优化,优化器会调用执行各类 Pass。所谓 Pass,就是遍历一遍 IR,在进行针对性的处理的代码。LLVM 内置了若干 Pass,开发者也可自定义 Pass 实现特定功能,比如插桩统计函数运行耗时等。
Xcode Optimization Level
在 Xcode - Build Setting - Apple Clang - Code Generation - Optimization Level 中,可以选定优化级别,-O0 表示无优化,即不调用任何优化 Pass。其他优化级别则调用执行对应的 Pass。
![编译器|Swift编译器Crash—Segmentation fault解决方案](https://img.it610.com/image/info8/6d1664189dbb4dc0be58cc07b1428903.jpg)
文章图片
Backend 后端将 IR 转成生成相应 CPU 架构的机器码。
Swiftc
不同于 OC 使用 clang 作为编译器前端,Swift 自定义了编译器前端 swiftc,如下图所示。
![编译器|Swift编译器Crash—Segmentation fault解决方案](https://img.it610.com/image/info8/83ff453c4d6545c78882f8513ba3846b.jpg)
文章图片
这里就体现出来 LLVM 三段式的好处了,支持新语言只需实现编译器前端即可。
对比 clang,Swift 新增了对 SIL(Swift Intermediate Language)的处理过程。SIL 是 Swift 引入的新的高级中间语言,用以实现更高级别的优化。
Swift 编译流程
Swift 源码经过词法分析,语法分析和语义分析生成 AST。SILGen 获取 AST 后生成 SIL,此时的 SIL 称为 Raw SIL。在经过分析和优化,生成 Canonical SIL。最后,IRGen 再将 Canonical SIL 转化为 LLVM IR 交给优化器和后端处理。
![编译器|Swift编译器Crash—Segmentation fault解决方案](https://img.it610.com/image/info8/2167f78f92a04cf48def86990a926934.jpg)
文章图片
SIL 指令
SIL 假设虚拟寄存器数量无上限,以%+数字命名,如%0,%1 等一直往上递增 以下介绍几个后续会用到的指令:
alloc_stack
`: 分配栈内存
apply
: 传参调用函数
Load
: 从内存中加载指定地址的值
function_ref
: 创建对 SIL 函数的引用
Identifier
LLVM IR 标识符有 2 种基本类型:
- 全局标识符:包含方法和全局变量等,以@开头
- 局部标识符:包含寄存器名和类型等,以%开头,其中%+数字代表未命名变量变量
- SIL function 名都以@+字母/数字命名,且通常都经过 mangle
- SIL value 同样以%+字母/数字命名,表示其引用着 instruction 或 Basic block 的参数
- 【编译器|Swift编译器Crash—Segmentation fault解决方案】
@convention(swift)
使用 Swift 函数的调用约定(Calling Convention),默认使用
@convention(c)
和@convention(objc_method)
分别表示使用 C 和 OC 的调用约定
@convention(method)
表示 Swift 实例方法的实现
@convention(witness_method)
表示 Swift protocol 方法的实现
SIL 实现了一整套和 IR 类似的结构,定制化实现了
SILModule SILFunction SILBasicBlock SILInstruction
。![编译器|Swift编译器Crash—Segmentation fault解决方案](https://img.it610.com/image/info8/627e624db1834b8898997ac512cc194b.png)
文章图片
调试过程 复现 Crash
根据前文的准备工作设置好编译参数后,启动编译,复现 Crash,两种 Crash 都有复现,场景如下图所示。
abort()
和EXC_BAD_ACCESS
会导致上文出现的Illegal instruction: 4
和Segmentation fault: 11
错误。由于二者的上层堆栈一致,以下以前者为例进行分析。![编译器|Swift编译器Crash—Segmentation fault解决方案](https://img.it610.com/image/info8/3ceb4b70cd734fa285995ff9b0ce44dd.jpg)
文章图片
![编译器|Swift编译器Crash—Segmentation fault解决方案](https://img.it610.com/image/info8/70fed4ce250248dfb1f9796f7f59ab66.jpg)
文章图片
堆栈分析
通过堆栈溯源可看出是在生成
SILFunction
后,执行postEmitFunction
校验SILFunction
的合法性时,使用SILVerifier
层层遍历并校验 BasicBlock(visitSILBasicBlock
)。对 BasicBlock 内部的SILInstruction
进行遍历校验(visitSILInstruction
)。在获取
SILInstruction
的类型时调用getKind()
返回异常,触发 Crash。![编译器|Swift编译器Crash—Segmentation fault解决方案](https://img.it610.com/image/info8/f1c4395066c046e38b5b63188dba7cf7.jpg)
文章图片
异常 SIL
- 由于此时
SILInstruction
异常,比较难定位是在校验哪段指令时异常,故在遍历SILInstruction
时打印上一段指令的内容。
- swift 源代码根目录执行以下命令,增量编译
cd build/Ninja-DebugAssert/swift-macosx-x86_64
ninja
复现后打印内容如下图所示:
调试小 tips:LLVM 中很多类都实现了 dump()函数用以打印内容,方便调试。
![编译器|Swift编译器Crash—Segmentation fault解决方案](https://img.it610.com/image/info8/2e2aa85cf249476a99cbd550ed16fbed.jpg)
文章图片
// function_ref Dictionary.subscript.setter
%32 = function_ref @$sSDyq_Sgxcis : $@convention(method) <τ_0_0, τ_0_1 where τ_0_0 : Hashable> (@in Optional<τ_0_1>, @in τ_0_0, @inout Dictionary<τ_0_0, τ_0_1>) -> () // user: %33
%33 = apply %32(%13, %11, %24) : $@convention(method) <τ_0_0, τ_0_1 where τ_0_0 : Hashable> (@in Optional<τ_0_1>, @in τ_0_0, @inout Dictionary<τ_0_0, τ_0_1>) -> ()
%34 = load [take] %24 : $*Dictionary // users: %43, %37
正常 SIL 命令行使用
swiftc -emit-silgen
能生成 Raw SIL,由于该类引用到了 OC 文件,故加上桥接文件的编译参数,完整命令如下:swiftc -emit-silgen /Users/cs/code/ThirdParty/Swift_MVP/Swift_MVP/SwiftCrash.swift -o test.sil-import-objc-header /Users/cs/code/ThirdParty/Swift_MVP/Swift_MVP/Swift_MVP-Bridging-Header.h
截取部分 SIL 如下
%24 = alloc_stack $Dictionary // users: %44, %34, %33, %31
%25 = metatype $@objc_metatype TestObject.Type// users: %40, %39, %27, %26
%34 = load [take] %24 : $*Dictionary // users: %42, %36
%35 = function_ref @$sSD10FoundationE19_bridgeToObjectiveCSo12NSDictionaryCyF : $@convention(method) <τ_0_0, τ_0_1 where τ_0_0 : Hashable> (@guaranteed Dictionary<τ_0_0, τ_0_1>) -> @owned NSDictionary // user: %37
%36 = begin_borrow %34 : $Dictionary // users: %38, %37
%37 = apply %35(%36) : $@convention(method) <τ_0_0, τ_0_1 where τ_0_0 : Hashable> (@guaranteed Dictionary<τ_0_0, τ_0_1>) -> @owned NSDictionary // users: %41, %40
SIL 分析 对正常 SIL 逐条指令分析
- 在栈中分配类型为
Dictionary
的内存,将其地址存到寄存器%24,该寄存器的使用者是%44, %34, %33, %31
- %25 表示类型
TestObject.Type
,即TestObject
的类型 metaType
- 加载%24 寄存器的值到%34 中,同时销毁%24 的值
- 创建对函数
_bridgeToObjectiveC()-> NSDictionary
的引用,存到%35 中
- 由于函数名被 mangle,先将函数名 demangle,如下图所示,得到函数
![编译器|Swift编译器Crash—Segmentation fault解决方案](https://img.it610.com/image/info8/e3c126a2697845b5ba8bf0aefc9436fb.jpg)
文章图片
![编译器|Swift编译器Crash—Segmentation fault解决方案](https://img.it610.com/image/info8/338381cbfbf24e288c0ae0aa023a3918.jpg)
文章图片
@convention(method)
表明是 Swift 实例方法,有 2 个泛型参数,其中第一个参数τ_0_0
实现了 Hashable 协议
- 生成一个和%34 相同类型的值,存入%36,%36 结束使用之前,%34 一直存在
- 执行%35 中存储的函数,传入参数%36,返回
NSDictionary
类型,结果存在%37。其作用就是将Dictionary
转成了NSDictionary
对比异常 SIL,可以看出是在执行桥接方法
_bridgeToObjectiveC()
时失败,遂查看源码,发现是一个 OC 的NSDictionary
不可变类型桥接到 Swift 的Dictionary
成为一个可变类型时,对其内容进行修改。虽然这种写法存在可能导致逻辑异常,但并不致编译器 Crash,属于编译器代码 bug。更有意思的是,只有在 OC 中将该属性声明为类属性(class)时,才会导致编译器 Crash。class SwiftCrash: NSObject {
func execute() {
//compiler crash
TestObject.cachedData[""] = ""
}
}
@interface TestObject : NSObject
@property (strong, nonatomic, class) NSDictionary *cachedData;
@end
解决方案 源码修改
找到错误根源就好处理了,将问题代码中的 NSDictionary 改成 NSMutableDictionary 即可解决。
重新运行 Swift 编译器编译源码,无报错。
修改抖音源码后,也再没出现编译器 Crash 的问题,问题修复。
静态分析
潜在问题 虽然
NSDictionary
正常情况下可以桥接成 Swift 的Dictionary
正常使用,但当在 Swift 中对 immutable 对象进行修改后,会重新生成新的对象,对原有对象无影响,测试代码和输出结果如下:可以看出变量
temp
内容无变化,Swift 代码修改无效。TestObject *t = [TestObject new];
t.cachedData = https://www.it610.com/article/[@{@"oc":@"oc"} mutableCopy];
NSDictionary *temp = t.cachedData;
NSLog(@"before execution : temp %p: %@",temp,temp);
NSLog(@"before execution : cachedData %p: %@",t.cachedData,t.cachedData);
[[[SwiftDataMgr alloc] init] executeWithT:t];
NSLog(@"after execution : temp %p: %@",temp,temp);
NSLog(@"after execution : cachedData %p: %@",t.cachedData,t.cachedData);
class SwiftDataMgr: NSObject {
@objc
func execute(t : TestObject) {
t.cachedData["swift"] = "swift"
}
}
![编译器|Swift编译器Crash—Segmentation fault解决方案](https://img.it610.com/image/info8/05ef39849d6d43d58a3847b1559e6427.jpg)
文章图片
新增规则 新增对抖音源码的静态检测规则,检测所有 OC immutable 类是否在 Swift 中被修改。防止编译器 crash 和导致潜在的逻辑错误。
所有需检测的类如下:
NSDictionary/NSSet/NSData/NSArray/NSString/NSOrderedSet/NSURLRequest/
NSIndexSet/NSCharacterSet/NSParagraphStyle/NSAttributedString
后记 行文至此,该编译器 Crash 问题已经解决。同时近期在升级 Xcode 至 12.5 版本时又遇到另一种编译器 Crash 且未提示具体报错文件,笔者如法炮制找出错误后并修复。待深入分析生成
SILInstruction
异常的根本原因后,另起文章总结。此外笔者为 Swift 编译器提交了 bug 报告并附上最小可复现 demo, 有需要的同学可以在此链接下载:
https://bugs.swift.org/browse/SR-14417
加入我们 我们是负责抖音客户端基础能力研发和新技术探索的团队。我们在工程/业务架构,研发工具,编译系统等方向深耕,支撑业务快速迭代的同时,保证超大规模团队的研发效能和工程质量。在性能/稳定性等方面不断探索,努力为全球数亿用户提供最极致的基础体验。
如果你对技术充满热情,欢迎加入抖音基础技术团队,让我们共建亿级全球化 App。目前我们在深圳、北京、上海和杭州均有招聘需求。
内推可以联系邮箱:chenshan.cc@bytedance.com,邮件标题:姓名-工作年限-抖音-基础技术-iOS/Android。
推荐阅读
- 深入理解|深入理解 Android 9.0 Crash 机制(二)
- Swift中willSet和didSet的简述
- Hacking|Hacking with iOS: SwiftUI Edition - SnowSeeker 项目(一)
- LeetCode算法题-11.|LeetCode算法题-11. 盛最多水的容器(Swift)
- iOS-Swift-map|iOS-Swift-map filter reduce、函数式编程
- Swift|Swift ----viewController 中addChildViewController
- SwiftUI|SwiftUI iOS 瀑布流组件之仿CollectionView不规则图文混合(教程含源码)
- Swift高级应用|Swift高级应用 -01
- Swift5.0|Swift5.0 UITexview的基本使用
- Swift7|Swift7 - 循环、函数