程序在異常終止時(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?*0x7fffffff04a8
Hardware watchpoint 2: *0x7fffffffe4a8 #觸發(fā)到條件斷點(diǎn)
(gdb) c
Continuing.
經(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的成員。