本刊物旨在为中文用户提供及时、深入、有态度的 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 先退出,堆栈信息中的有些函数就只显示地址。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。