ebpf 月報 - 2023 年 2 月

spacewander發表於2023-02-27

本刊物旨在為中文使用者提供及時、深入、有態度的 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 這個專案給出了自己的答案。

筆者泛泛看了下,外加和開發者討論,認為該專案主要是想要達到下面兩點目標:

  1. 讓控制器和 ebpf 一樣能夠跨平臺分發
  2. 支援將打包完的 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 先退出,堆疊資訊中的有些函式就只顯示地址。

相關文章