本刊物旨在為中文使用者提供及時、深入、有態度的 ebpf 資訊。
如果你吃了雞蛋覺得好吃,還想認識下蛋的母雞,歡迎關注:
筆者的 twitter:https://twitter.com/spacewanderlzx
bpftrace 釋出 0.17.0 版本
https://github.com/iovisor/bpftrace/releases/tag/v0.17.0
時隔數月,bpftrace 釋出了新版本 0.17.0
。這個版本,允許直接比較整數陣列,還新增了對以下幾個架構的支援:
此外,一個較大的改動是支援核心模組的 BTF 檔案:
https://github.com/iovisor/bpftrace/pull/2315
bpftrace 以前就已支援了處理核心的 BTF 檔案,新版本把這一功能擴充到核心模組上,算是百尺竿頭更進一步。
BTF 是 eBPF 世界內的 debuginfo。透過 BTF,我們可以在二進位制和程式程式碼間架起橋樑。舉個例子,bpftool 能夠 dump 一個 BPF map 中的資料。如果沒有 BTF 來註釋 BPF map 儲存的資料結構,dump 的結果只能是一堆二進位制。有了 BTF,才能看得懂在 map 裡面儲存的資訊。
作為一個 tracing 領域的工具,BTF 對於 bpftrace 非常重要。假如沒有 BTF,那麼 bpftrace 指令碼中有時需要顯式定義一個核心結構體,比如 https://github.com/iovisor/bpftrace/blob/master/tools/dcsnoop.bt
為了讓這段程式碼能夠編譯:
$nd = (struct nameidata *)arg0;
printf("%-8d %-6d %-16s R %s\n", elapsed / 1e6, pid, comm,
str($nd->last.name));
需要在檔案開頭定義相關的結構體:
#include <linux/fs.h>
#include <linux/sched.h>
// from fs/namei.c:
struct nameidata {
struct path path;
struct qstr last;
// [...]
};
有了 BTF,就能很自然地使用核心中的結構體定義。
好在較新的核心均已提供了 BTF。如果不幸沒有,可以到 btfhub 上找找。
Wasm-bpf:架起 Wasm 和 eBPF 間的橋樑
https://mp.weixin.qq.com/s/2InV7z1wcWic5ifmAXSiew
Wasm 和 eBPF 都是近年來流行的技術,兩者結合在一起,會碰撞出怎樣的火花?
Wasm-bpf 這個專案給出了自己的答案。
筆者泛泛看了下,外加和開發者討論,認為該專案主要是想要達到下面兩點目標:
- 讓控制器和 ebpf 一樣能夠跨平臺分發
- 支援將打包完的 Wasm 程式碼,作為網路 proxy 或者可觀測性 agent 的外掛
在筆者看來,Wasm-bpf 這個專案未來的發展,更多取決於 Wasm 的生態能不能起來。畢竟在 Wasm 和 eBPF 兩者中,Wasm 是相對缺乏複雜應用場景的那一個。比方說,如果想要在打包完的 Wasm 程式碼裡面完成資料上報的功能,如果不依靠 Wasm 宿主的能力,那麼需要等待 Wasi-socket 這樣正在開發中 的功能足夠成熟。所以現在結合 Wasm 做 eBPF,還更多地處於技術積累的階段。
老實說,即使對 Wasm 的支援能夠更加成熟,也不一定走 eBPF -> Wasm 的路線。比方說,bpf2go能夠把 eBPF 程式打包到 Go 程式碼中,那麼使用者現在可用 Go 來編寫並分發 eBPF 外掛,將來也可以走 eBPF -> Go -> Wasm 這條路線。(姑且先忽略 Go 不支援 Wasi 這一現實,畢竟我們的前提是“對 Wasm 的支援能夠更加成熟”,所以可以不負責任地幻想一番)
Exein Pulsar 釋出 0.5.0
https://github.com/Exein-io/pulsar/releases/tag/v0.5.0
初看還以為 Apache Pulsar 跨界搞 eBPF 了,再看一眼才發現原來是新東方廚藝和新東方英語的區別。Exein 的這個 Pulsar 同樣採用了“Pulsar”(脈衝星)這個比喻來形容事件流,只不過它的事件是由部署環境上的系統呼叫觸發的。
像許多同樣基於 eBPF 的可觀測性的軟體一樣,Pulsar 也選擇了 “控制器 + eBPF 模組” 的架構。跟許多同類軟體不同的是,Pulsar 採用 Rust 來作為控制器開發語言,載入 eBPF 的庫用的是Aya。他們之所以這麼選型,也許是因為 Exein 的人偏好 Rust,且他們的目標環境是 IoT。
Pulsar 採用一個宏來包裹 eBPF 的掛載點:
PULSAR_LSM_HOOK(path_mknod, struct path *, dir, struct dentry *, dentry,
umode_t, mode, unsigned int, dev);
static __always_inline void on_path_mknod(void *ctx, struct path *dir,
struct dentry *dentry, umode_t mode,
...
這個宏定義如下:
#define PULSAR_LSM_HOOK(hook_point, args...) \
static __always_inline void on_##hook_point(void *ctx, TYPED_ARGS(args)); \
\
SEC("lsm/" #hook_point) \
int BPF_PROG(hook_point, TYPED_ARGS(args), int ret) { \
on_##hook_point(ctx, UNTYPED_ARGS(args)); \
return ret; \
} \
\
SEC("kprobe/security_" #hook_point) \
int BPF_KPROBE(security_##hook_point, TYPED_ARGS(args)) { \
on_##hook_point(ctx, UNTYPED_ARGS(args)); \
return 0; \
}
可以看到,它會給每個函式設定兩個掛載點,一個是傳統的 BPF_PROG_TYPE_KPROBE,另一個是 Linux 5.7+ 引入的 BPF_PROG_TYPE_LSM 型別。
LSM(Linux 安全模組)其實是一套在核心相關函式增加的 hook 框架,開發者可以透過這些 hook 來加入細粒度的安全策略。大名鼎鼎的 selinux 和 apparmor 就都屬於一種 LSM 的實現。BPF_PROG_TYPE_LSM 型別旨在允許開發者透過 eBPF 來編寫策略程式碼,掛載到對應的 LSM hook 上。觀察上述宏定義,我們可以看到 lsm 掛載點上的函式允許 eBPF 程式碼裡返回一個 ret
值。在 BPF_PROG_TYPE_LSM 型別的 eBPF 中,開發者能夠在呼叫被 hook 的函式之前,返回一個錯誤碼,比如:
SEC("lsm/xxxxx")
int BPF_PROG(xxx, int ret)
{
// 前一個 hook 返回了非0值,表示該呼叫已經被拒絕。讓我們把錯誤碼繼續傳遞上去
if (ret) {
return ret;
}
// 做些安全策略
if (!ok) {
return -EPERM;
}
return 0;
}
當然我們可以看到上述宏定義裡其實並沒有設定 ret 的值。Pulsar 只是對關鍵呼叫做了事件上報,沒有做策略判斷。這也是為什麼它能夠在低版本的 Linux 上 fallback 到普通的 BPF_PROG_TYPE_KPROBE。
前面我們提到,LSM 其實是一套在核心中增加的 hook。這一類的 hook 的命名有一套規則,都以 security_
打頭。所以某個 BPF_PROG_TYPE_LSM 的載入點 xxx,也正好對應核心函式 security_xxx
。
使用 eBPF 加速 delve trace
https://developers.redhat.com/articles/2023/02/13/how-debuggi...
delve 是一個 Go 偵錯程式。類似於 strace,delve 有一個 trace Go 函式呼叫的功能,也同樣是基於 ptrace
系統呼叫實現的。
本文說明了他們是如何透過 eBPF 讓 trace 的速度比起之前有了天壤之別。原理很簡單:用 eBPF 的 uprobe 換掉了 ptrace 系統呼叫。沒有了頻繁的系統呼叫,效能自然上去了。
在這篇文章中,作者提到 eBPF 後端是實驗性的。確實如此,我嘗試使用 eBPF 後端的體驗並不如原本的 ptrace 實現。比如 ptrace 下,支援用如下方式列印涉及函式的呼叫棧:
$ ./go/bin/dlv trace -s 3 '.*Printf.*' --exec ./go/bin/dlv
...
> goroutine(1): fmt.(*pp).doPrintf((*fmt.pp)(0xc0000a6a90), "%%-%ds", []interface {} len: 824635347800, cap: 824635347800, [...])
Stack:
0 0x00000000004f91af in fmt.(*pp).doPrintf
at /usr/local/go/src/fmt/print.go:1021
1 0x00000000004f3719 in fmt.Sprintf
at /usr/local/go/src/fmt/print.go:239
2 0x0000000000962e3f in github.com/spf13/cobra.rpad
at ./go/pkg/mod/github.com/spf13/cobra@v1.1.3/cobra.go:153
3 0x00000000004675a9 in runtime.call32
at :0
(truncated)
Stack:
而 eBPF 後端目前並不支援列印呼叫棧。如果沒有呼叫棧資訊,其實很難知道某個函式是否在恰當的時機被呼叫。況且在非生產環境上,ptrace 的實現已經足夠快了。所以 eBPF 後端目前的功能就挺雞肋,只適合於在生產環境上了解某個函式是否被呼叫,而且對環境的要求比較高,又不如 strace 那麼通用。
如果只是想知道函式有沒有被呼叫到,用 bpftrace 也能達到同樣的效果:
$ bpftrace -e 'uprobe:./go/bin/dlv:"fmt.(*pp).doPrintf" {printf("%s\n", ustack(3));}' -c './go/bin/dlv exec ./go/bin/dlv'
...
fmt.(*pp).doPrintf+0
github.com/go-delve/delve/pkg/terminal.New+2103
github.com/go-delve/delve/cmd/dlv/cmds.connect+528
用下面的萬用字元形式,會更接近前面 dlv trace
的效果:
bpftrace -e 'uprobe:./go/bin/dlv:*Printf* {printf("%s\n", ustack(3));}' -c './go/bin/dlv exec ./go/bin/dlv'
細心的讀者可能注意到了,我這裡執行的命令換成了 ./go/bin/dlv exec ./go/bin/dlv
。這是因為 bpftrace 有個 bug,如果 traced 的程式比 bpftrace 先退出,堆疊資訊中的有些函式就只顯示地址。