程序调试器原理
調試器原理:
?
?? 調試器是一個程序,在開發工具中也是調用一個程序,在運行時就是一個進程,這個進程與普通進程沒有區別,只是這個進程調用了內核的一些特殊函數(系統調用)來操縱內核數據,這些數據就是被調試進程的內存數據。
?
而對操作系統調試的調試器則不同,因為沒有操作系統的支持,調試器本身就不再需要調用操作系統內核的程序來支持,但是此時的調試非常特殊,因為操作系統自己有中斷處理程序,調試器對中斷服務程序的劫持會讓操作系統的操作變得有些不同,調試器首先啟動先設置CPU為單步執行狀態,然后啟動操作系統的代碼,每執行一條指令,CPU均產生中斷進入調試器程序(中斷服務程序)。
通常內核調試緊緊是通過輸出信息(如printk)來調試,這其實已經不是調試,而是嵌入代碼進行測試,即使是kgdb也是在內核中插入了代碼來實現調試,如讓windows運行時啟動調試模式,那么其內核中的調試代碼就起作用了,這些調試是內核設計者預先設計好的。
?
在操作系統下調試程序,只是調試用戶態代碼,而用戶態代碼是出于操作系統控制之下的,因此調試就是借助操作系統來操作目標進程。
? 無論windows還是unix中,都是通過對系統調用的劫持來實現對目標程序的調試的,如果操作系統內核不實現對程序的調試功能,那么調試器是不可能實現對另一個進程控制的。調試器通過創建子進程,并告訴操作系統(創建進程的狀態設置)自己要對子進程進行調試,那么操作系統裝載目標程序時,如果發生了規定的事件,就會停止目標進程的執行,此時對于目標進程來說,根本不知道操作系統為什么將自己停了,因為目標進程此時正在處于系統調用中,如線程創建、退出、發生異常等,而這時的內核代碼就會檢查當前進程是否處于調試狀態,如果是,那么就啟動調試器進程,這里所說的事件的發生是由操作系統內核實現的,基本是與內核的進程(線程)操作有關。
當目標進程中斷后,調試器也必須通過內核才能實現讀取目標進程的內存,如果不是這樣,進程間可以互相讀寫進程就會讓操作系統的內存保護功能失效了。
內核對于目標進程的內存讀寫是相當簡單的,就是找到目標進程的頁表,遍歷頁表找到虛擬內存對應的實際內存地址(物理地址),然后讀寫這個內存,對目標進程的寄存器也是一樣,內核很容易獲得目標進程的進程塊,其中保存了目標進程的所有寄存器值。
如果沒有對目標進程啟動的事件中斷,那么目標進程就會一直執行,不會受調試器的控制。正因為內核實現了目標進程一旦創建后就會讓其中斷,然后等待調試器進程的指令。
調試器在目標進程剛準備好就獲得了目標進程的控制權,然后如果調試器直接run目標進程,那么目標進程就會處于失控狀態,只有當系統事件(如線程創建、退出、DLL裝卸載等)發生時內核才會中斷目標進程,啟動調試器。
因此調試時,通常在一開始就要實現對目標進程的單步調試或在目標進程中實現斷點,這樣目標進程才會頻繁中斷或在目標位置中斷,然后交給調試器處理。
設置單步調試比較簡單,設置CPU的狀態寄存器的TF位,那么CPU每執行一個指令就會產生中斷,當然如果目標程序是執行系統調用,那么內核會清除這個狀態位,因此一旦進入內核,CPU就不是單步執行了,而是只有等系統調用返回才能執行下一步調試。
?
單步調試可以讓我們實現很多功能,如果有源代碼,那么我們執行源代碼調試時,程序會編譯帶上調試信息,而在其中會構建符號表,行號等信息,每條指令會對應源程序行號,目標程序每執行一個指令,就產生中斷,而內核中斷服務程序就調用調試器來處理,調試器啟動后,當然還是通過系統調用來讀寫目標程序的內存,當然首先還是判斷當前執行的指令是屬于源程序的哪行產生的代碼,因此調試器會維護很多關于源程序與目標代碼之間的關聯信息。這需要編譯器的支持,經過優化后,源代碼與目標代碼之間的對應關系有時也許并不那么明顯。
?
當然要設置斷點調試,就不是這么單步執行,其實在源代碼調試時,也可以通過設置斷點來實現調試,調試器也通常這樣做。 調試器通過修改斷點地址的指令代碼來實現中斷,修改為中斷指令后,目標進程一執行到這里就會產生中斷(異常),然后操作系統就會啟動調試器來接管。因此必須在以上提到的系統事件發生時,調試器才能設置斷點,例如在進程啟動時刻,調試器會獲得控制權,此時就可以通過修改目標進程的內存來實現斷點。而設置斷點時,當然調試器會將斷點處的內存保存下來,一次可以設置多個斷點,那么調試器必然會保存地址與內存值對應的表。當斷點中斷發生時,調試器等待用戶指令操作完,進行run操作時,調試器就通過重新寫回目標斷點處的指令,目標程序接著執行。
當然要設置斷點,就必須對目標程序非常了解,否則,你設置斷點時,修改的是一個指令的部分(產生非法指令異常),或者是一個數據(沒有反應)。設置斷點是有目標的,首先我們要遍歷目標程序的內存,或者通過反編譯找到要調試的目標代碼,然后通過對可執行文件的分析,找到代碼加載到內存的位置,這樣我們才可以設置斷點。
當然對于源代碼級別的斷點設置,因為有源代碼和調試信息的支持,比較容易找到源代碼與加載到內存的指令地址的關系,設置斷點就比較容易。因此有源代碼的調試就比直接的內存進程調試相對簡單。
調試器中,經常要用到符號表,符號表是一個調試器用來解釋目標內存數據意義的數據表,如獲得一個目標進程內存地址的數據后,如果沒有符號表,那么我們只能看到一堆二進制數據,如果用符號表解釋出來,那么就會知道內存中保存了什么。
調試器可以將內存轉換成某個對象,按照某個結構體解析數據,然后我們就可以發現每個對象成員的值。前提是我們知道這個位置保存了某個類型的對象,如果實際上不是,那么解析結果就是無意義的。
還有一種符號表,假定對象在內存中的位置是不變的,那么我們可以通過地址中的數據就可以構建起對象。
對于內核調試中,我們通常通過結構體(符號表)解析某個地址內容的意義。當然調試器通常沒有對象的識別能力,但是完全可以設計一個調試器,可以根據預先定義的對象網絡(或樹)來構建起對目標對象內存的解釋。通常在內核調試時,我們也是通過獲得一個核心對象后,然后根據其中的地址來獲得其引用的其他對象,當然前提是調試者要非常清楚內存中對象的實際關系。
總結
- 上一篇: w ndows无法识别usb,surfa
- 下一篇: u-boot简单学习笔记(二)——AR9