為了進(jìn)行代碼及產(chǎn)品保護(hù),幾乎所有的非開(kāi)源App都會(huì)進(jìn)行代碼混淆,這樣當(dāng)收集到崩潰信息后,就需 要進(jìn)行符號(hào)化來(lái)還原代碼信息,以便開(kāi)發(fā)者可以定位Bug?;谑褂肧DK和NDK的不同,Android的崩潰分為兩類(lèi):Java崩潰和C/C++崩潰。Java崩潰通過(guò)mapping.txt文件進(jìn)行符號(hào)化,比較簡(jiǎn)單直觀,而C/C++崩潰的符號(hào)化則需要使用Google自帶的一些NDK工具,比如ndk-stack、addr2line、objdump等。本文不去討論如何使用這些工具,有興趣的朋友可以參考同事寫(xiě)的另一篇文章《如何定位Android NDK開(kāi)發(fā)中遇到的錯(cuò)誤》,里面做了詳細(xì)的描述。
公司主營(yíng)業(yè)務(wù):網(wǎng)站設(shè)計(jì)制作、網(wǎng)站制作、移動(dòng)網(wǎng)站開(kāi)發(fā)等業(yè)務(wù)。幫助企業(yè)客戶真正實(shí)現(xiàn)互聯(lián)網(wǎng)宣傳,提高企業(yè)的競(jìng)爭(zhēng)能力。創(chuàng)新互聯(lián)是一支青春激揚(yáng)、勤奮敬業(yè)、活力青春激揚(yáng)、勤奮敬業(yè)、活力澎湃、和諧高效的團(tuán)隊(duì)。公司秉承以“開(kāi)放、自由、嚴(yán)謹(jǐn)、自律”為核心的企業(yè)文化,感謝他們對(duì)我們的高要求,感謝他們從不同領(lǐng)域給我們帶來(lái)的挑戰(zhàn),讓我們激情的團(tuán)隊(duì)有機(jī)會(huì)用頭腦與智慧不斷的給客戶帶來(lái)驚喜。創(chuàng)新互聯(lián)推出皮山免費(fèi)做網(wǎng)站回饋大家。
基于NDK的Android的開(kāi)發(fā)都會(huì)生成一個(gè)動(dòng)態(tài)鏈接庫(kù)(so),它是基于C/C++編譯生成的。動(dòng)態(tài)鏈接庫(kù)在Linux系統(tǒng)下廣泛使用,而Android系統(tǒng)底層是基于Linux的,所以NDK so庫(kù)的編譯生成遵循相同的規(guī)則,只不過(guò)Google NDK把相關(guān)的交叉編譯工具都封裝了。
Ndk-build編譯時(shí)會(huì)生成的兩個(gè)同名的so庫(kù),位于不同的目錄/projectpath/libs/armeabi/xxx.so和/project path/obj/local/armeabi/xxx.so,比較兩個(gè)so文件會(huì)發(fā)現(xiàn)體積相差很大。前者會(huì)跟隨App一起發(fā)布,所以盡可能的小,而后者包含了很多調(diào)試信息,主要為了gdb調(diào)試的時(shí)候使用,當(dāng)然NDK的日志符號(hào)化信息也包含其中。
本文主要分析這個(gè)包含調(diào)試信息的so動(dòng)態(tài)庫(kù),深入分析它的組成結(jié)構(gòu)。在開(kāi)始之前,先來(lái)說(shuō)說(shuō)這樣做的目的或者好處?,F(xiàn)在的App基本都會(huì)采集上報(bào)崩潰時(shí)的日志信息,無(wú)論是采用第三方云平臺(tái)(如Testin崩潰分析+),還是自己搭建云服務(wù),都要將含調(diào)試信息的so動(dòng)態(tài)庫(kù)上傳,實(shí)現(xiàn)云端日志符號(hào)化以及云端可視化管理。移動(dòng)App的快速迭代,使得我們必須存儲(chǔ)管理每一個(gè)版本的debugso庫(kù),而其包含了很多與符號(hào)化無(wú)關(guān)的信息。如果我們只提取出符號(hào)化需要的信息,那么符號(hào)化文件的體積將會(huì)呈現(xiàn)數(shù)量級(jí)的減少。同時(shí)可以在自定義的符號(hào)化文件中添加App的版本號(hào)等信息,實(shí)現(xiàn)符號(hào)化提取、上傳到云端、云端解析及可視化等自動(dòng)化部署。另外,從技術(shù)角度講,你將不在害怕看到“unresolvedsymbol” linking errors,更從容地 debugging C/C++ crash或者h(yuǎn)acking一些so文件。
首先通過(guò)readelf來(lái)看看兩個(gè)不同目錄下的so庫(kù)有什么不同
從中可以清楚看到,包含調(diào)試信息的so庫(kù)多了8個(gè).debug_開(kāi)頭的條目以及.symtab和.strtab條目。符號(hào)化的本質(zhì),是通過(guò)堆棧中的地址信息,還原代碼本來(lái)的語(yǔ)句以及相應(yīng)的行號(hào),所以這里只需解析.debug_line和.symtab,最終獲取到如下的信息就可以實(shí)現(xiàn)符號(hào)化了。
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
通常,目標(biāo)文件分為三類(lèi):relocatable文件、executable文件和shared object文件,它們格式稱(chēng)為ELF(Executableand Linking Format),so動(dòng)態(tài)庫(kù)屬于第三類(lèi)shared object,它的整體組織結(jié)構(gòu)如下:
ELF Header |
Program header table optional |
Section 1 |
... |
Section n |
... |
Section header table required |
ELF Header文件頭的結(jié)構(gòu)如下,記錄了文件其他內(nèi)容在文件中的偏移以及大小信息。這里以32bit為例。
typedef struct { unsigned char e_ident[EI_NIDENT]; Elf32_Half e_type; //目標(biāo)文件類(lèi)型,如relocatable、executable和shared object Elf32_Half e_machine; // 指定需要的特定架構(gòu),如Intel 80386,Motorola 68000 Elf32_Word e_version; // 目標(biāo)文件版本,通e_ident中的EI_VERSION Elf32_Addr e_entry; //指定入口點(diǎn)地址,如C可執(zhí)行文件的入口是_start(),而不是main() Elf32_Off e_phoff; // program header table 的偏移量 Elf32_Off e_shoff; // section header table的偏移量 Elf32_Word e_flags; // 處理器相關(guān)的標(biāo)志 Elf32_Half e_ehsize; // 代表ELF Header部分的大小 Elf32_Half e_phentsize; // program header table中每一項(xiàng)的大小 Elf32_Half e_phnum; // program header table包含多少項(xiàng) Elf32_Half e_shentsize; // section header table中每一項(xiàng)的大小 Elf32_Half e_shnum; // section header table包含多少項(xiàng) Elf32_Half e_shstrndx; //section header table中某一子項(xiàng)的index,該子項(xiàng)包含了所有section的字符串名稱(chēng) } Elf32_Ehdr;
其中e_ident為固定16個(gè)字節(jié)大小的數(shù)組,稱(chēng)為ELF Identification,包含了處理器類(lèi)型、文件編碼格式、機(jī)器類(lèi)型等,具體結(jié)構(gòu)如下:
Name | Value | Purpose |
EI_MAG0 | 0 | 前四個(gè)字節(jié)稱(chēng)為magic number,分別為0x7f、’E’、’L’、’F’,表明文件類(lèi)型為ELF。 |
EI_MAG1 | 1 | |
EI_MAG2 | 2 | |
EI_MAG3 | 3 | |
EI_CLASS | 4 | 表明文件是基于32-bit還是64-bit,不同的方式,對(duì)齊方式不同,讀取某些內(nèi)容的大小不同。 |
EI_DATA | 5 | 表明文件數(shù)據(jù)結(jié)構(gòu)的編碼方式,主要分為大端和小端兩種 |
EI_VERSION | 6 | 指定了ELF文件頭的版本號(hào) |
EI_OSABI | 7 | 指定使用了哪種OS-或者ABI-的ELF擴(kuò)展 |
EI_ABIVERSION | 8 | 指定該ELF目標(biāo)文件的目標(biāo)ABI版本 |
EI_PAD | 9 | 保留字段起始處,直到第16個(gè)字節(jié) |
EI_NIDENT | 16 | 代表了e_ident數(shù)組的大小,固定為16 |
Sections
該部分包含了除ELF Header、program header table以及section header table之外的所有信息。通過(guò)section header table可以找到每一個(gè)section的基本信息,如名稱(chēng)、類(lèi)型、偏移量等。
先來(lái)看看Section Header的內(nèi)容,仍以32-bit為例:
typedef struct { Elf32_Word sh_name; // 指定section的名稱(chēng),該值為String Table字符串表中的索引 Elf32_Word sh_type; // 指定section的分類(lèi) Elf32_Word sh_flags; // 該字段的bit代表不同的section屬性 Elf32_Addr sh_addr; // 如果section出現(xiàn)在內(nèi)存鏡像中,該字段表示section第一個(gè)字節(jié)的地址 Elf32_Off sh_offset; // 指定section在文件中的偏移量 Elf32_Word sh_size; // 指定section占用的字節(jié)大小 Elf32_Word sh_link; // 相關(guān)聯(lián)的section header table的index Elf32_Word sh_info; // 附加信息,意義依賴于section的類(lèi)型 Elf32_Word sh_addralign; // 指定地址對(duì)其約束 Elf32_Word sh_entsize; // 如果section包含一個(gè)table,該值指定table中每一個(gè)子項(xiàng)的大小 } Elf32_Shdr;
通過(guò)Section Header的sh_name可以找到指定的section,比如.debug_line、.symbol、.strtab。
String Table
String Table包含一系列以\0結(jié)束的字符序列,最后一個(gè)字節(jié)設(shè)置為\0,表明所有字符序列的結(jié)束,比如:
String Table也屬于section,只不過(guò)它的偏移量直接在ELF Header中的e_shstrndx字段指定。String Table的讀取方法是,從指定的index開(kāi)始,直到遇到休止符。比如要section header中sh_name獲取section的名稱(chēng),比如sh_name = 7, 則從string table字節(jié)流的第7個(gè)index開(kāi)始(注意這里從0開(kāi)始),一直讀到第一個(gè)休止符(index=18),讀取到的名稱(chēng)為.debug_line
Symbol Table
該部分包含了程序符號(hào)化的定義相關(guān)信息,比如函數(shù)定義、變量定義等,每一項(xiàng)的定義如下:
# Symbol Table Entry typedef struct { Elf32_Word st_name; //symbol字符串表的索引 Elf32_Addr st_value; //symbol相關(guān)的值,依賴于symbol的類(lèi)型 Elf32_Word st_size; //symbol內(nèi)容的大小 unsigned char st_info; //symbol的類(lèi)型及其屬性 unsigned char st_other; //symbol的可見(jiàn)性,比如類(lèi)的public等屬性 Elf32_Half st_shndx; //與此symbol相關(guān)的section header的索引 } Elf32_Sym;
Symbol的類(lèi)型包含一下幾種
Name | Value |
STT_NOTYPE | 0 |
STT_OBJECT | 1 |
| 2 |
| 3 |
| 4 |
| 5 |
| 6 |
| 10 |
STT_HIOS | 12 |
| 13 |
| 15 |
其中STT_FUNC就是我們要找的函數(shù)symbol。然后通過(guò)st_name從symbol字符串表中獲取到相應(yīng)的函數(shù)名(如JNI_OnLoad)。當(dāng)symbol類(lèi)型為STT_FUNC時(shí),st_value代表該symbol的起始地址,而(st_value+st_size)代表該symbol的結(jié)束地址。
回顧之前的提到的.symtab和.strtab兩個(gè)部分,對(duì)應(yīng)的便是Symbol Section和Symbol String Section。
DWARF是一種調(diào)試文件格式,很多編譯器和調(diào)試器都通過(guò)它進(jìn)行源碼調(diào)試(gdb等)。盡管它是一種獨(dú)立 的目標(biāo)文件格式,但往往嵌入在ELF文件中。前面通過(guò)readelf看到的8個(gè).debug_* Section全部都屬于DWARF格式。本文將只討論與符號(hào)化相關(guān)的.debug_line部分,更多的DWARF信息請(qǐng)查看參考文獻(xiàn)的內(nèi)容。
.debug_line部分包含了行號(hào)信息,通過(guò)它可以將代碼語(yǔ)句和機(jī)器指令地址對(duì)應(yīng),從而進(jìn)行源碼調(diào)試。.debug_line有很多子項(xiàng)組成,每個(gè)子項(xiàng)都包含類(lèi)似數(shù)據(jù)塊頭的描述,稱(chēng)為Statement Program Prologue。Prologue提供了解碼程序指令和跳轉(zhuǎn)到其他語(yǔ)句的信息,它包含如下字段,這些字段是以二進(jìn)制格式順序存在的:
total_length | uword | 整個(gè)子項(xiàng)占用的字節(jié)大小,注意并不包括該字段本身 |
versio | uhalf | 該子項(xiàng)格式的版本號(hào),其實(shí)也是整個(gè)DWARF格式的版本號(hào),目前總共有四個(gè)版本。 |
prologue_length | uword | prologue的長(zhǎng)度,不包括該字段及前面的兩個(gè)字段占用的字節(jié)數(shù),即相對(duì)于本字段,程序語(yǔ)句本身的第一個(gè)字節(jié)的偏移量 |
minimum_instruction_length | ubyte | 最小的目標(biāo)機(jī)器指令 |
default_is_stmt | ubyte | is_stmt寄存器的初始值 |
line_base | sbyte | 不同的操作碼,代表不同的含義,只影響special opcodes |
line_range | ubyte | 不同的操作碼,代表不同的含義,只影響special opcodes |
opcode_base | ubyte | 第一個(gè)操作碼的數(shù)值 |
standard_opcode_lengths | array of ubyte | 標(biāo)準(zhǔn)操作碼的LEB128操作數(shù)的數(shù)值 |
include_directories | sequence | 目錄名字符序列 |
file_names | sequence | 源代碼所在文件名字符序列 |
這里用到的機(jī)器指令可以分為三類(lèi):
special opcodes | 單字節(jié)操作碼,不含參數(shù),大多數(shù)指令屬于此類(lèi) |
standard opcodes | 單字節(jié)操作碼,可以包含0個(gè)或者多個(gè)LEB128參數(shù) |
extended opcodes | 多字節(jié)操作碼 |
這里不做機(jī)器指令的解析說(shuō)明,感興趣的,可以查看參考文獻(xiàn)的內(nèi)容。
通過(guò).debug_line,我們最終可以獲得如下信息:文件路徑、文件名、行號(hào)以及起始地址。
最后我們匯總一下整個(gè)符號(hào)化提取的過(guò)程:
1、從ELF Header中獲知32bit或者64bit,以及大端還是小端,基于此讀取后面的內(nèi)容
2、從ELF Header中獲得Section Header Table在文件中的位置
3、讀取Section Header Table,從中獲得.debug_line、.symtab以及.strtab三個(gè)section在文中的位置
4、讀取.symtab和.strtab兩個(gè)section,最后獲得所有function symbol的名稱(chēng)、起始地址以及結(jié)束地址
5、讀取.debug_line,按照DWARF格式解析獲取文件名稱(chēng)、路徑、行號(hào)以及起始地址
6、對(duì)比步驟4和5中獲取的結(jié)果,進(jìn)行對(duì)比合并,形成最終的結(jié)果
參考文獻(xiàn):
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/
http://www.sco.com/developers/gabi/latest/ch5.intro.html
http://www.dwarfstd.org/
當(dāng)前標(biāo)題:深入理解AndroidNDK日志符號(hào)化
網(wǎng)站鏈接:http://www.rwnh.cn/article14/jdcsge.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供網(wǎng)站建設(shè)、網(wǎng)站收錄、定制開(kāi)發(fā)、App開(kāi)發(fā)、網(wǎng)站營(yíng)銷(xiāo)、營(yíng)銷(xiāo)型網(wǎng)站建設(shè)
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)