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