Linux上Core Dump文件的形成和分析

Core,又稱之爲Core Dump文件,是Unix/Linux操作系統的一種機制,對於線上服務而言,Core令人聞之色變,因爲出Core的過程意味着服務暫時不能正常響應,需要恢復,並且隨着吐Core進程的內存空間越大,此過程可能持續很長一段時間(例如當進程佔用60G+以上內存時,完整Core文件需要15分鐘才能完全寫到磁盤上),這期間產生的流量損失,不可估量。

凡事皆有兩面性,OS在出Core的同時,雖然會終止掉當前進程,但是也會保留下第一手的現場數據,OS彷彿是一架被按下快門的相機,而照片就是產出的Core文件。裏面含有當進程被終止時內存、CPU寄存器等信息,可以供後續開發人員進行調試。

 

關於Core產生的原因很多,比如過去一些Unix的版本不支持現代Linux上這種GDB直接附着到進程上進行調試的機制,需要先向進程發送終止信號,然後用工具閱讀core文件。在Linux上,我們就可以使用kill向一個指定的進程發送信號或者使用gcore命令來使其主動出Core並退出。如果從淺層次的原因上來講,出Core意味着當前進程存在BUG,需要程序員修復。從深層次的原因上講,是當前進程觸犯了某些OS層級的保護機制,逼迫OS向當前進程發送諸如SIGSEGV(即signal 11)之類的信號, 例如訪問空指針或數組越界出Core,實際上是觸犯了OS的內存管理,訪問了非當前進程的內存空間,OS需要通過出Core來進行警示,這就好像一個人身體內存在病毒,免疫系統就會通過發熱來警示,並導致人體發燒是一個道理(有意思的是,並不是每次數組越界都會出Core,這和OS的內存管理中虛擬頁面分配大小和邊界有關,即使不出Core,也很有可能讀到髒數據,引起後續程序行爲紊亂,這是一種很難追查的BUG)。

說了這些,似乎感覺Core很強勢,讓人感覺缺乏控制力,其實不然。控制Core產生的行爲和方式,有兩個途徑:

1.修改/proc/sys/kernel/core_pattern文件,此文件用於控制Core文件產生的文件名,默認情況下,此文件內容只有一行內容:“core”,此文件支持定製,一般使用%配合不同的字符,這裏羅列幾種:

%p  出Core進程的PID
%u  出Core進程的UID
%s  造成Core的signal號
%t  出Core的時間,從1970-01-0100:00:00開始的秒數
%e  出Core進程對應的可執行文件名

2.Ulimit –C命令,此命令可以顯示當前OS對於Core文件大小的限制,如果爲0,則表示不允許產生Core文件。如果想進行修改,可以使用:

Ulimit –cn

其中n爲數字,表示允許Core文件體積的最大值,單位爲Kb,如果想設爲無限大,可以執行:

Ulimit -cunlimited

產生了Core文件之後,就是如何查看Core文件,並確定問題所在,進行修復。爲此,我們不妨先來看看Core文件的格式,多瞭解一些Core文件。

首先可以明確一點,Core文件的格式ELF格式,這一點可以通過使用readelf -h命令來證實,如下圖:

從讀出來的ELF頭信息可以看到,此文件類型爲Core文件,那麼readelf是如何得知的呢?可以從下面的數據結構中窺得一二:

其中當值爲4的時候,表示當前文件爲Core文件。如此,整個過程就很清楚了。

瞭解了這些之後,我們來看看如何閱讀Core文件,並從中追查BUG。在Linux下,一般讀取Core的命令爲:

gdb exec_file core_file

使用GDB,先從可執行文件中讀取符號表信息,然後讀取Core文件。如果不與可執行文件攪合在一起可以嗎?答案是不行,因爲Core文件中沒有符號表信息,無法進行調試,可以使用如下命令來驗證:

Objdump –x core_file | tail

我們看到如下兩行信息:

SYMBOL TABLE:

no symbols

表明當前的ELF格式文件中沒有符號表信息。

爲了解釋如何看Core中信息,我們來舉一個簡單的例子:

#include “stdio.h”

int main(){

int stack_of[100000000];

int b=1;

int* a;

*a=b;

}

這段程序使用gcc –g a.c –o a進行編譯,運行後直接會Core掉,使用gdb a core_file查看棧信息,可見其Core在了這行代碼:

int stack_of[100000000];

原因很明顯,直接在棧上申請如此大的數組,導致棧空間溢出,觸犯了OS對於棧空間大小的限制,所以出Core(這裏是否出Core還和OS對棧空間的大小配置有關,一般爲8M)。但是這裏要明確一點,真正出Core的代碼不是分配棧空間的int stack_of[100000000], 而是後面這句int b=1, 爲何?出Core的一種原因是因爲對內存的非法訪問,在上面的代碼中分配數組stack_of時並未訪問它,但是在其後聲明變量並賦值,就相當於進行了越界訪問,繼而出Core。爲了解釋得更詳細些,讓我們使用gdb來看一下出Core的地方,使用命令gdb a core_file可見:

可知程序出現了段錯誤“Segmentation fault”, 代碼是int b=1這句。我們來查看一下當前的棧信息:

其中可見指令指針rip指向地址爲0×400473, 我們來看下當前的指令是什麼:

這條movl指令要把立即數1送到0xffffffffe8287bfc(%rbp)這個地址去,其中rbp存儲的是幀指針,而0xffffffffe8287bfc很明顯是一個負數,結果計算爲-400000004。這就可以解釋了:其中我們申請的int stack_of[100000000]佔用400000000字節,b是int類型,佔用4個字節,且棧空間是由高地址向低地址延伸,那麼b的棧地址就是0xffffffffe8287bfc(%rbp),也就是$rbp-400000004。當我們嘗試訪問此地址時:

可以看到無法訪問此內存地址,這是因爲它已經超過了OS允許的範圍。

下面我們把程序進行改進:

#include “stdio.h”

int main(){

int* stack_of = malloc(sizeof(int)*100000000);

int b=1;

int* a;

*a=b;

}

使用gcc –O3 –g a.c –o a進行編譯,運行後會再次Core掉,使用gdb查看棧信息,請見下圖:

可見BUG出在第7行,也就是*a=b這句,這時我們嘗試打印b的值,卻發現符號表中找不到b的信息。爲何?原因在於gcc使用了-O3參數,此參數可以對程序進行優化,一個負面效應是優化過程中會捨棄部分局部變量,導致調試時出現困難。在我們的代碼中,b聲明時即賦值,隨後用於爲*a賦值。優化後,此變量不再需要,直接爲*a賦值爲1即可,如果彙編級代碼上講,此優化可以減少一條MOV語句,節省一個寄存器。

此時我們的調試信息已經出現了一些扭曲,爲此我們重新編譯源程序,去掉-O3參數(這就解釋了爲何一些大型軟件都會有debug版本存在,因爲debug是未經優化的版本,包含了完整的符號表信息,易於調試),並重新運行,得到新的core並查看,如下圖:

這次就比較明顯了,b中的值沒有問題,有問題的是a,其指向的地址是非法區域,也就是a沒有分配內存導致的Core。當然,本例中的問題其實非常明顯,幾乎一眼就能看出來,但不妨礙它成爲一個例子,用來解釋在看Core過程中,需要注意的一些問題。

by zuoyulong

本文出自 “百度技術博客” 博客,請務必保留此出處http://baidutech.blog.51cto.com/4114344/1033622

發佈了81 篇原創文章 · 獲贊 61 · 訪問量 25萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章