一、什么是Android的C/C++ NativeCrash
Android上的Crash可以分兩種:
1、Java Crash
java代碼導(dǎo)致jvm退出,彈出“程序已經(jīng)崩潰”的對(duì)話框,最終用戶點(diǎn)擊關(guān)閉后進(jìn)程退出。
Logcat 會(huì)在“AndroidRuntime”tag下輸出Java的調(diào)用棧。
2、Native Crash
通過(guò)NDK,使用C/C++開發(fā),導(dǎo)致進(jìn)程收到錯(cuò)誤信號(hào),發(fā)生Crash,Android 5.0之前進(jìn)程直接退出(閃退) , Android 5.0之后會(huì)彈“程序已崩潰”的對(duì)話框。
Logcat 會(huì)在“debug”tag下輸出dump信息:
- 錯(cuò)誤信號(hào):11是信號(hào)量sigNum,SIGSEGV是信號(hào)的名字,SEGV_MAPERR是SIGSEGV下的一種類型。
- 寄存器快照:進(jìn)程收到錯(cuò)誤信號(hào)時(shí)保存下來(lái)的寄存器快照,其中PC寄存器存儲(chǔ)的就是下個(gè)要運(yùn)行的指令(出錯(cuò)的位置)。
- 調(diào)用棧:#00是棧頂,#02是棧底,#02調(diào)用#01調(diào)用#00方法,#00的方法時(shí)libspirit.so中的Spirit類下的testCrash方法,出錯(cuò)的地方是testCrash方法內(nèi)匯編偏移17(不是行號(hào)哦?。?/li>
二、什么是錯(cuò)誤信號(hào)
Android本質(zhì)就是一個(gè)Linux,信號(hào)跟Linux信號(hào)是同一個(gè)東西,信號(hào)本身是用于進(jìn)程間通信的沒(méi)有正確錯(cuò)誤之分,但官方給一些信號(hào)賦予了特定的含義及特定處理動(dòng)作,
通常我們說(shuō)的錯(cuò)誤信號(hào)有5個(gè)(Bugly全部都能上報(bào)),系統(tǒng)默認(rèn)處理就是dump出堆棧,并退出進(jìn)程:
通常的來(lái)源有三個(gè):
1、硬件發(fā)生異常,即硬件(通常是CPU)檢測(cè)到一個(gè)錯(cuò)誤條件并通知Linux內(nèi)核,內(nèi)核處理該異常,給相應(yīng)的進(jìn)程發(fā)送信號(hào)。硬件異常的例子包括執(zhí)行一條異常的機(jī)器語(yǔ)言指令,諸如,被0除,或者引用了無(wú)法訪問(wèn)的內(nèi)存區(qū)域。大部分信號(hào)如果沒(méi)有被進(jìn)程處理,默認(rèn)的操作就是殺死進(jìn)程。在本文中,SIGSEGV(段錯(cuò)誤),SIGBUS(內(nèi)存訪問(wèn)錯(cuò)誤),SIGFPE(算數(shù)異常)屬于這種信號(hào)。
2、進(jìn)程調(diào)用的庫(kù)發(fā)現(xiàn)錯(cuò)誤,給自己發(fā)送中止信號(hào),默認(rèn)情況下,該信號(hào)會(huì)終止進(jìn)程。在本文中,SIGABRT(中止進(jìn)程)屬于這種信號(hào)。
3、用戶(手賤)或第三方App(惡意)通過(guò)kill-信號(hào) pid的方式給錯(cuò)誤進(jìn)程發(fā)送,這時(shí)signal中的si_code會(huì)小于0。
三、抖幾個(gè)常見錯(cuò)誤
1. 空指針
代碼示例
|
int* p = 0; //空指針 *p = 1; //寫空指針指向的內(nèi)存,產(chǎn)生SIGSEGV信號(hào),造成Crash |
原因分析
在進(jìn)程的地址空間中,從0開始的第一個(gè)頁(yè)面的權(quán)限被設(shè)置為不可讀也不可寫,當(dāng)進(jìn)程的指令試圖訪問(wèn)該頁(yè)面中的地址時(shí)(如讀取空指針指向的內(nèi)存),處理器就會(huì)產(chǎn)生一個(gè)異常,然后Linux內(nèi)核會(huì)給該進(jìn)程發(fā)送一個(gè)段錯(cuò)誤信號(hào)(SIGSEGV),默認(rèn)的操作就是殺死進(jìn)程,并產(chǎn)生core文件。
解決方法
在使用指針前加以判斷,如果為空,則是不可訪問(wèn)的。
Bug評(píng)述
空指針是很容易出現(xiàn)的一種bug,在代碼量大,趕開發(fā)進(jìn)度時(shí)很容易出現(xiàn),但是它也很容易被發(fā)現(xiàn)和修復(fù)。
2. 野指針
代碼示例
|
int* p; //野指針,未初始化,其指向的地址通常是隨機(jī)的 *p = 1; //寫野指針指向的內(nèi)存,有可能不會(huì)馬上Crash,而是破壞了別處的內(nèi)存 |
原因分析
野指針指向的是一個(gè)無(wú)效的地址,該地址如果是不可讀不可寫的,那么會(huì)馬上Crash(內(nèi)核給進(jìn)程發(fā)送段錯(cuò)誤信號(hào)SIGSEGV),這時(shí)bug會(huì)很快被發(fā)現(xiàn)。
如果訪問(wèn)的地址為可寫,而且通過(guò)野指針修改了該處的內(nèi)存,那么很有可能會(huì)等一段時(shí)間(其它的代碼使用了該處的內(nèi)存后)才發(fā)生Crash。這時(shí)查看Crash時(shí)顯示的調(diào)用棧,和野指針?biāo)诘拇a部分,有可能基本上沒(méi)有任何關(guān)聯(lián)。
解決方法
- 在指針變量定義時(shí),一定要初始化,特別是在結(jié)構(gòu)體或類中的成員指針變量。
- 在釋放了指針指向的內(nèi)存后,要把該指針置為NULL(但是如果在別的地方也有指針指向該處內(nèi)存的話,這種方式就不好解決了)。
- 野指針造成的內(nèi)存破壞的問(wèn)題,有時(shí)候光看代碼很難查找,通過(guò)代碼分析工具也很難找出,只有通過(guò)專業(yè)的內(nèi)存檢測(cè)工具,才能發(fā)現(xiàn)這類bug。
Bug評(píng)述
野指針的bug,特別是內(nèi)存破壞的問(wèn)題,有時(shí)候查起來(lái)毫無(wú)頭緒,沒(méi)有一點(diǎn)線索,讓開發(fā)者感覺到很茫然和無(wú)助( Bugly上報(bào)的堆??床怀鋈魏螁?wèn)題)??梢哉f(shuō)內(nèi)存破壞bug是服務(wù)器穩(wěn)定性最大的殺手,也是C/C++在開發(fā)應(yīng)用方面相比于其它語(yǔ)言(如Java, C#)的最大劣勢(shì)之一。
3. 數(shù)組越界
代碼示例
|
int arr[10]; arr[10] = 1; //數(shù)組越界,有可能不會(huì)馬上Crash,而是破壞了別處的內(nèi)存 |
原因分析
數(shù)組越界和野指針類似,訪問(wèn)了無(wú)效的地址,如果該地址不可讀寫,則會(huì)馬上Crash(內(nèi)核給進(jìn)程發(fā)送段錯(cuò)誤信號(hào)SIGSEGV),如果修改了該處的內(nèi)存,造成內(nèi)存破壞,那么有可能會(huì)等一段時(shí)間才在別處發(fā)生Crash。
解決方法
- 所有數(shù)組遍歷的循環(huán),都要加上越界判斷。
- 用下標(biāo)訪問(wèn)數(shù)組時(shí),要判斷是否越界。
- 通過(guò)代碼分析工具可以發(fā)現(xiàn)絕大部分的數(shù)組越界問(wèn)題。
Bug評(píng)述
數(shù)組越界也是一種內(nèi)存破壞的bug,有時(shí)候與野指針一樣也是很難查找的。
4. 整數(shù)除以零
代碼示例
|
int a = 1; int b = a / 0; //整數(shù)除以0,產(chǎn)生SIGFPE信號(hào),導(dǎo)致Crash |
原因分析
整數(shù)除以零總是產(chǎn)生SIGFPE(浮點(diǎn)異常,產(chǎn)生SIGFPE信號(hào)時(shí)并非一定要涉及浮點(diǎn)算術(shù),整數(shù)運(yùn)算異常也用浮點(diǎn)異常信號(hào)是為了保持向下兼容性)信號(hào),默認(rèn)的處理方式是終止進(jìn)程,并生成core文件。
解決方法
在做整數(shù)除法時(shí),要判斷被除數(shù)是否為0的情況。
Bug評(píng)述
整數(shù)被0除的bug很容易被開發(fā)者忽視,因?yàn)橥ǔ1怀龜?shù)為0的情況在開發(fā)環(huán)境下很難出現(xiàn),但是到了生產(chǎn)環(huán)境,龐大的用戶量和復(fù)雜的用戶輸入,就很容易導(dǎo)致被除數(shù)為0的情況出現(xiàn)了。
5. 格式化輸出參數(shù)錯(cuò)誤
代碼示例
|
//格式化參數(shù)錯(cuò)誤,可能會(huì)導(dǎo)致非法的內(nèi)存訪問(wèn),從而造成宕機(jī) char text[200]; snprintf(text,200,"Valid %u, Invalid %u %s", 1);//format格式不匹配 |
原因分析
格式化參數(shù)錯(cuò)誤也和野指針類似,但是只會(huì)讀取無(wú)效地址的內(nèi)存,而不會(huì)造成內(nèi)存破壞,因此其結(jié)果是要么打印出錯(cuò)亂的數(shù)據(jù),要么訪問(wèn)了無(wú)讀寫權(quán)限的內(nèi)存(收到段錯(cuò)誤信號(hào)SIGSEGV)而立即宕機(jī)。
解決方法
- 在書寫輸出格式和參數(shù)時(shí),要做到參數(shù)個(gè)數(shù)和類型都要與輸出格式一致。
- 在GCC的編譯選項(xiàng)中加入-wformat,讓GCC在編譯時(shí)檢測(cè)出此類錯(cuò)誤。
6、緩沖區(qū)溢出
代碼示例
|
char szBuffer[10]; //由于函數(shù)棧是從高地址往低地址創(chuàng)建,而sprintf是從低地址往高地址打印字符, //如果超出了緩沖區(qū)的大小,函數(shù)的棧幀會(huì)被破壞,在函數(shù)返回時(shí)會(huì)跳轉(zhuǎn)到未知的地址上, //基本上都會(huì)造成訪問(wèn)異常,從而產(chǎn)生SIGABRT或SIGSEGV,造成Crash sprintf(szBuffer, "Stack Buffer Overrun!111111111111111" "111111111111111111111"); |
原因分析
通過(guò)往程序的緩沖區(qū)寫超出其長(zhǎng)度的內(nèi)容,造成緩沖區(qū)的溢出,從而破壞函數(shù)調(diào)用的堆棧,修改函數(shù)調(diào)用的返回地址。如果不是黑客故意攻擊,那么最終函數(shù)調(diào)用很可能會(huì)跳轉(zhuǎn)到無(wú)法讀寫的內(nèi)存區(qū)域,產(chǎn)生段錯(cuò)誤信號(hào)SIGSEGV或SIGABRT,造成程序崩潰,并生成core文件。
解決方法
- 檢查所有容易產(chǎn)生漏洞的庫(kù)調(diào)用,比如sprintf,strcpy等,它們都沒(méi)有檢查輸入?yún)?shù)的長(zhǎng)度。
- 使用帶有長(zhǎng)度檢查的庫(kù)調(diào)用,如用snprintf來(lái)代替sprintf,或者自己在sprintf上封裝一個(gè)帶長(zhǎng)度檢查的函數(shù)。
- 在GCC編譯時(shí),在-O1以上的優(yōu)化行為下,使用-D_FORTIFY_SOURCE=level進(jìn)行編譯(其中l(wèi)evel=1或2,level代表的是檢測(cè)級(jí)別的不同,數(shù)值越大越嚴(yán)格)。這樣GCC會(huì)在編譯時(shí)報(bào)告緩沖區(qū)溢出的錯(cuò)誤。
- 在GCC編譯時(shí)加上-fstack-protector或-fstack-protector-all選項(xiàng),使得堆棧保護(hù)(stack-smashingprotector, SSP)功能生效。該功能會(huì)在編譯后的匯編代碼中插入堆棧檢測(cè)的代碼,并在運(yùn)行時(shí)能夠檢測(cè)到棧破壞并輸出報(bào)告。
Bug評(píng)述
緩沖區(qū)溢出是一種非常普遍、非常危險(xiǎn)的漏洞,在各種操作系統(tǒng)、應(yīng)用軟件中廣泛存在。黑客在進(jìn)行攻擊時(shí),輸入的字符串一般不會(huì)讓程序崩潰,而是修改函數(shù)的返回地址,使程序跳轉(zhuǎn)到別的地方,轉(zhuǎn)而執(zhí)行黑客安排好的指令,以達(dá)到攻擊的目的。
緩沖區(qū)溢出后,調(diào)試生成的core,可以看見調(diào)用棧是混亂的,因?yàn)楹瘮?shù)的返回地址已經(jīng)被修改到隨機(jī)的地址上去了。
服務(wù)器宕機(jī)后,如果core文件和可執(zhí)行文件是匹配的,但是調(diào)用棧是錯(cuò)亂的,那么很大的可能性是發(fā)生了緩沖區(qū)溢出。
7、主動(dòng)拋出異常
代碼示例
|
if ((*env)->ExceptionOccurred(env) != 0) { //動(dòng)態(tài)庫(kù)在內(nèi)部運(yùn)行出現(xiàn)錯(cuò)誤時(shí),大都會(huì)主動(dòng)abort,終止運(yùn)行 abort(); //給當(dāng)前進(jìn)程發(fā)送信號(hào)SIGABRT } |
解決方法
查看堆棧找出abort的原因
Bug評(píng)述
如果是程序主動(dòng)abort的,通過(guò)堆棧加源碼還是很好定位的,但往往abort的位置是在系統(tǒng)庫(kù)中,就不好定位了,需要多查看系統(tǒng)API的使用方法,檢查是否使用不當(dāng)。
四、小編有話說(shuō)
Java異常已經(jīng)搞得大家焦頭爛額了,Native異常更是恐怖,數(shù)量比Java異常多得多,只是看堆棧還不好定位(畫小圈圈詛咒萬(wàn)惡的指針)。非常感謝王競(jìng)原童鞋能在日常開發(fā)遇到的崩潰中總結(jié)出這一篇寶貴的文章!
轉(zhuǎn)載自:http://bugly.qq.com/blog/?p=131 作者:王競(jìng)原
|