爲了進行代碼及產品保護,幾乎全部的非開源App都會進行代碼混淆,這樣當收集到崩潰信息後,就需 要進行符號化來還原代碼信息,以便開發者能夠定位Bug。基於使用SDK和NDK的不一樣,Android的崩潰分爲兩類:Java崩潰和C/C++崩潰。Java崩潰經過mapping.txt文件進行符號化,比較簡單直觀,而C/C++崩潰的符號化則須要使用Google自帶的一些NDK工具,好比ndk-stack、addr2line、objdump等。本文不去討論如何使用這些工具,有興趣的朋友能夠參考同事寫的另外一篇文章《如何定位Android NDK開發中遇到的錯誤》,裏面作了詳細的描述。html
基於NDK的Android的開發都會生成一個動態連接庫(so),它是基於C/C++編譯生成的。動態連接庫在Linux系統下普遍使用,而Android系統底層是基於Linux的,因此NDK so庫的編譯生成遵循相同的規則,只不過Google NDK把相關的交叉編譯工具都封裝了。數組
Ndk-build編譯時會生成的兩個同名的so庫,位於不一樣的目錄/projectpath/libs/armeabi/xxx.so和/project path/obj/local/armeabi/xxx.so,比較兩個so文件會發現體積相差很大。前者會跟隨App一塊兒發佈,因此儘量的小,然後者包含了不少調試信息,主要爲了gdb調試的時候使用,固然NDK的日誌符號化信息也包含其中。bash
本文主要分析這個包含調試信息的so動態庫,深刻分析它的組成結構。在開始以前,先來講說這樣作的目的或者好處。如今的App基本都會採集上報崩潰時的日誌信息,不管是採用第三方雲平臺(如Testin崩潰分析+),仍是本身搭建雲服務,都要將含調試信息的so動態庫上傳,實現雲端日誌符號化以及雲端可視化管理。移動App的快速迭代,使得咱們必須存儲管理每個版本的debugso庫,而其包含了不少與符號化無關的信息。若是咱們只提取出符號化須要的信息,那麼符號化文件的體積將會呈現數量級的減小。同時能夠在自定義的符號化文件中添加App的版本號等信息,實現符號化提取、上傳到雲端、雲端解析及可視化等自動化部署。另外,從技術角度講,你將不在懼怕看到「unresolvedsymbol」 linking errors,更從容地 debugging C/C++ crash或者hacking一些so文件。數據結構
首先經過readelf來看看兩個不一樣目錄下的so庫有什麼不一樣架構
從中能夠清楚看到,包含調試信息的so庫多了8個.debug_開頭的條目以及.symtab和.strtab條目。符號化的本質,是經過堆棧中的地址信息,還原代碼原本的語句以及相應的行號,因此這裏只需解析.debug_line和.symtab,最終獲取到以下的信息就能夠實現符號化了。app
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(Executableand Linking Format),so動態庫屬於第三類shared object,它的總體組織結構以下:ide
ELF Header |
Program header table optional |
Section 1 |
... |
Section n |
... |
Section header table required |
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,包含了處理器類型、文件編碼格式、機器類型等,具體結構以下:
工具
Name | Value | Purpose |
EI_MAG0 | 0 | 前四個字節稱爲magic number,分別爲0x7f、’E’、’L’、’F’,代表文件類型爲ELF。ui |
EI_MAG1 |
1 | |
EI_MAG2 | 2 | |
EI_MAG3 | 3 | |
EI_CLASS |
4 | 代表文件是基於32-bit仍是64-bit,不一樣的方式,對齊方式不一樣,讀取某些內容的大小不一樣。 |
EI_DATA | 5 | 代表文件數據結構的編碼方式,主要分爲大端和小端兩種 |
EI_VERSION |
6 | 指定了ELF文件頭的版本號 |
EI_OSABI |
7 | 指定使用了哪一種OS-或者ABI-的ELF擴展 |
EI_ABIVERSION | 8 | 指定該ELF目標文件的目標ABI版本 |
EI_PAD |
9 | 保留字段起始處,直到第16個字節 |
EI_NIDENT | 16 | 表明了e_ident數組的大小,固定爲16 |
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的類型包含一下幾種
Name | Value |
STT_NOTYPE | 0 |
STT_OBJECT | 1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
10 |
STT_HIOS | 12 |
|
13 |
|
15 |
其中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是一種調試文件格式,不少編譯器和調試器都經過它進行源碼調試(gdb等)。儘管它是一種獨立 的目標文件格式,但每每嵌入在ELF文件中。前面經過readelf看到的8個.debug_* Section所有都屬於DWARF格式。本文將只討論與符號化相關的.debug_line部分,更多的DWARF信息請查看參考文獻的內容。
.debug_line部分包含了行號信息,經過它能夠將代碼語句和機器指令地址對應,從而進行源碼調試。.debug_line有不少子項組成,每一個子項都包含相似數據塊頭的描述,稱爲Statement Program Prologue。Prologue提供瞭解碼程序指令和跳轉到其餘語句的信息,它包含以下字段,這些字段是以二進制格式順序存在的:
total_length |
uword |
整個子項佔用的字節大小,注意並不包括該字段自己 |
versio |
uhalf |
該子項格式的版本號,其實也是整個DWARF格式的版本號,目前總共有四個版本。 |
prologue_length |
uword |
prologue的長度,不包括該字段及前面的兩個字段佔用的字節數,即相對於本字段,程序語句自己的第一個字節的偏移量 |
minimum_instruction_length |
ubyte |
最小的目標機器指令 |
default_is_stmt |
ubyte |
is_stmt寄存器的初始值 |
line_base |
sbyte |
不一樣的操做碼,表明不一樣的含義,隻影響special opcodes |
line_range |
ubyte |
不一樣的操做碼,表明不一樣的含義,隻影響special opcodes |
opcode_base |
ubyte |
第一個操做碼的數值 |
standard_opcode_lengths | array of ubyte |
標準操做碼的LEB128操做數的數值 |
include_directories |
sequence |
目錄名字符序列 |
file_names |
sequence |
源代碼所在文件名字符序列 |
這裏用到的機器指令能夠分爲三類:
special opcodes |
單字節操做碼,不含參數,大多數指令屬於此類 |
standard opcodes |
單字節操做碼,能夠包含0個或者多個LEB128參數 |
extended opcodes |
多字節操做碼 |
這裏不作機器指令的解析說明,感興趣的,能夠查看參考文獻的內容。
經過.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中獲取的結果,進行對比合並,造成最終的結果
參考文獻:
http://www.csdn.net/article/2014-12-30/2823366-Locate-Android-NDK
http://eli.thegreenplace.net/2011/02/07/how-debuggers-work-part-3-debugging-information/