文章目錄
- 一、什么是 core dump
- 二、發生 core dump 的原因
- 1. 空指針或非法指針引起 core dump
- 2. 數組越界或指針越界引起的 core dump
- 3. 數據競爭導致 core dump
- 4. 代碼不規范
- 三、core dump 分析方法
- 1. 啟用 core dump
- 2. 觸發 core dump
- 2-1. 因空指針解引用而崩潰
- 2-2. 通過 SIGSEGV 信號觸發 core dump
- 3. gdb 分析 core dump
- 總結
在 Linux 系統開發領域中,core dump(核心轉儲)是一個不可或缺的工具,它為我們提供了在程序崩潰時分析程序狀態的重要線索。當程序因為某種原因(如段錯誤、非法指令等)異常終止時,Linux 系統會嘗試將程序在內存中的映像、程序計數器、寄存器狀態等信息寫入到一個名為 core 的文件中,這個文件就是所謂的 core dump。
對于開發者而言,core dump 文件如同一塊寶藏,其中蘊含著程序崩潰時的現場信息。通過對 core dump 文件的分析,我們可以了解到程序在崩潰時的內存布局、函數調用棧、變量值等重要信息,從而幫助我們快速定位問題原因,優化代碼,提高程序的健壯性。
在本文中,我們將探討 Linux 中 core dump 的分析方法。通過一些簡單的案例來演示 core dump 分析的實際應用,幫助讀者更好地理解和掌握這一技術。
一、什么是 core dump
核心轉儲(core dump),在漢語中有時戲稱為吐核,是操作系統在進程收到某些信號而終止運行時,將此時進程地址空間的內容以及有關進程狀態的其他信息寫出的一個磁盤文件。這種信息往往用于調試。
在 UNIX 系統中,常將“主內存稱為核心(core),因為在使用半導體作為內存材料之前,便是使用核心(core)。而核心映像(core image)就是 “進程”(process)執行當時的內存內容。當進程發生錯誤或收到 “信號”(signal)而終止執行時,系統會將核心映像寫入一個文件,以作為調試之用,這就是所謂的核心轉儲(core dump)。
有時程序并未經過徹底測試,這使得它在執行的時候一不小心就會找到破壞。這可能會導致核心轉儲(core dump)。幸好,現行的 UNIX 系統極少會面臨這樣的問題。即使遇到,程序員可以通過核心映像(core image)調試程序來找到錯誤原因。
——引用:核心轉儲_百度百科 (baidu.com)
可以這樣去理解,core dump 是程序運行時在突然崩潰的那一刻的一個內存快照。操作系統在程序發生異常而異常在進程內部又沒有被捕獲的情況下,會把進程此刻內存、寄存器狀態、運行堆棧等信息轉儲保存在一個 core 文件里。這個 core 文件是二進制文件,可以使用 gdb、elfdump、objdump 或者 Windows 下的 windebug 進行打開此文件,并分析里面的具體內容,找出 core dump 的具體原因,并解決問題。
[!NOTE]
core 是在半導體作為內存材料前的線圈,當時用線圈當做內存材料,線圈叫做 core。用線圈做的內存叫做 core memory。故 core dump 也可稱為 core memory dump,真是個充滿歷史味道的詞。
在 Linux 系統下開發,時常會遇到程序突然崩潰了,且沒有留下任何日志的情況,這時就可以查看 core 文件。從 core 文件中分析原因,通過 gdb 看出程序掛在哪里,分析前后的變量,找出問題的原因。
二、發生 core dump 的原因
C/C++ 程序員遇到的比較常見的一個問題,就是自己編寫的代碼, 在運行過程中出現了意想不到的 core dump。程序發生 core dump 的原因是多方面的,不同的 core dump 問題有著不同的解決辦法。同時,不同的 core dump 問題解決的難易程度也存在很大的區別。有些在短短幾秒鐘內就可以定位問題,但是也有一些可能需要花費數天時間才能解決。這種問題是對軟件開發人員的極大的挑戰。筆者從事 C/C++ 語言的軟件開發工作多年,前后解決了許多此類問題,久而久之積累了一定的經驗,現把常見 core dump 總結一下。
1. 空指針或非法指針引起 core dump
空指針或非法指針(野指針、懸空指針)引起 core dump 是一種最常見的核心轉儲,大致可以有 3 種原因導致程序出現異常:
-
對空指針進行解引用等操作;
-
聲明指針變量后未進行初始化,并直接進行操作,極大概率引發 core dump,此類未經初始化的指針,統稱野指針;
-
對某個指針,調用了
free
函數或者delete
函數,該指針指向的空間已經被釋放,但未將該指針重新指向NULL
,此類指針成為懸空指針。對懸空指針再次操作,也會引發 core dump;
此類問題通常是代碼編寫時的疏漏造成的,屬于低級 bug,也比較容易解決的問題。Linux 平臺常用的 core dump 文件分析工具是 gdb,調試一下產生的 core 文件,對照代碼定位問題出現的原因,可以輕松解決問題。
2. 數組越界或指針越界引起的 core dump
提到這個,筆者不由得想起互聯網大廠百度的一道 C 語言面試題,如下代碼:
#include <stdio.h>int main()
{int i;int array[6];for (i = 0; i < 8; i++) {array[i] = 0;printf("Grayson Zheng\n");}return 0;
}
問:以上代碼中的 printf
函數會執行多少次?
這個問題的答案在不同操作系統下有不同的答案,當下只討論 Linux 系統的結果,執行該程序,結果如下:
可以看出,在打印了 8 次之后,程序結束,但這并不是一次正常的結束,而是一次 core dump。不難看出這是數組越界導致的內存踩踏,數組定義了 6 個元素,遍歷完 6 個元素之后,還對數組之外的內存進行了操作,從而引發了這次的 core dump。
這種情況還相對簡單,而指針越界引發的 core dump,有的是就比較簡單,有的就屬于一種隱藏比較深的 core dump 了。遇到這種問題時,在調試 core 文件,盡管也能定位到代碼行,但是有可能唄定位到的那行代碼本身并沒有什么問題,它只是一個 “被陷害者”。
根據經驗,這種 core dump 問題很可能是其他代碼處理過程中的內存越界造成的(親身經歷:一個指針越界導致內存踩踏,讓 7.5 萬臺機器拆包重流,經濟損失估計超過 40 w。當然,我不是那個寫 bug 的人,哈哈),通常由以下兩個原因引起:
-
假如有以下三個全局變量:
int global_vsrisble_a; char global_vsrisble_b; char global_vsrisble_c;
在不同操作系統中,這個三個全局變量在內存的位置可能不一樣,以 Ubuntu 為例,三個全局變量的內存位置分布如下圖所示:
假設在某些做了如下代碼所作的事:
#include <stdio.h>int global_vsrisble_a = 0x11223344; char global_vsrisble_b = 0x55; char global_vsrisble_c = 0x66;int main() {printf("%p = 0x%X\n%p = 0x%X\n%p = 0x%X\n", &global_vsrisble_a,global_vsrisble_a, &global_vsrisble_b, global_vsrisble_b,&global_vsrisble_c, global_vsrisble_c);char *p_1 = (char *)(&global_vsrisble_a);p_1 += 2;int *p_2 = (int *)p_1;*p_2 = 0x09ABCDEF;printf("%p = 0x%X\n%p = 0x%X\n%p = 0x%X\n", &global_vsrisble_a,global_vsrisble_a, &global_vsrisble_b, global_vsrisble_b,&global_vsrisble_c, global_vsrisble_c);return 0; }
[!CAUTION]
以上代碼只是為了示范,現實情況并不可能如此。
執行代碼后如下:
0x6447cc49a010 = 0x11223344 0x6447cc49a014 = 0x55 0x6447cc49a015 = 0x66 0x6447cc49a010 = 0xCDEF3344 0x6447cc49a014 = 0xFFFFFFAB 0x6447cc49a015 = 0x9
從執行結果來看,
global_vsrisble_b
和global_vsrisble_c
的值被破環。舉這個例子是為了說明,如果通過調試工具定位到是因為
global_vsrisble_b
的值被破壞了,很可能不是操作global_vsrisble_b
的代碼有問題,而是操作global_vsrisble_a
或者global_vsrisble_c
失誤,導致了global_vsrisble_b
的出錯,進而引發 core dump。 -
內存變量的值莫名其妙出現奇怪的值。跟上面的情況有點類似,也是因為有些變量相鄰問題被覆蓋原有的值。例如,執行了
memcpy
、strcpy
等函數(string.h
涉及到復制功能的函數,在復制過程中是不會檢查是否有越界的風險的)引起的 core dump。對于這類問題,肯定是代碼走到了某個特殊的邏輯里面,代碼處理缺少必要的保護而引起的。此類 core dump 可以通過復現 bug,對比前后兩次的 core 文件,找出內存變量存在的某種共性特征,根據這個特征來分析解決問題。
[!NOTE]
曾經在工作中遇到過一個 core bump,起因是對一段未初始化的緩沖存儲區做字符串搜索(搜索并不會引發 core dump)。但是代碼流程走了很長一段之后,對一個與緩沖存儲區相鄰的變量執行了操作,導致了 core dump。
3. 數據競爭導致 core dump
多線程訪問全局變量,如果不進行適當的同步保護,確實可能導致內存值異常,從而引發不可預測的行為,甚至可能導致程序崩潰并生成核心轉儲文件(core dump)。這種問題通常稱為 “數據競爭” 或 “競態條件”(race condition)。
競態條件是指兩個或多個線程同時訪問共享數據,并且至少有一個線程在修改數據時未進行適當的同步。這可能導致以下問題:
- 數據不一致:多個線程讀取和修改全局變量時,可能會導致數據處于不一致的狀態。
- 程序崩潰:未同步的訪問可能導致非法的內存訪問,從而引發段錯誤(segmentation fault),導致程序崩潰并生成核心轉儲文件。
4. 代碼不規范
初學者有時候編譯一個程序,出現了一整頁的編譯錯誤,其實這種情況也不用擔心,很可能就是某一行代碼多了幾個字符,當把這些代碼刪去再編譯,幾百個編譯錯誤全都消失了。
有些時候,程序發生 core dump 的根本原因還是程序員自己進行程序設計時的編碼失誤造成的,這種代碼失誤絕大多數都是因為沒有嚴格遵守相應的代碼編寫規范(比如用 0 做為除數等)。所以,要從根本上杜絕或者減少程序 core dump 的發生,還是要從嚴格遵守代碼編寫規范來做起。
三、core dump 分析方法
1. 啟用 core dump
默認情況下,程序運行崩潰導致 core dump,是不會生成 core 文件的,因為系統的 RLIMIT_CORE(核心文件大小)資源限制,默認情況下設置為 0。
使用 ulimit -c
命令可以查看 core 文件的大小,其中 -c
的含義是 core file size
,單位是 blocks
也就是 KB 的意思。ulimit -c
命令后面可以寫整數,表示生成寫入值大小的 core 文件。如果使用 ulimit -c unlimited
設置無限大,則任意情況下都會產生 core 文件。
以下命令可在用戶進程觸發信號時啟用 core dump 生成,并使用合理的名稱將核心文件位置設置為 /tmp/
。請注意,這些設置不會永久存儲。
ulimit -c unlimited
echo 1 > /proc/sys/kernel/core_uses_pid
echo "/tmp/core-%e-%s-%u-%g-%p-%t" > /proc/sys/kernel/core_pattern
[!IMPORTANT]
后面兩條命令在運行時,即使是加了
sudo
執行,也可能會被提示權限不足。這可能是由于 shell 的重定向在命令前已經處理完成,因此重定向操作并沒有被提升到超級用戶權限,這就導致了 “Permission denied” 的錯誤。可以通過以下命令來解決這個問題:echo 1 | sudo tee /proc/sys/kernel/core_uses_pid echo "/tmp/core-%e-%s-%u-%g-%p-%t" | sudo tee /proc/sys/kernel/core_pattern
順便解釋一下 "/tmp/core-%e-%s-%u-%g-%p-%t"
的各個參數的含義:
%e
:導致 core dump 的程序的可執行文件名。%s
:導致 core dump 的信號編號。%u
:導致 core dump 的程序的實際用戶 ID。%g
:導致 core dump 的程序的實際組 ID。%p
:導致 core dump 的程序的進程 ID。%t
: core dump 發生時的時間戳(自 epoch 時間以來的秒數)。
因此,/tmp/core-%e-%s-%u-%g-%p-%t
會生成包含如下信息的 core 文件:
/tmp/core-<executable>-<signal>-<uid>-<gid>-<pid>-<timestamp>
舉個例子,如果一個進程名為 my_program
,用戶 ID 為 1000
,組 ID 為 1000
,進程 ID 為 12345
,并且在 1617701234
時間點崩潰于信號 11
,則生成的 core 文件名將是:
/tmp/core-my_program-11-1000-1000-12345-1617701234
2. 觸發 core dump
我們使用兩個簡單的 C 程序作為示例。
2-1. 因空指針解引用而崩潰
文件名為 example.c
:
#include <stdio.h>void func()
{int *p = NULL;*p = 13;
}int main()
{func();return 0;
}
編譯并運行程序:
gcc -g -o example example.c
./example
運行程序時后,會在 /tmp/
文件夾下生成一個 core 文件。
2-2. 通過 SIGSEGV 信號觸發 core dump
文件名為 example2.c
:
#include <stdio.h>
#include <unistd.h>int global_num;int main()
{while(1) {printf("global_num = %d\n", global_num++);sleep(1);}return 0;
}
編譯并運行程序:
gcc -g -o example2 example2.c
./example2
運行程序時后,在另一個終端查找進程的 PID,并用 kill -11
加上 PID,向進程發送段錯誤信號,結束掉進程。之后會在 /tmp/
文件夾下生成一個 core 文件。
3. gdb 分析 core dump
兩個例子都是段錯誤導致的 core dump,所以用 gdb 調試的方法也是一樣的,命令格式如下:
gdb <program_name> <core_dump_file>
比如先調試第一個例子的 core 文件,則輸入 gdb example
,再加上 core 文件名,命令如下(建議先提前復制 core 文件名,不知道為什么,按 Tab
鍵不給補齊):
gdb example /tmp/core-example-11-1000-1000-88496-1719910934
隨后可以看到,gdb 提示在代碼第 6 行的地方出現了段錯誤,如下圖:
如果函數關系調用關系很復雜,可以用 bt
命令(全稱 backtrace,堆棧的意思)查看調用堆棧(where
命令也有同樣功能),如下圖可知是在調用 func
函數時產生的段錯誤,可用 list
命令查看,具體就是 list
加函數名,如下圖。找到提示錯誤的那一行代碼,print
命令可以打出 p
的值,由下圖可知,p
是空指針,不能進行解引用操作。
輸入 quit
或 exit
可以退出 gdb。
第二個例子,也是同樣用 gdb 打開 core 文件:
gdb example2 /tmp/core-example2-11-1000-1000-88552-1719911473
執行結果如下圖:
雖然這個段錯誤是因為我們人為地發送了 SIGSEGV
信號,導致了程序地段錯誤,而在打開 core 文件后,可以看出在執行 __GI___clock_nanosleep
函數時,遇到了段錯誤。
[!NOTE]
通常情況下,分析 core dump 問題,除了 core 文件之外,還會結合程序的 log 信息和系統的 log 信息(包括 kernel log、systemd log 等)一起分析。
當然人為故意制造出來的 core dump,有時候是分析不出來的。所以這個例子的作用在于分析的過程,也順便告訴大家,不是所有的 core dump 都可以分析出具體原因。
如果我們不事先知道是由 SIGSEGV
信號導致段錯誤的,首先要用 bt
命令找到函數的調用關系鏈:
由上圖可知,先是在 main
函數調用了 __sleep
函數,接著 __sleep
函數調用了 __GI___nanosleep
函數,__GI___nanosleep
函數調用了 __GI___clock_nanosleep
函數,到這里,執行到了 __GI___clock_nanosleep
函數的第 78 行時,發生了段錯誤,使程序崩潰。
此時,我們是沒辦法通過 list
命令去找出問題的,因為棧區的那三個函數是封裝后的庫函數,根本看不到源碼:
在輸入 bt
命令查看堆棧情況時,有出現了兩個變量,分別是 req
和 rem
。使用過nanosleep
函數的小伙伴可能會很眼熟這兩個變量,因為這個兩個變量是 nanosleep
函數的形參,原型是 int nanosleep(const struct timespec *req, struct timespec *rem)
。
用 print
命令打印出兩個變量的地址:
使用 info registers
命令查看寄存器狀態,檢查程序在崩潰時的上下文:
從寄存器狀態來看,沒有明顯的錯誤跡象,函數的棧幀空間沒什么問題,形參的位置和值也沒什么問題,所有值看起來都在正常范圍內。
當下是沒辦法直接了當的判斷為人為干預造成 core dump,如果此時想到了信號會引發段錯誤,可以用 info signals
命令查看信號情況:
從 info signals
的輸出中可以看出,SIGSEGV
(Segmentation fault)信號是設置為在程序接收到該信號時停止執行并打印信息的。也就說,可以人為地使用 kill -11
發送了 SIGSEGV
信號來終止程序并生成 core dump。
總結
分析 core dump 的具體原因不可能僅憑兩個案例就學會,本文只是提供一個基本的排除思路和方法。通過查看調用堆棧、源代碼和變量的值,可以逐步確定程序崩潰的原因。通過向程序發送 SIGSEGV
信號來生成 core 文件是一個有效的調試手段。通過 gdb,可以詳細分析程序在崩潰時的狀態,并確定具體的崩潰原因。確保在信號觸發時,檢查程序的變量和內存狀態,能夠幫助你更好地理解和解決程序中的問題。
之后如果遇到一個實際工作中產生的 core dump,且具有學習價值,我一定會總結這個分析過程,并輸出成文檔的形式,分享給大家,共勉,respect~