摘要:在我們往期對(duì)coredump的分析中,是依賴于core文件的,而core文件中也幾乎包含了程序當(dāng)前的所有狀態(tài)(堆棧、內(nèi)存、寄存器等)。然而在實(shí)際的線上環(huán)境中,由于core文件太大、保存core文件耗時(shí)太久,出于線上系統(tǒng)的穩(wěn)定性與快速恢復(fù)考慮,我們往往不會(huì)保留core文件。同時(shí),程序堆棧被破壞的情況下,即使我們保留了core文件,也無(wú)法準(zhǔn)確獲取程序崩潰時(shí)準(zhǔn)確的上下文信息。本文主要介紹在不保留core文件的情況下,如何獲取程序崩潰時(shí)候的上下文信息(主要是函數(shù)調(diào)用棧)。
## 1.coredump原理
當(dāng)程序發(fā)生內(nèi)存越界訪問(wèn)等行為時(shí),會(huì)觸發(fā)OS的保護(hù)機(jī)制,此時(shí)OS會(huì)產(chǎn)生一個(gè)信號(hào)(signal)發(fā)送給對(duì)應(yīng)的進(jìn)程。當(dāng)進(jìn)程從內(nèi)核態(tài)到用戶態(tài)切換時(shí),該進(jìn)程會(huì)處理這個(gè)信號(hào)。此類信號(hào)(比如SEGV)的默認(rèn)處理行為生成一個(gè)coredump文件。
這里會(huì)涉及以下幾個(gè)問(wèn)題:
1. 保存的core文件在什么地方?
2. core文件,具體會(huì)把進(jìn)程地址空間的哪些內(nèi)容保存下來(lái)?
3. 如何控制core文件的大???
4. 如果在處理信號(hào)的時(shí)候,又產(chǎn)生了新的同類信號(hào),該如何處理?
5. 處理信號(hào)的代碼,是運(yùn)行在用戶態(tài)還是內(nèi)核態(tài)?
6. 在一個(gè)多線程的程序中,是由哪個(gè)線程在處理這個(gè)信號(hào)?
問(wèn)題4~問(wèn)題6是信號(hào)處理的相關(guān)內(nèi)容,我們不在這里解釋,會(huì)在信號(hào)處理的章節(jié)詳細(xì)分析。問(wèn)題1~問(wèn)題3解釋如下
* `/proc/sys/kernel/core_pattern` 指定core文件存儲(chǔ)的位置,缺省值是`core`,表示將core文件存儲(chǔ)到當(dāng)前目錄。這個(gè)pattern是可以定制的,模式如下:
```
%p? 出Core進(jìn)程的PID
%u? 出Core進(jìn)程的UID
%s? 造成Core的signal號(hào)
%t? 出Core的時(shí)間,從1970-01-0100:00:00開始的秒數(shù)
%e? 出Core進(jìn)程對(duì)應(yīng)的可執(zhí)行文件名
```
* `/proc/sys/kernel/core_uses_pid` 取值是0或者1,表示是否在core文件名字后面加上進(jìn)程號(hào)
* `/proc/$pid/coredump_filter` 設(shè)置那些內(nèi)存會(huì)被dump出來(lái)
```
? ? ? ? ? bit 0? Dump anonymous private mappings.
? ? ? ? ? bit 1? Dump anonymous shared mappings.
? ? ? ? ? bit 2? Dump file-backed private mappings.
? ? ? ? ? bit 3? Dump file-backed shared mappings.
? ? ? ? ? bit 4 (since Linux 2.6.24)
? ? ? ? ? ? ? ? ? Dump ELF headers.
? ? ? ? ? bit 5 (since Linux 2.6.28)
? ? ? ? ? ? ? ? ? Dump private huge pages.
? ? ? ? ? bit 6 (since Linux 2.6.28)
? ? ? ? ? ? ? ? ? Dump shared huge pages.
```
* `ulimit? -c ` 決定save的core文件大小限制
## 2.自定義信號(hào)處理函數(shù)
我們需要在自定義的信號(hào)處理函數(shù)中打印出程序崩潰時(shí)候的活躍函數(shù)堆棧信息。這里我們有兩種方式:1.使用backtrace等方法,讀取進(jìn)程堆棧上的信息;2.在函數(shù)調(diào)用的同時(shí),用戶自己維護(hù)一套數(shù)據(jù)結(jié)構(gòu),用于保存函數(shù)調(diào)用鏈,在信號(hào)處理函數(shù)中,將這個(gè)函數(shù)調(diào)用鏈打印出來(lái)。
### 2.1使用backtrace獲取函數(shù)調(diào)用鏈
在[從匯編語(yǔ)言看函數(shù)調(diào)用](http://www.uufool.com/?p=54)和[棧破壞下的coredump分析方法](http://www.uufool.com/?p=78)兩篇文章中,我們知道進(jìn)程堆棧上保存了rbp寄存器對(duì)應(yīng)的list。backtrace本質(zhì)上就是利用進(jìn)程堆棧上的數(shù)據(jù),推斷出來(lái)的當(dāng)前函數(shù)調(diào)用鏈。這里我們不分析backtrace的源碼,直接給出關(guān)鍵性質(zhì)的代碼。
```cpp
void dump_trace(int Signal)
{
? ? const int len = 200;
? ? void* buffer[len];
? ? printf("dump_trace\n");
? ? int nptrs = ::backtrace(buffer, len);
? ? printf("backtrace\n");
? ? char** buffer_array = ::backtrace_symbols(buffer, nptrs);
? ? printf("sig:%d nptrs:%d\n", Signal, nptrs);
? ? if (buffer_array) {
? ? ? ? for (int i = 0; i < nptrs; ++i) {
? ? ? ? ? ? printf("frame=%d||trace_back=%s||\n", i, buffer_array[i]);
? ? ? ? }
? ? ? ? free(buffer_array);
? ? }
? ? exit(0);
}
signal(SIGSEGV, dump_trace);//注冊(cè)信號(hào)處理函數(shù)
```
完整的代碼可以參考[這里](https://github.com/yukun89/draft/tree/master/dump)。利用signal函數(shù),我們將dump_trace注冊(cè)為SIGSEGV的信號(hào)處理函數(shù),來(lái)取代默認(rèn)的保存core文件的行為。
### 2.2 用戶自己維護(hù)一個(gè)函數(shù)調(diào)用鏈
為什么我們需要費(fèi)力自己去維護(hù)一個(gè)函數(shù)調(diào)用鏈而不是直接調(diào)用backtrace呢? 因?yàn)橛龅竭M(jìn)程堆棧被寫花的時(shí)候,我們是無(wú)法找到完整的函數(shù)調(diào)用棧信息的。自己去維護(hù)函數(shù)調(diào)用鏈的原理如下:維護(hù)一個(gè)堆棧,在函數(shù)調(diào)用的時(shí)候,將調(diào)用的函數(shù)入棧;函數(shù)調(diào)用結(jié)束時(shí),將這個(gè)函數(shù)出棧。這樣當(dāng)coredump發(fā)生時(shí),即使進(jìn)程堆棧被破壞的情況下,這個(gè)用戶自定義的函數(shù)堆棧中依然保存了函數(shù)調(diào)用鏈的信息。
那么如何在函數(shù)調(diào)用的開始和結(jié)束執(zhí)行對(duì)應(yīng)的操作呢?g++/gcc正好提供了這種功能,能夠讓我們?cè)诤瘮?shù)的開始和結(jié)束嵌入對(duì)應(yīng)的代碼。我們需要做的僅僅是實(shí)現(xiàn)兩個(gè)預(yù)先聲明的函數(shù),核心代碼如下
```cpp
#ifdef __cplusplus
extern "C" {
#endif
void __attribute__((no_instrument_function))
__cyg_profile_func_enter(void *this_func, void *call_site);
void __attribute__((no_instrument_function))
__cyg_profile_func_exit(void *this_func, void *call_site);
#ifdef __cplusplus
};
#endif
void __cyg_profile_func_enter(void *this_func, void *call_site)
{
? ? char buffer[64] = {0};
? ? int len = snprintf(buffer, 60, "%p call %p", this_func, call_site);
? ? std::string content = std::string(buffer);
? ? call_list.push(content);
? ? return ;
}
void __cyg_profile_func_exit(void *this_func, void *call_site)
{
? ? call_list.pop();
? ? return ;
}
```
代碼解釋:`void __cyg_profile_func_enter(void *this_func, void *call_site)` 函數(shù)有兩個(gè)參數(shù),第一參數(shù)表示調(diào)用方的地址,第二個(gè)參數(shù)表示被調(diào)用方的地址。需要注意的有以下幾點(diǎn):
* `__cyg_profile_func_enter`和`__cyg_profile_func_exit`這兩個(gè)函數(shù)本身是需要設(shè)置屬性`no_instrument_function`的;否則會(huì)陷入對(duì)這兩個(gè)函數(shù)本身的無(wú)限遞歸調(diào)用。
* 為了將上述代碼嵌入到每個(gè)函數(shù)的開始和結(jié)束,需要在編譯代碼的時(shí)候使用特定的編譯參數(shù)`-finstrument-functions`
* 上述代碼所在的編譯單元是必須不能使用`-finstrument-functions`的:否則會(huì)陷入循環(huán)調(diào)用(想想為什么)
相關(guān)demo代碼的說(shuō)明在[這里](https://github.com/yukun89/draft/tree/master/dump),大家可以自行測(cè)試。我們只給出函數(shù)在crash時(shí)候的輸出結(jié)果。對(duì)于文中的地址信息,我們可以使用addr2line獲得這些地址對(duì)應(yīng)的源文件地址。
```shell
sig:11
frame_0: 0x401172 call 0x4011f0
frame_1: 0x401172 call 0x4011e1
frame_2: 0x401172 call 0x4011f0
frame_3: 0x401172 call 0x4011e1
frame_4: 0x401172 call 0x4011f0
frame_5: 0x401172 call 0x40129f
frame_6: 0x40123e call 0x7f6e0bf52b15
```
## 3.coredump的各種可能性
綜合前面所講解的所有coredump的種類,我們總結(jié)coredump的各種可能性如下:
- 內(nèi)存訪問(wèn)越界
? + 下標(biāo)導(dǎo)致的數(shù)組訪問(wèn)越界
+ 字符串不包含對(duì)應(yīng)結(jié)束符導(dǎo)致的越界訪問(wèn)
+ 使用strcpy, strcat, sprintf, strcmp,strcasecmp等字符串操作函數(shù),將目標(biāo)字符串讀/寫爆
2. 多線程數(shù)據(jù)未進(jìn)行加鎖保護(hù):STL容器vector、map等都是非線程安全的
3. 指針相關(guān)
? + 空指針解引用
? + 非法的指針轉(zhuǎn)化
? + use after free
? + double free
4. 堆棧相關(guān)
? + 棧變量過(guò)大導(dǎo)致的堆棧溢出
? + 棧變量的非法寫入,導(dǎo)致程序調(diào)用棧被破壞無(wú)法回溯
原文發(fā)表于:[如何調(diào)試沒有core文件的coredump](http://www.uufool.com/?p=151) 更多內(nèi)容請(qǐng)?jiān)L問(wèn)[優(yōu)孚網(wǎng)](www.uufool.com)