一文教你如何使用 eBPF 檢測分析用戶態(tài)程序
前言
eBPF 徹底改變了 Linux 內(nèi)核中的可觀察性。在我之前的系列文章中,我介紹了eBPF 生態(tài)系統(tǒng)的基本構(gòu)建模塊,簡要介紹了XDP,并展示了它與 eBPF 基礎(chǔ)設(shè)施如何密切合作,以便在網(wǎng)絡(luò)堆棧中引入一個(gè)快速處理的數(shù)據(jù)路徑。
然而,eBPF 并不只是用在內(nèi)核空間跟蹤。如果我們可以在生產(chǎn)環(huán)境中運(yùn)行的應(yīng)用程序上也能享受 eBPF 驅(qū)動(dòng)的跟蹤的,這是不是很好呢?
這就是 uprobes 發(fā)揮作用的地方??梢詫⑺鼈兛醋魇且环N kprobes ,它加載到了用戶空間跟蹤點(diǎn)而不是內(nèi)核符號(hào)。多語言運(yùn)行時(shí)、數(shù)據(jù)庫系統(tǒng)和其他軟件棧都包含了可以被 BCC 工具使用的鉤子。具體地說,ustat 工具收集了大量有用事件,如垃圾收集事件、對(duì)象創(chuàng)建統(tǒng)計(jì)信息、方法調(diào)用等。
但是“官方”語言運(yùn)行時(shí)的版本,如 Node.js 和 Python,不帶 DTrace 支持,這就需要你必須從源代碼構(gòu)建,將 –with-dtrace_ 標(biāo)志傳遞給編譯器。這不是說必須一定要本機(jī)編譯語言。只要符號(hào)表可用,就可以對(duì)二進(jìn)制文本段中出現(xiàn)的任何符號(hào)應(yīng)用動(dòng)態(tài)跟蹤。在運(yùn)行的二進(jìn)制文件上檢測 Go 或 Rust stdlib 函數(shù)調(diào)用就是通過這種方式完成的。
可用于檢測分析應(yīng)用程序的 eBPF 技術(shù)
跟蹤用戶空間進(jìn)程有多種方法:
靜態(tài)聲明的 USDT
動(dòng)態(tài)聲明的 USDT
使用 uprobes 進(jìn)行動(dòng)態(tài)跟蹤
靜態(tài)聲明的 USDT
USDT (Userland Statically Defined Tracing) 的做法是直接在用戶代碼中嵌入探測。該技術(shù)的起源可以追溯到 Solaris/BSD DTrace 時(shí)代,包括使用 DTRACE_PROBE() 宏在重要代碼位置上聲明跟蹤點(diǎn)。與常規(guī)符號(hào)不同,USDT 鉤子保證即使代碼被重構(gòu)也能保持穩(wěn)定。下圖描述了在用戶代碼中聲明 USDT 跟蹤點(diǎn)的過程,直到在內(nèi)核中執(zhí)行為止。

開發(fā)人員可以先通過 DTRACE_PROBE 和 DTRACE_PROBE1 宏來在需要的代碼塊中植入跟蹤點(diǎn)。兩個(gè)宏都接受兩個(gè)強(qiáng)制參數(shù),如提供者/探測名稱,后面跟著你希望從跟蹤點(diǎn)查詢的任何值。編譯器將把USDT 跟蹤點(diǎn)塞進(jìn)目標(biāo)二進(jìn)制文件 ELF 段中 。編譯器和跟蹤工具之間規(guī)定了 USDT 元數(shù)據(jù)所在的位置必須存在 .note.stapstd 段。
USDT 跟蹤工具檢查 ELF 段,并在被轉(zhuǎn)為 int3 中斷的跟蹤點(diǎn)位置上放置一個(gè)斷點(diǎn)。每當(dāng)在跟蹤點(diǎn)的標(biāo)記處執(zhí)行時(shí),就會(huì)觸發(fā)中斷處理程序,并在內(nèi)核中調(diào)用與 uprobe 關(guān)聯(lián)的程序來處理事件并將它們廣播到用戶空間,執(zhí)行映射聚合等等。
【文章福利】小編推薦自己的Linux內(nèi)核技術(shù)交流群:【891587639】整理了一些個(gè)人覺得比較好的學(xué)習(xí)書籍、視頻資料共享在群文件里面,有需要的可以自行添加哦?。。。ê曨l教程、電子書、實(shí)戰(zhàn)項(xiàng)目及代碼)? ??


動(dòng)態(tài)聲明的 USDT
由于 USDT 被加入靜態(tài)生成的 ELF 段,所以USDT不能運(yùn)行在解釋性語言或基于 jit 的語言上的軟件上。幸運(yùn)的是,可以通過 libstapsdt 在運(yùn)行時(shí)中定義跟蹤點(diǎn)。它生成一個(gè)帶有 USDT 信息的小型共享對(duì)象,該共享對(duì)象被映射到進(jìn)程的地址空間,因此跟蹤工具可以附加到目標(biāo)跟蹤點(diǎn)上。在許多語言中都有 libstapsdt 。要了解如何在 Node.js 中安裝 USDT 探測可以參考這個(gè) example。
使用 uprobes 進(jìn)行動(dòng)態(tài)跟蹤
這種類型的跟蹤機(jī)制不需要目標(biāo)進(jìn)程提供任何額外的功能,只需要它的符號(hào)表是可訪問的。這是最通用和最強(qiáng)大的插裝方法,因?yàn)樗试S在任意指令上注入斷點(diǎn),甚至不需要重啟運(yùn)行的進(jìn)程。
跟蹤例子
After a brief theoretical introduction, let’s see some concrete examples on how to instrument real-world apps crafted in diverse languages. 在簡單的理論介紹之后,我們來看一些具體的例子,看看如何用跟蹤分析不同的語言的應(yīng)用程序。
C 語言
Redis is a popular key-value data structures server built in C. Taking a sneak peek into the Redis symbol table reveals a vast number of functions that are candidates for capturing via uprobes. Redis 是一個(gè)用 C 語言開發(fā)的非常流行的 k-v 數(shù)據(jù)庫服務(wù),仔細(xì)看一下 Redis 符號(hào)表,就會(huì)發(fā)現(xiàn)大量可以通過 uprobes 捕獲的函數(shù)。
有一個(gè)有趣的 createStringObject 函數(shù),Redis 內(nèi)部利用它來分配圍繞 robj 結(jié)構(gòu)建模的字符串。Redis 命令通過 createStringObject 調(diào)用來執(zhí)行。通過跟蹤這個(gè)函數(shù),我們可以監(jiān)視發(fā)送到 Redis 服務(wù)器的任何命令。為此,我將使用 BCC 工具包中的 trace 功能。
以上是在 Redis CLI 客戶端執(zhí)行 “set octi fest” 和 “get octi” 的輸出結(jié)果。
Java語言
現(xiàn)代 JVM 版本自帶對(duì) USDT 的內(nèi)置支持。所有探針都是用 libjvm 共享對(duì)象帶來的。我們可以在 ELF 段中到可用的追蹤點(diǎn)。
為了捕獲所有的類加載事件,我們可以使用以下命令:
類似的,我們可以觀察線程創(chuàng)建事件:
$ /usr/share/bcc/tools/trace'u:/usr/lib/jvm/jdk-11-oracle/lib/server/libjvm.so:thread__start "%s", arg1'
當(dāng)擴(kuò)展探測通過 -XX:+ExtendedDTraceProbes 屬性啟用時(shí),uflow 工具 能夠?qū)崟r(shí)跟蹤和繪制所有方法執(zhí)行的圖形。
但是,就擴(kuò)展探針?biāo)a(chǎn)生的開銷而言,這往往非常昂貴,因此不適合在生產(chǎn)環(huán)境中調(diào)試。
Go語言
我將用一個(gè) Go 中的例子來結(jié)束跟蹤技術(shù)的演示。因?yàn)?Go 是一種原生編譯語言,所以使用跟蹤工具將 uprobe 程序附加到目標(biāo)符號(hào)上是嘗試性的。你可以用下面這個(gè)簡單的代碼片段自己嘗試一下:
除了打印這里的 " Hi “字符串,我們在參數(shù)列中看到打印了一些隨機(jī)的垃圾信息。這在一定程度上是由于 trace 不能處理 Println 可變參數(shù)造成的,但也可能和 ABI 調(diào)用約定的參數(shù)使用的錯(cuò)誤假設(shè)有關(guān)。在 C/C++ 中,傳遞參數(shù)的首選方式是在常規(guī)寄存器中,而 Go 在堆棧上傳遞參數(shù)。
由于我們不能依賴 trace 工具來來演示如何跟蹤 Go 代碼,所以我將構(gòu)建一個(gè)簡單的工具來跟蹤由 http.Get 函數(shù)發(fā)出的所有 HTTP GET 請(qǐng)求。你可以很容易地修改它來捕獲其他 HTTP 請(qǐng)求動(dòng)詞,大家可以參與貢獻(xiàn)。完整的源代碼可以在這個(gè) repo 中找到。
我不會(huì)詳細(xì)介紹 uprobe 附加/加載過程,因?yàn)槲覀冋谑褂?Go 綁定 來 幫 libbcc 完成復(fù)雜的工作。讓我們分析一下實(shí)際的 uprobe 程序。
在必需的 include 語句之后,有宏的定義,該宏通過偏移量處理的方式負(fù)責(zé)從堆棧中獲取參數(shù)。
接下來,我們聲明數(shù)據(jù)結(jié)構(gòu)用于封裝通過 reqs map流傳遞的事件。map 用 BPF_PERF_OUTPUT 宏來定義。我們程序的核心是__uprobe_http_get 函數(shù)。當(dāng) http.Get 被調(diào)用,則在內(nèi)核空間中觸發(fā)前面的函數(shù)。我們知道 HTTP . get 只有一個(gè)參數(shù),表示 HTTP 請(qǐng)求被發(fā)送到的 URL。C 語言和 Go 語言的另一個(gè)區(qū)別是字符串內(nèi)存中布局處理。
C 字符串是以空結(jié)束的序列串,而 Go 用 2 個(gè)值來描述:指向內(nèi)存緩沖區(qū)的指針和字符串長度。這就解釋了我們需要對(duì) bpf_probe_read 進(jìn)行兩個(gè)調(diào)用——一個(gè)用于讀取字符串,另一個(gè)用于提取字符串的長度。
稍后在用戶空間中,URL 將從字節(jié)片裁剪到相應(yīng)的長度。作為一個(gè)附加說明,我想提到的是,該工具的草案版本能夠通過注入 uretprobe 來檢測出每個(gè) HTTP GET 請(qǐng)求的延遲。然而,每次 Go 運(yùn)行時(shí)決定收縮/增長堆棧就會(huì)有一個(gè)災(zāi)難性的影響,因?yàn)?uretprobes 補(bǔ)丁的返回地址是在堆棧上的跳轉(zhuǎn)函數(shù),它在 eBPF VM 的上下文中執(zhí)行。在退出 uretprobe 函數(shù)時(shí),指令指針被恢復(fù)到原始的返回地址,該地址可能指向一個(gè)無效的位置,打亂堆棧并導(dǎo)致進(jìn)程崩潰。有一些建議來解決這個(gè)問題。
結(jié)論
我們的 eBPF 之旅已經(jīng)走到了終點(diǎn)。在這最后一篇文章中,我們介紹了一些 eBPF 特性用于用戶空間進(jìn)程插裝 。通過幾個(gè)實(shí)用案例,我們展示了 BCC 框架在捕獲可觀察性信號(hào)方面的多功能性。最后,我們親自動(dòng)手建立了一個(gè)小工具,用于跟蹤實(shí)時(shí) Go 應(yīng)用程序上的 HTTP 請(qǐng)求。
