深入理解Android NDK日誌符號化
前言
為了進行程式碼及產品保護,幾乎所有的非開源App都會進行程式碼混淆。這樣,當收集到崩潰資訊後,就需要進行符號化來還原始碼資訊,以便開發者可以定 位Bug。基於使用SDK和NDK的不同,Android的崩潰分為兩類:Java崩潰和C/C++崩潰。Java崩潰通過mapping.txt檔案進 行符號化,比較簡單直觀。而C/C++崩潰的符號化則需要使用Google自帶的一些NDK工具,比如ndk-stack、addr2line、 objdump等。本文不去討論如何使用這些工具,有興趣的朋友可以參考之前尹春鵬寫的另一篇文章《 如何定位Android NDK開發中遇到的錯誤》,裡面做了詳細的描述。
基於NDK的Android開發都會生成一個動態連結庫(so),它是基於C/C++編譯生成的。動態連結庫在Linux系統下廣泛使用,而Android系統底層是基於Linux的,所以NDK so庫的編譯生成遵循相同的規則,只不過Google NDK把相關的交叉編譯工具都封裝了。
Ndk-build編譯時會生成的兩個同名的so庫,位於不同的目錄/project path/libs/armeabi/xxx.so和/project path/obj/local/armeabi/xxx.so,比較兩個so檔案會發現體積相差很大。前者會跟隨App一起釋出,所以儘可能地小,而後者包含了很多除錯資訊,主要為了gdb除錯的時候使用,當然,NDK的日誌符號化資訊也包含其中。
深入分析so動態庫組成結構
本文主要針對這個包含除錯資訊的so動態庫,深入分析它的組成結構。在開始之前,先來說說這樣做的目的或者好處。現在的App基本都會採集 上報崩潰時的日誌資訊,無論是採用第三方雲平臺,還是自己搭建雲服務,都要將含除錯資訊的so動態庫上傳,實現雲端日誌符號化以及雲端視覺化管理。
移動App的快速迭代,使得我們必須儲存管理每一個版本的debug so庫,而其包含了很多與符號化無關的資訊。如果我們只提取出符號化需要的資訊,那麼符號化檔案的體積將會呈現數量級的減少。同時可以在自定義的符號化文 件中新增App的版本號等定製化資訊,實現符號化提取、上傳到雲端、雲端解析及視覺化等自動化部署。另外,從技術角度講,開發者將不再害怕看到 “unresolved symbol” linking errors,可以更從容地debugging C/C++ crash或進行一些hacking操作。
首先通過readelf來看看兩個不同目錄下的so庫有什麼不同。
從中可以清楚看到,包含除錯資訊的so庫多了8個.debug_開頭的條目以及.symtab和.strtab條目。符號化的本質,是通過堆疊中的地址資訊,還原始碼本來的語句以及相應的行號,所以這裡只需解析.debug_ line和.symtab,最終獲取到如下的資訊就可以實現符號化了。
c85 c8b willCrash jni/hello-jni.c:27-29 c8b c8d willCrash jni/hello-jni.c:32 c8d c8f JNI_OnLoad jni/hello-jni.c:34 c8f c93 JNI_OnLoad jni/hello-jni.c:35 c93 c9d JNI_OnLoad jni/hello-jni.c:37
通常,目標檔案分為三類:relocatable檔案、executable檔案和shared object檔案,它們格式稱為ELF(Executable and Linking Format),so動態庫屬於第三類shared object,它的整體組織結構如下:
ELF Header
ELF Header檔案頭的結構如下,記錄了檔案其他內容在檔案中的偏移以及大小資訊。這裡以32bit為例:
typedef struct { unsigned char e_ident[EI_NIDENT]; Elf32_Half e_type; // 目標檔案型別,如relocatable、executable和shared object Elf32_Half e_machine; // 指定需要的特定架構,如Intel 80386,Motorola 68000 Elf32_Word e_version; // 目標檔案版本,通e_ident中的EI_VERSION Elf32_Addr e_entry; // 指定入口點地址,如C可執行檔案的入口是_start(),而不是main() Elf32_Off e_phoff; // program header table 的偏移量 Elf32_Off e_shoff; // section header table的偏移量 Elf32_Word e_flags; // 處理器相關的標誌 Elf32_Half e_ehsize; // 代表ELF Header部分的大小 Elf32_Half e_phentsize; // program header table中每一項的大小 Elf32_Half e_phnum; // program header table包含多少項 Elf32_Half e_shentsize; // section header table中每一項的大小 Elf32_Half e_shnum; // section header table包含多少項 Elf32_Half e_shstrndx; //section header table中某一子項的index,該子項包含了所有section的字串名稱 } Elf32_Ehdr;
其中e_ident為固定16個位元組大小的陣列,稱為ELF Identification,包含了處理器型別、檔案編碼格式、機器型別等,具體結構如下:
Sections
該部分包含了除ELF Header、program header table以及section header table之外的所有資訊。通過section header table可以找到每一個section的基本資訊,如名稱、型別、偏移量等。
先來看看Section Header的內容,仍以32-bit為例:
typedef struct { Elf32_Word sh_name; // 指定section的名稱,該值為String Table字串表中的索引 Elf32_Word sh_type; // 指定section的分類 Elf32_Word sh_flags; // 該欄位的bit代表不同的section屬性 Elf32_Addr sh_addr; // 如果section出現在記憶體映象中,該欄位表示section第一個位元組的地址 Elf32_Off sh_offset; // 指定section在檔案中的偏移量 Elf32_Word sh_size; // 指定section佔用的位元組大小 Elf32_Word sh_link; // 相關聯的section header table的index Elf32_Word sh_info; // 附加資訊,意義依賴於section的型別 Elf32_Word sh_addralign; // 指定地址對其約束 Elf32_Word sh_entsize; // 如果section包含一個table,該值指定table中每一個子項的大小 } Elf32_Shdr;
通過Section Header的sh_name可以找到指定的section,比如.debug_line、.symbol、.strtab。
String Table
String Table包含一系列以/0結束的字元序列,最後一個位元組設定為/0,表明所有字元序列的結束,比如:
String Table也屬於section,只不過它的偏移量直接在ELF Header中的e_shstrndx欄位指定。String Table的讀取方法是,從指定的index開始,直到遇到休止符。比如要section header中sh_name獲取section的名稱,假設sh_name = 7, 則從string table位元組流的第7個index開始(注意這裡從0開始),一直讀到第一個休止符(index=18),讀取到的名稱為.debug_line。
Symbol Table
該部分包含了程式符號化的定義相關資訊,比如函式定義、變數定義等,每一項的定義如下:
# Symbol Table Entry typedef struct { Elf32_Word st_name; //symbol字串表的索引 Elf32_Addr st_value; //symbol相關的值,依賴於symbol的型別 Elf32_Word st_size; //symbol內容的大小 unsigned char st_info; //symbol的型別及其屬性 unsigned char st_other; //symbol的可見性,比如類的public等屬性 Elf32_Half st_shndx; //與此symbol相關的section header的索引 } Elf32_Sym;
Symbol的型別包含以下幾種:
其中STT_FUNC就是我們要找的函式symbol。然後通過st_name從symbol字串表中獲取到相應的函式名(如 JNI_OnLoad)。當symbol型別為STT_FUNC時,st_value代表該symbol的起始地址,而 (st_value+st_size)代表該symbol的結束地址。
回顧之前提到的.symtab和.strtab兩個部分,對應的便是Symbol Section和Symbol String Section。
DWARF(Debugging With Attributed Record Formats)
DWARF是一種除錯檔案格式,很多編譯器和偵錯程式都通過它進行原始碼除錯(gdb等)。儘管它是一種獨立的目標檔案格式,但往往嵌入在ELF檔案中。前面通過readelf看到的8個.debug_* Section全部都屬於DWARF格式。本文將只討論與符號化相關的.debug_line部分,更多的DWARF資訊請檢視參考文獻的內容。
.debug_line部分包含了行號資訊,通過它可以將程式碼語句和機器指令地址對應,從而進行原始碼除錯。.debug_line由很多子項組成,每個子項都包含類似資料塊頭的描述,稱為Statement Program Prologue。Prologue提供瞭解碼程式指令和跳轉到其他語句的資訊,它包含如下欄位,這些欄位是以二進位制格式順序存在的:
這裡用到的機器指令可以分為三類:
這裡不做機器指令的解析說明,感興趣的,可以檢視參考文獻的內容。
通過.debug_line,我們最終可以獲得如下資訊:檔案路徑、檔名、行號以及起始地址。
最後,我們彙總一下整個符號化提取的過程:
- 從ELF Header中獲知32bit或者64bit,以及大端還是小端,基於此讀取後面的內容;
- 從ELF Header中獲得Section Header Table在檔案中的位置;
- 讀取Section Header Table,從中獲得.debug_line、.symtab以及.strtab三個section在文中的位置;
- 讀取.symtab和.strtab兩個section,最後獲得所有function symbol的名稱、起始地址以及結束地址;
- 讀取.debug_line,按照DWARF格式解析獲取檔名稱、路徑、行號以及起始地址;
- 對比步驟4和5中獲取的結果,進行對比合並,形成最終的結果。
參考文獻
- 如何定位Android NDK開發中遇到的錯誤
- How debuggers work: Part 3 – Debugging information
- ELF (Executable and Linking Format)
- The DWARF Debugging Standard
作者簡介:
賈志凱 Testin技術總監,主要負責崩潰分析專案Android平臺架構設計、效能及穩定性優化。
相關文章
- Crash 日誌符號化符號
- crash日誌符號化,以分析崩潰符號
- 深入理解ES6--6.符號與符號屬性符號
- iOS系統app崩潰日誌手動符號化iOSAPP符號
- iOS Crash日誌分析必備:符號化系統庫方法iOS符號
- iOS應用崩潰日誌.crash報告符號化/.dYSM符號表手動解析(Objective-C)iOS應用崩潰符號Object
- Android NDK隱藏jni動態庫的內部符號表Android符號
- 【譯】深入理解G1的GC日誌(一)GC
- 深入理解Logger日誌——框架繫結原理框架
- 深入理解typeof操作符
- Android優化系列一:日誌清理Android優化
- 深入SQLServer日誌收縮SQLServer
- 深入理解AndroidAndroid
- 深入理解 Java 中 protected 修飾符Java
- 結構化日誌記錄 - 更好地理解系統
- Android Jetpack - Emoji表情符號初探AndroidJetpack符號
- Android NDKAndroid
- 深入分析Oracle日誌檔案Oracle
- Android日誌Log使用Android
- Android除錯----日誌Android除錯
- android ndk整合Android
- 日誌 ** 序列號 ** 無法歸檔
- eclipse設定檢視GC日誌和如何理解GC日誌EclipseGC
- ORA-16068: 重做日誌檔案的啟用識別符號匹配出錯符號
- Android-Crash日誌抓取Android
- Android ANR日誌分析指南Android
- Android 深入理解 Notification 機制Android
- 深入理解 Android 中的 MatrixAndroid
- matlab符號表示式的化簡Matlab符號
- [深入理解Android卷二 全文-第五章]深入理解PowerManagerServiceAndroid
- 加深C# 中字串前加@符號理解以及使用~~C#字串符號
- Redis持久化——AOF日誌Redis持久化
- MySQL慢日誌優化MySql優化
- tinylog簡化日誌
- 無符號數相減得到的是無符號還是有符號?符號
- 使用去中心化識別符號 (DID) 作為識別符號元系統中心化符號
- Android NDK初識Android
- 深入理解 Java 序列化Java