之前初步了解過(guò)Windows 下強(qiáng)大的調(diào)試工具WinDbg足丢,也簡(jiǎn)單的整理了一個(gè)初級(jí)的文章《使用WinDbg、Map文件讨永、Dump文件定位Access Violation的代碼行》,在Linux 下面也有對(duì)應(yīng)的功能強(qiáng)大的調(diào)試工具:GDB遇革,它可以用來(lái)斷點(diǎn)調(diào)試C/C++ 的程序卿闹,也可以用于分析Linux 下的C/C++ 程序運(yùn)行崩潰產(chǎn)生的Core 文件……
另外對(duì)于GDB 工具,在《Linux gdb調(diào)試器用法全面解析》這篇文章中詳細(xì)的介紹了怎么使用GDB 去調(diào)試C/C++ 代碼
本文通過(guò)一個(gè)簡(jiǎn)單的例子展示怎么使用GDB 分析Core 文件萝快,但就像我一直強(qiáng)調(diào)的锻霎,完全停留在一個(gè)很膚淺的入門(mén)級(jí)的水平,只是先讓自己能有一個(gè)對(duì)GDB 的感性的認(rèn)知揪漩,其實(shí)GDB 很強(qiáng)大旋恼,它能做的事情遠(yuǎn)不止于本文所提到的這些皮毛
本文的內(nèi)容也是參考了網(wǎng)絡(luò)上很多的文章,然后結(jié)合自己的驗(yàn)證整理出來(lái)的
- 《Linux gdb調(diào)試器用法全面解析》
- 《詳解coredump》
- 《Unix 用gdb分析core dump文件》
- 《gdb core 調(diào)試》
- 《linux下用core和gdb查詢(xún)出現(xiàn)”段錯(cuò)誤”的地方》
Core文件和段錯(cuò)誤
當(dāng)一個(gè)程序奔潰時(shí)冰更,在進(jìn)程當(dāng)前工作目錄的Core 文件中復(fù)制了該進(jìn)程的存儲(chǔ)圖像。Core 文件僅僅是一個(gè)內(nèi)存映像(同時(shí)加上調(diào)試信息)昂勒,主要用來(lái)調(diào)試的
通常情況下蜀细,Core 文件會(huì)包含程序運(yùn)行時(shí)的內(nèi)存、寄存器狀態(tài)戈盈、堆棧指針奠衔、內(nèi)存管理信息還有各種函數(shù)調(diào)用堆棧信息等。我們可以理解為是程序工作當(dāng)前狀態(tài)存儲(chǔ)生成第一個(gè)文件塘娶,許多程序出錯(cuò)時(shí)都會(huì)產(chǎn)生一個(gè)Core 文件归斤,通過(guò)工具分析這個(gè)文件,我們可以定位到程序異常退出時(shí)對(duì)應(yīng)的堆棧調(diào)用等信息刁岸,找出問(wèn)題所在并進(jìn)行及時(shí)解決
段錯(cuò)誤脏里,就是大名鼎鼎的Segmentation Fault,這通常是由指針錯(cuò)誤引起的难捌。簡(jiǎn)而言之膝宁,產(chǎn)生段錯(cuò)誤就是訪(fǎng)問(wèn)了錯(cuò)誤的內(nèi)存段,一般是你沒(méi)有權(quán)限根吁,或者根本就不存在對(duì)應(yīng)的物理內(nèi)存,尤其常見(jiàn)的是訪(fǎng)問(wèn)0 地址合蔽。
一般而言击敌,段錯(cuò)誤就是指訪(fǎng)問(wèn)的內(nèi)存超出了系統(tǒng)所給這個(gè)程序的內(nèi)存空間,通常這個(gè)值是由gdtr 來(lái)保存的拴事,這是一個(gè)48位的寄存器沃斤,其中的32位是保存由它指向的gdt 表圣蝎,后13位保存相應(yīng)于gdt 的下標(biāo),最后3位包括了程序是否在內(nèi)存中以及程序在CPU 中的運(yùn)行級(jí)別衡瓶。指向的gdt 是由以64位為一個(gè)單位的表徘公,在這張表中就保存著程序運(yùn)行的代碼段以及數(shù)據(jù)段的起始地址以及與此相應(yīng)的段限、頁(yè)面交換哮针、程序運(yùn)行級(jí)別关面、內(nèi)存粒度等的信息。一旦一個(gè)程序發(fā)生了越界訪(fǎng)問(wèn)十厢,CPU 就會(huì)產(chǎn)生相應(yīng)的異常保護(hù)等太,于是Segmentation fault就出現(xiàn)了
在編程中有以下幾種做法容易導(dǎo)致段錯(cuò)誤,基本都是錯(cuò)誤地使用指針引起的:
- 訪(fǎng)問(wèn)系統(tǒng)數(shù)據(jù)區(qū)蛮放,尤其是往系統(tǒng)保護(hù)的內(nèi)存地址寫(xiě)數(shù)據(jù)缩抡,最常見(jiàn)的就是給指針以0地址
- 內(nèi)存越界(數(shù)據(jù)越界、變量類(lèi)型不一致等)訪(fǎng)問(wèn)到不屬于你的內(nèi)存區(qū)域
程序在運(yùn)行過(guò)程中如果出現(xiàn)段錯(cuò)誤包颁,那么就會(huì)收到SIGSEGV 信號(hào)瞻想,SIGSEGV 默認(rèn)handler 的動(dòng)作是打印“段錯(cuò)誤”的出錯(cuò)信息,并產(chǎn)生Core 文件
GDB 斷點(diǎn)調(diào)試以定位錯(cuò)誤代碼行
testCrash.cpp
void testCrash()
{
int* p = 1; //p指針指向常量1 所在的內(nèi)存地址
*p = 3; //將p指針指向的地址的值改為3娩嚼,
//因?yàn)楸緛?lái)p指向一個(gè)常量内边,是不允許被修改的
//強(qiáng)行訪(fǎng)問(wèn)系統(tǒng)保護(hù)的內(nèi)存地址就會(huì)出現(xiàn)段錯(cuò)誤
}
main.cpp
#include <stdio.h>
void testCrash();
int main()
{
testCrash();
return 0
}
編譯執(zhí)行,報(bào)段錯(cuò)誤
注意g++ 編譯的時(shí)候待锈,需要使用參數(shù)-g漠其,否則GDB 無(wú)法找到symbol 信息,從而無(wú)法定位問(wèn)題
斷點(diǎn)調(diào)試
很明顯竿音,在GDB 斷點(diǎn)調(diào)試的過(guò)程中和屎,已經(jīng)將錯(cuò)誤的代碼行輸出了:在testCrash.cpp 的第4行,在testCrash()方法里面春瞬,而且也將錯(cuò)誤的代碼*p = 3;
打印出來(lái)了
還發(fā)現(xiàn)進(jìn)程是由于收到了SIGSEGV 信號(hào)而結(jié)束的柴信。通過(guò)進(jìn)一步的查閱文檔(man 7 signal),SIGSEGV 默認(rèn)handler 的動(dòng)作是打印”段錯(cuò)誤”的出錯(cuò)信息宽气,并產(chǎn)生Core 文件
分析Core 文件
設(shè)置Core文件大小随常,運(yùn)行程序生成Core文件
執(zhí)行ulimit -c unlimited
表示不限制生成的Core 文件的大小,注意這個(gè)命令只在當(dāng)前的bash 下生效萄涯!然后運(yùn)行這個(gè)有bug 的程序绪氛,可以看到在當(dāng)前目錄下生成了core文件
GDB 分析Core 文件
同樣也是一步到位的定位到錯(cuò)誤所在的代碼行!
為了獲取更詳細(xì)的函數(shù)調(diào)用信息涝影,在執(zhí)行
gdb 可執(zhí)行文件 core文件
啟動(dòng)gdb后枣察,調(diào)用gdb的where或bt命令可以查看當(dāng)時(shí)的調(diào)用棧信息!確定是什么樣的函數(shù)調(diào)用棧導(dǎo)致的程序崩潰!
接著考慮下去序目,在Windows 系統(tǒng)下的運(yùn)行程序時(shí)臂痕,可能會(huì)出現(xiàn)“運(yùn)行時(shí)錯(cuò)誤”,這個(gè)時(shí)侯如果恰好你的機(jī)器上又裝有Windows 的編譯器的話(huà)猿涨,它會(huì)彈出來(lái)一個(gè)對(duì)話(huà)框握童,問(wèn)你是否進(jìn)行調(diào)試,如果你選擇是叛赚,編譯器將被打開(kāi)澡绩,并進(jìn)入調(diào)試狀態(tài),開(kāi)始調(diào)試
Linux下可以做到嗎红伦?可以讓它在SIGSEGV 的handler中調(diào)用gdb
段錯(cuò)誤時(shí)啟動(dòng)調(diào)試
testCrash.cpp
void testCrash()
{
int* p = 1; //p指針指向常量1 所在的內(nèi)存地址
*p = 3; //將p指針指向的地址的值改為3英古,
//因?yàn)楸緛?lái)p指向一個(gè)常量,是不允許被修改的
//強(qiáng)行訪(fǎng)問(wèn)系統(tǒng)保護(hù)的內(nèi)存地址就會(huì)出現(xiàn)段錯(cuò)誤
}
main.cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
void testCrash();
void dump(int signo)
{
char buf[1024];
char cmd[1024];
FILE *fh;
snprintf(buf, sizeof(buf), "/proc/%d/cmdline", getpid());
if(!(fh = fopen(buf, "r")))
{
exit(0);
}
if(!fgets(buf, sizeof(buf), fh))
{
exit(0);
}
fclose(fh);
if(buf[strlen(buf) - 1] == '\n')
{
buf[strlen(buf) - 1] = '\0';
}
snprintf(cmd, sizeof(cmd), "gdb %s %d", buf, getpid());
system(cmd);
exit(0);
}
int main()
{
signal(SIGSEGV, &dump);
testCrash();
return 0;
}
編譯程序
注意g++ 編譯的時(shí)候,需要使用參數(shù)-g,否則GDB 無(wú)法找到symbol 信息支子,從而無(wú)法定位問(wèn)題
運(yùn)行程序
首先必須要切換到root 用戶(hù)運(yùn)行,否則因?yàn)闄?quán)限問(wèn)題導(dǎo)致無(wú)法調(diào)試唠叛,另外就是進(jìn)入調(diào)試模式后執(zhí)行bt
以顯示程序的調(diào)用棧信息!
路漫漫其修遠(yuǎn)兮
以上展示的這些東西很簡(jiǎn)單沮稚,在你對(duì)Linux 進(jìn)程的虛擬內(nèi)存艺沼、進(jìn)程的堆棧結(jié)構(gòu)等沒(méi)有任何了解的情況下,完全照葫蘆畫(huà)瓢也能簡(jiǎn)單的使用GDB
但是上面的程序蕴掏、上面的代碼障般、上面的場(chǎng)景都完全是一個(gè)極其理想化的場(chǎng)景,在這種場(chǎng)景下排查問(wèn)題當(dāng)然是很簡(jiǎn)單的
而在實(shí)際的場(chǎng)景中盛杰,往往比這個(gè)要復(fù)雜的多
- 程序遠(yuǎn)不止上面的十幾二十幾行挽荡,可能是上萬(wàn)、上百萬(wàn)行即供!
- 絕不是簡(jiǎn)單的單線(xiàn)程程序定拟,可能會(huì)有多進(jìn)程、多線(xiàn)程逗嫡,這種場(chǎng)景該怎么調(diào)試青自?
- 假如程序崩潰了,但調(diào)用的是外部提供的.so文件驱证,根本沒(méi)有對(duì)應(yīng)源碼延窜,此時(shí)就無(wú)法結(jié)合代碼分析了!
- 假如編譯時(shí)沒(méi)有加-g 參數(shù)雷滚,那么GDB 無(wú)法找到symbol信息需曾,那怎么辦?
- 等等等等
針對(duì)上面的這些復(fù)雜的場(chǎng)景祈远,上面展示的這些GDB 的簡(jiǎn)單招式可能就沒(méi)有效果了呆万,所以就需要更深層次的研究GDB 的使用,以及GDB 調(diào)試進(jìn)程车份、分析Core 文件背后的操作系統(tǒng)谋减、編譯原理層面的機(jī)制是什么
在Windows 下使用WinDbg 調(diào)試進(jìn)程、分析dump 文件也是一樣的情況扫沼!