手把手教你寫一個(gè)GDB(基本功能~)
什么是 GDB
GDB 全稱 the GNU Project debugger,主要用來調(diào)試用戶態(tài)應(yīng)用程序。
根據(jù)官方文檔介紹,GDB 支持調(diào)試以下語言編寫的應(yīng)用程序:
Ada
Assembly
C
C++
D
Fortran
Go
Objective-C
OpenCL
Modula-2
Pascal
Rust
當(dāng)然,最常用的還是用于調(diào)試 C/C++ 編寫的應(yīng)用程序。
本文并不是 GDB 的使用教程,所以不會(huì)對(duì) GDB 的使用進(jìn)行詳細(xì)的介紹。本文的目的是,教會(huì)大家自己動(dòng)手?jǐn)]一個(gè)簡(jiǎn)易的 GDB。所以閱讀本文前,最好先了解下 GDB 的使用。
在編程圈中流傳一句話:不要重復(fù)造輪子。但是本人覺得,重復(fù)造輪子才能真正理解輪子的實(shí)現(xiàn)原理。
ptrace 系統(tǒng)調(diào)用
GDB 實(shí)現(xiàn)的核心技術(shù)是 ptrace() 系統(tǒng)調(diào)用。
如果你對(duì) ptrace 的實(shí)現(xiàn)原理有興趣,可以閱讀這篇文章進(jìn)行了解:《ptrace實(shí)現(xiàn)原理》
ptrace() 是一個(gè)復(fù)雜的系統(tǒng)調(diào)用,主要用于編寫調(diào)試程序。你可以通過以下命令來查看 ptrace() 的介紹:
ptrace() 系統(tǒng)調(diào)用的功能很強(qiáng)大,但我們并不會(huì)用到所有的功能。所以,本文的約定是:在編寫程序的過程中,使用到的功能才會(huì)進(jìn)行詳細(xì)介紹。
可見,運(yùn)行 ls 這個(gè)命令需要執(zhí)行 40 多萬條指令。
【文章福利】小編推薦自己的Linux內(nèi)核技術(shù)交流群:【891587639】整理了一些個(gè)人覺得比較好的學(xué)習(xí)書籍、視頻資料共享在群文件里面,有需要的可以自行添加哦?。。。ê曨l教程、電子書、實(shí)戰(zhàn)項(xiàng)目及代碼)? ??


簡(jiǎn)易的 GDB
我們要實(shí)現(xiàn)一個(gè)有如下功能的 GDB:
可以對(duì)一個(gè)可執(zhí)行程序進(jìn)行調(diào)試。
可以在調(diào)試程序時(shí),設(shè)置斷點(diǎn)。
可以在調(diào)試程序時(shí),打印程序的信息。
下面主要圍繞這三個(gè)功能進(jìn)行闡述。
1. 調(diào)試可執(zhí)行文件
我們使用 GDB 調(diào)試程序時(shí),一般使用 GDB 直接加載程序的可執(zhí)行文件,如下命令:
上面命令的執(zhí)行過程如下:
首先,GDB 調(diào)用 fork() 系統(tǒng)調(diào)用創(chuàng)建一個(gè)新的子進(jìn)程。
然后,子進(jìn)程會(huì)調(diào)用 exec() 系統(tǒng)調(diào)用加載程序的可執(zhí)行文件到內(nèi)存。
接著,子進(jìn)程便進(jìn)入停止?fàn)顟B(tài)(停止運(yùn)行),并且等待 GDB 主進(jìn)程發(fā)送調(diào)試命令。
流程如下圖所示:

我們可以按照上面的流程來編寫代碼:
第一步:創(chuàng)建被調(diào)試子進(jìn)程
調(diào)試程序一般分為 被調(diào)試進(jìn)程 與 調(diào)試進(jìn)程。
被調(diào)試進(jìn)程:就是需要被調(diào)試的進(jìn)程。
調(diào)試進(jìn)程:主要用于向 被調(diào)試進(jìn)程 發(fā)送調(diào)試命令。
實(shí)現(xiàn)代碼如下:
上面的代碼執(zhí)行流程如下:
主進(jìn)程首先調(diào)用 fork() 系統(tǒng)調(diào)用創(chuàng)建一個(gè)子進(jìn)程。
然后子進(jìn)程會(huì)調(diào)用 load_executable_file() 函數(shù)加載要進(jìn)行調(diào)試的程序,并且等待主進(jìn)程發(fā)送調(diào)試命令。
最后主進(jìn)程會(huì)調(diào)用 send_debug_command() 向被調(diào)試進(jìn)程(子進(jìn)程)發(fā)送調(diào)試命令。
所以,接下來我們主要介紹 load_executable_file() 和 send_debug_command() 這兩個(gè)函數(shù)的實(shí)現(xiàn)過程。
第二步:加載被調(diào)試程序
前面我們說過,子進(jìn)程主要用于加載被調(diào)試的程序,并且等待調(diào)試進(jìn)程(主進(jìn)程)發(fā)送調(diào)試命令,現(xiàn)在我們來分析下 load_executable_file() 函數(shù)的實(shí)現(xiàn):
load_executable_file() 函數(shù)的實(shí)現(xiàn)很簡(jiǎn)單,主要執(zhí)行流程如下:
調(diào)用 ptrace(PTRACE_TRACEME...) 系統(tǒng)調(diào)用告知內(nèi)核,當(dāng)前進(jìn)程可以被進(jìn)行跟蹤,也就是可以被調(diào)試。
調(diào)用 execl() 系統(tǒng)調(diào)用加載并且執(zhí)行被調(diào)試的程序可執(zhí)行文件。
首先,我們來看看 ptrace() 系統(tǒng)調(diào)用的原型定義:
下面我們對(duì)其各個(gè)參數(shù)進(jìn)行說明:
request:向進(jìn)程發(fā)送的調(diào)試命令,可以發(fā)送的命令很多。比如上面代碼的 PTRACE_TRACEME 命令定義為 0,表示能夠?qū)M(jìn)程進(jìn)行調(diào)試。
pid:指定要對(duì)哪個(gè)進(jìn)程發(fā)送調(diào)試命令的進(jìn)程ID。
addr:如果要讀取或者修改進(jìn)程某個(gè)內(nèi)存地址的內(nèi)容,就可以通過這個(gè)參數(shù)指定。
data:如果要修改進(jìn)程某個(gè)地址的內(nèi)容,要修改的值可以通過這個(gè)參數(shù)指定,配合 addr 參數(shù)使用。
所以,代碼:
的作用就是告知內(nèi)核,當(dāng)前進(jìn)程能夠被跟蹤(調(diào)試)。
接著,當(dāng)調(diào)用 execl() 系統(tǒng)調(diào)用加載并且執(zhí)行被調(diào)試的程序時(shí),內(nèi)核會(huì)把當(dāng)前被調(diào)試的進(jìn)程掛起(把運(yùn)行狀態(tài)設(shè)置為停止?fàn)顟B(tài)),等待主進(jìn)程發(fā)送調(diào)試命令。
當(dāng)進(jìn)程的運(yùn)行狀態(tài)被設(shè)置為停止?fàn)顟B(tài)時(shí),內(nèi)核會(huì)停止對(duì)此進(jìn)程進(jìn)行調(diào)度,除非有其他進(jìn)程把此進(jìn)程的運(yùn)行狀態(tài)改為可運(yùn)行狀態(tài)。
第三步:向被調(diào)試進(jìn)程發(fā)送調(diào)試命令
我們來到最重要的一步了,就是要向被調(diào)試的進(jìn)程發(fā)送調(diào)試命令。
用過 GDB 調(diào)試程序的同學(xué)都非常熟悉,我們可以向被調(diào)試的進(jìn)程發(fā)送 單步調(diào)試、打印當(dāng)前堆棧信息、查看某個(gè)變量的值 和 設(shè)置斷點(diǎn) 等操作。
這些命令都可以通過 ptrace() 系統(tǒng)調(diào)用發(fā)送,下面我們介紹一下怎么使用 ptrace() 系統(tǒng)調(diào)用來對(duì)被調(diào)試進(jìn)程進(jìn)行調(diào)試操作。
send_debug_command() 函數(shù)的實(shí)現(xiàn)有點(diǎn)小復(fù)雜,我們來分析下這個(gè)函數(shù)的主要執(zhí)行流程吧。
當(dāng)被調(diào)試進(jìn)程被內(nèi)核掛起時(shí),內(nèi)核會(huì)向其父進(jìn)程發(fā)送一個(gè) SIGCHLD 信號(hào),父進(jìn)程可以通過調(diào)用 wait() 系統(tǒng)調(diào)用來捕獲這個(gè)信息。
然后我們?cè)谝粋€(gè)循環(huán)內(nèi),跟蹤進(jìn)程執(zhí)行指令的過程。
通過調(diào)用 ptrace(PTRACE_GETREGS...) 來獲取當(dāng)前進(jìn)程所有寄存器的值。
通過調(diào)用 ptrace(PTRACE_PEEKTEXT...) 來獲取某個(gè)內(nèi)存地址的值。
通過調(diào)用 ptrace(PTRACE_SINGLESTEP...) 將被調(diào)試進(jìn)程設(shè)置為單步調(diào)試模式,這樣當(dāng)被調(diào)試進(jìn)程每執(zhí)行一條指令,都會(huì)進(jìn)入停止?fàn)顟B(tài)。
整個(gè)調(diào)試流程可以歸納為以下的圖片:

測(cè)試程序
最后,我們來測(cè)試一下這個(gè)簡(jiǎn)單的調(diào)試工具的效果。我們使用以下命令編譯程序:
編譯之后,我們會(huì)獲得一個(gè)名為 tdb 的可執(zhí)行文件。然后,我們可以使用以下命令來調(diào)試程序:
例如我們要調(diào)試 ls 命令這個(gè)程序,可以輸入以下命令:
可見,運(yùn)行 ls 這個(gè)命令需要執(zhí)行 40 多萬條指令。
