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

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

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

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

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

1、段錯(cuò)誤,觸發(fā)信號(hào)?SIGSEGV

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

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

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

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

3、浮點(diǎn)異常,觸發(fā)信號(hào)?SIGFPE

也就是除0操作;

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

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

5、總線錯(cuò)誤,觸發(fā)信號(hào)?SIGBUS

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

如何開啟coredump

1、開啟coredump:ulimit -c unlimited;

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

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

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

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

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

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

GDB調(diào)試

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

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

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

info signals查看信號(hào)是否會(huì)引起段錯(cuò)誤,info registers?命令查看寄存器狀態(tài)、函數(shù)調(diào)用時(shí)的??臻g。

多線程場(chǎng)景,需要切換到具體的線程查看堆棧進(jìn)行分析。查看所有線程:info threads

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

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

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

info mutex:查看當(dāng)前程序中的互斥鎖信息。

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

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

GDB附著命令

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

附加到正在運(yùn)行的線程:gdb -p pid

附加到進(jìn)程:gdb?attach pid

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

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

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

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

丟日志問題

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

內(nèi)存延遲分配

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

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

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

內(nèi)存問題分析

靜態(tài)檢測(cè):

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

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

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

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

動(dòng)態(tài)檢測(cè):

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

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

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

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

address sanitizer工具

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

使用步驟

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

-fno-omit-frame-pointer選項(xiàng)保留堆棧指針。

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

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

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

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

兩個(gè)案例

1、netfilter回調(diào)

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

比如,如果是經(jīng)過(guò)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對(duì)象地址>dev?=?0xffff8881171a8000??

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

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

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

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

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

4、獲取 NF_INET_LOCAL_IN 鉤子的條目:

nf_hook_entries顯示了鉤子條目,num_hook_entries = 1?表示當(dāng)前有一個(gè)鉤子條目,hooks = 0xffff88810ba7ff88?表示鉤子條目的地址。

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

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

2、包版本管理問題

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

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

Data.h:

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

data.cc:

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

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

相關(guān)推薦

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

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