抖音上線 Swift 後,編譯時偶現Segmentation fault: 11
和Illegal instruction: 4
的錯誤,CI/CD 和本地均有出現,且從新編譯後都可恢復正常。前端
因爲屬於編譯器層拋出的 Crash,加之提示的錯誤代碼不固定且非必現,一時較爲棘手。網上相似錯誤較多,但Segmentation fault
屬於訪問了錯誤內存的通用報錯,參考意義較小。和公司內外的團隊交流過,也有遇到相似錯誤,但緣由各不相同,難以借鑑。git
雖然 Swift 庫二進制化後,相關代碼不會參與編譯,本地出現的機率大大減小,但在 CI/CD/倉庫二進制化任務中依舊使用源碼,出現問題須要手動重試,影響效率且繁瑣,故深刻編譯器尋求解決方案。github
簡而言之,是 Swift 代碼中將在 OC 中聲明爲類屬性的NSDictionary
變量,當成 Swift 的Dictionary
使用。即一個 immutable 變量看成 mutable 變量使用了。編譯器在校驗SILInstruction
時出錯,主動調用abort()
結束進程或出現EXC_BAD_ACCESS
的 Crash。macos
因爲本地重現過錯誤,故拉取和本地一致的 swift-5.3.2-RELEASE 版本,同時推薦使用 VSCode 進行調試,Ninja 進行構建。編程
Ninja 是專一於速度的小型構建系統。json
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
選擇合適的 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,經典的 LLVM 三段式架構以下圖所示,分爲前端(Frontend),優化器(Optimizer)和後端(Backend)。當須要支持新語言時只需實現前端部分,須要支持新的架構只需實現後端部分,而先後端的鏈接樞紐就是 IR(Intermediate Representation),IR 獨立於編程語言和機器架構,故 IR 階段的優化能夠作到抽象而通用。架構
前端通過詞法分析(Lexical Analysis),語法分析(Syntactic Analysis)生成 AST,語義分析(Semantic Analysis),中間代碼生成(Intermediate Code Generation)等步驟,生成 IR。
格式
IR 是 LLVM 先後端的橋接語言,其主要有三種格式:
這三種格式徹底等價。
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 對應着指令,是程序執行的基本單元。
IR 通過優化器進行優化,優化器會調用執行各種 Pass。所謂 Pass,就是遍歷一遍 IR,在進行鍼對性的處理的代碼。LLVM 內置了若干 Pass,開發者也可自定義 Pass 實現特定功能,好比插樁統計函數運行耗時等。
Xcode Optimization Level
在 Xcode - Build Setting - Apple Clang - Code Generation - Optimization Level 中,能夠選定優化級別,-O0 表示無優化,即不調用任何優化 Pass。其餘優化級別則調用執行對應的 Pass。
後端將 IR 轉成生成相應 CPU 架構的機器碼。
不一樣於 OC 使用 clang 做爲編譯器前端,Swift 自定義了編譯器前端 swiftc,以下圖所示。
這裏就體現出來 LLVM 三段式的好處了,支持新語言只需實現編譯器前端便可。
對比 clang,Swift 新增了對 SIL(Swift Intermediate Language)的處理過程。SIL 是 Swift 引入的新的高級中間語言,用以實現更高級別的優化。
Swift 源碼通過詞法分析,語法分析和語義分析生成 AST。SILGen 獲取 AST 後生成 SIL,此時的 SIL 稱爲 Raw SIL。在通過分析和優化,生成 Canonical SIL。最後,IRGen 再將 Canonical SIL 轉化爲 LLVM IR 交給優化器和後端處理。
SIL 假設虛擬寄存器數量無上限,以%+數字命名,如%0,%1 等一直往上遞增 如下介紹幾個後續會用到的指令:
alloc_stack
: 分配棧內存apply
: 傳參調用函數Load
: 從內存中加載指定地址的值function_ref
: 建立對 SIL 函數的引用SIL 詳細的指令解析可參考官方文檔。
LLVM IR 標識符有 2 種基本類型:
在 SIL 中,標識符以@開頭
@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
。
根據前文的準備工做設置好編譯參數後,啓動編譯,復現 Crash,兩種 Crash 都有復現,場景以下圖所示。abort()
和EXC_BAD_ACCESS
會致使上文出現的Illegal instruction: 4
和Segmentation fault: 11
錯誤。因爲兩者的上層堆棧一致,如下之前者爲例進行分析。
經過堆棧溯源可看出是在生成SILFunction
後,執行postEmitFunction
校驗SILFunction
的合法性時,使用SILVerifier
層層遍歷並校驗 BasicBlock(visitSILBasicBlock
)。對 BasicBlock 內部的SILInstruction
進行遍歷校驗(visitSILInstruction
)。
在獲取SILInstruction
的類型時調用getKind()
返回異常,觸發 Crash。
SILInstruction
異常,比較難定位是在校驗哪段指令時異常,故在遍歷SILInstruction
時打印上一段指令的內容。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
複製代碼
命令行使用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 逐條指令分析
Dictionary<AnyHashable, Any>
的內存,將其地址存到寄存器%24,該寄存器的使用者是%44, %34, %33, %31TestObject.Type
,即TestObject
的類型 metaType_bridgeToObjectiveC()-> NSDictionary
的引用,存到%35 中@convention(method)
代表是 Swift 實例方法,有 2 個泛型參數,其中第一個參數τ_0_0
實現了 Hashable 協議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, 有須要的同窗能夠在此連接下載:
咱們是負責抖音客戶端基礎能力研發和新技術探索的團隊。咱們在工程/業務架構,研發工具,編譯系統等方向深耕,支撐業務快速迭代的同時,保證超大規模團隊的研發效能和工程質量。在性能/穩定性等方面不斷探索,努力爲全球數億用戶提供最極致的基礎體驗。
若是你對技術充滿熱情,歡迎加入抖音基礎技術團隊,讓咱們共建億級全球化 App。目前咱們在深圳、北京、上海和杭州均有招聘需求。
內推能夠聯繫郵箱:chenshan.cc@bytedance.com,郵件標題:姓名-工做年限-抖音-基礎技術-iOS/Android。