動態追蹤技術原理

單步跟蹤是什麼

什麼是動態追蹤

動態追蹤技術是一種高階的軟體除錯技術。它可以幫助你快速定位和解決生產系統中問題。網際網路快速發展,軟體系統規模越來越大,系統的業務邏輯越來越複雜。慢慢的,我們的程式設計師喪失了對軟體系統的洞察力和掌控力。很多人處理問題的方法是錯誤的,一旦軟體系統出現問題,很多人立馬去搜索對應的case,一般情況下你踩過的坑應該有人已經踩過了,如果你運氣比較好的話,能夠很快fix掉問題。但是這些熱心網友的解決方案不一定適合你,我們的系統產生問題的原因不盡相同。更糟糕的是由於你不斷試錯,可能已經破壞現場,離正確答案越來越遠。我們應該像醫生一樣,瞭解患者的病歷,給病人拍片子,去觀察病人,找到問題再對症下藥。

日誌是一種很好的追蹤工具。我們可以用 log。error 記錄一些系統出現的錯誤事件,包含錯誤的產生時間,函式呼叫棧等詳細資訊,這個方便我們對問題的處理,也可以使用 log。info 記錄一些我們感興趣的執行資訊,便於我們對應用系統的觀察。日誌往往摻雜大量無效資料,同時如果自己感興趣的內容日誌沒有記錄下來就比較麻煩。而且日誌是一種侵入性的,混合在我們應用程式的程式碼裡面,修改程式碼需要重新打包,釋出,重啟服務。更重要的是重啟服務很有可能破環現場。

有沒有什麼觀測系統的方法可以按照自己需要捕獲自己感興趣的事件嗯,不需要修改程式碼,不需要重啟服務,對服務效能損失很小。就像醫生給病人拍片子一樣,基本對患者健康沒有損失,但可以很好觀察病人體內情況。我們也可以給軟體系統“拍片子”,在給系統性能帶來極少損失的前提下實現對系統的觀察, 這就是今天要介紹的動態追蹤技術。

異常

異常

(exception)就是控制流中的突變,用來響應處理器狀態中的某些變化。理解異常有助於理解本文介紹的動態追蹤技術原理。如圖 1 所示處理器在執行時執行時,發生了一個重要的變化,我們把它稱為

事件

(event)。事件可能與當前指令直接相關,如缺頁異常,算術溢位,嘗試除以 0 。也有可能無關如定時器產生訊號,I/O 完成。在任何情況下處理器透過異常表進行一個間接過程呼叫到專門的異常處理程式來處理。

動態追蹤技術原理

圖1 異常刨析

異常可以分成四類:

中斷

(interrupt),

陷阱

(trap),

故障

(fault)和

終止

(abort)。

中斷

是非同步發生的,是來自處理器外部 I/O (滑鼠,鍵盤,網絡卡等)裝置訊號的結果。硬體中斷不是由任何一條專門的指令造成,從這個意義講它是非同步的。剩下的異常型別(陷阱,故障,終止)是同步發生的,是執行當前指令的結果。我們把這種指令稱為

故障指令。

陷阱

有意

的異常,是程式設計師“主動”觸發的,就像是自己在程式碼埋下一個陷阱一樣。陷阱最常見的使用者是程序發起

系統呼叫,

從使用者態切換到核心態。

故障

由錯誤情況引起,能夠被故障處理程式修正。當故障發生時,處理器講控制轉移給故障處理程式。例如當缺頁異常發生時,故障處理程式可以從磁碟中間對應的頁 swap 進物理記憶體。

終止

是不可恢復的致命錯誤造成的結果,通常是一些硬體錯誤。

程式設計師平常除錯程式碼時,給程式新增斷點,讓程式在我們想要的地方停住。偵錯程式能夠隨心所欲控制程式執行,主要靠軟體中斷。軟體斷點在 X86 系統中就是指令

INT 3

,它的二進位制程式碼opcode 是 0xCC。當程式執行到

INT 3

指令時,會引發軟體中斷。這就是上文提到的陷阱。不同於我們在 Visual Studio 和 GDB 中互動式的斷點,如果程式在 trap 發生時,自動執行預定義和 handle 記錄和統計執行情況,不影響程式的正常執行,從而達到醫生拍片子的效果。

常見的動態追蹤技術原理

為了捕捉程式執行情況,我們在程式中設定一些 “ 陷阱 ”,並設定處理程式,我們稱之為

探針

。有的探針是在程式碼中預定義的,有的是在執行時動態新增的。

靜態探針

靜態探針是事先在程式中定義好,並編譯到程式或者核心的探針。靜態探針只有被開啟時才會執行,不開啟就不會執行,常見的靜態探針包括核心中的

跟蹤點

(tracepoints)和

USDT

(Userland Statically Defined Tracing)探針。

tracepoints

tracepoints

在程式碼中埋下鉤子,在執行時呼叫相連線的探針。它有“開啟”(已連線探針)和“關閉”(未連線探針)兩種狀態。當跟蹤點處於“關閉”狀態時,它沒有任何作用,只增加微小的時間損失(檢查分支的條件)和空間損失。當跟蹤點為“ 開啟”時,每次在呼叫者的執行上下文中執行跟蹤點時,都會呼叫相連線的探針。探針函式執行完後,將返回到呼叫方。 可以將 tracepoints 放置在程式碼中的重要位置, 用於應用觀察和效能統計。如果使用者準備為 kernel 加入新的 tracepoint,每個 tracepoint 必須以下列格式宣告:

動態追蹤技術原理

上面的宏定義了一個新的 tracepoint 叫 tracepoint_name。與這個 tracepoint 關聯的 probe 函式必須與 TPPROTO 宏定義的函式 prototype 一致,probe 函式的引數列表必須與TPARGS 宏定義的一致。

使用 perf list 檢視核心中預定義的 tracepoints, 例如檢視磁碟 I/O 相關的 tracepoints

動態追蹤技術原理

USDT

USDT和tracepoint類似,只不過是使用者態的,在程式碼中插入DTRACE_PROBE()即可。

動態探針

動態探針是應用程式沒有定義,在程式執行時動態新增的探針。動態探針類似於異常處理機制,當系統產生一個異常,就會跳轉去執行對應的 handle。動態探針會在函式入口和出口插入一些斷點,程式執行到斷點時候會去執行對應的 handle,從而達到觀測應用程式的目的。這裡的中斷是指 trap(陷阱),在X86體系是int3指令。

kprobes

KProbes

是 Linux 核心的除錯機制,也可以用於監視生產系統中的事件。您可以使用它來解決效能瓶頸,記錄特定事件,跟蹤問題等。KProbes 由 IBM 開發,是另一個名為 DProbes 的跟蹤工具的基礎開發而來。 KProbes 的實現取決於處理器體系結構,它根據執行它的體系結構使用略有不同的機制。我們僅討論X86體系結構。

KProbe需要定義

pre-handler

post-

handler

,當被探測的指令要被執行時,先執行pre-handler程式。同樣,當被探測指令執行之後立即執行post-handler。

動態追蹤技術原理

kprobes 工作流程如下所示:

當用戶註冊一個探測點後,kprobe 首先備份被探測點的對應指令,然後將原始指令的入口點替換為斷點指令,該指令是 CPU 架構相關的,如 i386 和 x86_64 是 int3

當CPU流程執行到探測點的斷點指令時,就觸發了一個 trap,在 trap 處理流程中會儲存當前CPU的暫存器資訊並呼叫對應的 trap 處理函式,該處理函式會設定 kprobe 的呼叫狀態並呼叫使用者註冊的 pre_handler 回撥函式,kprobe 會向該函式傳遞註冊的 struct kprobe結構地址以及儲存的 CPU 暫存器資訊;

隨後 kprobe 單步執行前面所複製的被探測指令,具體執行方式各個架構不盡相同,arm 會在異常處理流程中使用模擬函式執行,而 x86_64 架構則會設定單步除錯 flag 並回到異常觸發前的流程中執行;

在單步執行完成後,kprobe 執使用者註冊的 post_handler 回撥函式;

最後,執行流程回到被探測指令之後的正常流程繼續執行。

實時修改核心 code text 聽起來會帶來極大的風險。但是 kprobes 有很多安全上的考慮,例如為了避免遞迴陷阱,kprobes不能給一些在黑名單上的函式加探針,kprobes相關的函式就在黑名單中。kprobes還利用安全的技術來插入斷點:x86 體系下使用 int3 或者 jmp 時,使用stop_machine 用於確保其他CPU在被修改時停止執行。實際上 kprobes 最大風險是給一些呼叫十分頻繁的函式加上探針,如在網路模組中,頻繁中斷可能造成一定的效能風險。

uprobes

uprobes

是Linux提供使用者態的動態探針,合併於2012年7月釋出的 Linux 3。5 核心中。uprobes 和 kprobes 十分相似,只不過用在使用者態而已。 uprobes 可以檢測到使用者態函式入口和出口的位置。uprobes 工作原理和 kprobes 差不多,它會在目標位置插入一個斷點,這樣當程式執行流執行到這個地方會去執行我們設定的 uprobe handle。當我們不再不需探測和收集資訊時候,可以移除斷點恢復原狀。

硬體追蹤

我們看到動態追蹤技術在軟體系統的分析當中可以扮演非常關鍵的角色,那麼很自然地會想到,是否也可以用類似的方法和思想去追蹤硬體。

我們知道其實作業系統是直接和硬體打交道的,那麼透過追蹤作業系統的某些驅動程式或者其他方面,我們也可以間接地去分析與之相接的硬體裝置的一些行為和問題。同時,現代硬體,比如說像 Intel 的 CPU,一般會內建一些效能統計方面的暫存器(Hardware Performance Counter),透過軟體讀取這些特殊暫存器裡的資訊,我們也可以得到很多有趣的直接關於硬體的資訊。比如說 Linux 世界裡的 perf 工具最初就是為了這個目的。