偵錯程式是個大騙子!

軒轅之風發表於2023-05-05

我叫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,是你除錯程式的好夥伴,現在你該知道我是如何工作的了吧!

【完】

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

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

相關文章