之前初步了解過Windows 下強大的調試工具WinDbg,也簡單的整理了一個初級的文章《使用WinDbg、Map文件、Dump文件定位Access Violation的代碼行》,在Linux 下面也有對應的功能強大的調試工具:GDB,它可以用來斷點調試C/C++ 的程序,也可以用于分析Linux 下的C/C++ 程序運行崩潰產生的Core 文件……
另外對于GDB 工具,在《Linux gdb調試器用法全面解析》這篇文章中詳細的介紹了怎么使用GDB 去調試C/C++ 代碼
本文通過一個簡單的例子展示怎么使用GDB 分析Core 文件,但就像我一直強調的,完全停留在一個很膚淺的入門級的水平,只是先讓自己能有一個對GDB 的感性的認知,其實GDB 很強大,它能做的事情遠不止于本文所提到的這些皮毛
本文的內容也是參考了網絡上很多的文章,然后結合自己的驗證整理出來的
- 《Linux gdb調試器用法全面解析》
- 《詳解coredump》
- 《Unix 用gdb分析core dump文件》
- 《gdb core 調試》
- 《linux下用core和gdb查詢出現”段錯誤”的地方》
Core文件和段錯誤
當一個程序奔潰時,在進程當前工作目錄的Core 文件中復制了該進程的存儲圖像。Core 文件僅僅是一個內存映像(同時加上調試信息),主要用來調試的
通常情況下,Core 文件會包含程序運行時的內存、寄存器狀態、堆棧指針、內存管理信息還有各種函數調用堆棧信息等。我們可以理解為是程序工作當前狀態存儲生成第一個文件,許多程序出錯時都會產生一個Core 文件,通過工具分析這個文件,我們可以定位到程序異常退出時對應的堆棧調用等信息,找出問題所在并進行及時解決
段錯誤,就是大名鼎鼎的Segmentation Fault,這通常是由指針錯誤引起的。簡而言之,產生段錯誤就是訪問了錯誤的內存段,一般是你沒有權限,或者根本就不存在對應的物理內存,尤其常見的是訪問0 地址。
一般而言,段錯誤就是指訪問的內存超出了系統所給這個程序的內存空間,通常這個值是由gdtr 來保存的,這是一個48位的寄存器,其中的32位是保存由它指向的gdt 表,后13位保存相應于gdt 的下標,最后3位包括了程序是否在內存中以及程序在CPU 中的運行級別。指向的gdt 是由以64位為一個單位的表,在這張表中就保存著程序運行的代碼段以及數據段的起始地址以及與此相應的段限、頁面交換、程序運行級別、內存粒度等的信息。一旦一個程序發生了越界訪問,CPU 就會產生相應的異常保護,于是Segmentation fault就出現了
在編程中有以下幾種做法容易導致段錯誤,基本都是錯誤地使用指針引起的:
- 訪問系統數據區,尤其是往系統保護的內存地址寫數據,最常見的就是給指針以0地址
- 內存越界(數據越界、變量類型不一致等)訪問到不屬于你的內存區域
程序在運行過程中如果出現段錯誤,那么就會收到SIGSEGV 信號,SIGSEGV 默認handler 的動作是打印“段錯誤”的出錯信息,并產生Core 文件
GDB 斷點調試以定位錯誤代碼行
testCrash.cpp
void testCrash()
{
int* p = 1; //p指針指向常量1 所在的內存地址
*p = 3; //將p指針指向的地址的值改為3,
//因為本來p指向一個常量,是不允許被修改的
//強行訪問系統保護的內存地址就會出現段錯誤
}
main.cpp
#include <stdio.h>
void testCrash();
int main()
{
testCrash();
return 0
}
編譯執行,報段錯誤
注意g++ 編譯的時候,需要使用參數-g,否則GDB 無法找到symbol 信息,從而無法定位問題
斷點調試
很明顯,在GDB 斷點調試的過程中,已經將錯誤的代碼行輸出了:在testCrash.cpp 的第4行,在testCrash()方法里面,而且也將錯誤的代碼*p = 3;
打印出來了
還發現進程是由于收到了SIGSEGV 信號而結束的。通過進一步的查閱文檔(man 7 signal),SIGSEGV 默認handler 的動作是打印”段錯誤”的出錯信息,并產生Core 文件
分析Core 文件
設置Core文件大小,運行程序生成Core文件
執行ulimit -c unlimited
表示不限制生成的Core 文件的大小,注意這個命令只在當前的bash 下生效!然后運行這個有bug 的程序,可以看到在當前目錄下生成了core文件
GDB 分析Core 文件
同樣也是一步到位的定位到錯誤所在的代碼行!
為了獲取更詳細的函數調用信息,在執行
gdb 可執行文件 core文件
啟動gdb后,調用gdb的where或bt命令可以查看當時的調用棧信息!確定是什么樣的函數調用棧導致的程序崩潰!
接著考慮下去,在Windows 系統下的運行程序時,可能會出現“運行時錯誤”,這個時侯如果恰好你的機器上又裝有Windows 的編譯器的話,它會彈出來一個對話框,問你是否進行調試,如果你選擇是,編譯器將被打開,并進入調試狀態,開始調試
Linux下可以做到嗎?可以讓它在SIGSEGV 的handler中調用gdb
段錯誤時啟動調試
testCrash.cpp
void testCrash()
{
int* p = 1; //p指針指向常量1 所在的內存地址
*p = 3; //將p指針指向的地址的值改為3,
//因為本來p指向一個常量,是不允許被修改的
//強行訪問系統保護的內存地址就會出現段錯誤
}
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++ 編譯的時候,需要使用參數-g,否則GDB 無法找到symbol 信息,從而無法定位問題
運行程序
首先必須要切換到root 用戶運行,否則因為權限問題導致無法調試,另外就是進入調試模式后執行bt
以顯示程序的調用棧信息!
路漫漫其修遠兮
以上展示的這些東西很簡單,在你對Linux 進程的虛擬內存、進程的堆棧結構等沒有任何了解的情況下,完全照葫蘆畫瓢也能簡單的使用GDB
但是上面的程序、上面的代碼、上面的場景都完全是一個極其理想化的場景,在這種場景下排查問題當然是很簡單的
而在實際的場景中,往往比這個要復雜的多
- 程序遠不止上面的十幾二十幾行,可能是上萬、上百萬行!
- 絕不是簡單的單線程程序,可能會有多進程、多線程,這種場景該怎么調試?
- 假如程序崩潰了,但調用的是外部提供的.so文件,根本沒有對應源碼,此時就無法結合代碼分析了!
- 假如編譯時沒有加-g 參數,那么GDB 無法找到symbol信息,那怎么辦?
- 等等等等
針對上面的這些復雜的場景,上面展示的這些GDB 的簡單招式可能就沒有效果了,所以就需要更深層次的研究GDB 的使用,以及GDB 調試進程、分析Core 文件背后的操作系統、編譯原理層面的機制是什么
在Windows 下使用WinDbg 調試進程、分析dump 文件也是一樣的情況!