Linux中國

調試器到底怎樣工作

調試器是大多數(即使不是每個)開發人員在軟體工程職業生涯中至少使用過一次的那些軟體之一,但是你們中有多少人知道它們到底是如何工作的?我在悉尼 linux.conf.au 2018 的演講中,將討論從頭開始編寫調試器……使用 Rust

在本文中,術語 調試器 debugger 跟蹤器 tracer 可以互換。 「 被跟蹤者 Tracee 」是指正在被跟蹤器跟蹤的進程。

ptrace 系統調用

大多數調試器嚴重依賴稱為 ptrace(2) 的系統調用,其原型如下:

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

這是一個可以操縱進程幾乎所有方面的系統調用;但是,在調試器可以連接到一個進程之前,「被跟蹤者」必須以請求 PTRACE_TRACEME 調用 ptrace。這告訴 Linux,父進程通過 ptrace 連接到這個進程是合法的。但是……我們如何強制一個進程調用 ptrace?很簡單!fork/execve 提供了在 fork 之後但在被跟蹤者真正開始使用 execve 之前調用 ptrace 的簡單方法。很方便地,fork 還會返回被跟蹤者的 pid,這是後面使用 ptrace 所必需的。

現在被跟蹤者可以被調試器追蹤,重要的變化發生了:

  • 每當一個信號被傳送到被跟蹤者時,它就會停止,並且一個可以被 wait 系列的系統調用捕獲的等待事件被傳送給跟蹤器。
  • 每個 execve 系統調用都會導致 SIGTRAP 被傳遞給被跟蹤者。(與之前的項目相結合,這意味著被跟蹤者在一個 execve 完全發生之前停止。)

這意味著,一旦我們發出 PTRACE_TRACEME 請求並調用 execve 系統調用來實際在被跟蹤者(進程上下文)中啟動程序時,被跟蹤者將立即停止,因為 execve 會傳遞一個 SIGTRAP,並且會被跟蹤器中的等待事件捕獲。我們如何繼續?正如人們所期望的那樣,ptrace 有大量的請求可以用來告訴被跟蹤者可以繼續:

  • PTRACE_CONT:這是最簡單的。 被跟蹤者運行,直到它接收到一個信號,此時等待事件被傳遞給跟蹤器。這是最常見的實現真實世界調試器的「繼續直至斷點」和「永遠繼續」選項的方式。斷點將在下面介紹。
  • PTRACE_SYSCALL:與 PTRACE_CONT 非常相似,但在進入系統調用之前以及在系統調用返回到用戶空間之前停止。它可以與其他請求(我們將在本文後面介紹)結合使用來監視和修改系統調用的參數或返回值。系統調用追蹤程序 strace 很大程度上使用這個請求來獲知進程發起了哪些系統調用。
  • PTRACE_SINGLESTEP:這個很好理解。如果您之前使用過調試器(你會知道),此請求會執行下一條指令,然後立即停止。

我們可以通過各種各樣的請求停止進程,但我們如何獲得被調試者的狀態?進程的狀態大多是通過其寄存器捕獲的,所以當然 ptrace 有一個請求來獲得(或修改)寄存器:

  • PTRACE_GETREGS:這個請求將給出被跟蹤者剛剛被停止時的寄存器的狀態。
  • PTRACE_SETREGS:如果跟蹤器之前通過調用 PTRACE_GETREGS 得到了寄存器的值,它可以在參數結構中修改相應寄存器的值,並使用 PTRACE_SETREGS 將寄存器設為新值。
  • PTRACE_PEEKUSERPTRACE_POKEUSER:這些允許從被跟蹤者的 USER 區讀取信息,這裡保存了寄存器和其他有用的信息。 這可以用來修改單一寄存器,而避免使用更重的 PTRACE_{GET,SET}REGS 請求。

在調試器僅僅修改寄存器是不夠的。調試器有時需要讀取一部分內存,甚至對其進行修改。GDB 可以使用 print 得到一個內存位置或變數的值。ptrace 通過下面的方法實現這個功能:

  • PTRACE_PEEKTEXTPTRACE_POKETEXT:這些允許讀取和寫入被跟蹤者地址空間中的一個字。當然,使用這個功能時被跟蹤者要被暫停。

真實世界的調試器也有類似斷點和觀察點的功能。 在接下來的部分中,我將深入體系結構對調試器支持的細節。為了清晰和簡潔,本文將只考慮 x86。

體系結構的支持

ptrace 很酷,但它是如何工作? 在前面的部分中,我們已經看到 ptrace 跟信號有很大關係:SIGTRAP 可以在單步跟蹤、execve 之前以及系統調用前後被傳送。信號可以通過一些方式產生,但我們將研究兩個具體的例子,以展示信號可以被調試器用來在給定的位置停止程序(有效地創建一個斷點!):

  • 未定義的指令:當一個進程嘗試執行一個未定義的指令,CPU 將產生一個異常。此異常通過 CPU 中斷處理,內核中相應的中斷處理程序被調用。這將導致一個 SIGILL 信號被發送給進程。 這依次導致進程被停止,跟蹤器通過一個等待事件被通知,然後它可以決定後面做什麼。在 x86 上,指令 ud2 被確保始終是未定義的。
  • 調試中斷:前面的方法的問題是,ud2 指令需要佔用兩個位元組的機器碼。存在一條特殊的單位元組指令能夠觸發一個中斷,它是 int $3,機器碼是 0xCC。 當該中斷髮出時,內核向進程發送一個 SIGTRAP,如前所述,跟蹤器被通知。

這很好,但如何我們才能脅迫被跟蹤者執行這些指令? 這很簡單:利用 ptracePTRACE_POKETEXT 請求,它可以覆蓋內存中的一個字。 調試器將使用 PTRACE_PEEKTEXT 讀取該位置原來的值並替換為 0xCC ,然後在其內部狀態中記錄該處原來的值,以及它是一個斷點的事實。 下次被跟蹤者執行到該位置時,它將被通過 SIGTRAP 信號自動停止。 然後調試器的最終用戶可以決定如何繼續(例如,檢查寄存器)。

好吧,我們已經講過了斷點,那觀察點呢? 當一個特定的內存位置被讀或寫,調試器如何停止程序? 當然你不可能為了能夠讀或寫內存而去把每一個指令都覆蓋為 int $3。有一組調試寄存器為了更有效的滿足這個目的而被設計出來:

  • DR0DR3:這些寄存器中的每個都包含一個地址(內存位置),調試器因為某種原因希望被跟蹤者在那些地址那裡停止。 其原因以掩碼方式被設定在 DR7 寄存器中。
  • DR4DR5:這些分別是 DR6DR7 過時的別名。
  • DR6:調試狀態。包含有關 DR0DR3 中的哪個寄存器導致調試異常被引發的信息。這被 Linux 用來計算與 SIGTRAP 信號一起傳遞給被跟蹤者的信息。
  • DR7:調試控制。通過使用這些寄存器中的位,調試器可以控制如何解釋 DR0DR3 中指定的地址。位掩碼控制監視點的尺寸(監視1、2、4 或 8 個位元組)以及是否在執行、讀取、寫入時引發異常,或在讀取或寫入時引發異常。

由於調試寄存器是進程的 USER 區域的一部分,調試器可以使用 PTRACE_POKEUSER 將值寫入調試寄存器。調試寄存器只與特定進程相關,因此在進程搶佔並重新獲得 CPU 控制權之前,調試寄存器會被恢復。

冰山一角

我們已經瀏覽了一個調試器的「冰山」:我們已經介紹了 ptrace,了解了它的一些功能,然後我們看到了 ptrace 是如何實現的。 ptrace 的某些部分可以用軟體實現,但其它部分必須用硬體來實現,否則實現代價會非常高甚至無法實現。

當然有很多我們沒有涉及。例如「調試器如何知道變數在內存中的位置?」等問題由於空間和時間限制而尚未解答,但我希望你從本文中學到了一些東西;如果它激起你的興趣,網上有足夠的資源可以了解更多。

想要了解更多,請查看 linux.conf.au 中 Levente Kurusa 的演講 Let's Write a Debugger!,於一月 22-26 日在悉尼舉辦。

via: https://opensource.com/article/18/1/how-debuggers-really-work

作者:Levente Kurusa 譯者:stephenxs 校對:wxy

本文由 LCTT 原創編譯,Linux中國 榮譽推出


本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive

對這篇文章感覺如何?

太棒了
0
不錯
0
愛死了
0
不太好
0
感覺很糟
0
雨落清風。心向陽

    You may also like

    Leave a reply

    您的郵箱地址不會被公開。 必填項已用 * 標註

    此站點使用Akismet來減少垃圾評論。了解我們如何處理您的評論數據

    More in:Linux中國