概述
在UNIX系統中,常將“主記憶體”(main memory) 稱為核心(core),因為在使用半導體作為記憶體材料之前,便是使用核心(core)。而核心映像(core image) 就是 “進程”(process)執行當時的記憶體內容。當進程發生錯誤或收到“信號”(signal) 而終止執行時,系統會將核心映像寫入一個檔案,以作為調試之用,這就是所謂的核心轉儲(core dump)。
有時程式並未經過徹底測試,這使得它在執行的時候一不小心就會找到破壞。這可能會導致核心轉儲(core dump)。幸好,現行的UNIX系統極少會面臨這樣的問題。即使遇到,程式設計師可以通過核心映像(core image)調試程式來找到錯誤原因 。
背景
核心檔案一詞來源於磁芯記憶體(core memory),1950-1970年代的主要的隨機存取存儲介質。
使用
核心檔案通常在系統收到特定的信號時由作業系統生成。信號可以由程式執行過程中的異常觸發,也可以由外部程式傳送。動作的結果一般是生成一個某個進程的記憶體轉儲的檔案,檔案包含了此進程當前的運行堆疊信息。有時程式並未經過徹底測試,這使得它在執行的時候一不小心就會找到破壞。這可能會導致核心轉儲(core dump)。現在的UNIX系統極少會面臨這樣的問題。即使遇到,程式設計師可以通過核心映像調試程式來找到錯誤原因。
分析
程式自身產生的coredump檔案一般可以用來分析程式運行到哪裡出錯了。
Linux平台常用的coredump檔案分析工具是gdb;Solaris平台用pstack和pflags;Windows平台用userdump和windbg。
外部程式觸發的dump一般用來分析進程的運行情況,比如分析記憶體使用/執行緒狀態等。
Solaris的常用記憶體分析工具umem就是需要先通過gcore pid得到coredump的檔案然後繼續分析記憶體情況。
C/C++程式設計師遇到的比較常見的一個問題, 就是自己編寫的代碼, 在運行過程中出現了意想不到的核心轉儲。程式發生核心轉儲的原因是多方面的, 不同的核心轉儲問題有著不同的解決辦法, 同時, 不同的核心轉儲問題解決的難易程度也存在很大的區別, 有些在短短几秒鐘內就可以定位問題, 但是也有一些可能需要花費數天時間才能解決, 這種問題是對軟體開發人員的極大的挑戰。筆者從事C/C++語言的軟體開發工作多年, 前後解決了許多此類問題, 久而久之積累了一定的經驗, 現把常見程式核心轉儲總結一下, 供軟體開發人員共饗。
1.無效指針引起的程式核心轉儲這種情況是一種最常見的核心轉儲, 大致可以有4 種原因導致程式出現異常:
(1) 對空指針進行了操作。
(2) 對一個未初始化的指針進行了操作。
(3) 對一個已經調用delete 釋放了記憶體的指針再次調用了
delete 去重複釋放(誰讓你不在第一次delete 後, 將指針賦值為NULL 呢)。
(4) 多執行緒訪問全局變數, 導致記憶體值異常而程式核心轉儲。
此類問題通常是代碼編寫時的疏漏造成的, 屬於低級故障, 也比較容易解決, 用調試工具調試一下產生的core 檔案,對照代碼定位問題出現的原因,10 分鐘就可以搞定。
2. 指針越界引起的程式核心轉儲
這種情況屬於一種隱藏比較深的核心轉儲, 比較難以解決。遇到這種問題時, 用調試工具調試這個core 檔案, 儘管也能定位到代碼行, 但是從對應行的代碼看, 可能這行代碼本身並沒有什麼問題, 它只是一個“被陷害者”。這種核心轉儲很難發現, 解決起來難度較大。根據筆者的經驗, 這種核心轉儲很可能是其他代碼處理過程中的記憶體越界造成的, 通常由以下兩個因素引起。第一個因素:核心轉儲所在的代碼行是一個很簡單的操作, 例如賦值語句, “這怎么可能出錯呢?” 注釋掉該語句運行程式, 核心轉儲又發生在下一行代碼上。此時,相應代碼行的操作很可能是對某個全局變數B 的操作, 在這種情況下, 需要將視線轉移到該全局變數的定義行代碼, 仔細看看該全局變數前後附近定義的變數A,C 因為作業系統不同, 變數位置也不同,有的需要關注B 變數前面定義的變數A, 有的需要關注B 變數後面定義的變數C, 仔細搜尋代碼, 看看對A,C 變數的處理有沒有可能導致記憶體越界的地方, 很可能就是因為對A,C 操作出現記憶體越界導致B 變數的操作受到傷害, B 夠背運吧。
第二因素: 核心轉儲的位置記憶體變數的值莫名其妙, 出現
了異常的值。此時, 需要仔細分析代碼和處理流程了。首先排查本函式的代碼處理是否有問題, 重點關注memcpy、strstr、sprintf、strcpy 和strcat 等極易出現問題的代碼行, 如果確認本函式處理沒有問題, 那么就需要根據流程來仔細走查代碼, 在這種情況下, 最需要的是耐心和信心。對於這類問題, 肯定是代碼走到了某個特殊的邏輯裡面, 代碼處理缺少必要的保護而引起的, 出現一次, 沒有足夠的日誌記錄流程, 很難分析, 從core 檔案的記憶體變數的值也無法定位問題原因。但是, 如果再次出現, 那么就具有比較大的參考價值了, 前後兩次的core檔案記憶體變數必然存在某種共性, 需要根據這個特徵來分析並復現故障了。筆者曾經遇到對一個未初始化的緩衝區A 做字元串操作strstr (此時它並不會核心轉儲), 但是當流程走了很多之後走到另一個對變數B 的操作時, 出現了核心轉儲; 更有甚者, 模組內一個鍊表算法出現了失誤, 導致指針越界。
3. 作業系統相關的特殊性造成的程式核心轉儲
初學者對於這種情況, 必然讓人備感莫名其妙的, “這么簡單而又規範的代碼, 怎么會出這種問題?” 這種問題與2 的區別在於, 問題很容易復現, 可能程式一運行就核心轉儲。儘管對這種核心轉儲很不解, 但應該相信: 越容易出現的問題,越容易解決。就像作為程式設計師編譯一個程式, 一下子出現了幾百個編譯錯誤, 根本不用擔心, 很可能就是某一行代碼多了幾個字元, 當把這些代碼刪去再編譯, 幾百個編譯錯誤全都消失了。
常遇到的此類問題有兩種情況。
第一種情況: 位元組對齊方式引起的程式核心轉儲。可能有兩個原因: 其一, 合作夥伴的模組與自身模組所定義的結構體的位元組對齊方式不同, 而導致程式出現核心轉儲; 其二, 在代碼中, 把引用到的別的模組的頭檔案包含到自身檔案中的位元組對齊方式語法聲明的中間了, 結果導致位元組對齊方式出現了變化。此類問題不是很常見, 不過一旦出現, 往往讓人覺得很蹊蹺。其實此類問題從源頭上解決應該還是比較簡單的, 關鍵在於一個良好的習慣, 如果在定義接口訊息的時候, 多花點時間規整結構體的欄位定義, 把它往4 位元組的倍數上靠即可, 應該沒必要處處節省那么點記憶體。第二種情況: 程式核心轉儲情況, 是與程式編譯時的連結參數不正確而導致程式運行在反覆操作大記憶體時出現核心轉儲。經過反覆的代碼刪減、編譯和運行的試驗, 最終發現問題規律, 於是懷疑和作業系統或者編譯有關, 最終解決了這個問題。程式發生核心轉儲的根本原因還是程式設計師自己進行程式設計時的編碼失誤造成的, 這種代碼失誤絕大多數都是因為沒有嚴格遵守相應的代碼編寫規範, 所以, 要從根本上杜絕或者減少程式核心轉儲現象的發生, 還是要從嚴格遵守代碼編寫規範來做起 。