ebpf 月报 - 2023 年 2 月
Tags: ebpf
Published: 2023-02-27 12:18:51

本刊物旨在为中文用户提供及时、深入、有态度的 ebpf 资讯。

如果你吃了鸡蛋觉得好吃，还想认识下蛋的母鸡，欢迎关注：
笔者的 twitter：https://twitter.com/spacewanderlzx

## bpftrace 发布 0.17.0 版本

https://github.com/iovisor/bpftrace/releases/tag/v0.17.0

时隔数月，bpftrace 发布了新版本 `0.17.0`。这个版本，允许[直接比较整数数组](https://github.com/iovisor/bpftrace/pull/2387)，还新增了对以下几个架构的支持：
* 龙芯：https://github.com/iovisor/bpftrace/pull/2466
* ARM32：https://github.com/iovisor/bpftrace/pull/2360

此外，一个较大的改动是支持内核模块的 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](https://github.com/aquasecurity/btfhub) 上找找。

## Wasm-bpf：架起 Wasm 和 eBPF 间的桥梁

https://mp.weixin.qq.com/s/2InV7z1wcWic5ifmAXSiew

Wasm 和 eBPF 都是近年来流行的技术，两者结合在一起，会碰撞出怎样的火花？

[Wasm-bpf](https://github.com/eunomia-bpf/wasm-bpf) 这个项目给出了自己的答案。

笔者泛泛看了下，外加和开发者讨论，认为该项目主要是想要达到下面两点目标：
1. 让控制器和 ebpf 一样能够跨平台分发
2. 支持将打包完的 Wasm 代码，作为网络 proxy 或者可观测性 agent 的插件

在笔者看来，Wasm-bpf 这个项目未来的发展，更多取决于 Wasm 的生态能不能起来。毕竟在 Wasm 和 eBPF 两者中，Wasm 是相对缺乏复杂应用场景的那一个。比方说，如果想要在打包完的 Wasm 代码里面完成数据上报的功能，如果不依靠 Wasm 宿主的能力，那么需要等待 Wasi-socket 这样[正在开发中](https://github.com/bytecodealliance/wasmtime/issues/4276) 的功能足够成熟。所以现在结合 Wasm 做 eBPF，还更多地处于技术积累的阶段。

老实说，即使对 Wasm 的支持能够更加成熟，也不一定走 eBPF -> Wasm 的路线。比方说，[bpf2go](https://github.com/cilium/ebpf/tree/master/cmd/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](https://github.com/aya-rs/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-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:
```

而 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](https://github.com/iovisor/bpftrace/issues/246)，如果 traced 的进程比 bpftrace 先退出，堆栈信息中的有些函数就只显示地址。