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