自己動手寫一個GDB|設(shè)置斷點(原理篇)
什么是斷點
當(dāng)使用 GDB 調(diào)試程序時,如果想在程序執(zhí)行到某個位置(某一行代碼)時停止運行,我們可以通過在此處位置設(shè)置一個 斷點 來實現(xiàn)。
當(dāng)程序執(zhí)行到斷點的位置時,會停止運行。這時,我們可以對進程進行調(diào)試,比如打印當(dāng)前進程的堆棧信息或者打印變量的值等。如下圖所示:

斷點原理
要說明 斷點 的原理,我們首先需要了解下什么是 中斷。
想深入了解中斷原理的,可以看看上文。下面簡單介紹一下什么是中斷:
中斷 是為了解決外部設(shè)備完成某些工作后通知CPU的一種機制(譬如硬盤完成讀寫操作后通過中斷告知CPU已經(jīng)完成)。從物理學(xué)的角度看,中斷是一種電信號,由硬件設(shè)備產(chǎn)生,并直接送入中斷控制器(如 8259A)的輸入引腳上,然后再由中斷控制器向處理器發(fā)送相應(yīng)的信號。處理器一經(jīng)檢測到該信號,便中斷自己當(dāng)前正在處理的工作,轉(zhuǎn)而去處理中斷。此后,處理器會通知 OS 已經(jīng)產(chǎn)生中斷。這樣,OS 就可以對這個中斷進行適當(dāng)?shù)奶幚?。不同的設(shè)備對應(yīng)的中斷不同,而每個中斷都通過一個唯一的數(shù)字標識,這些值通常被稱為中斷請求線。
如果進程在運行的過程中,發(fā)生了中斷,CPU 將會停止運行當(dāng)前進程,轉(zhuǎn)而執(zhí)行內(nèi)核設(shè)置好的 中斷服務(wù)例程。如下圖所示:

【文章福利】小編推薦自己的Linux內(nèi)核技術(shù)交流群:【891587639】整理了一些個人覺得比較好的學(xué)習(xí)書籍、視頻資料共享在群文件里面,有需要的可以自行添加哦!?。。ê曨l教程、電子書、實戰(zhàn)項目及代碼)? ? ?


大概了解中斷的原理后,接下來我們將會介紹 斷點 會用到的 軟中斷 功能。軟中斷跟上面介紹的中斷(也稱為 硬中斷)類似,不過軟中斷并不是由外部設(shè)備產(chǎn)生,而是有特殊的指令觸發(fā),這個特殊的指令稱為 int3。
int3 是一個單字節(jié)的操作碼(十六進制為 0xcc)。當(dāng) CPU 執(zhí)行到 int3 指令時,將會停止運行當(dāng)前進程,轉(zhuǎn)而執(zhí)行內(nèi)核定義好的 int3 中斷處理例程:do_int3()。
do_int3() 例程會向當(dāng)前進程發(fā)送一個 SIGTRAP 信號,當(dāng)進程接收到 SIGTRAP 信號后,CPU 將會停止執(zhí)行當(dāng)前進程。這時調(diào)試進程(GDB)就可以對進程進行調(diào)試,如:打印變量的值、打印堆棧信息等。
設(shè)置斷點
從上面的介紹可知,設(shè)置斷點的目的是讓進程停止運行,從而調(diào)試進程(GDB)就可以對其進行調(diào)試。
接下來,我們將會介紹如何設(shè)置一個斷點。
我們知道,當(dāng) CPU 執(zhí)行到 int3 指令(0xcc)時會停止運行當(dāng)前進程。所以,我們只需要在要進行設(shè)置斷點的位置改為 int3 指令即可。如下圖所示:

從上圖可以看出,設(shè)置斷點時,只需要在要設(shè)置斷點的位置修改為 int3 指令即可。但我們還需要保存原來被替換的指令,因為調(diào)試完畢后,我們還需要把 int3 指令修改為原來的指令,這樣程序才能正常運行。
斷點實現(xiàn)
既然,我們已經(jīng)知道了斷點的原理。那么,現(xiàn)在是時候介紹怎么實現(xiàn)斷點功能了。
我們來說說設(shè)置斷點的步驟吧:
第一步:找到要設(shè)置斷點的地址。
第二步:保存此地址處的數(shù)據(jù)(為了調(diào)試完能夠恢復(fù)原來的指令)。
第三步:我們把此地址處的指令替換成 int3 指令。
第四步:讓被調(diào)試的進程繼續(xù)運行,直到執(zhí)行到 int3 指令(也就是斷點)。此時,被調(diào)試進程會停止運行,調(diào)試進程(GDB)就可以對進程進行調(diào)試。
第五步:調(diào)試完畢后,恢復(fù)斷點處原來的指令,并且讓 IP 寄存器回退一個字節(jié)(因為斷點處原來的代碼還沒執(zhí)行)。
第六步:把被調(diào)試進程設(shè)置為單步調(diào)試模式,這是因為要在執(zhí)行完斷點處原來的指令后,重新設(shè)置斷點(為什么?這是因為在一些循環(huán)語句中,可能需要重新執(zhí)行原來的斷點)。
知道斷點實現(xiàn)的步驟后,我們可以開始編寫代碼了。
我們定義一個結(jié)構(gòu)體 breakpoint_context 用于保存斷點被設(shè)置前的信息:
圍繞 breakpoint_context 結(jié)構(gòu),我們定義幾個輔助函數(shù),分別是:
create_breakpoint():用于創(chuàng)建一個斷點。
enable_breakpoint():用于啟用斷點。
disable_breakpoint():用于禁用斷點。
free_breakpoint():用于釋放斷點。
現(xiàn)在我們來實現(xiàn)這幾個輔助函數(shù)。
1. 創(chuàng)建斷點
首先,我們來實現(xiàn)用于創(chuàng)建一個斷點的輔助函數(shù) create_breakpoint():
create_breakpoint() 函數(shù)需要提供一個類型為 void * 的參數(shù),表示要設(shè)置的斷點地址。
create_breakpoint() 函數(shù)的實現(xiàn)比較簡單,首先調(diào)用 malloc() 函數(shù)申請一個 breakpoint_context 結(jié)構(gòu),然后把 addr 字段設(shè)置為斷點的地址,并且把 data 字段設(shè)置為 NULL。
2. 啟用斷點
啟用斷點的原理是:首先讀取斷點處的數(shù)據(jù),并且保存到 breakpoint_context 結(jié)構(gòu)的 data 字段中。然后將斷點處的指令設(shè)置為 int3 指令。
獲取某個內(nèi)存地址處的數(shù)據(jù)可以使用 ptrace(PTRACE_PEEKTEXT,...) 函數(shù)來實現(xiàn),如下所示:
在上面代碼中,pid 參數(shù)指定了目標進程的PID,而 address 參數(shù)指定了要獲取此內(nèi)存地址處的數(shù)據(jù)。
而要將某內(nèi)存地址處設(shè)置為制定的值,可以使用 ptrace(PTRACE_POKETEXT,...) 函數(shù)來實現(xiàn),如下所示:
在上面代碼中,pid 參數(shù)指定了目標進程的PID,而 address 參數(shù)指定了要將此內(nèi)存地址處的值設(shè)置為 data。
有了上面的基礎(chǔ),現(xiàn)在我們可以來編寫 enable_breakpoint() 函數(shù)的代碼了:
enable_breakpoint() 函數(shù)的原理,上面已經(jīng)詳細介紹過了。
不過有一點我們需要注意的,就是使用 ptrace() 函數(shù)一次只能獲取和設(shè)置一個 4 字節(jié)大小的長整型數(shù)據(jù)。但是 int3 指令是一個單子節(jié)指令,所以設(shè)置斷點時,需要對設(shè)置的數(shù)據(jù)進行處理。如下圖所示:

3. 禁用斷點
禁用斷點的原理與啟用斷點剛好相反,就是把斷點處的 int3 指令替換成原來的指令,原理如下圖所示:

由于 breakpoint_context 結(jié)構(gòu)的 data 字段保存了斷點處原來的指令,所以我們只需要把斷點處的指令替換成 data 字段的數(shù)據(jù)即可,代碼如下:
4. 釋放斷點
釋放斷點的實現(xiàn)就非常簡單了,只需要調(diào)用 free() 函數(shù)把 breakpoint_context 結(jié)構(gòu)占用的內(nèi)存釋放掉即可,代碼如下:
