在Linux上編寫運行C語言程序,常常會遇到程序崩潰、卡死等異常的狀況。程序崩潰時最多見的就是程序運行終止,報告Segmentation fault (core dumped)錯誤。而程序卡死通常來源於代碼邏輯的缺陷,致使了死循環、死鎖等問題。總的來看,常見的程序異常問題通常能夠分爲非法內存訪問和資源訪問衝突兩大類。php
非法內存訪問是最多見的程序異樣緣由,可能開發者看的「表象」不盡相同,可是不少狀況下都是因爲非法內存訪問引發的。html
非法指針是最典型的非法內存訪問案例,空指針、指向非法地址的指針是代碼中最常出現的錯誤。linux
示例代碼以下:編程
long *ptr; *ptr = 0; // 空指針 ptr = (long *)0x12345678; *ptr = 100; // 非法地址訪問
不管是訪問地址爲0的空指針,仍是用戶態無效的地址,都會致使非法指針訪問錯誤。實際編程過程當中,強制類型轉換一不當心就會產生非法指針,所以作強制類型轉換時要格外注意,最好事先作好類型檢查,以免該問題。數組
在多線程程序中,非法指針的產生可能就沒那麼容易發現了。通常狀況下,多個線程對共享的數據同時寫,或者一寫多讀時,若是不加鎖保證共享數據的同步訪問,則會很容易致使數據訪問衝突,繼而引起非法指針、產生錯誤數據,甚至影響執行邏輯。安全
示例代碼以下:bash
// 全局變量 long *ptr = (long *)malloc(sizeof(long)); // 線程1 if (ptr) { *ptr = 100; // 潛在的非法地址訪問 } // 線程2 free(ptr); ptr = NULL;
上述代碼中,全局初始化了指針ptr,線程1會判斷該指針不爲NULL時進行寫100操做,而線程2會釋放ptr指向的內存,並將ptr置爲NULL。雖然線程1作了判斷處理,可是多線程環境下,則會出現線程2剛調用完free操做,還將來得及將ptr設爲NULL
時,發生線程上下文切換,轉而執行線程1的寫100操做,從而引起非法地址訪問。服務器
解決併發數據訪問衝突的方案是使用鎖同步線程。針對圖中的線程同步問題,只須要在線程1和線程2的處理邏輯前,使用讀寫鎖同步便可。操做系統或者gcc的庫函數內也存在不少線程不安全的API,在使用這些API時,必定要仔細閱讀相關的API文檔,使用線程鎖進行同步訪問。多線程
內存訪問越界常常出如今對數組處理的過程當中。自己C語言並未有對數組邊界的檢查機制,所以在越界訪問數組內存時並不必定會產生運行時錯誤,可是由於越界訪問繼而引起的連鎖反應就沒法避免了。併發
示例代碼以下:
void out_of_bound() { long *ptr; long buffer[] = {0}; ptr = buffer; buffer[1] = 0; // 越界訪問致使ptr被覆蓋 ptr[0]++; }
示例代碼在函數out_of_bound內定義了兩個變量:指針ptr和數組buffer。指針ptr指向buffer其實地址,正常狀況下使用ptr[0]能夠訪問訪問到buffer的第一個元素。然而對buffer[1]的越界寫操做會直接覆蓋ptr的值爲0,從而致使ptr爲空指針。
瞭解該問題的緣由須要清楚局部變量在棧內的存儲機制。在函數調用時,會將調用信息、局部變量等保存在進程的棧內。棧是從高地址到低地址增加的,所以先定義的局部變量的地址通常大於後定義的局部變量地址。上述代碼中,buffer和ptr的大小都是8Byte,所以buffer[1]實際就是ptr所在的內存。這樣對buffer[1]的寫操做會覆蓋ptr的值就不足爲怪了。總之,對數組訪問的時候,作好邊界檢查是重中之重。相似的問題也出如今對字符串的操做中,包括gcc提供的字符串庫函數也存在該問題,使用時須要尤爲注意。
說到邊界檢查,這裏引伸出一個話題。在對數組處理時,常常會遇到逆序遍歷數組後n-1個元素的狀況,有些時候一不當心就會這樣實現代碼:
void backScanArray(long buffer[]) { for(unsigned int index = sizeof(buffer) / sizeof(long) - 1; index > 0; index--) { printf("%ld\n", buffer[index]); } }
乍一看,代碼邏輯幾乎沒什麼問題,但是一旦buffer長度爲0時,就會觸發死循環了。舉出這個極端的例子主要是爲了說明數組邊界檢查時要格外當心。
緩衝區溢出攻擊是系統安全領域常見的話題,其本質仍是數組越界訪問的一個特殊例子。爲了方便討論,這裏仍舉緩衝區在棧內存的例子。(緩衝區溢出攻擊也能夠發生的堆內存中,感興趣的讀者可閱讀《0day安全軟件漏洞分析技術》一書)
咱們仍使用第三節的示例代碼,不過修改了一個字符:
void stack_over_flow() { long *ptr; long buffer[] = {0}; ptr = buffer; buffer[3] = 0; // 緩衝區溢出攻擊 ptr[0]++; }
雖然只是修改了一個字符,可是行爲已經和以前的代碼徹底不一樣了。實際在函數調用時,棧內不止保存了局部變量,還包括調用參數、調用後返回地址、調用前的rbp(棧基址寄存器)的值,俗稱棧幀。
隨着buffer越界的索引不斷增大,能夠覆蓋的信息能夠愈來愈多,甚至是上級調用的函數棧幀信息均可以被覆蓋。修改buffer[3]的值意味着stack_over_flow函數調用返回後,會跳轉到buffer[3]的值對應的地址上執行,而這個地址是0,程序會直接崩潰。試想若是將該值設置爲一個惡意的代碼入口地址,那麼就意味着潛在的巨大系統安全風險。緩衝求溢出攻擊的具體操做方式其實更復雜,這裏只是描述了其基本思想,感興趣的讀者能夠參考我以前的博文《緩衝區溢出攻擊》。
此處的棧內存溢出和前邊討論的棧內緩衝區溢出並非同一個概念。操做系統爲每一個進程分配的最大的棧內存大小是有最大上限的,所以當函數的局部變量的大小超過必定大小後(考慮到進程自己使用了部分棧內存),進程的棧內存便不夠使用了,因而就發生了溢出。
經過Linux命令能夠查看當前系統設置的進程最大棧大小(單位:KB):
$ ulimit -s 8192
若是函數內申請的數組大小超過該值(實際上比該值略小),則會引起棧內存溢出異常。
另外一種觸發棧內存溢出的方式是左遞歸(無限遞歸):
void left_recursive() { left_recursive(); }
因爲每次函數調用都會開闢新棧幀保存函數調用信息,而左遞歸邏輯上是不會終止的,所以總有進程棧內存被耗盡的時候,屆時便發生了棧內存溢出。
堆內存溢出與棧內存溢出是同一類概念,不過進程堆空間的大小上限,由於操做系統的分頁機制,理論上只受限於機器位長,即使物理內存和swap分區大小不足,也能夠經過操做系統的配置進行擴展。鑑於堆內存大小的這些性質,通常的程序不太容易觸發堆內存溢出異常。可是長期駐留內存的服務器進程,若是由於程序邏輯的缺陷,致使程序的部份內存一直申請,而得不到釋放的話,長此以往,就會觸發堆內存溢出,從而進程被操做系統強制kill掉,這就是常說的內存泄漏問題。
C語言使用malloc/free盡享堆內存的申請和釋放,開發者編寫程序時,必須當心翼翼地控制這兩對函數的調用邏輯,以防申請和釋放不對等誘發內存泄漏問題。而實際開發過程當中,人工保證這樣的準確性是十分困難的,後邊咱們會介紹如何使用分析工具幫咱們排查程序中潛在的內存漏洞問題。
前面講到,爲了解決多線程共享數據訪問衝突的問題,須要使用線程鎖同步線程的執行邏輯。而對鎖的不正當使用,一樣會產生程序異常,即死鎖。死鎖不會致使前邊所述的直接致使程序崩潰的異常,而是會掛起進程的線程,從而致使程序的部分任務卡死,不能提供正常的服務。
最典型的死鎖產生方式,就是熟知的ABBA鎖。
圖中仍使用兩個線程做爲示例,假設線程1在申請完A鎖後,發生了上下文切換執行線程2,線程2申請B鎖成功後,再去申請A鎖就會失敗,從而致使線程2掛起。此時,上下文即使再次切換到線程1,線程1也沒法成功申請到B鎖,從而線程1也會掛起。這樣,線程1和2都沒法成功申請到本身想要的鎖,也沒法釋放本身已經申請到的鎖給其餘其餘線程使用,從而致使死鎖。
解決此類死鎖的辦法就是讓每一個線程申請鎖時是批量申請,要麼一次性所有申請成功,要麼一次性都不申請。還有一種辦法就是提早預知該狀況的發生,不使用兩個鎖同步線程,這就須要人工費力地排查潛在的死鎖可能,固然,也有分析工具幫助開發者完成此類工做,稍後會做介紹。
前面提到的程序異常類型,除了死循環和死鎖致使進程卡死以外,其餘的異常都會致使進程崩潰,觸發Segmentation fault (core dumped)錯誤。Linux操做系統提供了容許程序core dumped時生成core dumped文件紀錄程序崩潰時的「進程快照」,以供開發者分析程序的出錯行爲和緣由,使用gdb就能夠調試分析core dumped文件。而對於內存泄漏和死鎖,開源工具Valgrind提供了相關的分析功能(Valgrind也提供了大量的內存監測工具,能夠和core dumped文件分析互補使用)。至於死循環能夠經過gdb直接調試跟蹤解決,這裏再也不贅述。
讓程序運行崩潰時生成core dumped文件,須要對操做系統進行簡單的配置。
$ sudo ulimit -c unlimited $ sudo echo core > /proc/sys/kernel/core_pattern
第一條命令是打開系統core dumped文件生成開關,第二條命令是將進程崩潰時生成的core dumped文件放在程序執行目錄下,並以core做爲文件名前綴。
接下來,之內存訪問越界的例子做爲示例,完整代碼以下,源文件名爲main.c。
void out_of_bound() { long *ptr; long buffer[] = {0}; ptr = buffer; buffer[1] = 0; // 越界訪問致使ptr被覆蓋 ptr[0]++; } void main() { out_of_bound(); }
編譯運行main.c,編譯時使用須要使用-g選項,保留可執行文件調試信息,方便後續分析。
$ gcc main.c -o main -g $ ./main Segmentation fault (core dumped) $ ls core.* core.9251
咱們看到程序崩潰後,生成了core dumped文件core.9251,其中9251爲程序運行時進程的pid。
調試core dumped文件。
$ gdb main core.9251 ./x: line 4: 9251 Segmentation fault (core dumped) ./main Reading symbols from demo...done. [New LWP 9251] [Thread debugging using libthread_db enabled] Using host libthread_db library "/usr/lib/libthread_db.so.1". Core was generated by `./main'. Program terminated with signal SIGSEGV, Segmentation fault. #0 0x00000000004004d6 in out_of_bound () at main.c:6 6 ptr[0]++; (gdb) backtrace #0 0x00000000004004d6 in out_of_bound () at main.c:6 #1 0x00000000004004f5 in main () at main.c:10 (gdb) print ptr $1 = (long *) 0x0 (gdb)
gdb輸出了程序崩潰時代碼的執行位置,main.c文件的第6行。使用backtrace命令能夠打印當時的函數調用棧信息,以方便定位出錯的上層調用邏輯。使用print命令打印ptr指針的值,確實爲0,與咱們以前的討論一致。上面分析僅僅是一個很是簡單的示例,實際開發過程當中遇到的出錯位置可能更隱蔽,甚至是庫函數的二進制代碼,屆時須要根據我的經驗來具體問題具體分析了。
Valgrind是很是強大的內存調試、內存泄漏檢測以及性能分析工具,它能夠模擬執行用戶二進制程序,幫助用戶分析潛在的內存泄漏和死鎖的可能邏輯。
開源工具Valgrind提供了源碼tar包,須要下載、編譯、安裝使用(最新版本Valgrind若是編譯報錯,請將gcc更新到最新版本)。
$ wget http://valgrind.org/downloads/valgrind-3.12.0.tar.bz2 $ tar xf valgrind-3.12.0.tar.bz2 $ cd valgrind-3.12.0 $ ./configure --prefix=/usr/local/ $ make && sudo make install $ valgrind --version valgrind-3.12.0
準備內存泄漏示例代碼。
#include <stdlib.h> void main() { malloc(4); }
使用Valgrind進行內存檢測。
$ valgrind --tool=memcheck --leak-check=full ./main ==24470== Memcheck, a memory error detector ==24470== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al. ==24470== Using Valgrind-3.12.0 and LibVEX; rerun with -h for copyright info ==24470== Command: ./main ==24470== ==24470== ==24470== HEAP SUMMARY: ==24470== in use at exit: 4 bytes in 1 blocks ==24470== total heap usage: 1 allocs, 0 frees, 4 bytes allocated ==24470== ==24470== LEAK SUMMARY: ==24470== definitely lost: 0 bytes in 0 blocks ==24470== indirectly lost: 0 bytes in 0 blocks ==24470== possibly lost: 0 bytes in 0 blocks ==24470== still reachable: 4 bytes in 1 blocks ==24470== suppressed: 0 bytes in 0 blocks ==24470== Reachable blocks (those to which a pointer was found) are not shown. ==24470== To see them, rerun with: --leak-check=full --show-leak-kinds=all ==24470== ==24470== For counts of detected and suppressed errors, rerun with: -v ==24470== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
咱們看到LEAK SUMMARY節內容中顯示程序退出時,仍有4B的內存能夠訪問,也就是產生了內存泄漏。
準備死鎖示例代碼,咱們實現了前面討論的ABBA鎖的代碼。
#include <stdlib.h> #include <pthread.h> #include <unistd.h> pthread_mutex_t lock_A = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_t lock_B = PTHREAD_MUTEX_INITIALIZER; void *run1(void *args) { pthread_mutex_lock(&lock_A); sleep(1); pthread_mutex_lock(&lock_B); pthread_mutex_unlock(&lock_B); pthread_mutex_unlock(&lock_A); } void *run2(void *args) { pthread_mutex_lock(&lock_B); sleep(1); pthread_mutex_lock(&lock_A); pthread_mutex_unlock(&lock_A); pthread_mutex_unlock(&lock_B); } void main() { pthread_t tid[2]; if (pthread_create(&tid[0], NULL, &run1, NULL) != 0) { exit(1); } if (pthread_create(&tid[1], NULL, &run2, NULL) != 0) { exit(1); } pthread_join(tid[0], NULL); pthread_join(tid[1], NULL); pthread_mutex_destroy(&lock_A); pthread_mutex_destroy(&lock_B); }
編譯(須要連接pthread庫),並使用Valgrind進行死鎖檢測。
$ gcc main.c -o main -g -lpthread $ valgrind --tool=helgrind ./main ==24652== Helgrind, a thread error detector ==24652== Copyright (C) 2007-2015, and GNU GPL'd, by OpenWorks LLP et al. ==24652== Using Valgrind-3.12.0 and LibVEX; rerun with -h for copyright info ==24652== Command: ./main ==24652== Ctrl + C ==24652== ==24652== Process terminating with default action of signal 2 (SIGINT) ==24652== at 0x4E4568D: pthread_join (in /usr/lib/libpthread-2.24.so) ==24652== by 0x4C2EAA7: pthread_join_WRK (hg_intercepts.c:553) ==24652== by 0x4C32908: pthread_join (hg_intercepts.c:572) ==24652== by 0x400806: main (main.c:39) ==24652== ---Thread-Announcement------------------------------------------ ==24652== ==24652== Thread #2 was created ==24652== at 0x51427AE: clone (in /usr/lib/libc-2.24.so) ==24652== by 0x4E431A9: create_thread (in /usr/lib/libpthread-2.24.so) ==24652== by 0x4E44C12: pthread_create@@GLIBC_2.2.5 (in /usr/lib/libpthread-2.24.so) ==24652== by 0x4C31810: pthread_create_WRK (hg_intercepts.c:427) ==24652== by 0x4C328FD: pthread_create@* (hg_intercepts.c:460) ==24652== by 0x4007BA: main (main.c:30) ==24652== ==24652== ---------------------------------------------------------------- ==24652== ==24652== Thread #2: Exiting thread still holds 1 lock ==24652== at 0x4E4CF1C: __lll_lock_wait (in /usr/lib/libpthread-2.24.so) ==24652== by 0x4E46B44: pthread_mutex_lock (in /usr/lib/libpthread-2.24.so) ==24652== by 0x4C2EE18: mutex_lock_WRK (hg_intercepts.c:894) ==24652== by 0x4C32CE1: pthread_mutex_lock (hg_intercepts.c:917) ==24652== by 0x40073F: run1 (main.c:12) ==24652== by 0x4C31A04: mythread_wrapper (hg_intercepts.c:389) ==24652== by 0x4E44453: start_thread (in /usr/lib/libpthread-2.24.so) ==24652== ==24652== ---Thread-Announcement------------------------------------------ ==24652== ==24652== Thread #3 was created ==24652== at 0x51427AE: clone (in /usr/lib/libc-2.24.so) ==24652== by 0x4E431A9: create_thread (in /usr/lib/libpthread-2.24.so) ==24652== by 0x4E44C12: pthread_create@@GLIBC_2.2.5 (in /usr/lib/libpthread-2.24.so) ==24652== by 0x4C31810: pthread_create_WRK (hg_intercepts.c:427) ==24652== by 0x4C328FD: pthread_create@* (hg_intercepts.c:460) ==24652== by 0x4007E7: main (main.c:34) ==24652== ==24652== ---------------------------------------------------------------- ==24652== ==24652== Thread #3: Exiting thread still holds 1 lock ==24652== at 0x4E4CF1C: __lll_lock_wait (in /usr/lib/libpthread-2.24.so) ==24652== by 0x4E46B44: pthread_mutex_lock (in /usr/lib/libpthread-2.24.so) ==24652== by 0x4C2EE18: mutex_lock_WRK (hg_intercepts.c:894) ==24652== by 0x4C32CE1: pthread_mutex_lock (hg_intercepts.c:917) ==24652== by 0x400780: run2 (main.c:21) ==24652== by 0x4C31A04: mythread_wrapper (hg_intercepts.c:389) ==24652== by 0x4E44453: start_thread (in /usr/lib/libpthread-2.24.so) ==24652== ==24652== ==24652== For counts of detected and suppressed errors, rerun with: -v ==24652== Use --history-level=approx or =none to gain increased speed, at ==24652== the cost of reduced accuracy of conflicting-access information ==24652== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 1 from 1)
第8行日誌,程序由於死鎖卡死,使用Ctrl+C強制退出。第27和48顯示:線程2和3(主線程編號爲1)在退出時仍然格持有1個鎖,很明顯,這兩個線程相互死鎖了,與以前的討論一致。
本文從Linux上C語言編程中遇到的異常開始討論,將異常大體分爲非法內存訪問和資源訪問衝突兩大類,並對每類典型的案例作了解釋和說明,最後經過core dumped文件分析和Valgrind工具的測試,給讀者提供了遇到程序運行時異常時的解決方案。但願看到此文的讀者,在之後遇到程序異常時都能泰然自若,冷靜分析,順利地找到問題的根源,便不枉費筆者撰寫此文之心血。