Swift编译器Crash—Segmentation fault解决方案

背景

抖音上线 Swift 后,编译时偶现Segmentation fault: 11Illegal instruction: 4的错误,CI/CD 和本地均有出现,且从新编译后都可恢复正常。前端

因为属于编译器层抛出的 Crash,加之提示的错误代码不固定且非必现,一时较为棘手。网上相似错误较多,但Segmentation fault属于访问了错误内存的通用报错,参考意义较小。和公司内外的团队交流过,也有遇到相似错误,但缘由各不相同,难以借鉴。git

虽然 Swift 库二进制化后,相关代码不会参与编译,本地出现的几率大大减小,但在 CI/CD/仓库二进制化任务中依旧使用源码,出现问题须要手动重试,影响效率且繁琐,故深刻编译器寻求解决方案。github

Crash 堆栈

结论

简而言之,是 Swift 代码中将在 OC 中声明为类属性的NSDictionary变量,当成 Swift 的Dictionary使用。即一个 immutable 变量看成 mutable 变量使用了。编译器在校验SILInstruction时出错,主动调用abort()结束进程或出现EXC_BAD_ACCESS的 Crash。macos

准备工做

编译 Swift

因为本地重现过错误,故拉取和本地一致的 swift-5.3.2-RELEASE 版本,同时推荐使用 VSCode 进行调试,Ninja 进行构建。编程

Ninja 是专一于速度的小型构建系统。json

注意事项

  • 提早预留 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
复制代码

主要目录

提取编译参数

笔者将相关代码抽离抖音工程, 本地复现编译报错问题后,从 Xcode 中提取编译参数:swift

VSCode 调试

选择合适的 LLDB 插件,以 CodeLLDB 为例配置以下的 launch.json。后端

其中args内容为获取前一步提取的编译参数,批量将其中每一个参数用双引号包裹,再用逗号隔开所得。markdown

{
    "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 阶段的优化能够作到抽象而通用。架构

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 是可被优化移除的赋值语句。

结构

基础结构由 Module 组成,每一个 Module 大概至关于一个源文件。Module 包含全局变量和 Function 等。Function 对应着函数,包括方法的声实现,参数和返回值等。Function 最重要的部分就是各种 Basic Block。

Basic Block(BB) 对应着函数的控制流图,是 Instruction 的集合,且必定以 Terminator Instructions 结尾,其表明着 Basic Block 执行结束,进行分支跳转或函数返回。

Instruction 对应着指令,是程序执行的基本单元。

Optimizer

IR 通过优化器进行优化,优化器会调用执行各种 Pass。所谓 Pass,就是遍历一遍 IR,在进行针对性的处理的代码。LLVM 内置了若干 Pass,开发者也可自定义 Pass 实现特定功能,好比插桩统计函数运行耗时等。

Xcode Optimization Level

在 Xcode - Build Setting - Apple Clang - Code Generation - Optimization Level 中,能够选定优化级别,-O0 表示无优化,即不调用任何优化 Pass。其余优化级别则调用执行对应的 Pass。

Backend

后端将 IR 转成生成相应 CPU 架构的机器码。

Swiftc

不一样于 OC 使用 clang 做为编译器前端,Swift 自定义了编译器前端 swiftc,以下图所示。

这里就体现出来 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 交给优化器和后端处理。

SIL 指令

SIL 假设虚拟寄存器数量无上限,以%+数字命名,如%0,%1 等一直往上递增 如下介绍几个后续会用到的指令:

  • alloc_stack : 分配栈内存
  • apply : 传参调用函数
  • Load : 从内存中加载指定地址的值
  • function_ref : 建立对 SIL 函数的引用

SIL 详细的指令解析可参考官方文档。

Identifier

LLVM IR 标识符有 2 种基本类型:

  • 全局标识符:包含方法和全局变量等,以@开头
  • 局部标识符:包含寄存器名和类型等,以%开头,其中%+数字表明未命名变量变量

在 SIL 中,标识符以@开头

  • SIL function 名都以@+字母/数字命名,且一般都通过 mangle
  • SIL value 一样以%+字母/数字命名,表示其引用着 instruction 或 Basic block 的参数
  • @convention(swift)使用 Swift 函数的调用约定(Calling Convention),默认使用
  • @convention(c)@convention(objc_method)分别表示使用 C 和 OC 的调用约定
  • @convention(method)表示 Swift 实例方法的实现
  • @convention(witness_method)表示 Swift protocol 方法的实现

SIL 结构

SIL 实现了一整套和 IR 相似的结构,定制化实现了SILModule SILFunction SILBasicBlock SILInstruction

调试过程

复现 Crash

根据前文的准备工做设置好编译参数后,启动编译,复现 Crash,两种 Crash 都有复现,场景以下图所示。abort()EXC_BAD_ACCESS会致使上文出现的Illegal instruction: 4Segmentation fault: 11错误。因为两者的上层堆栈一致,如下之前者为例进行分析。

堆栈分析

经过堆栈溯源可看出是在生成SILFunction后,执行postEmitFunction校验SILFunction的合法性时,使用SILVerifier层层遍历并校验 BasicBlock(visitSILBasicBlock)。对 BasicBlock 内部的SILInstruction进行遍历校验(visitSILInstruction)。

在获取SILInstruction的类型时调用getKind()返回异常,触发 Crash。

异常 SIL

  • 因为此时SILInstruction异常,比较难定位是在校验哪段指令时异常,故在遍历SILInstruction时打印上一段指令的内容。
  • swift 源代码根目录执行如下命令,增量编译
cd build/Ninja-DebugAssert/swift-macosx-x86_64
ninja
复制代码

复现后打印内容以下图所示:

调试小 tips:LLVM 中不少类都实现了 dump()函数用以打印内容,方便调试。

// 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<AnyHashable, Any>(%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<AnyHashable, Any> // 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<AnyHashable, Any> // users: %44, %34, %33, %31
%25 = metatype $@objc_metatype TestObject.Type  // users: %40, %39, %27, %26
%34 = load [take] %24 : $*Dictionary<AnyHashable, Any> // 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<AnyHashable, Any> // users: %38, %37
%37 = apply %35<AnyHashable, Any>(%36) : $@convention(method) <τ_0_0, τ_0_1 where τ_0_0 : Hashable> (@guaranteed Dictionary<τ_0_0, τ_0_1>) -> @owned NSDictionary // users: %41, %40
复制代码

SIL 分析

对正常 SIL 逐条指令分析

  1. 在栈中分配类型为Dictionary<AnyHashable, Any>的内存,将其地址存到寄存器%24,该寄存器的使用者是%44, %34, %33, %31
  2. %25 表示类型TestObject.Type,即TestObject的类型 metaType
  3. 加载%24 寄存器的值到%34 中,同时销毁%24 的值
  4. 建立对函数_bridgeToObjectiveC()-> NSDictionary的引用,存到%35 中
  • 因为函数名被 mangle,先将函数名 demangle,以下图所示,获得函数

  • @convention(method)代表是 Swift 实例方法,有 2 个泛型参数,其中第一个参数τ_0_0实现了 Hashable 协议
  1. 生成一个和%34 相同类型的值,存入%36,%36 结束使用以前,%34 一直存在
  2. 执行%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 = [@{@"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"
  }
}
复制代码

新增规则

新增对抖音源码的静态检测规则,检测全部 OC immutable 类是否在 Swift 中被修改。防止编译器 crash 和致使潜在的逻辑错误。

全部需检测的类以下:

NSDictionary/NSSet/NSData/NSArray/NSString/NSOrderedSet/NSURLRequest/
NSIndexSet/NSCharacterSet/NSParagraphStyle/NSAttributedString
复制代码

后记

行文至此,该编译器 Crash 问题已经解决。同时近期在升级 Xcode 至 12.5 版本时又遇到另外一种编译器 Crash 且未提示具体报错文件,笔者如法炮制找出错误后并修复。待深刻分析生成SILInstruction异常的根本缘由后,另起文章总结。

此外笔者为 Swift 编译器提交了 bug 报告并附上最小可复现 demo, 有须要的同窗能够在此连接下载:

bugs.swift.org/browse/SR-1…

加入咱们

咱们是负责抖音客户端基础能力研发和新技术探索的团队。咱们在工程/业务架构,研发工具,编译系统等方向深耕,支撑业务快速迭代的同时,保证超大规模团队的研发效能和工程质量。在性能/稳定性等方面不断探索,努力为全球数亿用户提供最极致的基础体验。

若是你对技术充满热情,欢迎加入抖音基础技术团队,让咱们共建亿级全球化 App。目前咱们在深圳、北京、上海和杭州均有招聘需求。

内推能够联系邮箱:chenshan.cc@bytedance.com,邮件标题:姓名-工做年限-抖音-基础技术-iOS/Android。

相关文章
相关标签/搜索