偵錯程式是個大騙子!

2023-05-05 15:00:33

我叫GDB,是一個偵錯程式,程式設計師通過我可以偵錯他們編寫的軟體,分析其中的bug。

作為一個偵錯程式,偵錯分析是我的看家本領,像是給目標程序設定斷點,或者讓它單步執行,又或是檢視程序中的變數、記憶體資料、CPU的寄存等等操作,我都手到擒來。

你只要輸入對應的命令,我就能幫助你偵錯你的程式。

我之所以有這些本事,都得歸功於一個強大的系統函數,它的名字叫ptrace。

不管是開始偵錯程序,還是下斷點、讀寫程序資料、讀寫暫存器,我都是通過這個函數來進行,要是沒了它,我可就廢了。

它的第一個引數是一個列舉型的變數,表示要執行的操作,我支援的偵錯命令很多都是靠它來實現的:

你可以通過我來啟動一個新的程序偵錯,我會使用fork建立出一個新的子程序,然後在子程序中通過execv來執行你指定的程式。

不過在執行你的程式之前,我會在子程序中呼叫ptrace函數,然後指定第一個引數為PTRACE_TRACEME,這樣一來,我就能監控子程序中發生的事情了,也才能對你指定的程式進行偵錯。

你也可以讓我attach到一個已經執行的程序分析,這樣的話,我直接呼叫ptrace函數,並且指定第一個引數為PTRACE_ATTACH就可以了,然後我就會變成那個程序的父程序。

具體要選擇哪種方式來偵錯,這就看你的需要了。不過不管哪種方式,最終我都會「接管」被偵錯的程序,它裡面發生的各種訊號事件我都能得到通知,方便我對它進行偵錯操作。

軟體斷點
作為一個偵錯程式,最常用的功能就是給程式下斷點了。

你可以通過break命令告訴我,你要在程式的哪個位置新增斷點。

當我收到你的命令之後,我會偷偷把被偵錯程序中那個位置的指令修改為一個0xCC,這是一條特殊指令的CPU機器碼——int 3,是x86架構CPU專門用來支援偵錯的指令。

我的這個修改是偷偷進行的,你如果通過我來檢視被偵錯程序的記憶體資料,或者在反組合視窗檢視那裡的指令,會發現跟之前一樣,這其實是我使的障眼法,讓你看起來還是原來的資料,實際上已經被我修改過了,你要是不信,你可以另外寫個程式來檢視那裡的資料內容,看看我說的是不是真的。

一旦被偵錯的程序執行到那個位置,CPU執行這條特殊的指令時,會陷入核心態,然後取出中斷描述符表IDT中的3號表項中的處理常式來執行。

IDT中的內容,作業系統一啟動早就安排好了,所以系統核心會拿到CPU的執行權,隨後核心會傳送一個SIGTRAP訊號給到被偵錯的程序。

而因為我的存在,這個訊號會被我截獲,我收到以後會檢查一下是不是程式設計師之前下的斷點,如果是的話,就會顯示斷點觸發了,然後等待程式設計師的下一步指示。

在沒有下一步指示之前,被偵錯的程序都不會進入就緒佇列被排程執行。

直到你使用continue命令告訴我繼續,我再偷偷把替換成int 3的指令恢復,然後我再次呼叫ptrace函數告訴作業系統讓它繼續執行。

這就是我給程式下斷點的祕密。

不知道你有沒有發現一個問題,當我把替換的指令恢復後讓它繼續執行,以後就再也不會中斷在這裡了,可程式設計師並沒有復原這個斷點,而是希望每次執行到這裡都能中斷,這可怎麼辦呢?

我有一個非常巧妙的辦法,就是讓它單步執行,只執行一條指令,然後又會中斷到我這裡,但這時候我並不會通知程式設計師,而僅僅是把剛才恢復的斷點又給打上(替換指令),然後就繼續執行。這一切都發生的神不知鬼不覺,程式設計師根本察覺不到。

單步偵錯
說到單步執行,應該算是程式設計師偵錯程式的時候除了下斷點之外最常見的操作了,每一次只讓被偵錯的程序執行一條指令,這樣方便跟蹤排查問題。

你可能很好奇我是如何讓它單步執行的呢?

單步執行的實現可比下斷點簡單多了,我不用去修改被偵錯程序記憶體中的指令,只需要呼叫ptrace函數,傳遞一個PTRACE_SINGLESTEP引數就行了,作業系統會自動把它設定為單步執行的模式。

我也很好奇作業系統是怎麼辦到的,就去打聽了一下。

原來x86架構CPU有一個標誌暫存器,名叫eflags,它裡面不止包含了程式執行的一些狀態,還有一些工作模式的設定。

其中就有一個TF標記,用來告訴CPU進入單步執行模式,只要把這個標記為設定為1,CPU每執行一條指令,就會觸發一次偵錯異常,偵錯異常的向量號是1,所以觸發的時候,都會取出IDT中的1號表項中的處理常式來執行。

接下來的事情就跟命中斷點差不多了,我會截獲到核心發給被偵錯程序的SIGTRAP訊號,然後等待程式設計師的下一步指令。

如果你繼續進行單步偵錯,那我便繼續重複這個過程。

如果你有程式的原始碼,你還可以進行原始碼級別的單步偵錯,不過這裡的單步就指的是原始碼中的一行了。

這種情況下要稍微麻煩一點,我還要分析出每一行程式碼對應的指令有哪些,然後用上面說的單步執行指令的方法,一條條指令快速掠過,直到這一行程式碼對應的指令都執行完成。

記憶體斷點
有的時候,直接給程式中程式碼的位置下斷點並不能包治百病。比如程式設計師發現某個記憶體地址的內容老是莫名其妙被修改,想知道到底是哪個函數乾的,這時候連地址都沒有,根本沒法下斷點。

單步執行也不行,那麼多條指令,得執行到猴年馬月去才能找到?

不用擔心,我可以幫你解決這個煩惱。

你可以通過watch命令告訴我,讓我監視被偵錯程序中某個記憶體地址的資料變化,一旦發現被修改,我都會把它給停下來報告給你。

猜猜我是如何做到的呢?

我可以用單步執行的方式,每執行一步,就檢查一下內容有沒有沒修改,一旦發現就停下來通知你們程式設計師。

不過這種方式實在是太麻煩了,會嚴重拖垮被偵錯程序的效能。

好在x86架構的CPU提供了硬體斷點的能力,幫我解決了大問題。

在x86架構CPU的內部內建了一組偵錯暫存器,從DR0到DR7,總共8個。通過在DR0-DR3中設定要監控的記憶體地址,然後在DR7中設定要監控的模式,是讀還是寫,剩下的交給CPU就好了。

CPU執行的時候,一旦發現有符合偵錯暫存器中設定的情況發生時,就會產生偵錯異常,然後取出IDT中的1號表項中的處理常式來執行,接下來的事情就跟單步偵錯產生的異常差不多了。

CPU內部依靠硬體電路來完成監控,可比我們軟體一條一條的檢查快多了!

現在,你不止可以使用watch命令來監控記憶體被修改,還可以使用rwatch、awatch命令來告訴我去監控記憶體被讀或者被寫。

我叫GDB,是你偵錯程式的好夥伴,現在你該知道我是如何工作的了吧!

【完】

這裡是程式設計技術宇宙,一個專注用故事分享硬核又有趣計算機知識的公眾號~

覺得不錯的話,歡迎一鍵三連哦~