int 3中斷與軟件調試

摘要:平常編程調試的過程中,我們可能會有這樣的疑惑:“爲什麼使用硬件模擬器,比如bochs調試的時候,開始設置的調試斷點都不會生效?”,“斷點調試的本質是什麼,爲什麼程序能夠在特定的地方停留下來?既然程序是指令流,爲何CPU沒有一直執行下去?”,“在軟件中斷的情況下,如何進行調試?”。斷點和單步執行是兩個經常使用的調試功能,也是調試器的核心功能。本章我們將介紹IA-32 CPU是如何支持斷點和單步執行功能的,然後逐一爲你解答這些疑問。    
   

1.軟件斷點  

   
   x86系列處理器從其第一代產品英特爾8086開始就提供了一條專門用來支持調試的指令,即INT 3。簡單地說,這條指令的目的就是使CPU中斷(break)到調試器,以供調試者對執行現場進行各種分析。當我們調試程序時,可以在可能有問題的地方插入一條INT 3指令,使CPU執行到這一點時停下來。這便是軟件調試中經常用到的斷點(breakpoint)功能,因此INT 3指令又被稱爲斷點指令。    
 下面,我們來測試一個程序:  
 

1	#include2	#include3	#include4	5	int main()6	{7		printf("hello world");8		__asm__("int $0x03");9		printf("hello world");10		return 0;11	}

 編譯:gcc hello.c -o hello  
 調試:gdb hello   
 

(gdb) r
Starting program: /home/huangyk/doc/major/操作系統/hello 
Program received signal SIGTRAP, Trace/breakpoint trap.
main () at hello.c:99		printf("hello world");

     
   可以看到,即使我們不在調試器中設置斷點,也能正常中斷    
   查看當前堆棧,定位IP(在64b機器下面,是rip)    
 

(gdb) info frame
Stack level 0, frame at 0x7fffffffdaa0:
 rip = 0x4004db in main (hello.c:9); saved rip 0x30b3a1ed1d
 source language c.
 Arglist at 0x7fffffffda90, args: 
 Locals at 0x7fffffffda90, Previous frame's sp is 0x7fffffffdaa0
 Saved registers:
  rbp at 0x7fffffffda90, rip at 0x7fffffffda98

 查看內存區域的對應數據  
 

(gdb) x/10i $rip-20   0x4004c7 :	in     $0xb8,%eax
   0x4004c9 :	clc    
   0x4004ca :	add    $0x89480040,%eax
   0x4004cf :	(bad)  
   0x4004d0 :	mov    $0x0,%eax
   0x4004d5 :	callq  0x4003b8 
   0x4004da :	int3   
=> 0x4004db :	mov    $0x4005f8,%eax
   0x4004e0 :	mov    %rax,%rdi
   0x4004e3 :	mov    $0x0,%eax

     我們可以清楚看見,剛纔執行了一個int 3指令。斷點異常(INT 3)屬於陷阱類異常,當CPU產生異常時,其程序指針是指向導致異常的下一條指令的。    
   注意:在windows其他的調試器中不是這樣,eip被設定成指向int 3指令。    
   

2.在調試器中設置斷點  

   
   考慮一下調試器是如何設置斷點的。當我們在調試器中對代碼的某一行設置斷點時,調試器會先把這裏的本來指令的第一個字節保存起來,然後寫入一條INT 3指令。因爲INT 3指令的機器碼爲11001100b(0xCC),僅有一個字節,所以設置和取消斷點時也只需要保存和恢復一個字節,這是設計這條指令時須考慮好的。    
   
 

(gdb) b 8Breakpoint 1 at 0x4004da: file hello2.c, line 8.
(gdb) r
Starting program: /home/huangyk/doc/major/操作系統/two Breakpoint 1, main () at hello2.c:88		printf("hello world");
Missing separate debuginfos, use: debuginfo-install glibc-2.12-1.132.el6.x86_64
(gdb) x/10i $rip-20
   0x4004c6 :	mov    %esp,%ebp   0x4004c8 :	mov    $0x4005f8,%eax   0x4004cd :	mov    %rax,%rdi   0x4004d0 :	mov    $0x0,%eax   0x4004d5 :	callq  0x4003b8 => 0x4004da :	mov    $0x4005f8,%eax   0x4004df :	mov    %rax,%rdi   0x4004e2 :	mov    $0x0,%eax   0x4004e7 :	callq  0x4003b8 
   0x4004ec :	mov    $0x0,%eax

     
   你看到了什麼?怎麼沒有int 3指令呢?    
   值得說明的是,在調試器下,我們是看不到動態替換到程序中的INT 3指令的。大多數調試器的做法是在被調試程序中斷到調試器時,會先將所有斷點位置被替換爲INT 3的指令恢復成原來的指令,然後再把控制權交給用戶。    
   

3.斷點命中  

當CPU執行到INT 3指令時,由於INT 3指令的設計目的就是中斷到調試器,因此,CPU執行這條指令的過程也就是產生斷點異常(breakpoint exception,簡稱#BP)並轉去執行異常處理例程的過程。在跳轉到處理例程之前,CPU會保存當前的執行上下文,包括段寄存器、程序指針寄存器等內容。      
   注意:斷點命中之後的中斷服務程序是調試器來定義的,然後將服務入口註冊到IDT中。    
   

4.恢復執行  

   
   這裏有一個問題,前面我們說當斷點命中中斷到調試器時,調試器會把所有斷點處的INT 3指令恢復成本來的內容。因此,在用戶發出了恢復執行命令後,調試器在通知系統真正恢復程序執行前,調試器需要將斷點列表中的所有斷點再落實一遍。但是對於剛纔命中的這個斷點需要特別對待,試想如果把這個斷點處的指令也替換爲INT 3,那麼程序一執行便又觸發斷點了。但是如果不替換,那麼這個斷點便沒有被落實,程序下次執行到這裏時就不會觸發斷點,而用戶並不知道這一點。對於這個問題,大多數調試器的做法都是先單步執行一次。也就是說,先設置單步執行標誌(下一節將詳細討論),然後恢復執行,將斷點所在位置的指令執行完。因爲設置了單步標誌,所以,CPU執行完斷點位置的這條指令後會立刻再中斷到調試器中,這一次調試器不會通知用戶,會做一些內部操作後便立刻恢復程序執行,而且將所有的斷點都落實(使用INT 3替換)。如果用戶在恢復程序執行前,已經取消了當前的斷點,那麼就不需要先單步執行一次了。    
   

5.特別用途—— 燙燙燙燙燙  

   
   因爲INT 3指令的特殊性,所以它有一些特別的用途。讓我們從一個有趣的現象說起。當我們用VC6進行調試時,常常會觀察到一塊剛分配的內存或字符串數組裏面被填充滿了"CC"。如果是在中文環境下,因爲0xCCCC恰好是漢字"燙"字的簡碼,所以會觀察到很多"燙燙燙……",而0xCC又正好是INT 3指令的機器碼,這是偶然的麼?答案是否定的。因爲這是編譯器故意這樣做的。爲了輔助調試,編譯器在編譯調試版本時會用0xCC來填充剛剛分配的緩衝區。這樣,如果因爲緩衝區或堆棧溢出時程序指針意外指向了這些區域,那麼便會因爲遇到INT 3指令而馬上中斷到調試器。    
   

6.系統對int 3的優待  

   
   關於INT 3指令還有一點要說明的是,INT 3指令與當n=3時的INT n指令(通常所說的軟件中斷)並不同。INT n指令對應的機器碼是0xCD後跟1字節n值,比如INT 23H 會被編譯爲0xCD23。與此不同的是,INT 3指令具有獨特的單字節機器碼0xCC。而且系統會對INT 3指令給予一些特殊的待遇,比如在虛擬8086模式下免受IOPL檢查等。    
   

7.爲什麼看不到調試期寫入的int 3指令  

   因爲,調試器總是“執行到b line,替換爲int 3指令,調用中斷,恢復int 3之前的指令,將現場返回給用戶”,所以,int 寫入但是又被置換,整個過程對用於是透明的。    
   

8.歸納與解惑  

   
   因爲使用INT 3指令產生的斷點是依靠插入指令和軟件中斷機制工作的,因此人們習慣把這類斷點稱爲軟件斷點,軟件斷點具有如下侷限性。    
   屬於代碼類斷點,即可以讓CPU執行到代碼段內的某個地址時停下來,不適用於數據段和I/O空間。    
   對於在ROM(只讀存儲器)中執行的程序(比如BIOS或其他固件程序),無法動態增加軟件斷點。因爲目標內存是隻讀的,無法動態寫入斷點指令。這時就要使用我們後面要介紹的硬件斷點。    
   在中斷向量表或中斷描述表(IDT)沒有準備好或遭到破壞的情況下,這類斷點是無法或不能正常工作的,比如系統剛剛啓動時或IDT被病毒篡改後,這時只能使用硬件級的調試工具。    
   雖然軟件斷點存在以上不足,但因爲它使用方便,而且沒有數量限制(硬件斷點需要寄存器記錄斷點地址,有數量限制),所以目前仍被廣泛應用。    
   回到我們最開始提出的問題:由於調試是和調試期密切相關的,在用bochs+freedos 來調試操作系統的時候,如果在我們自己的操作系統起來之前,這時候不滿足軟件中斷的使用條件,所以會設置斷點失敗,需要利用硬件中斷,xchg bx,bx;進入到系統之後,然後就可以使用正常的軟件中斷了。    
   參考:<軟件調試>第四章第一節    http://book.51cto.com/art/200812/100663.htm  


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章