ebpf 月报 - 2023 年 2 月
spacewander
Posted on February 27, 2023
本刊物旨在为中文用户提供及时、深入、有态度的 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/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 上找找。
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
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 先退出,堆栈信息中的有些函数就只显示地址。
Posted on February 27, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.