类似于常量的使用,如今在 eBPF 里也能够使用全局变量了。
使用例子
源代码:global-variable example
以 kprobe
TCP 连接建立事件为例子,当有新的 TCP 连接时,就打印一条记录:
1
2
3
4
5
|
root@leonhwang-svr ~/P/ebpf-globalvar# ./ebpf-globalvar -daddr 204.79.197.200 -dport 443
2022/10/27 15:26:42 Attached kprobe(tcp_connect)
2022/10/27 15:26:42 Attached kprobe(inet_csk_complete_hashdance)
2022/10/27 15:26:42 Listening events...
2022/10/27 15:27:02 new tcp connection: 149.28.12.x:59586 -> 204.79.197.200:443
|
全局变量的声明与使用的代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// 声明
__be32 filter_daddr;
__be16 filter_dport;
static __always_inline void
handle_new_connection(void *ctx, struct sock *sk)
{
event_t ev = {};
ev.saddr = BPF_CORE_READ(sk, __sk_common.skc_rcv_saddr);
ev.daddr = BPF_CORE_READ(sk, __sk_common.skc_daddr);
ev.sport = BPF_CORE_READ(sk, __sk_common.skc_num);
ev.dport = bpf_ntohs(BPF_CORE_READ(sk, __sk_common.skc_dport));
if (ev.daddr == filter_daddr && ev.dport == filter_dport) // 使用
bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &ev, sizeof(ev));
}
|
正如例子,该如何将命令行参数 -daddr
和 -dport
传给 eBPF 中全局变量呢?
全局变量对应的 Go struct
在给全局变量赋值时,有个问题:全局变量对应的 bpf map 的 value 对应的 Go struct 是怎样的?(在 Go ebpf 中,该 bpf map 的名字是 .bss
。)
在使用 bpf2go 去编译 eBPF 程序时,bpf2go
并没有为全局变量生成对应的 Go struct。
bpf2go
是怎么生成 Go struct 的呢?深入研究一下它的源代码,终于搞明白了。该问题的解决办法如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
func genBssStruct() {
bpfSpec, err := loadTcpconn()
if err != nil {
log.Fatalf("Failed to load bpf spec: %v", err)
}
m, ok := bpfSpec.Maps[".bss"]
if !ok {
log.Fatalf(".bss map not found")
}
fmt.Printf(".bss map spec: %v\n", m)
var gof btf.GoFormatter
out, err := gof.TypeDeclaration("bssValue", m.Value)
if err != nil {
log.Fatalf("Failed to generate Go struct for .bss value")
}
fmt.Println(".bss map value:", out)
}
// output:
//root@leonhwang-svr ~/P/ebpf-globalvar# ./ebpf-globalvar -gen
// .bss map spec: Array(keySize=4, valueSize=6, maxEntries=1, flags=0)
// .bss map value: type bssValue struct { filter_daddr uint32; filter_dport uint16; }
|
由此可知,.bss
bpf map 的类型是 BPF_MAP_TYPE_ARRAY,key 的大小是 4(可以认为 key 的类型是 uint32
),value 的大小是 6(无确定的类型),最大条目数量是 1。
对生成的 Go struct 稍作整理便于使用:
1
2
3
4
|
type bssValue struct {
Daddr [4]byte
Dport uint16
}
|
赋值给全局变量
不同于 C 的 libbpf 库,Go 的 ebpf 库 使用起来就不是很方便了。
应该挺简单的,毕竟未初始化的全局变量会保存到一个名为 .bss
的 bpf map 中。给全局变量赋值,就是更新一下这个 .bss
bpf map 的 value。
不简单的地方,则是 Go ebpf 库没有提供直接操作 .bss
bpf map 的方法。
不过,解决办法还是有的,而且还不少呢。
解决办法一:MapSpec.Contents
查看 MapSpec 的文档,关注其中的 Contents
字段。该字段是用于初始化 bpf map 的时候填充 bpf map,即在创建 bpf map 后将该字段中的内容更新到 bpf map 中。
所以,具体用法如下:
1
2
3
4
5
6
7
|
bpfSpec.Maps[".bss"].Contents = []ebpf.MapKV{
{Key: uint32(0), Value: bssVal},
}
if err := bpfSpec.LoadAndAssign(&obj, nil); err != nil {
log.Fatalf("Failed to load bpf obj: %v", err)
}
|
用法比较简单,却有个缺陷:后续该如何更新 .bss
bpf map 的 value 呢?
解决办法二:封装一下 bpf2go 生成的 Objects struct
具体代码片段如下:
1
2
3
4
5
6
7
8
|
var objWithBss struct {
tcpconnObjects
Bss *ebpf.Map `ebpf:".bss"`
}
if err := bpfSpec.LoadAndAssign(&objWithBss, nil); err != nil {
log.Fatalf("Failed to load bpf obj: %v", err)
}
|
后面,便可通过 objWithBss.Bss
拿到 .bss
bpf map 了,就可以 pin 该 map 了。
解决办法三:MapReplacements
对应的代码逻辑有点复杂,请查看 replace .bss map。
实现逻辑如下:
spec := bpfSpec.Maps[".bss"]
。
- 创建并 pin
.bss
bpf map。
- load 时提供选项
MapReplacements
。
解决办法四:Collection
请查看文档 NewCollection。它的用法比较简单,使用起来有点麻烦,不过能够通过 Collection.Maps[".bss"]
获取到 .bss
bpf map。
此处不展开详细的代码片段,请自行探索吧。
pin 全局变量的 bpf map
既然 .bss
bpf map 是一个 BPF_MAP_TYPE_ARRAY map,它肯定可以 pin。
尝试直接 pin .bss
bpf map:
1
2
3
4
5
6
7
8
9
10
11
|
spec := bpfSpec.Maps[".bss"]
spec.Pinning = ebpf.PinByName
m, err := ebpf.NewMapWithOptions(spec, ebpf.MapOptions{
PinPath: bpffs.BPFFSPath,
})
if err != nil {
log.Fatalf("Failed to new bpf map %s: %v", bssMapName, err)
}
// output:
// 2022/10/29 09:14:45 Failed to new bpf map gvar_bss: creating map: load pinned map: operation not permitted
|
这是怎么回事? (或许 retsnoop 可以帮忙查到 pin 失败的原因。)
经过一番探索,问题出在 .bss
bpf map 的名字 .bss
上:
1
2
3
4
|
root@leonhwang-svr ~/P/ebpf-globalvar# mount -t bpf
none on /sys/fs/bpf type bpf (rw,nosuid,nodev,noexec,relatime,mode=700)
root@leonhwang-svr ~/P/ebpf-globalvar# touch /sys/fs/bpf/test.bss
touch: setting times of '/sys/fs/bpf/test.bss': Operation not permitted
|
更改下 .bss
bpf map 的名字试试:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
const bssMapName = "gvar_bss"
spec := bpfSpec.Maps[".bss"]
spec.Name = bssMapName
spec.Pinning = ebpf.PinByName
m, err := ebpf.NewMapWithOptions(spec, ebpf.MapOptions{
PinPath: bpffs.BPFFSPath, // const BPFFSPath = "/sys/fs/bpf"
})
if err != nil {
log.Fatalf("Failed to new bpf map %s: %v", bssMapName, err)
}
// result:
// # ll /sys/fs/bpf/gvar_bss
// -rw------- 1 root root 0 Oct 29 09:39 /sys/fs/bpf/gvar_bss
|
知道问题出在 .bss
bpf map 的名字上,pin 失败的问题便能轻松解决了。
全局变量 VS 常量
对比 |
全局变量 |
常量 |
声明: |
__be32 filter_addr; |
volatile const __be32 filter_addr; |
使用: |
直接使用 |
直接使用 |
更新: |
运行时 |
加载时 |
bpf map 名字: |
.bss 或者 .data |
.rodata |
在项目中落地时,主要需要明白:全局变量是可以在运行的时候去更新它的值,而常量只能在加载的时候确定它的值、而不能在运行的时候去更新它的值。所以,比较好的处理办法是将全局变量的 bpf map pin 到文件系统里,pin 后就可以在需要的时候去更新它的值。
小结
踩了坑才学习到,原来全局变量所使用的 .bss
bpf map 也是可以 pin 的,并且更新 .bss
bpf map 的值需要一些技巧才能成功。