GDB調試原理 ptrace系統調用

  • gdb基本上大家都在用,你有沒有想過它的實現原理是什麼?為什麼它可以控制程序執行、中斷、訪問內存甚至直接使程序流程改變?

  • 在使用gdb調試程序時,程序的進程狀態是”T”,但又似乎並非接到了SIGSTOP信號,那麼這個”T”是什麼呢?

追根溯源,我們今天來研究一下Linux下這個強大的系統調用:ptrace()

首先,linux的進程狀態大體分為以下幾種:

D (TASK_UNINTERRUPTIBLE),不可中斷的睡眠狀態。
R (TASK_RUNNING),進程執行中。
S (TASK_INTERRUPTIBLE),可中斷的睡眠狀態。
T (TASK_STOPPED),暫停狀態。
t (TASK_TRACED),進程被追蹤。
w (TASK_PAGING),進程調頁中,2.6以上版本的內核中已經被移除。
X (TASK_DEAD – EXIT_DEAD),退出狀態,進程即將被銷燬。
Z (TASK_DEAD – EXIT_ZOMBIE),退出狀態,進程成為殭屍進程。

(以上內容來自ps命令的manual手冊,原文請看↓)

images

其中上面的5就是我們要討論的,gdb調試程序時的t狀態,程序被追蹤。(關於進程的其他狀態請自行百度)。

請看ptrace系統調用手冊↓

images

ptrace的原型可以看到是:

long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);

4個參數的含義分別為:

enum __ptrace_request request:指示了ptrace要執行的命令。
pid_t pid: 指示ptrace要跟蹤的進程。
void *addr: 指示要監控的內存地址。
void *data: 存放讀取出的或者要寫入的數據。

描述譯文如下:

ptrace()系統調用提供了一個方法,該方法使一個程序(追蹤者)可以觀察和控制另外一個程序(被追蹤者)的執行,並檢查和改變被追蹤者的內存及寄存器。它主要用於實現斷點調試和追蹤系統調用。

被追蹤者首先需要被追蹤者attach(這個詞實在不知道咋翻譯了……但是程序員應該都懂@_@)。該行為以及後續操作是線程獨立的:在一個多線程的進程中,每一個線程可以被一個獨立的(可能是不同的)追蹤者attach,或者乾脆不理會。因此,被追蹤者永遠是“一個線程”,而不是一個(可能是多線程的)進程。使用ptrace命令的方法是追蹤程序發送如下命令給被追蹤程序:

ptrace(PTRACE_foo, pid, …)

pid即linux系統分配的線程號。

images

當被追蹤時,被追蹤線程在接收信號時會被停止,即使那個信號是被忽略的也是如此(SIGKILL除外)。追蹤程序會在一個調用waitpid(或者其他類wait系統調用)時收到通知,該調用會返回一個包含被追蹤線程停止的原因的狀態值。當被追蹤線程停止時,追蹤程序可以使用多種ptrace請求來檢查和編輯被追蹤線程。追蹤程序可以讓被追蹤線程繼續運行,有選擇地忽略發過來的信號(甚至可以發送一個完全不同的信號給被追蹤線程)。

可以看到,ptrace確實是一個強大的系統調用;gdb就是基於ptrace這個系統調用來做的。其原理是利用ptrace系統調用,在被調試程序和gdb之間建立追蹤關係。然後所有發送給被調試程序(被追蹤線程)的信號(除SIGKILL)都會被gdb截獲,gdb根據截獲的信號,查看被調試程序相應的內存地址,並控制被調試的程序繼續運行。GDB常用的使用方法有斷點設置和單步調試,接下來我們來分析一下他們是如何實現的。

###1.建立調試關係:

用gdb調試程序有2種模式,包括使用gdb啟動程序,以及attach到現有進程。分別對應下面2種建立調試關係的方法:

    1. fork: 利用fork+execve執行被測試的程序,子進程在執行execve之前調用ptrace(PTRACE_TRACEME),建立了與父進程(debugger)的跟蹤關係。
    1. attach: debugger可以調用ptrace(PTRACE_ATTACH,pid,...),建立自己與進程號為pid的進程間的跟蹤關係。即利用PTRACE_ATTACH,使自己變成被調試程序的父進程(用ps可以看到)。用attach建立起來的跟蹤關係,可以調用ptrace(PTRACE_DETACH,pid,...)來解除。注意attach進程時的權限問題,如一個非root權限的進程是不能attach到一個root進程上的。

###2.斷點原理:

    1. 斷點的實現原理,就是在指定的位置插入斷點指令,當被調試的程序運行到斷點的時候,產生SIGTRAP信號。該信號被gdb捕獲並進行斷點命中判定,當gdb判斷出這次SIGTRAP是斷點命中之後就會轉入等待用戶輸入進行下一步處理,否則繼續。
    1. 斷點的設置原理: 在程序中設置斷點,就是先將該位置的原來的指令保存,然後向該位置寫入int 3。當執行到int 3的時候,發生軟中斷,內核會給子進程發出SIGTRAP信號,當然這個信號會被轉發給父進程。然後用保存的指令替換int3,等待恢復運行。
    1. 斷點命中判定:gdb把所有的斷點位置都存放在一個鏈表中,命中判定即把被調試程序當前停止的位置和鏈表中的斷點位置進行比較,看是斷點產生的信號,還是無關信號。
    1. 條件斷點的判定:原理同3),只是恢復斷點處的指令後,再多加一步條件判斷。若表達式為真,則觸發斷點。由於需要判斷一次,因此加入條件斷點後,不管有沒有觸發到條件斷點,都會影響性能。在x86平臺,某些硬件支持硬件斷點,在條件斷點處不插入int 3,而是插入一個其他指令,當程序走到這個地址的時候,不發出int 3信號,而是先去比較一下特定寄存器和某個地址的內容,再決定是否發送int 3。因此,當你的斷點的位置會被程序頻繁地“路過”時,儘量使用硬件斷點,會對提高性能有幫助

3.單步跟蹤原理:

這個最簡單,因為ptrace本身支持單步功能,調用ptrace(PTRACE_SINGLESTEP,pid,...)即可,如下圖說明:

images


书籍推荐