這是本系列文章的第一篇,講述了我們如何在生產環境中使用 eBPF 調試應用程序而無需重新編譯/重新部署。這篇文章介紹了如何使用 gobpf 和 uprobe 來為 Go 程序構建函數參數跟蹤程序。這項技術也可以擴展應用于其他編譯型語言,例如 C++,Rust 等。本系列的后續文章將討論如何使用 eBPF 來跟蹤 HTTP/gRPC/SSL 等。
簡介
在調試時,我們通常對了解程序的狀態感興趣。這使我們能夠檢查程序正在做什么,并確定缺陷在代碼中的位置。觀察狀態的一種簡單方法是使用調試器來捕獲函數的參數。對于 Go 程序來說,我們經常使用 Delve 或者 GDB。
在開發環境中,Delve 和 GDB 工作得很好,但是在生產環境中并不經常使用它們。那些使調試器強大的特性也讓它們不適合在生產環境中使用。調試器會導致程序中斷,甚至允許修改狀態,這可能會導致軟件產生意外故障。
為了更好地捕獲函數參數,我們將探索使用 eBPF(在 Linux 4.x+ 中可用)以及高級的 Go 程序庫 gobpf。
eBPF 是什么?
擴展的 BPF(eBPF) 是 Linux 4.x+ 里的一項內核技術。你可以把它想像成一個運行在 Linux 內核中的輕量級的沙箱虛擬機,可以提供對內核內存的經過驗證的訪問。
如下概述所示,eBPF 允許內核運行 BPF 字節碼。盡管使用的前端語言可能會有所不同,但它通常是 C 的受限子集。一般情況下,使用 Clang 將 C 代碼編譯為 BPF 字節碼,然后驗證這些字節碼,確保可以安全運行。這些嚴格的驗證確保了機器碼不會有意或無意地破壞 Linux 內核,并且 BPF 探針每次被觸發時,都只會執行有限的指令。這些保證使 eBPF 可以用于性能關鍵的工作負載,例如數據包過濾,網絡監控等。
從功能上講,eBPF 允許你在某些事件(例如定時器,網絡事件或函數調用)觸發時運行受限的 C 代碼。當在函數調用上觸發時,我們稱這些函數為探針,它們既可以用于內核里的函數調用(kprobe) 也可以用于用戶態程序中的函數調用(uprobe)。本文重點介紹使用 uprobe 來動態跟蹤函數參數。
Uprobe
uprobe 可以通過插入觸發軟中斷的調試陷阱指令(x86 上的 int3)來攔截用戶態程序。這也是調試器的工作方式。uprobe 的流程與任何其他 BPF 程序基本相同,如下圖所示。經過編譯和驗證的 BPF 程序將作為 uprobe 的一部分執行,并且可以將結果寫入緩沖區。
讓我們看看 uprobe 是如何工作的。要部署 uprobe 并捕獲函數參數,我們將使用這個簡單的示例程序。這個 Go 程序的相關部分如下所示。
main() 是一個簡單的 HTTP 服務器,在路徑 /e 上公開單個 GET 端點,該端點使用迭代逼近來計算歐拉數(e)。computeE接受單個查詢參數(iterations),該參數指定計算近似值要運行的迭代次數。迭代次數越多,近似值越準確,但會消耗指令周期。理解函數背后的數學并不是必需的。我們只是想跟蹤對 computeE 的任何調用的參數。
// 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 通過插入調試陷阱指令來工作,因此我們需要獲取函數所在的地址。Linux 上的 Go 二進制文件使用 ELF 存儲調試信息。除非刪除了調試數據,否則即使在優化過的二進制文件中也可以找到這些信息。我們可以使用 objdump 命令檢查二進制文件中的符號:
[0] % objdump --syms app|grep computeE
00000000006609a0 g F .text 000000000000004b main.computeE
從這個輸出中,我們知道函數 computeE 位于地址 0x6609a0。要看到它前后的指令,我們可以使用 objdump 來反匯編二進制文件(通過添加 -d 選項實現)。反匯編后的代碼如下:
[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
由此可見,當 computeE 被調用時會發生什么。第一條指令是 mov 0x8(%rsp), %rax。它把 rsp 寄存器偏移 0x8 的內容移動到 rax 寄存器。這實際上就是上面的輸入參數 iterations。Go 的參數在棧上傳遞。
有了這些信息,我們現在就可以繼續深入,編寫代碼來跟蹤 computeE 的參數了。
構建跟蹤程序
要捕獲事件,我們需要注冊一個 uprobe 函數,還需要一個可以讀取輸出的用戶空間函數。如下圖所示。我們將編寫一個稱為跟蹤程序的二進制文件,它負責注冊 BPF 代碼并讀取 BPF 代碼的結果。如圖所示,uprobe 簡單地寫入 perf buffer,這是用于 perf 事件的 Linux 內核數據結構。
現在,我們已了解了涉及到的各個部分,下面讓我們詳細研究添加 uprobe 時發生的情況。下圖顯示了 Linux 內核如何使用uprobe 修改二進制文件。軟中斷指令(int3)作為第一條指令被插入 main.computeE 中。這將導致軟中斷,從而允許 Linux 內核執行我們的 BPF 函數。然后我們將參數寫入 perf buffer,該緩沖區由跟蹤程序異步讀取。
BPF 函數相對簡單,C代碼如下所示。我們注冊這個函數,每次調用 main.computeE 時都將調用它。一旦調用,我們只需讀取函數參數并寫入 perf buffer。設置緩沖區需要很多樣板代碼,可以在完整的示例中找到。
#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;
}
現在我們有了一個用于 main.computeE 函數的功能完善的端到端的參數跟蹤程序!下面的視頻片段展示了這一結果。
另一個很棒的事情是,我們可以使用 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
而這是在我們運行跟蹤程序之后。我們可以清楚地看到,第一個指令現在變成 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
盡管我們為該特定示例對跟蹤程序進行了硬編碼,但是這個過程是可以通用化的。Go 的許多方面(例如嵌套指針,接口,通道等)讓這個過程變得有挑戰性,但是解決這些問題可以使用現有系統中不存在的另一種檢測模式。另外,因為這一過程工作在二進制層面,它也可以用于其他語言(C++,Rust 等)編譯的二進制文件。我們只需考慮它們各自 ABI 的差異。
下一步是什么?
使用 uprobe 進行 BPF 跟蹤有其自身的優缺點。當我們需要觀察二進制程序的狀態時,BPF 很有用,甚至在連接調試器會產生問題或者壞處的環境(例如生產環境二進制程序)。最大的缺點是,即使是最簡單的程序狀態的觀測性,也需要編寫代碼來實現。編寫和維護 BPF 代碼很復雜。沒有大量高級工具,不太可能把它當作一般的調試手段。
編輯:lyn
-
LINUX內核
+關注
關注
1文章
316瀏覽量
21617 -
函數參數
+關注
關注
0文章
6瀏覽量
5982 -
BPF
+關注
關注
0文章
24瀏覽量
3976
原文標題:在生產環境中使用 eBPF 調試 GO 程序
文章出處:【微信號:LinuxDev,微信公眾號:Linux閱碼場】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論