(一)gdb調試原理
此部分轉自:https://blog.csdn.net/u012658346/article/details/51159971 https://www.cnblogs.com/xsln/p/ptrace.html
gdb調試的原理是基於ptrace系統調用,ptrace()系統調用提供了一個方法,該方法使一個程序(追蹤者)可以觀察和控制另外一個程序(被追蹤者)的執行,並檢查和改變被追蹤者的內存及寄存器。它主要用於實現斷點調試和追蹤系統調用。
當被追蹤時,被追蹤線程在接收信號時會被停止,即使那個信號是被忽略的也是如此(SIGKILL除外)。追蹤程序會在一個調用waitpid(或者其他類wait系統調用)時收到通知,該調用會返回一個包含被追蹤線程停止的原因的狀態值。當被追蹤線程停止時,追蹤程序可以使用多種ptrace請求來檢查和編輯被追蹤線程。追蹤程序可以讓被追蹤線程繼續運行,有選擇地忽略發過來的信號(甚至可以發送一個完全不同的信號給被追蹤線程)
利用ptrace系統調用,可在被調試程序和gdb之間建立追蹤關系。然后所有發送給被調試程序(被追蹤線程)的信號(除SIGKILL)都會被gdb截獲,gdb根據截獲的信號,查看被調試程序相應的內存地址,並控制被調試的程序繼續運行。
ptrace系統調用原型:
long ptrace(enum __ptrace_request request, pid_t pid,void *addr,void *data);
request參數的主要選項:
PTRACE_TRACEME:由子進程調用,表示本進程將被其父進程跟蹤,交付給這個進程的所有信號,即使信號是忽略處理的(除SIGKILL之外),都將使其停止,父進程將通過wait()獲知這一情況。
PTRACE_ATTACH: attach到一個指定的進程,使其成為當前進程跟蹤的子進程,而子進程的行為等同於它進行了一次PTRACE_TRACEME操作。但是,需要注意的是,雖然當前進程成為被跟蹤進程的父進程,但是子進程使用getppid()的到的仍將是其原始父進程的pid。當你在gdb中使用attach命令來跟蹤一個指定進程/線程的時候,gdb就自動成為改進程的父進程,而被跟蹤的進程則使用了一次PTRACE_TRACEME,gdb也就順理成章的接管了這個進程。
PTRACE_CONT:繼續運行之前停止的子進程。可同時向子進程交付指定的信號。
gdb三種調試方式:
1)attach並調試一個已經運行的進程:
確定需要進行調試的進程id,運行gdb,輸入attch pid,如:gdb 12345。gdb將對指定進行執行如下操作:ptrace(PTRACE_ATTACH,pid,0,0), 建立自己與進程號為pid的進程間的跟蹤關系。即利用PTRACE_ATTACH,使自己變成被調試程序的父進程。用attach建立起來的跟蹤關系,可以調用ptrace(PTRACE_DETACH,pid,...)來解除。注意attach進程時的權限問題,如一個非root權限的進程是不能attach到一個root進程上的 。
2)運行並調試一個新的進程,利用fork+execve執行被測試的程序,子進程在執行execve之前調用ptrace(PTRACE_TRACEME),建立了與父進程(debugger)的跟蹤關系:
運行gdb,通過命令行參數或file指定目標調試程序,如gdb ./test
輸入run命令,gdb執行下述操作:
通過fork()系統調用創建一個新進程
在新創建的子進程中調用ptrace(PTRACE_TRACEME,0,0,0)
在子進程中通過execv()系統調用加載用戶指定的可執行文件
3)遠程調試目標主機上新創建的進程
gdb運行在調試機,gdbserver運行在目標機,通過二者之間定義的數據格式進行通信
gdb調試基礎--信號
gdb調試的實現都是建立在信號的基礎上的,在使用參數為PTRACE_TRACEME或PTRACE_ATTACH的ptrace系統調用建立調試關系后,交付給目標程序的任何信號首先都會被gdb截獲。 因此gdb可以先行對信號進行相應處理,並根據信號的屬性決定是否要將信號交付給目標程序。
.斷點原理:
1) 斷點的實現原理,就是在指定的位置插入斷點指令,當被調試的程序運行到斷點的時候,產生SIGTRAP信號。該信號被gdb捕獲並進行斷點命中判定,當gdb判斷出這次SIGTRAP是斷點命中之后就會轉入等待用戶輸入進行下一步處理,否則繼續。
2) 斷點的設置原理: 在程序中設置斷點,就是先將該位置的原來的指令保存,然后向該位置寫入int 3。當執行到int 3的時候,發生軟中斷,內核會給子進程發出SIGTRAP信號,當然這個信號會被轉發給父進程。然后用保存的指令替換int3,等待恢復運行。
3) 斷點命中判定:gdb把所有的斷點位置都存放在一個鏈表中,命中判定即把被調試程序當前停止的位置和鏈表中的斷點位置進行比較,看是斷點產生的信號,還是無關信號。
4) 條件斷點的判定:原理同3),只是恢復斷點處的指令后,再多加一步條件判斷。若表達式為真,則觸發斷點。由於需要判斷一次,因此加入條件斷點后,不管有沒有觸發到條件斷點,都會影響性能。在x86平台,某些硬件支持硬件斷點,在條件斷點處不插入int 3,而是插入一個其他指令,當程序走到這個地址的時候,不發出int 3信號,而是先去比較一下特定寄存器和某個地址的內容,再決定是否發送int 3。因此,當你的斷點的位置會被程序頻繁地“路過”時,盡量使用硬件斷點,會對提高性能有幫助。
單步跟蹤:
next指令可以實現單步調試,即每次只執行一行語句。一行語句可能對應多條及其指令,當執行next指令時,gdb會計算下一條語句對應的第一條指令的地址,然后控制目標程序走到該位置停止。
(二)qemu中的gdbserver
正常情況下進行遠程調試需要被調試端安裝有gdbserver程序,而qemu中內置了gdbserver模塊,基於此可使用gdb實現對qemu虛擬機的遠程調試,GDB/GDBSERVER調試模型的原理如下:
在GDB/GDBSERVER調試模型中,GDBSERVER是一個輕量級的GDB調試器,在調試過程中擔任着調試代理的角色。在調試過程中,主機和目標機之間使用串口或者網絡作為通信的通道。在主機上GDB通過這條通道使用一種基於ASCII的簡單通訊協議RSP與在目標機上運行的GDBSERVER進行通訊。GDB發送指令,如內存、寄存器讀寫,GDBSERVER則首先與運行被調試程序映像的進程進行綁定,然后等待GDB發來的數據,對包含命令的數據包進行解析之后便進行相關處理,然后將結果返回給主機上的GDB。
RSP協議將GDB/GDBSERVER間通訊的內容更看做是數據包,數據包的內容都使用ASCII字符。每一個數據包都遵循這樣的格式:$ <調試信息>#<校驗碼>.
如上圖所示,包的內容會以16進制的形式來編碼(enhex),#后面的兩位數字是校驗碼,具體的計算方式是數據包中所有字符求和再用256求余數。而數據包的內容,也就是RSP協議的載體,將會是gdb接收的命令。接受方在收到數據包之后,對數據包進行校驗,若正確回應“+”,反之回應“-”。
RSP 協議中定義的主要命令可以分為 3 類:
(1)寄存器/內存讀寫命令
命令 g: 讀所有寄存器的值
命令 G:寫所有寄存器的值
命令 P: 寫某個寄存器
命令 m: 讀某個內存單元
命令 M:寫某個內存單元
(2)程序控制命令
命令?: 報告上一次的信號
命令 s: 單步執行
命令 c: 繼續執行
命令 k: 終止程序
(3)其它命令
命令 O:控制台輸出(Console Output )
命令 E:出錯回應(Error response)
當主機使用gdb調試時,gdb和qemu中內置的gdbserver就使用上述模型進行交互,從而對qemu虛擬機進行調試。如gdb調試端發送x/ <n/f/u> <addr>表示讀取addr處的內容,命令經RSP協議封裝成數據包發送至qemu的gdbserver端,gdbserver收到數據包后對其進行校驗,校驗成功后進行解析處理並返回至gdb客戶端。
開啟gdbserver之后,會等待來自gdb的連接請求,默認端口為1234,gdb使用ip和端口與gdbserver連接:
連接建立后會調用gdb_handlesig()等待stdin傳來的gdb指令,調用gdb_read_byte()解析用戶的輸入,並對數據包進行校驗,若校驗正確調用gdb_handle_packet()進行gdb命令的處理。
如下解析字符為m時,表示讀取某個內存單元,則調用函數target_memory_rw_debug()進行內存單元的讀取,該函數最后調用cpu_memory_rw_debug()讀取內存內容
當解析字符為g時,使用gdb_read_register讀取寄存器信息,該函數會調用特定CPU類型的回調函數:
x86下調用如下函數,通過qemu為虛擬機維護的CPUX86State結構體得到虛擬機的寄存器信息:
類似地,插入一個斷點時:
在kvm_enabled的情況下,調用kvm_insert_breakpoint:
該函數進行斷點的插入並最終使用kvm_update_guest_debug向kvm更新客戶機的debug狀態,該函數調用kvm_invoke_set_guest_debug,進一步調用kvm_vcpu_ioctl(cpu, KVM_SET_GUEST_DEBUG,&dbg_data->dbg)執行ioctl至kvm中設置相關異常向量,BP(breakpoint,int3),DB(int 1)(插一句,可通過設置異常位圖中的這兩個位對上述指令進行攔截)