這是本系列文章的第一篇,講述了我們?nèi)绾卧谏a(chǎn)環(huán)境中使用 eBPF 調(diào)試應(yīng)用程序而無需重新編譯/重新部署。這篇文章介紹了如何使用 gobpf 和 uprobe 來為 Go 程序構(gòu)建函數(shù)參數(shù)跟蹤程序。這項技術(shù)也可以擴展應(yīng)用于其他編譯型語言,例如 C++,Rust 等。本系列的后續(xù)文章將討論如何使用 eBPF 來跟蹤 HTTP/gRPC/SSL 等。
簡介
在調(diào)試時,我們通常對了解程序的狀態(tài)感興趣。這使我們能夠檢查程序正在做什么,并確定缺陷在代碼中的位置。觀察狀態(tài)的一種簡單方法是使用調(diào)試器來捕獲函數(shù)的參數(shù)。對于 Go 程序來說,我們經(jīng)常使用 Delve 或者 GDB。
在開發(fā)環(huán)境中,Delve 和 GDB 工作得很好,但是在生產(chǎn)環(huán)境中并不經(jīng)常使用它們。那些使調(diào)試器強大的特性也讓它們不適合在生產(chǎn)環(huán)境中使用。調(diào)試器會導(dǎo)致程序中斷,甚至允許修改狀態(tài),這可能會導(dǎo)致軟件產(chǎn)生意外故障。
為了更好地捕獲函數(shù)參數(shù),我們將探索使用 eBPF(在 Linux 4.x+ 中可用)以及高級的 Go 程序庫 gobpf。
eBPF 是什么?
擴展的 BPF(eBPF) 是 Linux 4.x+ 里的一項內(nèi)核技術(shù)。你可以把它想像成一個運行在 Linux 內(nèi)核中的輕量級的沙箱虛擬機,可以提供對內(nèi)核內(nèi)存的經(jīng)過驗證的訪問。
如下概述所示,eBPF 允許內(nèi)核運行 BPF 字節(jié)碼。盡管使用的前端語言可能會有所不同,但它通常是 C 的受限子集。一般情況下,使用 Clang 將 C 代碼編譯為 BPF 字節(jié)碼,然后驗證這些字節(jié)碼,確??梢园踩\行。這些嚴格的驗證確保了機器碼不會有意或無意地破壞 Linux 內(nèi)核,并且 BPF 探針每次被觸發(fā)時,都只會執(zhí)行有限的指令。這些保證使 eBPF 可以用于性能關(guān)鍵的工作負載,例如數(shù)據(jù)包過濾,網(wǎng)絡(luò)監(jiān)控等。
從功能上講,eBPF 允許你在某些事件(例如定時器,網(wǎng)絡(luò)事件或函數(shù)調(diào)用)觸發(fā)時運行受限的 C 代碼。當(dāng)在函數(shù)調(diào)用上觸發(fā)時,我們稱這些函數(shù)為探針,它們既可以用于內(nèi)核里的函數(shù)調(diào)用(kprobe) 也可以用于用戶態(tài)程序中的函數(shù)調(diào)用(uprobe)。本文重點介紹使用 uprobe 來動態(tài)跟蹤函數(shù)參數(shù)。
Uprobe
uprobe 可以通過插入觸發(fā)軟中斷的調(diào)試陷阱指令(x86 上的 int3)來攔截用戶態(tài)程序。這也是調(diào)試器的工作方式。uprobe 的流程與任何其他 BPF 程序基本相同,如下圖所示。經(jīng)過編譯和驗證的 BPF 程序?qū)⒆鳛?uprobe 的一部分執(zhí)行,并且可以將結(jié)果寫入緩沖區(qū)。
讓我們看看 uprobe 是如何工作的。要部署 uprobe 并捕獲函數(shù)參數(shù),我們將使用這個簡單的示例程序。這個 Go 程序的相關(guān)部分如下所示。
main() 是一個簡單的 HTTP 服務(wù)器,在路徑 /e 上公開單個 GET 端點,該端點使用迭代逼近來計算歐拉數(shù)(e)。computeE接受單個查詢參數(shù)(iterations),該參數(shù)指定計算近似值要運行的迭代次數(shù)。迭代次數(shù)越多,近似值越準確,但會消耗指令周期。理解函數(shù)背后的數(shù)學(xué)并不是必需的。我們只是想跟蹤對 computeE 的任何調(diào)用的參數(shù)。
// computeE computes the approximation of e by running a fixed number of iterations.
func computeE(iterations int64) float64 {
res := 2.0
fact := 1.0
for i := int64(2); i 《 iterations; i++ {
fact *= float64(i)
res += 1 / fact
}
return res
}
func main() {
http.HandleFunc(“/e”, func(w http.ResponseWriter, r *http.Request) {
// Parse iters argument from get request, use default if not available.
// 。.. removed for brevity 。..
w.Write([]byte(fmt.Sprintf(“e = %0.4f
”, computeE(iters))))
})
// Start server.。.
}
要了解 uprobe 的工作原理,讓我們看一下二進制文件中如何跟蹤符號。由于 uprobe 通過插入調(diào)試陷阱指令來工作,因此我們需要獲取函數(shù)所在的地址。Linux 上的 Go 二進制文件使用 ELF 存儲調(diào)試信息。除非刪除了調(diào)試數(shù)據(jù),否則即使在優(yōu)化過的二進制文件中也可以找到這些信息。我們可以使用 objdump 命令檢查二進制文件中的符號:
[0] % objdump --syms app|grep computeE
00000000006609a0 g F .text 000000000000004b main.computeE
從這個輸出中,我們知道函數(shù) computeE 位于地址 0x6609a0。要看到它前后的指令,我們可以使用 objdump 來反匯編二進制文件(通過添加 -d 選項實現(xiàn))。反匯編后的代碼如下:
[0] % objdump -d app | less
00000000006609a0 《main.computeE》:
6609a0: 48 8b 44 24 08 mov 0x8(%rsp),%rax
6609a5: b9 02 00 00 00 mov $0x2,%ecx
6609aa: f2 0f 10 05 16 a6 0f movsd 0xfa616(%rip),%xmm0
6609b1: 00
6609b2: f2 0f 10 0d 36 a6 0f movsd 0xfa636(%rip),%xmm1
由此可見,當(dāng) computeE 被調(diào)用時會發(fā)生什么。第一條指令是 mov 0x8(%rsp), %rax。它把 rsp 寄存器偏移 0x8 的內(nèi)容移動到 rax 寄存器。這實際上就是上面的輸入?yún)?shù) iterations。Go 的參數(shù)在棧上傳遞。
有了這些信息,我們現(xiàn)在就可以繼續(xù)深入,編寫代碼來跟蹤 computeE 的參數(shù)了。
構(gòu)建跟蹤程序
要捕獲事件,我們需要注冊一個 uprobe 函數(shù),還需要一個可以讀取輸出的用戶空間函數(shù)。如下圖所示。我們將編寫一個稱為跟蹤程序的二進制文件,它負責(zé)注冊 BPF 代碼并讀取 BPF 代碼的結(jié)果。如圖所示,uprobe 簡單地寫入 perf buffer,這是用于 perf 事件的 Linux 內(nèi)核數(shù)據(jù)結(jié)構(gòu)。
現(xiàn)在,我們已了解了涉及到的各個部分,下面讓我們詳細研究添加 uprobe 時發(fā)生的情況。下圖顯示了 Linux 內(nèi)核如何使用uprobe 修改二進制文件。軟中斷指令(int3)作為第一條指令被插入 main.computeE 中。這將導(dǎo)致軟中斷,從而允許 Linux 內(nèi)核執(zhí)行我們的 BPF 函數(shù)。然后我們將參數(shù)寫入 perf buffer,該緩沖區(qū)由跟蹤程序異步讀取。
BPF 函數(shù)相對簡單,C代碼如下所示。我們注冊這個函數(shù),每次調(diào)用 main.computeE 時都將調(diào)用它。一旦調(diào)用,我們只需讀取函數(shù)參數(shù)并寫入 perf buffer。設(shè)置緩沖區(qū)需要很多樣板代碼,可以在完整的示例中找到。
#include 《uapi/linux/ptrace.h》
BPF_PERF_OUTPUT(trace);
inline int computeECalled(struct pt_regs *ctx) {
// The input argument is stored in ax.
long val = ctx-》ax;
trace.perf_submit(ctx, &val, sizeof(val));
return 0;
}
現(xiàn)在我們有了一個用于 main.computeE 函數(shù)的功能完善的端到端的參數(shù)跟蹤程序!下面的視頻片段展示了這一結(jié)果。
另一個很棒的事情是,我們可以使用 GDB 來查看對二進制文件所做的修改。在運行我們的跟蹤程序之前,我們輸出地址 0x6609a0 的指令。
(gdb) display /4i 0x6609a0
10: x/4i 0x6609a0
0x6609a0 《main.computeE》: mov 0x8(%rsp),%rax
0x6609a5 《main.computeE+5》: mov $0x2,%ecx
0x6609aa 《main.computeE+10》: movsd 0xfa616(%rip),%xmm0
0x6609b2 《main.computeE+18》: movsd 0xfa636(%rip),%xmm1
而這是在我們運行跟蹤程序之后。我們可以清楚地看到,第一個指令現(xiàn)在變成 int3 了。
(gdb) display /4i 0x6609a0
7: x/4i 0x6609a0
0x6609a0 《main.computeE》: int3
0x6609a1 《main.computeE+1》: mov 0x8(%rsp),%eax
0x6609a5 《main.computeE+5》: mov $0x2,%ecx
0x6609aa 《main.computeE+10》: movsd 0xfa616(%rip),%xmm0
盡管我們?yōu)樵撎囟ㄊ纠龑Ω櫝绦蜻M行了硬編碼,但是這個過程是可以通用化的。Go 的許多方面(例如嵌套指針,接口,通道等)讓這個過程變得有挑戰(zhàn)性,但是解決這些問題可以使用現(xiàn)有系統(tǒng)中不存在的另一種檢測模式。另外,因為這一過程工作在二進制層面,它也可以用于其他語言(C++,Rust 等)編譯的二進制文件。我們只需考慮它們各自 ABI 的差異。
下一步是什么?
使用 uprobe 進行 BPF 跟蹤有其自身的優(yōu)缺點。當(dāng)我們需要觀察二進制程序的狀態(tài)時,BPF 很有用,甚至在連接調(diào)試器會產(chǎn)生問題或者壞處的環(huán)境(例如生產(chǎn)環(huán)境二進制程序)。最大的缺點是,即使是最簡單的程序狀態(tài)的觀測性,也需要編寫代碼來實現(xiàn)。編寫和維護 BPF 代碼很復(fù)雜。沒有大量高級工具,不太可能把它當(dāng)作一般的調(diào)試手段。
編輯:lyn
-
LINUX內(nèi)核
+關(guān)注
關(guān)注
1文章
315瀏覽量
21557 -
函數(shù)參數(shù)
+關(guān)注
關(guān)注
0文章
6瀏覽量
5976 -
BPF
+關(guān)注
關(guān)注
0文章
24瀏覽量
3926
原文標題:在生產(chǎn)環(huán)境中使用 eBPF 調(diào)試 GO 程序
文章出處:【微信號:LinuxDev,微信公眾號:Linux閱碼場】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論