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。

相關文章
相關標籤/搜索