DEV Community

spacewander
spacewander

Posted on

ebpf 月报 - 2023 年 2 月

本刊物旨在为中文用户提供及时、深入、有态度的 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));
Enter fullscreen mode Exit fullscreen mode

需要在文件开头定义相关的结构体:

#include <linux/fs.h>
#include <linux/sched.h>

// from fs/namei.c:
struct nameidata {
        struct path     path;
        struct qstr     last;
        // [...]
};
Enter fullscreen mode Exit fullscreen mode

有了 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,
...
Enter fullscreen mode Exit fullscreen 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;                                                                  \
  }
Enter fullscreen mode Exit fullscreen mode

可以看到,它会给每个函数设置两个挂载点,一个是传统的 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;
}
Enter fullscreen mode Exit fullscreen mode

当然我们可以看到上述宏定义里其实并没有设置 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-debugging-go-programs-delve-and-ebpf-faster#the_inefficiencies_of_ptrace

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:
Enter fullscreen mode Exit fullscreen mode

而 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
Enter fullscreen mode Exit fullscreen mode

用下面的通配符形式,会更接近前面 dlv trace 的效果:

bpftrace -e 'uprobe:./go/bin/dlv:*Printf* {printf("%s\n", ustack(3));}' -c './go/bin/dlv exec ./go/bin/dlv'
Enter fullscreen mode Exit fullscreen mode

细心的读者可能注意到了,我这里执行的命令换成了 ./go/bin/dlv exec ./go/bin/dlv。这是因为 bpftrace 有个 bug,如果 traced 的进程比 bpftrace 先退出,堆栈信息中的有些函数就只显示地址。

Top comments (0)