类似于常量的使用,如今在 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

实现逻辑如下:

  1. spec := bpfSpec.Maps[".bss"]
  2. 创建并 pin .bss bpf map。
  3. 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 的值需要一些技巧才能成功。