一文讀懂Coredump文件是如何生成的
本文轉(zhuǎn)載自微信公眾號「Linux內(nèi)核那些事」,作者songsong001。轉(zhuǎn)載本文請聯(lián)系Linux內(nèi)核那些事公眾號。
人都會犯錯,所以在編寫程序時難免會出現(xiàn) BUG。
有些 BUG 是業(yè)務(wù)邏輯上的錯誤導(dǎo)致的,一般不會導(dǎo)致程序崩潰,例如:原本要將兩個數(shù)相加,但不小心把這兩個數(shù)相減,而導(dǎo)致結(jié)果出錯。這時我們可以通過在程序中,使用 printf 這類輸出函數(shù)來進行打點調(diào)試。
但有些 BUG 是由于某些致命的操作而導(dǎo)致的,一般會導(dǎo)致程序崩潰,例如:訪問未經(jīng)申請的內(nèi)存地址。由于程序會異常退出,所以一般不能通過 printf 這類輸出函數(shù)進行打點調(diào)試。
另外,對于必現(xiàn)的 BUG (就是不管什么條件都會發(fā)生),一般可以通過 GDB 設(shè)置斷點進行調(diào)試。但對于偶現(xiàn)的 BUG,由于在某些特定的條件下才會發(fā)生,所以比較難直接通過 GDB 進行調(diào)試。
那么,這時可以通過 Linux 提供的 coredump 文件進行調(diào)試。
一、coredump 文件生成過程
在程序發(fā)生某些錯誤而導(dǎo)致進程異常退出時,Linux 內(nèi)核會根據(jù)進程當(dāng)時的內(nèi)存信息,生成一個 coredump 文件。而 GDB 可以通過這個 coredump 文件重現(xiàn)當(dāng)時導(dǎo)致進程異常退出的場景,并且可以通過 GDB 來找到導(dǎo)致進程異常退出的原因。
當(dāng)進程接收到某些 信號 而導(dǎo)致異常退出時,就會生成 coredump 文件。那么,哪些信號會導(dǎo)致生成 coredump 文件呢?
會導(dǎo)致生成 coredump 文件的信號,如下表所示:
Signal | Action | Comment |
---|---|---|
SIGQUIT | Core | Quit from keyboard |
SIGILL | Core | Illegal Instruction |
SIGABRT | Core | Abort signal from abort |
SIGSEGV | Core | Invalid memory reference |
SIGTRAP | Core | Trace/breakpoint trap |
下面我們通過一個例子來說明怎么生成 coredump 文件。
從上面的表格可知,當(dāng)進程接收到 SIGSEGV 信號時會生成 coredump 文件。SIGSEGV 信號是當(dāng)進程訪問錯誤(未經(jīng)申請)內(nèi)存地址時觸發(fā)的,所以下面我們編寫一個訪問錯誤內(nèi)存地址的程序:
- int main(int argc, char *argv[])
- {
- char *addr = (char *)0; // 設(shè)置 addr 變量為內(nèi)存地址 "0"
- *addr = '\0'; // 向內(nèi)存地址 "0" 寫入數(shù)據(jù)
- return 0;
- }
在上面的例子中,由于內(nèi)存地址 ”0“ 并沒有通過調(diào)用 malloc 函數(shù)申請,所以當(dāng)向地址 ”0“ 寫入數(shù)據(jù)時將會導(dǎo)致 段錯誤,進程將會接收到 SIGSEGV 信號。
當(dāng)進程接收到 SIGSEGV 信號后,內(nèi)核將會根據(jù)進程當(dāng)時的內(nèi)存信息生成 coredump 文件,并且把進程殺死。
我們將上面的程序編譯并且運行后,會發(fā)現(xiàn)程序異常退出,并且生成一個名為 core.xxx 的文件,這個文件就是 coredump 文件。如下圖所示:
注意:
- 編譯的時候記得加上 -g 參數(shù)表示保留調(diào)試信息,否則使用 GDB 調(diào)試時會找不到函數(shù)名或者變量名。
- 如果沒有生成 coredump 文件的話,一般是受到資源限制,先使用命令 ulimit -c unlimited 設(shè)置資源不受限制。
coredump 文件點后面的數(shù)字是進程的 PID。
現(xiàn)在我們只需要輸入如下命令,即可使用 GDB 配合 coredump 文件來調(diào)試程序了:
- $ gdb ./coredump ./core.6359
GDB 運行后會停止在發(fā)生異常的代碼處,并且將發(fā)生異常的代碼打印出來,如下圖:
從上面的輸出可以看到,GDB 除了會將發(fā)生異常的代碼打印到終端外,還會將其所在的函數(shù)、文件名和所在文件的行數(shù)也打印出來,這樣我們就很快能定位到哪行代碼導(dǎo)致異常的。
二、coredump文件生成原理
前面介紹過,當(dāng)進程接收到某些 信號 而導(dǎo)致異常退出時,就會生成 coredump 文件。
當(dāng)進程從 內(nèi)核態(tài) 返回到 用戶態(tài) 前,內(nèi)核會查看進程的信號隊列中是否有信號沒有處理,如果有就調(diào)用 do_signal 內(nèi)核函數(shù)處理信號。我們可以通過下圖來展示內(nèi)核是怎么生成 coredump 文件的:
進程從內(nèi)核態(tài)返回到用戶態(tài)的地方有很多,如 從系統(tǒng)調(diào)用返回、從硬中斷處理程序返回 和 從進程調(diào)度程序返回 等。上圖主要通過 從進程調(diào)度程序返回 作為示例,來展示內(nèi)核是怎么生成 coredump 文件的。
下面我們來分析一下 coredump 文件生成過程的步驟:
1. 信號處理 do_signal()
當(dāng)進程從內(nèi)核態(tài)返回到用戶態(tài)前,內(nèi)核會查看進程的信號隊列中是否有信號沒有被處理,如果有就調(diào)用 do_signal 內(nèi)核函數(shù)處理信號。我們來看看 do_signal 函數(shù)的實現(xiàn):
- static void fastcall do_signal(struct pt_regs *regs)
- {
- siginfo_t info;
- int signr;
- struct k_sigaction ka;
- sigset_t *oldset;
- ...
- signr = get_signal_to_deliver(&info, &ka, regs, NULL);
- ...
- }
上面代碼去掉了很多與生成 coredump 文件無關(guān)的邏輯,最終我們可以看到,do_signal 函數(shù)主要調(diào)用 get_signal_to_deliver 內(nèi)核函數(shù)來進行進一步的處理。
get_signal_to_deliver 內(nèi)核函數(shù)的主要工作是從進程的信號隊列中獲取一個信號,然后根據(jù)信號的類型來進行不同的操作。我們主要關(guān)注生成 coredump 文件相關(guān)的邏輯,如下代碼:
- int get_signal_to_deliver(siginfo_t *info, struct k_sigaction *return_ka,
- struct pt_regs *regs, void *cookie)
- {
- sigset_t *mask = ¤t->blocked;
- int signr = 0;
- ...
- for (;;) {
- ...
- // 1. 從進程信號隊列中獲取一個信號
- signr = dequeue_signal(current, mask, info);
- ...
- // 2. 判斷是否會生成 coredump 文件的信號
- if (sig_kernel_coredump(signr)) {
- // 3. 調(diào)用 do_coredump() 函數(shù)生成 coredump 文件
- do_coredump((long)signr, signr, regs);
- }
- ...
- }
- ...
- }
上面代碼去掉了與生成 coredump 文件無關(guān)的邏輯,最后我們可以看到 get_signal_to_deliver 函數(shù)主要完成三個工作:
調(diào)用 dequeue_signal 函數(shù)從進程的信號隊列中獲取一個信號。
調(diào)用 sig_kernel_coredump 函數(shù)判斷信號是否會生成 coredump 文件。
如果信號會生成 coredump 文件,那么就調(diào)用 do_coredump 函數(shù)生成 coredump 文件。
2. 生成 coredump 文件
如果要處理的信號會觸發(fā)生成 coredump 文件,那么內(nèi)核就會調(diào)用 do_coredump 函數(shù)來生成 coredump 文件。do_coredump 函數(shù)的實現(xiàn)如下:
- int do_coredump(long signr, int exit_code, struct pt_regs *regs)
- {
- char corename[CORENAME_MAX_SIZE + 1];
- struct mm_struct *mm = current->mm;
- struct linux_binfmt *binfmt;
- struct inode *inode;
- struct file *file;
- int retval = 0;
- int fsuid = current->fsuid;
- int flag = 0;
- int ispipe = 0;
- binfmt = current->binfmt; // 當(dāng)前進程所使用的可執(zhí)行文件格式(如ELF格式)
- ...
- // 1. 判斷當(dāng)前進程可生成的 coredump 文件大小是否受到資源限制
- if (current->signal->rlim[RLIMIT_CORE].rlim_cur < binfmt->min_coredump)
- goto fail_unlock;
- ...
- // 2. 生成 coredump 文件名
- ispipe = format_corename(corename, core_pattern, signr);
- ...
- // 3. 創(chuàng)建 coredump 文件
- file = filp_open(corename, O_CREAT|2|O_NOFOLLOW|O_LARGEFILE|flag, 0600);
- ...
- // 4. 把進程的內(nèi)存信息寫入到 coredump 文件中
- retval = binfmt->core_dump(signr, regs, file);
- fail_unlock:
- ...
- return retval;
- }
經(jīng)過代碼精簡后,最終可以看到 do_coredump 函數(shù)完成四個工作:
- 判斷當(dāng)前進程可生成的 coredump 文件大小是否受到資源限制。
- 如果不受限制,那么調(diào)用 format_corename 函數(shù)生成 coredump 文件的文件名。
- 接著調(diào)用 filp_open 函數(shù)創(chuàng)建 coredump 文件。
- 最后根據(jù)當(dāng)前進程所使用的可執(zhí)行文件格式來選擇相應(yīng)的填充方法來填充 coredump 文件的內(nèi)容,對于 ELF文件格式 使用的是 elf_core_dump 方法。
elf_core_dump 方法的主要工作是:把進程的內(nèi)存信息和內(nèi)容寫入到 coredump 文件中,并且以 ELF文件格式 作為 coredump 文件的存儲格式。有興趣的可以自行閱讀 elf_core_dump 方法的代碼,這里就不作進一步的解說了。
三、生產(chǎn)環(huán)境應(yīng)該打開 coredump 功能嗎?
最后,我們來討論一下在生產(chǎn)環(huán)境應(yīng)不應(yīng)該打開 coredump 功能。
筆者遇過在生產(chǎn)環(huán)境打開 coredump 功能而導(dǎo)致的事故,故事如下:
我們上線了一個有 BUG 的 WEB 服務(wù),這個程序是以 master-worker 模式運行的。master 進程的主要工作是監(jiān)控 worker 進程的運行情況,如果 worker 進程掛掉,master 進程會創(chuàng)建新的 worker 進程來繼續(xù)工作。
由于 worker 進程的代碼存在漏洞,會導(dǎo)致 worker 進程訪問非法的內(nèi)存地址而產(chǎn)生 SIGSEGV 信號(段錯誤),而 SIGSEGV 信號會觸發(fā)生成 coredump 文件。
由于每次 worker 進程異常退出后,master 進程都會創(chuàng)建新的 worker 進程來補充,所以最終導(dǎo)致 worker 進程不斷的異常退出和被創(chuàng)建。這樣就不斷的生成 coredump 文件,最終導(dǎo)致磁盤被打滿。
所以,經(jīng)過上面的事故,我建議大家不要在生成環(huán)境打開 coredump 功能。那么,如果程序有問題怎么排查呢?
我的建議是摘掉線上的某一臺機器,打開 coredump 功能,然后模擬發(fā)生異常的情況來進行排查。如果人工比較難模擬,那么可以通過使用 tcpcopy 這些工具來把線上的流量導(dǎo)入到調(diào)試機器進行調(diào)試。生成 coredump 文件后,可以使用 GDB 來進行調(diào)試。