如何快速定位程序Core?

導讀:程序core是指應用程序沒法保持正常running狀態而發生的崩潰行爲。程序core時會生成相關的core-dump文件,是程序崩潰時程序狀態的數據備份。core-dump文件中包含內存、處理器、寄存器、程序計數器、棧指針等狀態信息。本文將介紹一些利用core-dump文件定位程序core緣由的方法和技巧。 shell

全文7023字,預計閱讀時間 13分鐘。 數據庫

1、程序Core定義及分類

程序core是指應用程序沒法保持正常running狀態而發生的崩潰行爲。程序core時會生成相關的core-dump文件,core-dump文件是程序崩潰時程序狀態的狀態數據備份。core-dump文件包含內存、處理器、寄存器、程序計數器、棧指針等狀態信息。咱們能夠藉助core-dump文件來分析定位程序Core的緣由。vim

這裏咱們從三個方面對程序Core進行分類:機器、資源、程序Bug。下表對常見的Core緣由進行了分類:後端

圖片

2、函數棧介紹

當咱們打開core文件時,首先關注的是程序崩潰時的函數調用棧狀態,爲了方便理解後續定位core的一些技巧,這裏先簡單介紹一下函數棧。markdown

2.1 寄存器介紹

目前生產環境都爲64位機,這裏只介紹64位機的寄存器,以下:數據結構

圖片

對於x86-64架構,共有16個64位寄存器,每一個寄存器的用途並不單一,如%rax一般保存函數返回結果,但也被應用於imul和idiv指令。這裏重點關注%rsp(棧頂指針寄存器)、%rbp(棧底指針寄存器)、%rdi、%rsi、%rdx、%rcx、%r八、%r9(分別對應第1~6函數參數)。架構

Callee Save說明是否須要被調用者保存寄存器的值。數據庫設計

2.2 函數調用

圖片

2.2.1 調用函數棧幀:

在調用一個函數時首先進行的是參數壓棧,參數壓棧的順序跟參數定義的順序相反。注意,並非參數必定會壓棧,在x86-64架構中會針對可使用寄存器傳遞的變量,直接經過寄存器傳值,如數字、指針、引用等。ide

接着是返回地址壓棧,返回地址爲被調用函數執行完後,調用函數執行的下一個指令地址。這裏牢記返回地址的位置,後續章節會利用到這個返回地址的特性。函數

針對上面的介紹舉個例子說明:

圖片

如上圖,在main函數中調用了foo函數,首先對參數壓棧,三個參數均可以直接用寄存器傳遞(分別對應%edi、%esi、%edx),而後call指令將下一個指令壓棧。

2.2.2 被調用函數棧幀:

被調用函數首先會將上一個函數的棧底指針(%rbp)保存,即%rbp壓棧。而後再保存須要被保存的寄存器值,即Callee Save爲True的寄存器。接着爲臨時變量、局部變量申請棧空間。

針對被調用函數,舉個例子說明:

圖片

如上圖,在foo函數執行時,先對main函數的%rbp壓棧,再把寄存器中的參數值存放到局部變量(a, b, c)中。

2.3 總結

經過對函數調用的簡單介紹,咱們能夠發現函數棧是一個縝密且脆弱的結構,內存結構必須按照嚴格的方式被訪問,如稍有不慎就可能致使程序崩潰。

3、GDB定位Core

這一節將介紹從core文件打開到定位全流程中可能會遇到的問題以及解決技巧。

3.1 Core文件

core文件在哪裏?

查看「/proc/sys/kernel/core_pattern」肯定core文件生成規則。

3.2 變量打印

程序debug過程當中經常要查看各類變量(內存、寄存器、函數表等)的值是否正確,維持單獨用一節介紹下經常使用的變量打印方法以及一些冷門小技巧。

3.2.1 print命令

print [Expression]
print $[Previous value number]
print {[Type]}[Address]
print [First element]@[Element count]
print /[Format] [Expression]

Format格式:
o - 8進制
x - 16進制
u - 無符號十進制
t - 二進制
f - 浮點數
a - 地址
c - 字符
s - 字符串

3.2.2 x命令

x /<n/f/u>  <addr>
n:是正整數,表示須要顯示的內存單元的個數,即從當前地址向後顯示n個內存單元的內容,一個內存單元的大小由第三個參數u定義。

f:表示addr指向的內存內容的輸出格式,
s對應輸出字符串,此處需特別注意輸出整型數據的格式:  
x 按十六進制格式顯示變量.  
d 按十進制格式顯示變量。  
u 按十進制格式顯示無符號整型。  
o 按八進制格式顯示變量。  
t 按二進制格式顯示變量。  
a 按十六進制格式顯示變量。  
c 按字符格式顯示變量。  
f 按浮點數格式顯示變量。
u:就是指以多少個字節做爲一個內存單元-unit,默認爲4。
u還能夠用被一些字符表示:  
如b=1 byte, h=2 bytes,w=4 bytes,g=8 bytes.<addr>:表示內存地址。

3.2.3 容器對象打印

利用上面的print和x命令,再結合容器的數據結構,咱們就能知道容器的詳細信息。這裏舉個完整打印二進制string的例子,string的數據結構以下:

圖片

string爲空時,_M_dataplus._M_p是指向nullptr的。當賦值後會在堆上申請一段內存,分爲兩段,前半段是meta信息(類型爲std::string::_Rep),如length、capacity、refcount,後半段爲數據區,_M_p指向數據區。

一般狀況下非二進制的string,直接print便可顯示數據內容,但當數據爲二進制時,'\0'會截斷打印內容。所以,打印二進制string的首要任務是確認string的size。

string的size信息保存在std::string::_Rep結構體中,根據上面的數據結構能夠發現,_Rep與_M_dataplus._M_p相差一個結構體大小,所以打印_Rep結構體的命令爲:

#先把_M_p轉成_Rep指針,再讓指針向低地址偏移一個結構體大小
p *((std::string::_Rep*)(s._M_dataplus._M_p) - 1)

找到string的size(_M_length)後,再經過x命令打印相關的內存區便可,命令爲:

#這裏的n是_Rep._M_length
x /ncb s._M_dataplus._M_p

運行效果以下:

圖片

爲了方便,這裏推薦一個方便的腳本:stl-views.gdb(連接:https://sourceware.org/gdb/wiki/STLSupport?action=AttachFile&do=view&target=stl-views-1.0.3.gdb,直接在gdb終端**source stl-views.gdb**便可,支持常見的容器打印,如vector、map、list、string等。

3.2.4 靜態變量打印

程序中常常會使用到靜態變量,有時咱們須要查看某個靜態對象的值是否正確,就涉及到靜態對象的打印。看以下例子:

void foo() {
    static std::string s_foo("foo");
}

這裏能夠藉助nm -C ./bin | grep xx找到靜態變量的內存地址,再經過gdb的print打印。

圖片

3.2.5 內存dump

dump [format] memory filename start_addr end_addr
dump [format] value filename expr
format通常使用binary,其餘的能夠查看gdb手冊。

好比咱們能夠結合上面查看string內容的例子dump整個string數據到文件中。
dump binary memory file1 s._M_dataplus._M_p s._M_dataplus._M_p + length

若是想查看文件內容的話可把vim -b和xxd結合使用。

接上面string的例子,舉一個dump string內存數據到文件的例子:

圖片

3.3 定位代碼行

定位core的緣由,首先要定位崩潰時正在執行的代碼行,這一節主要介紹一些定位代碼行的方法。一般狀況下直接經過gdb的breaktrace便可一覽整個函數棧,但有時候函數棧信息並不是如此清晰明瞭,這時就可利用一些小技巧來查看函數棧。

3.3.1 去編譯優化


有時候會發現core的函數棧跟實際的代碼行不匹配,若是是在線下環境中,能夠嘗試把編譯優化設置成-O0,而後再從新復現core問題。

3.3.2 程序**計數器 + addr2line**

對於線上core問題,通常無法再對程序進行去編譯優化操做,只能在現有的core文件基礎上進行代碼定位。這一節咱們採用一個例子來介紹如何使用程序計數器 + addr2line來定位代碼行。

圖片

從截圖能夠發現frame 20指示的代碼行與實際的代碼行是不匹配的,定位步驟以下:

# 跳轉到第20號棧
frame 20

# 使用display命令顯示程序計數器
display /i $rip

# 使用addrline工具作地址轉換
shell /opt/compiler/gcc-8.2/bin/addr2line -e bin address

3.3.3 函數棧修復

有時候咱們會發現函數調用棧裏面會出現不少??的狀況,這常發生於棧被寫花,某些狀況下手動進行修復。函數棧的修復利用的函數棧內存分佈知識,見第一節。

-----------------------------------
Low addresses
-----------------------------------
0(%rsp)  | top of the stack frame          
         | (this is the same as -n(%rbp))
---------|--------------------------
n(%rbp) | variable sized stack frame-
8(%rbp) | varied
0(%rbp)  | previous stack frame address
8(%rbp)  | return address
-----------------------------------
High addresses

從上面的棧示意圖能夠發現,利用%rbp寄存器便可找到上一個函數的返回地址棧底指針,再利用addr2line命令找到對應的代碼行。這裏舉一個例子:

圖片

#首先找到當前被調用棧上一個棧的棧底指針值和返回地址
x /2ag $rbp # 2個單位,a=十六進制,g=8字節單元

#使用上一條命令獲得的棧底指針值依次遞歸
x /2ag address

3.3.4 無規律core棧

無規律core棧問題通常發生於堆內存寫壞。函數調用是一個很是精密的過程,任何一個位置發生非預期的讀寫都會致使程序崩潰。這裏能夠舉個小例子來講明:

int main(int argc, char* argv[]) {
    std::string s("abcd");
    *reinterpret_cast<uint64_t*>(&s) = 0x11;
    return 0;
}

上面的例子core在string析構上,緣由是由於string的_M_ptr被改寫成了0x11,析構流程變成了非法內存操做。

同理,因爲進程堆空間是共享的,一個線程對堆的非法操做就可能會影響另外一個線程的正常操做,因爲堆分配的隨機性,表現出來的現象就是無規律core棧。

針對無規律core棧最好的方式仍是藉助AddressSanitizer。

#設置編譯參數CXXFLAGS
CXXFLAGS="-fPIC -fsanitize=address  -fno-omit-frame-pointer"

#設置連接參數
LDFLAGS="-lasan"

# 設置啓動環境變量
export ASAN_OPTIONS=halt_on_error=0:abort_on_error=1:disable_coredump=0

# 啓動
LD_PRELOAD=/opt/compiler/gcc-8.2/lib/libasan.so ./bin/xxx

3.3.5 總結

上面提到的幾種方法都是爲了找到具體的問題代碼行,爲後續分析core的具體緣由提供線索。

3.4 定位Core緣由

這一節主要介紹定位Core緣由的方法以及一些常見緣由的介紹。

3.4.1 確認信號量


從上面的Core分類咱們能夠發現某些場景的core是因爲機器故障致使的,如SIGBUS,所以能夠先經過信號量排除掉一些core緣由。

3.4.2 定位異常彙編指令


經過上面的代碼行定位咱們能夠大體找到程序core在哪一行,比較簡單的core直接print程序上下文便可找到core的緣由。

但有些場景下,經過排查上下文無任何異常,這個時候就須要準肯定位具體的異常彙編指令,根據指令找緣由。

查看彙編指令比較簡單的方法是使用layout asm命令,frame指向那個棧,就顯示對應棧的彙編。這裏舉個core例子,以下:

圖片

程序顯示core在start函數,查看相關上下文變量均無異常。使用layout asm打開正在執行的彙編指令,以下:

圖片

查看彙編定位到程序core在mov指令,mov指令上一個指令爲sub,爲棧申請了3M空間,懷疑是棧空間不足。採用frame 0的%rsp - frame N的%rbp排查爲棧空間不足。

經過上面的例子,能夠發現定位異常彙編指令位置後,咱們可以把異常點進一步壓縮,定位到是哪一個指令、變量、地址致使的core問題。

3.4.3 排查異常變量

經過上面的操做咱們能夠準肯定位到具體是哪一行代碼的哪一條指令出現了問題,根據異常指令咱們能夠排查相關的變量,肯定變量值是否符合預期。

這裏舉一個比較經典的空指針例子,以下:

int main(int argc, char* argv[]) {
    int* a = nullptr;
    *a = 1;
    return *a;
}

圖片

經過彙編指令咱們能夠發現是movl $0x1, (%rax)出現了問題,%rax的值來自於0x8(%rbp)x命令打印相關的地址就能夠發現爲空指針錯誤。

3.4.4 查看被優化變量

一般狀況下程序都是開啓了編譯優化的,就會出現變量沒法被print,提示變量被優化,有時可利用彙編 + 寄存器的方式查看被優化的變量。

這裏舉一個例子說明下:

void foo(char const* str) {
    char buf[1024] = {'\0'};
    memcpy(buf, str, sizeof(buf));
}

int main(int argc, char* argv[]) {
    foo("abcd");
    return 0;
}

一般狀況下在foo函數內部,str變量是會直接別優化掉的,由於能夠直接利用%rdi寄存器傳遞參數。爲了可以打印出str的值,這個時候咱們能夠藉助彙編 + 寄存器的方式找到具體的變量值,以下:

圖片

首先找到main函數調用foo函數的參數壓棧彙編:mov $0x402011, %edi,這裏的0x402011即爲str的內存地址,經過x命令便可顯示str的值了。

比較複雜的場景可能無法直接找到被優化變量,這時能夠採用彙編回溯的方式找到變量。

3.4.5 異常函數地址排查

有時的core問題是由於數據異常致使,有時也多是優化函數地址致使,如調用虛函數地址錯誤、函數返回地址錯誤、函數指針值錯誤。

異常函數地址排查同理於異常變量排查,根據彙編指令確認調用是否異常便可。這裏舉一個虛函數地址異常的例子,以下:

class
A {
public:
    virtual ~A() = default;
    virtual void foo() = 0;
};
class
B : public A {
public:
    void foo() {}
};

int main(int argc, char* argv[])
{
    A* a = new B;
    a->foo();
    A* b = new B;
    *reinterpret_cast<void**>(b) = 0x0;
    b->foo();

    return 0;
}

圖片

從彙編指令看是core在了mov (%rax), %rax,結合指令上下文可發現是在虛函數地址尋址操做,對比兩個變量的虛函數表便可發現是函數地址load錯誤致使的core。

3.4.6 總結

定位core的基本流程可總結爲如下幾步:

  1. 明確core的大體觸發緣由。機器問題?自身程序問題?

  2. 定位代碼行。哪一行代碼出現了問題。

  3. 定位執行指令。哪一行指令幹了什麼事。

  4. 定位異常變量。指令不會有問題,是指令操做的變量不符合預期。

善於利用彙編指令以及打印指令(x、print、display)能夠更有效的定位Core。

參考資料:

彙編查看工具:https://godbolt.org/ https://cppinsights.io/
標準GDB文檔:https://sourceware.org/gdb/current/onlinedocs/gdb/

招聘信息:

歡迎加入百度移動生態事業羣內容中臺架構團隊,咱們常年需求後端、C++、模型架構、大數據、性能調優的同窗、社招,實習,校招都要哦

簡歷投遞郵箱:geektalk@baidu.com (投遞備註【內容架構】)

推薦閱讀

|面向大規模商業系統的數據庫設計和實踐

百度愛番番移動端網頁秒開實踐

解密百TB數據分析如何跑進45秒

---------- END ----------

百度Geek說

百度官方技術公衆號上線啦!

技術乾貨 · 行業資訊 · 線上沙龍 · 行業大會

招聘信息 · 內推信息 · 技術書籍 · 百度周邊

歡迎各位同窗關注

相關文章
相關標籤/搜索