加入星計劃,您可以享受以下權(quán)益:

  • 創(chuàng)作內(nèi)容快速變現(xiàn)
  • 行業(yè)影響力擴散
  • 作品版權(quán)保護
  • 300W+ 專業(yè)用戶
  • 1.5W+ 優(yōu)質(zhì)創(chuàng)作者
  • 5000+ 長期合作伙伴
立即加入
  • 正文
    • 如何開啟coredump
    • GDB調(diào)試
    • GDB附著命令
    • address sanitizer工具
    • 兩個案例
  • 相關(guān)推薦
  • 電子產(chǎn)業(yè)圖譜
申請入駐 產(chǎn)業(yè)圖譜

如何通過Core Dump和GDB快速定位程序崩潰根因:從內(nèi)存溢出到死鎖,一文帶你玩轉(zhuǎn)調(diào)試

11/18 09:12
3777
閱讀需 15 分鐘
加入交流群
掃碼加入
獲取工程師必備禮包
參與熱點資訊討論

程序在異常終止時,會觸發(fā)對應(yīng)的錯誤信號,此時操作系統(tǒng)會將程序的內(nèi)存態(tài)內(nèi)容包括程序內(nèi)存、寄存器狀態(tài)、調(diào)用棧等信息寫入一個core文件。異常終止原因根據(jù)對應(yīng)信號主要分為如下幾種:

1、段錯誤,觸發(fā)信號?SIGSEGV

包括訪問空指針、數(shù)組越界、棧溢出等;

2、非法指令,觸發(fā)信號SIGILL

比如把一些隨機數(shù)據(jù)當成指令執(zhí)行:

void (*func)() = ptr; // 將內(nèi)存空間作為函數(shù)指針func(); // 觸發(fā) SIGILL

3、浮點異常,觸發(fā)信號?SIGFPE

也就是除0操作;

4、非法內(nèi)存訪問,觸發(fā)信號SIGMEM

如訪問已釋放的內(nèi)存;

5、總線錯誤,觸發(fā)信號?SIGBUS

比如收到異常的網(wǎng)絡(luò)包等。

如何開啟coredump

1、開啟coredump:ulimit -c unlimited;

2、對于某些設(shè)置了suid的程序如網(wǎng)卡抓包程序,在需要開啟coredump時,需要修改 /etc/sysctl.conf 文件來啟用。

排查問題時,如果有core文件,使用gdb分析;否則使用dmesg分析內(nèi)核日志。分析問題時,首先確認是否是OOM導(dǎo)致進程消失。

grep xxx?/var/log/messages?獲取到程序crash的地址,然后使用ldd查看外部依賴庫地址基址,使用

objdump -d /lib64/libc-2.12.so --start-address=0x3ab9a7500 | head -n2000 | grep 75f62

查找crash的系統(tǒng)調(diào)用。

在排查問題時,coredump通常需要配合持久化日志綜合分析。

GDB調(diào)試

gdb進入coredump堆棧后,bt可以展示棧幀,默認是當前棧幀也就是0棧幀。要查看對應(yīng)棧幀的變量情況,可以使用f+棧幀號切換。list func可以查看對應(yīng)函數(shù)的反編譯源碼,print p、print &p可以打印對應(yīng)變量的值。

frame +數(shù)字可以切換函數(shù)幀,disassemble可以查看匯編代碼。

使用print可以查看寄存器狀態(tài)、函數(shù)的棧幀空間、形參的位置和值是否有問題。

info signals查看信號是否會引起段錯誤,info registers?命令查看寄存器狀態(tài)、函數(shù)調(diào)用時的棧空間。

多線程場景,需要切換到具體的線程查看堆棧進行分析。查看所有線程:info threads

切換到對應(yīng)線程,如線程2:

thread 2 //這里使用的是gdb的id,不是pid

查看當前狀態(tài):bt/where

info mutex:查看當前程序中的互斥鎖信息。

dis可以查看地址的匯編指令,如:

dis -l c000000000255900 0xc000000000255900 <main+0>: push %rbp 0xc000000000255901 <main+1>: mov %rsp,%rbp 0xc000000000255904 <main+4>: sub $0x10,%rsp...

rd可以查看內(nèi)存內(nèi)容,如:

rd 0x7fffffffe000 32

這將從0x7fffffffe000地址開始,讀取 32 個字節(jié)的內(nèi)存內(nèi)容。不同版本可能不一樣,可以使用x命令代替,這個命令用來分析函數(shù)比較方便,打印函數(shù)堆棧內(nèi)容,第一個參數(shù)一般為函數(shù)返回地址,從第二個參數(shù)開始為函數(shù)的入?yún)ⅲ?/p>

比如分析ipv4報文,查找4500開頭的內(nèi)容,找到對應(yīng)的地址,然后使用iphdr <棧地址>可以打印報文內(nèi)容,根據(jù)偏移查找udphdr、tcphdr內(nèi)容。

GDB附著命令

遇到死鎖之類的,可以使用非調(diào)試手段進行定位。

附加到正在運行的線程:gdb -p pid

附加到進程:gdb?attach pid

使用gdb break設(shè)置條件斷點,可以抓取偶現(xiàn)bug。

Gdb還可以用于調(diào)試程序,p打點,watch可以設(shè)置觀察點,c繼續(xù)執(zhí)行:

#rbp是當前函數(shù)調(diào)用棧中的基指針寄存器,向下偏移8字節(jié)指向存放金絲雀值的地方(gdb) p $rbp - 0x8  $8 = (void *) 0x7fffffff04a8#這里對canary在棧中存放的地址打數(shù)據(jù)斷點(gdb)?watch?*0x7fffffff04a8Hardware watchpoint 2: *0x7fffffffe4a8   #觸發(fā)到條件斷點(gdb) cContinuing.

經(jīng)驗:大型工程多個.so會依賴相同的開源頭文件,如果不能保證每個.so各自依賴的頭文件版本一致,就可能出現(xiàn)棧溢出踩內(nèi)存問題。

丟日志問題

可以把日志寫入到一個mmap打開的文件中(mmap不支持調(diào)整文件大小,需要預(yù)先分配好空間),如果進程崩潰了系統(tǒng)會自動把 mmap 后的內(nèi)存寫入到文件里,不會丟失。

內(nèi)存延遲分配

用戶調(diào)用API進行內(nèi)存分配的時候,操作系統(tǒng)并不會直接分配給用戶這么多內(nèi)存,而是直到用戶真的訪問了申請的page時產(chǎn)生一個page fault,然后將這個page真的分配給用戶,并重新執(zhí)行產(chǎn)生page fault的語句。所以即使使用了new,在真正使用之前是沒有被真正的分配虛擬內(nèi)存。

所以為了加速,可以提前初始化,或者使用memset對每個頁讀取一個字節(jié),使其在內(nèi)存中cache。

使用htop -p可以查看進程內(nèi)存占用等情況。

內(nèi)存問題分析

靜態(tài)檢測:

gcc使用-fstack-usage選項,能輸出每個函數(shù)棧的最大使用量,具體含義:

1、static: 堆棧使用量在編譯時是已知的,不依賴于任何運行時條件。

2、dynamic: 堆棧使用量依賴于運行時條件,例如遞歸調(diào)用或基于輸入數(shù)據(jù)的條件分支。

3、bounded: 堆棧使用量雖然依賴于運行時條件,但有一個可預(yù)知的上限。

動態(tài)檢測:

1、使用pmap或查看/proc/pid/maps中的stack。

2、通過注冊一個自定義的信號處理函數(shù)來攔截?SIGSEGV段錯誤信號,處理函數(shù)會收到一個?siginfo_t?結(jié)構(gòu)體,其中包含錯誤的地址和寄存器狀態(tài)等上下文信息,可以判斷是否發(fā)生了棧溢出。

3、棧緩沖溢出一般主要是數(shù)組越界,使用gcc的-fstack-protector選項保護棧,可觸發(fā)檢測函數(shù)。如果canary值被修改,程序會認為發(fā)生了棧溢出攻擊,通常會立即終止,例如通過調(diào)用?__stack_chk_fail()?函數(shù)。這個也叫做“金絲雀分析法”。

另外,使用STL容器可以減少大部分棧緩沖溢出問題。

address sanitizer工具

開address sanitizer可以進行內(nèi)存錯誤檢測,且支持多線程環(huán)境,對程序性能影響夜宵,避免coredump。

使用步驟

編譯時添加-fsanitize=address選項,插入內(nèi)存錯誤檢測的相關(guān)代碼。

-fno-omit-frame-pointer選項保留堆棧指針。

-g選項添加調(diào)試符號和源碼行號。

-O1或者更高的優(yōu)化級別。

打包并鏈接 libasan.so。示例:

gcc -fsanitize=address -fno-omit-frame-pointer -O1 -g xx.cc?-o xx

兩個案例

1、netfilter回調(diào)

netfilter可以自定義增加hook點,而這些鉤子函數(shù)可能修改skb報文,導(dǎo)致數(shù)據(jù)或者程序異常。Netfilter的五個鉤子點,分別為NF_INET_PRE_ROUTING、NF_INET_LOCAL_IN、NF_INET_FORWARD、NF_INET_LOCAL_OUT、NF_INET_POST_ROUTING。

比如,如果是經(jīng)過NF_INET_LOCAL_IN之后數(shù)據(jù)異常了,那么查找掛載在NF_INET_LOCAL_IN上的鉤子:

查找處理 sk_buff (skb) 的 NF_INET_LOCAL_IN 鉤子的流程如下:

1、使用struct命令查看sk_buff結(jié)構(gòu)體中的dev字段:

#表示 skb包 關(guān)聯(lián)的網(wǎng)絡(luò)設(shè)備struct?sk_buff.dev?<sk_buff對象地址>dev?=?0xffff8881171a8000??

2、從net_device結(jié)構(gòu)體中獲取net字段:

#顯示網(wǎng)絡(luò)設(shè)備關(guān)聯(lián)的網(wǎng)絡(luò)上下文 (init_net),即當前的網(wǎng)絡(luò)命名空間。struct net_device.nd_net <net_device結(jié)構(gòu)體對象地址>nd_net?=?{?net?=?0xffffffff8322dc80?<init_net>?}

3、從net結(jié)構(gòu)體中獲取nf.hooks_ipv4:

nf.hooks_ipv4 = {0xfff88810ba7fe00, 0xfff888810ba7ff830, 0x0, 0xffff88810ba7fe80, 0x0}

hooks_ipv4對象表示IPv4的NF (netfilter)鉤子數(shù)組,五個地址對應(yīng)五個回調(diào)鉤子函數(shù)地址,其中NF_INET_LOCAL_IN的位置是第一個元素 (0xfff88810ba7fe00)。

4、獲取 NF_INET_LOCAL_IN 鉤子的條目:

nf_hook_entries顯示了鉤子條目,num_hook_entries = 1?表示當前有一個鉤子條目,hooks = 0xffff88810ba7ff88?表示鉤子條目的地址。

5、查看具體的鉤子條目:

nf_hook_entry顯示了鉤子條目的內(nèi)容,其中?hook = 0xffffffffc0e6b260?是實際的處理函數(shù)地址,然后查看處理函數(shù)的具體邏輯。

2、包版本管理問題

程序依賴了third.lib和data.h文件,data.h包含Data結(jié)構(gòu)體的實現(xiàn),third.lib里也依賴data.h文件。當版本更新,如Data結(jié)構(gòu)體里面增加了一個字段,而data.lib因為種種原因如包管理不規(guī)范而沒有重新編譯和更新時,可能會引起 core dump。

為了避免這種情況,可以使用PImpl解決。PImpl 模式用于將接口和實現(xiàn)分離,避免直接暴露結(jié)構(gòu)體的實現(xiàn),進而提高了ABI二進制接口兼容性。舉例:

Data.h:

class DataImpl; // 前向聲明class Data {public:Data();~Data();private:std::unique_ptr<DataImpl> pImpl; // 通過指針持有實現(xiàn)};

data.cc:

#include "data.h"struct DataImpl{int a;double b;std::string c;};...

PImpl 模式通過將實現(xiàn)細節(jié)封裝在私有的實現(xiàn)類中,使內(nèi)存布局變化與代碼解耦,可以隨意改變DataImpl的內(nèi)存布局和實現(xiàn)細節(jié),而不需要擔心外部代碼受到影響,因為外部代碼只會與Data指針交互,而不是直接訪問DataImpl的成員。

相關(guān)推薦

電子產(chǎn)業(yè)圖譜

機械轉(zhuǎn)行IT狗,目前在阿里巴巴淘寶事業(yè)群。日常記錄Linux應(yīng)用開發(fā)、嵌入式操作系統(tǒng)、無線網(wǎng)絡(luò)協(xié)議棧。剛深入使用Java,跟大家一起入門交流。