扣丁書屋

iOS 符號化:基礎與進階

前言

符號化能幫助我們在定位 bug 、崩潰和性能瓶頸時,從運行時日志與堆棧找到根本的代碼原因;相信大家了解 atos 或 dSYM 等常用符號化工具,但這些工具是如何運作的?本篇文章將圍繞符號化的定義、原理、實踐與技巧,帶領大家對符號化進一步深層次了解;本篇文章是基于 Session 10211 - Symbolication: Beyond the basics 撰寫,Session 的演講者是Apple - 性能工具團隊的 Alejandro Lucena 工程師

什么是符號化?

「將 App 運行時信息映射為源碼」長話短說就是將運行時信息轉換為源碼信息,符號化是一種機制,將我們在設備運行時 App 的內存地址和關聯的指令信息轉換為源碼文件中具體文件名、方法名、行數等;可以理解為將運行時機器如何看待處理我們 App 的信息轉換成我們開發者如何看待處理我們的 App(源碼)。如果缺少這層轉換,哪怕只有幾行的代碼的 App,bug 定位也變得難以進行;

Demo

本文為了帶領大家了解符號化的原理,全文所用到的項目是一個簡單的只有幾行的Demo App,他所有代碼如下: demo 的邏輯很簡單:randomValue() 可以生成值區間在1-100間的隨機數numberChoices() 可以生成一個包含 10 個上述隨機數的數組selectMagicNumber(choices: numbers) 可以從入參 numbers 數組中,取出一個指定下標的元素generateMagicNumber() 按部執行上述操作,返回取出下標的元素 此處的 MAGIC_CHOICE 是一個隨機值

日常崩潰日志的符號化

第一次執行這個 App 就崩潰了,查看生成的錯誤日志,里面沒有很直觀的信息,是一堆內存地址,我只能看到 App 在主線程上 crash 了;

我嘗試直接 debug 我的 App,但在執行中沒有復現該問題,看來調試器也不一定能幫得了忙;多次嘗試之后終于復現了,但程序崩潰在匯編中,也沒有直觀的信息,匯編太硬核,搞不定。

上面的崩潰日志和匯編堆棧顯然都不能直接解決問題,但在符號化的幫助下,我們可以不從這些原始內存地址中挖掘錯誤;相信大家都知道在 Xcode Organizer 中載入 AppdSYM 文件,他會重新處理崩潰日志,載入后我們就可以得到下面這種可讀的、可以獲得調用信息、文件名、具體行數的崩潰日志,崩潰日志直接告訴我,崩潰時發生了數組越界訪問,非常直觀;根據這些信息回溯到代碼,我們也容易發現隨機值 MAGIC_CHOICE 容易導致,在訪問只有 10 個長度數組訪問時,發生數組越界;

使用 atos 命令行工具,我們也可以得到上述信息

日常 Instruments 堆棧的符號化

另一個符號化的例子是,在 Instruments 中進行性能優化時,檢測提示該 App 會周期性的執行大量寫入操作,出現了周期性的高負荷區間和低負荷區間;但是默認右下角顯示的堆棧信息只能提示 App 正在寫入文件,無論高負荷還是低負荷,都提示了同樣堆棧;

很明顯這兩個區間不會執行同樣代碼,這原因是因為當前的 Instruments 堆棧是被部分符號化的,一般而言,在堆棧中沒有具體文件名和具體行數時,符號化是不徹底,此時我們也可以手動在 Instruments 載入 dSYM 文件,載入后,我們再查看高負荷區,明確提示有多余的調試代碼 addDebugLog() ,而在低負荷區沒有該方法調用;dSYM 不僅可以使只包含內存地址信息的崩潰日志可讀,還可以幫助 Instruments 堆棧信息更加有用,這些都能幫我們找到問題背后的代碼問題;

符號化原理

既然符號化的工具可以幫助我們定位代碼問題,你肯定會問,What ?why?為什么dSYM 可以幫助符號化?How?dSYM如何幫助完成了符號化?dSYM是符號化的一切嗎?除了崩潰日志和Instruments ,別的地方還能載入 dSYM 嗎?atos-o``-i``-l 各自有什么用處?Instruments 為什么未能直接提供完全符號化的堆棧?Xcode 編譯設置對符號化有何影響?帶著這些問題,讓我們深入探究一些符號化的原理。

為此我們首先分解介紹符號化的兩個步驟:第一步:從內存地址回溯到文件**第二步:還原運行時調試信息**

第一步 - 與符號化相關的地址與轉換

從內存地址回溯到文件地址,指的是將運行時隨機的內存地址轉換為磁盤上二進制文件中穩定可用的文件信息;正如內存地址有內存空間一樣,二進制文件在磁盤上也有地址空間;但這兩種地址空間不能直接轉換,需要一種地址轉換機制;

磁盤上的地址空間與二進制文件地址

磁盤地址空間的地址是編譯時 Linker 鏈接器賦予二進制文件的地址;具體而言,linker 會把二進制代碼分組,分組后的部分稱為段 Segment,每個二進制段都包含了一些數據和屬性,例如段的名稱,大小,地址等;舉例來說,二進制文件中的 __TEXT 段會包含對應的方法和函數,__DATA 段會包含程序的全局狀態,例如全局變量;每個段都被賦予了一個獨一無二的起始地址,這種設計保證了段與段之間不會重疊;

-w871

具體而言 linker 會把段信息記錄在可執行文件頭部,作為 Mach-O 頭的一部分;眾所周知, Mach-O 是一種可執行文件和庫的文件格式,Mach-O 頭中包含許多與段的屬性信息相關的 load 指令,操作系統內核通過讀取這些 load 指令來把對應的二進制段加載入內存;如果 App 用到了 Universal2 打包技術,那每種架構都會有與之對應的 Mach-O 頭和相關段信息;

-w860

上面講了段信息和 load 指令,讓我們來結合最初的小 demo,實踐查看一下相關的 load 指令;我們可以通過 otool -l 來輸出 load 指令信息,結合 grep(字符串篩選工具)可以過濾出 LC_SEGMENT_64load 指令,如下圖所示;輸出結果提示 __TEXT 段的起始位置為 vmaddr 所示地址,段的長度為 vmsize 所示字節大??;

將二進制文件載入內存

由以上信息,我們了解到 load 指令會包含載入的地址和大小,那為什么內核實際通過 load 指令載入后,二進制段的內存地址和這個 linker 生成的地址不一致?下圖中內存地址和 linkerA、B、C 地址有啥關系?后文中會將 linker 生成的地址簡稱為 A、B、C

Address space layout randomization - 地址空間布局隨機化技術

事實上,現在操作系統中都會有一種「地址空間布局隨機化」技術,該技術是一種防范內存損壞漏洞被利用的計算機安全技術。ASLR 通過隨機放置進程關鍵數據區域的地址空間來防止攻擊者能可靠地跳轉到內存的特定位置來攻擊制定函數?,F代操作系統一般都加設這一機制,以防范惡意程序對已知地址進行 Return-to-libc 攻擊。簡言之,內核在加載二進制段前,會初始化一個隨機值,稱為 ASLR Slide 「內存空間隨機分布偏移量」,后文中會把該偏移量簡稱為 S;之后內核會將該偏移量 S 疊加到 linker 生成的 load 指令的地址 A、B、C上;因此,內核在執行 load 指令時,不會按照原始的 linker 地址直接載入到內存地址 A、B、C 中,而是載入到 A+S、B+S、C+S,我們可以把這些實際的 load 加載地址稱為 Load Address「加載地址」,后文中,Load Address 將簡稱為 L

通過了解 ASLR 技術,我們弄明白了 linker addressload address 之間的差值是 ASLR Slide 隨機內存地址分布偏移量;我們可以得到該公式 ALSR Slide = Load Address - Linker Address, 簡化為 S = L - A

如何獲取實際的 Linker Address 和 Load Address

前面已經提到 otool 可以幫助我們查看二進制文件的 load 指令信息,進而得到 linker address (該地址也可以視為 file address 「文件地址」) 而獲得運行時內存地址中的 Load Address,可以通過崩潰日志中的 Binary Image 列表,Instruments 提供的堆棧,或者通過 vmmap 命令行工具來獲??;具體如何使用 vmmap 在后文中會有講解

計算 ASLR Slide 隨機內存偏移量

結合實踐,我們需要知道 ASLR Slide 隨機內存偏移量,才能夠從崩潰日志和 Instruments 堆棧中的內存地址,減去 ASLR Slide 而獲得文件地址;因此需要先計算出 ASLR Slide,計算 ASLR Slide 一般以特定段(如 __TEXT)的 load addresslinker address 來相減得出,如何獲取這倆地址上面已經說了,結合實踐我們從崩潰日志中獲取了 __TEXT 二進制段的 load address0x10045c000 ;通過 otool 我可以獲得 __TEXT 二進制段的 linker address0x100000000 ;將這兩者相減我們就可以的得到 ASLR Slide = 0x45c000;

有了 ASLR Slide ,我們可以從崩潰日志的運行時內存地址,換算出磁盤地址空間中的文件地址,如下圖所示,我們可以得到我們 demo 中崩潰的堆棧的文件地址為 0x10003b70 ,有了文件地址,我們可以用來查看源碼,這個后續再說。我們先繼續探索一下其他計算 ASLR Slide 的姿勢

如下圖所示,otool 命令行工具可以用來查看崩潰時發生問題的指令信息, 傳入 -tV 可以輸出匯編堆棧;-arch arm64 是為了讓 otool 正確處理 Universal 2 技術編譯的產物;輸出結構對應上述文件地址,顯示此是 brk 指令,匯編中的 brk 一般代表著 App 出現了異?;騿栴};

atos 命令行工具也可以幫我們計算 ASLR Slide, atos-o 指令會輸出 file segment address, -l 指令會輸出 load address;

除了 atosotool ,還有 vmmap 命令行工具也可以幫助我們獲取 load address ,我們可以用 vmmap 來驗證上面的計算結果, vmmap 輸出崩潰時 __TEXT segmentload address ,使用之前公式可以計算出本次運行的 ASLR Slide0x104d14000 ,將本次崩潰日志中的 runtime address - ASLR Slide 得到了 file address0x100003b70 ,和之前計算的 file address 一樣;

上述兩次不同運行時, 不同崩潰日志,不同的 ASLR Slide 能夠得到同一個 file address ,這不是巧合;是因為內核每次運行的 ASLR Slide 都不同,因此不同時間,不同設備的崩潰日志中所對應的內存地址會變化,但實際的 linkder address 是一樣的;基于此,雖然內存地址每次變化,我們仍然可以定位到相同的 file address;至此,我們發現了一種機制,能讓我在隨機的運行時內存中,定位到我們 App 源碼級別所發生的的事;通過這種映射機制能夠讓我們從運行時的堆棧信息中,回溯到 App 源碼中;

小結 - 從內存地址回溯到文件地址

以上內容就是「符號化兩步走」中的第一步:從內存地址回溯到文件,總結一下該步驟中的內容和工具

  1. App 和 庫的二進制文件格式是 Mach-O ,其中 Mach-O 的頭中存放了二進制段的關聯信息和 load 指令,這些二進制段是 linker 創建的,其中包括了二進制段的地址信息 linker address;
  2. otool -l 可以幫助我們輸出 Mach-O 中指定二進制段的地址和屬性信息,其中包括 linker address;
  3. 崩潰日志中的 binary image 列表中可以獲取崩潰發生時的 load address;
  4. vmmap 也可以獲得正在運行 Appload address
  5. ASLR Slide + Linker address = Load address

第二步 - 分析調試信息

有了以上基礎,我們可以進一步討論符號化的第二步:分析調試信息;調試信息一般包含了 file address 和源碼之間的關系信息;Xcode 會在編譯時生成這些關系信息并存放為 dSYM 文件,也可以把這些關系信息內置在二進制編譯產物中;

這些調試信息有三種類型,每一種都提供了不同級別與 file address 關聯的調試信息;

  1. Function starts
  2. Nlist symbol table
  3. DWARF

下圖中展示了這三種工具分別提供了對應維度的調試信息

Function Starts

從上圖中可知,function starts 相較于其他工具提供最少的信息,該工具只能提供函數對應的起始地址,具體而言,function starts 會提供函數的起始地址和其調用的所在的地址;但這其中不會告訴你這調用地址里是否有其他函數,他只能告訴你這里有個函數出問題 ;

function starts 通過編碼 __LINKEDIT 二進制段中的 linker 地址列表來提供該功能;function starts 基于直接內置在 App 編譯產物中,通過 mach-O 文件的 load 指令的 LC_FUNCTION_STARTS 來描述 function starts;

實踐中,可以通過 symbols -onlyFuncStartsData 命令行工具來輸出 function starts 相關信息,如下圖所示,其中的 null 是因為 function starts 不提供函數名稱,所以用 null 來做函數名稱的占位符;

基于 function starts 我們可以對未符號化的崩潰日志進行處理,先從崩潰日志的內存地址 0x10045fb70 減去之前計算好的 ASLR Slide``0x45c000得到 file address``0x100003b70; 然后結合 function starts 輸出結果,我們發現只有第一個地址 0x100003a68 小于我們算出的 file address``0x100003b70,所以只有這第一個地址包含了錯誤發生的地址;基于此我們計算這兩個地址之間偏移了 0x108,換算成十進制 是 264,也就是我們 file address 與實際錯誤發生地址之間有 264 字節的偏移量;

至此 function starts 可以幫助我們理解崩潰日志中的函數如何被設置,修改了哪些寄存器;但因為 function starts 不提供函數名,我們只能在低級的機器碼層面來分析這些錯誤日志,對于調試開發 App 來說挺有用,但對于分析錯誤日志,我們還需要其他工具;

Nlist symbols List - Nlist 符號表

nlist 是一個結構體,他具體結構如下圖所示,nlist 符號表建立在 function starts 和一個編碼后的 __LINKEDIT``segment 的信息列表,當然 nlist 有自己的 load 指令;與 function starts 不同的是 nlist 不只是編碼內存地址,他在其結構體中編碼了更多信息;如下圖所示,nlist 結構體中包含了名稱和其他幾個屬性,具體而言 nlist 的類型由 n_type 所決定

n_type 有三種類型是我們符號化所感興趣的,這里我們先著重聊一聊其中兩種;第一種是 direct symbole - 直接符號;直接符號關聯的是在 App 和二方庫中,包含了已被完整定義的方法和函數;直接符號在 nlist_64 結構體中存儲了函數名字和函數文件地址;

Nlist 直接符號

n_type 中的指定二進制位的值決定了該 nlist 的類型,具體而言,n_type 中的第二、三、四的二進制位為 1 時,表明該 nlist 類型為直接符號,這三個位的組合還被叫做 N_SECT;

我們可以通過 nm -defined-only —numberic-sort 命令行工具來查看 N_SECT;在這里 nm 遍歷了 magicNumbers``App 的制定符號,并以地址順序羅列出來,具體參照下圖中的輸出;注意此處我們還是用了 xcrun -swift-demangle 來解析 Swift mangling 后的函數名稱;

上圖所示,我們已經可以從結果中獲得了方法名 numberChoices()、類名 MagicNumbers、文件名 main;這是因為這些信息直接在 App 內定義;symbols 查看直接符號 和 nm 工具相似, symbols 命令行工具也提供查看 nlist 數據的方法,并且支持自動 demangle ,具體如下圖 以上兩個方法,讓我們從崩潰日志中的內存地址,關聯到了源碼中的具體函數名稱,至此,崩潰日志的符號化的信息豐富程度更進一步;至此,我們通過 fuction starts 提供的函數入口偏移地址從 direct symbols 中匹配到一個函數入口,并且這個入口有名字,把這些信息放在一起,我們可以發現 crash 發生在 main 方法地址的 264字節偏移處;但 main 并不是崩潰中唯一的函數,這表明我們還有更多的信息有待挖掘;例如我們還沒有弄清楚代碼中的行數信息

我們已經弄清 main 并不是唯一與崩潰關聯的函數,我們還有更多的信息有待挖掘;例如我們還沒獲得文件的行數信息;并且在上述符號化中,部分函數被序列化,還有部分堆棧和崩潰日志信息沒有被符號化

我們在 Instruments 的堆棧中遇到了類似的情況,一些函數名被符號化而可讀,但部分仍是內存地址;發生這種現象的原因是,直接符號表中所包含的函數,只限于在鏈接時被直接鏈接的部分,動態庫等運行時加載的二進制文件不被包含在內,這些未能符號化的方法就是跨模塊從動態庫中調用的方法;我們需要其他手段了符號化這些調試信息;

這種直接符號表的邏輯,有助于減少編譯產物體積;畢竟換位思考,如果把打包時所有相關函數信息都存入符號表,這種操作才有違常識;對于 FrameworksLibraries,我們需要處理記錄那些被調用的方法,而剝離沒用到的;當然了如果把直接符號表里的主程序內的函數剝離,那符號表里啥也不剩了;

Xcode 編譯設置對 nlist 直接符號的影響

Xcode 的編譯設置中,strip 配置項有 strip linked product、strip style 、strip swift symbols 三個選項。這些編譯設置的選項控制了 App 在編譯鏈接過程中的剝離多余符號表的邏輯;具體來講,strip linked productYES 時,二進制文件中將根據 strip style 的值進行符號表剝離;舉例來說,strip style 值為 all symbols 時,符號表中將執行最激進的剝離策略,最終符號表中只包含最核心的方法;Non globals 類型會剝離應用中不同模塊中共同使用的直接符號,但會留下用于其他 APP 中的符號;Debugging symbols 則刪除了第三種 nlist 類型的符號,這個后續討論 DWARF 時會講到,但該類型的剝離會保留直接用到的符號。

-w875舉例來說,這里有一個定義了兩個 public interface接口和一個 internal shared 實現的方法的 framework ,由于所有這些函數在鏈接環節中有用,他們都擁有直接的符號項。

如果我按照 non globals 進行剝離,那只有兩個 interface 會留下;由于共享實現的函數只在 framework 內使用,所以它不是全局的,進而也不會被放入符號表; 類似的如果是 all symbols 剝離策略時時,如果這兩個 interface 有被 framework 外部所調用時,他們仍然會被留下;

symbols —onlyNListData 會輸出一些分布在直接符號之間 function starts 的條目;這些條目也表示了函數是存在于直接符號表中,亦或是已經被剝離了。你可以利用這些剝離設定,來實現你需要的符號表可見性;有了這些信息,我們就可以確定什么時候需要直接符號表。在實際應用中,有時候我們能符號化出函數名,但沒有具體行數和文件名;或者符號化結果包含了方法名和方法起始地址,正如此處 frameworksymbols 指令的例子;

間接符號 - Indirect symbols

與直接符號類似,間接符號的 n_type 的第一位二進制位為 1 ,或稱為 n_EXT

通過 nm -m -arch arm64 -undefined-only --numberic-sort MagicNumbers 輸出間接符號的信息;這其中使用 —undefined-only 來替換 —defined-only ,該指令用于查看間接符號;-m ,這可以讓你看到這些方法源自哪個 frameworklibraries。下面圖中的輸出結果提示 MagicNumbers``App 依賴了 libSwiftCore 中的一系列 Swift 基礎方法如 print()。

#### 小結 - Function starts 與 nlist 符號表 文章開頭,我們約定了要討論 function starts 、nlist 符號表DWARF 三種符號化工具;截止現在已經討論了前兩種,在此回顧一下;

  • Function starts 能提供地址列表,缺少方法名,可以幫助計算崩潰對應的文件地址偏移量;
  • Nlist 符號表把關聯到一個地址的詳細信息構成結構體存儲,nlist 符號能提供函數名稱,還可以描述在 App 內定義的直接符號和在二方庫中提供的間接符號;直接符號表通常保留與鏈接有關的函數,Xcode 項目設置中的 strip build style 會影響直接符號表中的內容;
  • 這兩種符號表都直接嵌入在 App 二進制文件 Mach-O 頭中的 __LINKEDIT 二進制段中

DWARF

截止現在我們還沒能看到諸如文件名、函數所在行數、崩潰所在行數等符號化信息;這些信息在 DWARF 中都有提供,我們在此詳細討論一下 DWARF ;相較于 nlist 符號表只保留函數部分信息,DWARF 幾乎記錄了函數的所有上下文信息;回顧 function starts 只在一個維度上提供偏移量信息;nlist 基于編碼 nlist_64 結構體將調試信息升級到兩個維度,即地址信息和函數名稱;作為比較 DWARF 增加了第三個維度:關系信息;實際項目中函數不是孤立存在的,函數會被調用和在其內部調用其他函數,函數會有出參入參;通過記錄這些函數的上下文關系信息;DWARF 會帶我們解鎖符號化最牛逼的姿勢;

當我們分析 DWARF 時,一般指的是引用分析一個 dSYM bundle,該 bundle 中存在由元數據組成的 plist,還包括一個 DWARF 二進制文件;二進制文件中將 DWARF 的信息記錄在 __DWARF 二進制段中;DWARF 在該二進制段中記錄了我們需要關注的三個數據流;具體而言三個數據流分別是 debug_info, debug_abbrev, debug_line ;debug_info 包含了原始數據,debug_abbrev 為原始數據進行了結構化處理,debug_line 包含了文件名和行號;除此之外 DWARF 還定義了需要討論的兩種 vocabulary list 詞匯表:compile unit 編譯單元和 subprogram 子程序;后文會提到第三種詞匯表 - 內聯子程序

Compile Unit - 編譯單元

編譯單元表示了在項目中會被編譯的單個源碼文件;具體來說,在項目中的每個 swift 文件都會有一個編譯單元與之對應;DWARF 為每一個編譯單元賦予了一些屬性,諸如文件名、模塊名稱、__TEXT segment 的函數占位部分等;main.swift 文件對應的編譯單元在 debug_info 數據流中儲存了這些屬性,如左側所示;與之對應的,在 debug_addrev 數據流中包含了一個相關的條目,這些條目告訴我們這些值代表了什么,如右側所示;我們看到圖中右側包含了文件名、語言和一個 low/high 對,用來表述 __TEXT``segment 的范圍

Subprogram - 子程序

子程序表示已被定義的函數;我們已經在 nlist 符號表中找到過已定義的方法,但子程序還可以用來描述靜態方法和本地方法;子程序當然也有自己的名稱和對應的 __TEXT``segment 地址起始范圍

DWARF 關系樹

編譯單元和子程序之間的一個基本關系是,子程序是在編譯單元中被定義的;DWARF 使用樹來表述這種關系;編譯單元在根節點上,子程序是根節點的孩子節點;這些子節點可以通過他們的地址范圍而被檢索到;

我們可以通過 dwarfdump 命令行工具來驗證上述 DWARF 的編譯單元、子程序和關系樹細節 首先我們將查看到一個編譯單元,這句之前提到的編譯單元所攜帶的屬性相吻合(文件名、語言、行數等),dwarfdump 工具結合了 debug_infodebug_abbrev 內容來展示 dSYMs 文件中的數據結構與內容

輸出很長,我們往下看,會看到一個子程序 subprogram;它所占用的地址范圍存在于該編譯單元的地址范圍內,并且可以看到方法名;之前提到過 DWARF 非常詳細的描述符號表和關系信息,我們不會在深入探究 DWARF 的關系樹 設計細節,但了解這些細節能夠幫助我們理解符號化背后的邏輯;

繼續往下看輸出結果,會發現其中還包括參數信息,DWARF 持有一個自己的詞匯表,來描述參數的名稱和類型;參數是子程序的一個子節點;下圖中的輸出,可以發現 numberofChoice 函數的參數 choices 的相關信息;文件名與行數信息

此外,debug_line 數據流中存儲了函數關聯的文件名和具體行數;但 debug_line 數據流不是樹狀結構,相反的,該數據流定義了一個 line table program 行表程序,這個航標程序可以讓鏈接后的文件地址映射到源碼文件中的具體行數;我們可以利用這個行表程序來查找文件地址關聯的具體源碼和行數;

綜上,基于 debug_info 的樹狀結構和 debug_line 的行表程序,我們可以得到一個下面的結構;通過遍歷這棵樹,我們可以找到想要的文件地址;首先從編譯單元開始,遍歷其子節點,然后篩選出包含 debug_line 的子節點;

DWARF 與編譯時函數內聯優化

我們可以使用 atos 命令行工具來完成上述操作,這次我們省略 -i``flag,可以看到輸出結果少了很多,只剩下方法名、文件名和行數;這里的結果提供了行數,因此我們可以斷定我們在使用 DWARF 來進行符號化;但除了文件名和行數,這個輸出結果和 nlist 符號表的符號化結果沒有太大區別;然后我們再試一試給 atos 加上 -i``flag,輸出結果是下面第二張圖,大家可以對比這兩個輸出的差異,他們的命令只差了一個 -i``atos -o MagicNumbers.dSYM/Contents/Resources/DWARF/MagicNumbers -arch arm64 -l 0x10045c000 0x10045fb70``atos -o MagicNumbers.dSYM/Contents/Resources/DWARF/MagicNumbers -arch arm64 -l 0x10045c000 -i 0x10045fb70

-w974

大家也許會猜,這 -i 意味著什么;事實上 atos-i 意味著 inlined function 內聯函數,內聯化是一種編譯器執行的常規優化;詳細而言,內聯化就是在編譯中把函數的實現代碼直接替換函數被調用的代碼;這樣的替換操作可以讓函數調用的代碼和函數的定義代碼都「消失了」;在我們的 Demo 中也就是使用 numberOfChoice() 的實現代碼替換了調用代碼;numberOfChoice() 調用代碼不見了~

Inlined subroutines - 內聯子程序

DWARF 使用內聯子程序來表述這種編譯時內聯優化;這就是我們要討論的第三種 vocabulary list 詞匯表類型 :inlined subroutines 內聯子程序;內聯子程序是子程序的一種,所以他也是一種方法,一種被內聯到另一個子程序的方法;所以內聯函數在 DWARF 關系樹中是子程序的一個子節點;這樣的定義意味著會出現遞歸關系;也就是說一個內聯子程序可以有其他內聯子程序作為子節點; 再次使用 dwarfdump 命令行工具,我們可以來檢查一下 DWARF 中的內聯子程序;這些內聯子程序被列為其他節點的子節點,并且有著與子程序類似的屬性,諸如名稱和地址;但是在DWARF 文件中,這些屬性一般會通過一個公共節點來訪問,這種設計叫抽象源;如果存在一個特定函數有很多內聯拷貝,則該函數的公共共享屬性將存儲在抽象源中,如此這些內聯函數就不會被重復多余的拷貝;內聯子程序有一個獨特的屬性是 call site 調用位置;該屬性表述了在源碼中實際調用函數的位置,編譯優化器會替換這些函數調用代碼;例如,我們在 main.swift 文件中第36行調用了 generateANumber() ,這使得需要在樹中新增子節點來記錄這個函數調用;

-w1010

到這里,我們對 DWARF 符號化有了更全面的了解,如下圖所示,我們對 App 的調用邏輯也有了更廣闊的視角。了解內聯函數的優化方式和細節是完全符號化崩潰日志的關鍵所在;-i 指令實際會要求 atos 符號化過程中考慮到上述內聯函數;這些內聯函數的信息同樣在 Instruments 堆棧中缺失;我們在崩潰日志和 Instruments 堆棧中都需要 dSYM 文件,正是由于 dSYM 中精確地包含了上述三種類型的信息:編譯單元、子程序和 DWARF 關系樹;

從庫和目標文件中獲取 DWARF

除了 dSYM 文件中,還可以在靜態庫和目標文件中找到 DWARF;也就是說即使沒有 dSYM 文件,你仍然可以從靜態庫或目標文件中鏈接的函數,來生成 DWARF;這種情況下,你會找到調試符號表的 nlist 類型,這些本是可以被 strip 剝離的符號類型之一;但這些 nlist 類型并不直接包含 DWARF,相反,他們直接把函數關聯到其源碼文件;如果一個庫在構建中包含調試信息,此時,這些 nlist 條目可以給我們提供 DWARF 的相關信息

上述類型的 nlist 條目可以通過 dsymutil -dump-debug-map 命令行工具來輸出和詳細查看;在此我們列出了不同函數方法和他們的出處;這些地址信息可以被掃描并處理成 DWARF 文件中所需的信息;

小結 - DWARF

  • DWARF 是深度符號化數據的重要來源
  • DWARF 描述了函數與文件之間的重要關系信息;
  • DWARF 妥當處理了編譯時內斂優化的問題;
  • dSYM 文件和靜態庫可以都可包含 DWARF;
  • 實踐中推薦使用 dSYM 獲取 DWARF,因為從 dSYM 中獲取的 DWARF 可以方便的在其他工具中使用,并且 Xcode 許多內置工具也支持 DWARF;

開發工具與符號化實踐

Xcode 編譯設置 - Debug info format

  • 針對本地開發配置建議設置為直接生成 DWARF
  • 針對發布編譯配置,請確保生成包含 DWARFdSYM 文件
  • 提交至 App Store ConnectApp,你可以在那下載到 dSYM
  • 即使使用了 bitcode 技術 ,你也可以從 App Store Connect 下載到 dSYM 文件

查找和確認 dSYM 文件

如下圖所示,在本地 Mac 上可以接住 mdfind 命令行工具檢查 dSYM 文件;這個字母數字組成的字符串是編譯二進制產物的 UUID,也是運行時 load 指令的唯一標識符;你還可以通過 symbols -uuid 來查看 dSYM 文件的 UUID;

在少數情況下,編譯過程會生成一個無效的 DWARF,你可以通過 draftdump -verify 命令來檢驗 DWARF 的有效性;如果這個檢查命令輸出任何錯誤,請直接通過 https://feedbackassistant.apple.com 來進行Developer Tool - 開發工具bug 反饋;

單個 DWARF 二進制文件大小上線是 4GB,如果上述校驗中報告超過 4GB 的錯誤,你可以考慮將項目的進行組件化拆分,以便每個組件會有一個較小的 dSYM

實際操作中,通過比較 dSYMUUID 和崩潰日志中 binary imageUUID 性來匹配兩者;除了在崩潰日志中查看 App 二進制鏡像的 UUID ,你還可以通過 symbols 命令行工具來獲取 UUID,參照下圖;實際符號化中,需要 dSYM 和崩潰日志的 UUID 匹配;

其他符號化的細節

symbols 命令行工具還可以幫你檢查你 App 編譯產物中包含的可用調試信息;輸出內容的方括號中的標簽,告訴了這些調試信息的來源;當你不知道在調試時使用哪些調試信息時,使用該指令可以看看有哪些調試信息可用;

如果你確信已經有可用 dSYM 文件了,但是仍舊未能將 Instruments 中的堆棧信息符號化,請檢查一下項目的 Entitlements 和代碼簽名配置;具體來說使用 codesign 命令行工具,你可以驗證是否擁有正確的代碼簽名配置;

同時,你還需要檢查本地開發的 entitlement 中是否包含了 get-task-allow 項,該配置授予 Instruments 這類工具在調試中執行對應 App 符號化的權利;一般來說,Xcode 默認自動會設置這個 get-task-allow 配置項;但 Instruments 不能符號化的時候,可以排查一下這個配置項;如果你發現 entitlement 中沒有 get-task-allow ,可以檢查確保 build-setting -> code signing -> code signing inject base entitlemens 的值為 true ,來解決該問題;

最后,對于使用 Universal 2 技術的 App, 在使用文章中提到的命令行工具時,都可以指定架構,諸如 symbols、otool、dwarfdump 都有 -arch 的參數可供配置,如此可以只執行特定架構的相關操作;

總結

正如名稱中的「符號化進階」,用以下幾個關鍵點來總結本 Session

  • 符號化UUID 和文件地址是一致且可靠的方式來識別 App 在運行時的問題,因為這兩者不受 ASLR Slide 偏移量的影響;UUID 和文件地址是運行時信息符號化關鍵的第一步
  • 實踐中,盡可能利用 dSYM 完成符號化;dSYMDWARF 的形式記錄了最豐富細節的調試信息,并且被 XcodeInstruments 所良好支持
  • 文中介紹了幾款命令行符號化工具,諸如 otool, vmmap, nm, symbols, dwarfdump, atos;這些工具包含在 Xcode Command line tool中,提供了強大的診斷和檢視符號化過程與細節信息的能力;必要時,大家可以將這些工具集成進自己的工作流;

如果你有興趣學習更多鏈接與符號化知識,我在此推薦兩個WWDC18的Session :他們幫助你了解 App 在啟動時如何運行起來,一個是Optimizing app startup time - 優化 App 啟動速度,另一個是App startup time: past ,present, and future - App 啟動的時間線:過去、現在和將來;


https://mp.weixin.qq.com/s/iRxvrOsEdW1wPZ3tSPKeIg

最多閱讀

iOS 性能檢測新方式?——AnimationHitches 8月以前  |  18061次閱讀
快速配置 Sign In with Apple 2年以前  |  5484次閱讀
APP適配iOS11 3年以前  |  4436次閱讀
App Store 審核指南[2017年最新版本] 3年以前  |  4262次閱讀
所有iPhone設備尺寸匯總 3年以前  |  4184次閱讀
使用 GPUImage 實現一個簡單相機 3年以前  |  3916次閱讀
開篇 關于iOS越獄開發 3年以前  |  3794次閱讀
在越獄的iPhone設置上使用lldb調試 3年以前  |  3719次閱讀
給數組NSMutableArray排序 3年以前  |  3642次閱讀
使用ssh訪問越獄iPhone的兩種方式 3年以前  |  3346次閱讀
UITableViewCell高亮效果實現 3年以前  |  3344次閱讀
關于Xcode不能打印崩潰日志 3年以前  |  3242次閱讀
使用ssh 訪問越獄iPhone的兩種方式 3年以前  |  3083次閱讀
為對象添加一個釋放時觸發的block 3年以前  |  2857次閱讀
使用最高權限操作iPhone手機 3年以前  |  2828次閱讀

手機掃碼閱讀
18禁止午夜福利体验区,人与动人物xxxx毛片人与狍,色男人窝网站聚色窝
<蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <文本链> <文本链> <文本链> <文本链> <文本链> <文本链>