全局变量的用法请参考 eBPF Talk: 全局变量实战指南

常量的使用例子请参考 为 eBPF 程序注入黑魔法

有同事提了个问题:在 eBPF 运行的时候是怎么访问它们的内存的呢?

疑惑

被问到的时候,一脸懵逼,无从回答。

按照曾经的经验,全局变量和常量都是通过 bpf map 进行管理的。那么,在 eBPF 运行的时候是不是通过 bpf_map_lookup_elem() 去访问它们呢?

全局变量、常量的设计目标是对标 C 语言的全局变量、常量,在 eBPF 运行的时候是不是直接访问它们的内存呢?

层层解剖

我们知道,eBPF 程序需要经过编译、加载、加载进内核、verify、JIT、attach 等步骤后,才能真正运行起来。

解剖编译得到的 eBPF 汇编

因为 llvm-objdump -g -S xxx_bpfel.o 里没有源代码,所以使用 cilium/ebpf 来查看编译后的 eBPF 汇编吧。参考 eBPF Talk: bpf2bpf 特性揭秘

得到全局变量、常量、eBPF map 的汇编,如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 全局变量:
; struct xxx x = global_var.xxx;
639: LoadMapValue dst: r2, fd: 0 off: 0 <.bss>
641: LdXMemW dst: r1 src: r2 off: 0 imm: 0

# 常量:
; int index = (int)PROG_INDEX;
645: LoadMapValue dst: r2, fd: 0 off: 20 <.rodata>
647: LdXMemW dst: r2 src: r2 off: 0 imm: 0

# bpf map
; val = (typeof(val))bpf_map_lookup_elem(&yyy, &key);
657: LoadMapPtr dst: r1 fd: 0 <yyy>
659: Call FnMapLookupElem

解剖 JIT 前的 eBPF 汇编

在 attach 后 eBPF 程序运行的时候,使用 bpftool prog dump xlated id ${PROG ID} 查看 JIT 前的 eBPF 汇编。

得到全局变量、常量、eBPF map 的汇编,如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 全局变量:
; struct xxx x = global_var.xxx;
629: (18) r2 = map[id:36][0]+0
631: (61) r1 = *(u32 *)(r2 +0)

# 常量:
; int index = (int)PROG_INDEX;
635: (18) r2 = map[id:468][0]+20
637: (61) r2 = *(u32 *)(r2 +0)

# bpf map:
; val = (typeof(val))bpf_map_lookup_elem(&yyy, &key);
647: (18) r1 = map[id:37]
649: (85) call __htab_map_lookup_elem#179248

解剖 JIT 后的机器汇编

在 attach 后 eBPF 程序运行的时候,使用 bpftool prog dump jited id ${PROG ID} 查看 JIT 后的机器汇编。

得到全局变量、常量、eBPF map 的汇编,如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 全局变量:
; struct xxx x = global_var.xxx;
b1e:    movabs $0xffff8bad8b529710,%rsi     # 将内存地址写到 %rsi 寄存器里
b28:    mov    0x0(%rsi),%edi               # 读取 %rsi 寄存器对应内存的 4 个字节内容到 %edi 寄存器

# 常量:
; int index = (int)PROG_INDEX;
b3a:    movabs $0xffff8badc7247924,%rsi     # 将内存地址写到 %rsi 寄存器里
b44:    mov    0x0(%rsi),%esi               # 读取 %rsi 寄存器对应内存的 4 个字节内容到 %esi 寄存器

# bpf map:
; val = (typeof(val))bpf_map_lookup_elem(&yyy, &key);
b74:    movabs $0xffff8bad8d430c00,%rdi     # 将内存地址写到 %rdi 寄存器里
b7e:    callq  0xfffffffffa278e6c           # 调用 bpf_map_lookup_elem() 函数

小结

纵观这 3 段汇编代码片段,可知对于全局变量、常量的访问并不是通过 bpf_map_lookup_elem() 函数去访问的,而是直接访问它们的内存。这可由第 3 段的机器汇编分析得到。

全局变量及其 eBPF map

通过 eBPF Talk: 全局变量实战指南 可知,eBPF 使用了一个名称是 .bss 的 eBPF map 来管理全局变量。当 eBPF 程序运行起来后,可以使用 bpftool 查看 .bss eBPF map:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# bpftool map list
39: array  name .bss  flags 0x4
    key 4B  value 12B  max_entries 1  memlock 4096B
    btf_id 182

# bpftool map dump id 39
[{
        "value": {
            ".bss": [{
                    "global_var": {
                        "xxx": {
                            "enabled": 1,
                            "ifindex": [78,80
                            ]
                        }
                    }
                }
            ]
        }
    }
]

而在最初的 eBPF 实现中,在 eBPF 程序运行的时候的确是通过一次 bpf map 查询来访问全局变量的。

在 5.2 内核的 bpf: implement lookup-free direct value access for maps 这次 patch 里,就实现了无需 bpf map 查询的访问方式,即直接访问全局变量所在的内存地址。

在 eBPF 程序运行的时候,因为访问全局变量是不上锁的,所以如果想要在运行的时候去变更全局变量,需要使用原子性操作,即使用 __sync_fetch_and_XXX() 系列函数去变更全局变量(参考 [BPF] support atomic instructions)。

对于用户态应用程序而言,把全局变量当作一个普通的 ARRAY 类型的 bpf map 对待即可,可按需通过 LOOKUP, UPDATE 等 bpf map 操作来读取、变更全局变量。

全局变量的更新

当用户态应用程序通过 bpf map 的 UPDATE 操作更新全局变量时,具体是怎么更新的呢?如果运行中的 eBPF 程序同时变更全局变量,会发生什么?

直接看下内核更新 ARRAY bpf map 的函数调用栈吧。

1
2
3
4
5
6
7
8
// ${KERNEL}/kernel/bpf/{syscall,arraymap}.c

map_lookup_elem()
|-->bpf_map_copy_value()
    |-->ptr = map->ops->map_lookup_elem(map, key);
    |-->copy_map_value(map, value, ptr);
        |-->__copy_map_value()
            |-->memcpy()

最终,是使用 memcpy() 去更新全局变量的。

然而,memcpy() 不会保证内存更新的原子性,所以如果运行中的 eBPF 程序同时变更全局变量,则有可能会被覆盖掉、也有可能只更新了一部分。

关于全局变量的使用建议:

  • 要么用户态应用程序可读可写全局变量,而 eBPF 程序只读全局变量。
  • 要么用户态应用程序只读全局变量,而 eBPF 程序可读可写全局变量。

常量及其 eBPF map

类似于全局变量,常量使用了一个名称是 .rodata 的只读的 ARRAY bpf map 来管理。当 eBPF 程序运行起来后,可以使用 bpftool 查看 .rodata eBPF map:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# bpftool map list
270: array  name .rodata  flags 0x80
    key 4B  value 4B  max_entries 1  memlock 4096B
    btf_id 275  frozen                                  # frozen 代表不可写

# bpftool map dump id 270
[{
        "value": {
            ".rodata": [{
                    "TARGET_IFINDEX": 6
                }
            ]
        }
    }
]

eBPF 程序对常量的访问也是在 5.2 内核的 bpf: implement lookup-free direct value access for maps 这次 patch 里,就实现了无需 bpf map 查询的访问方式,即直接访问常量所在的内存地址。

对于用户态应用程序而言,常量 是一个只读不可写的 ARRAY 类型的 bpf map,只可通过 LOOKUP 操作进行读取。

总结

在 eBPF 运行的时候直接访问全局变量、常量的内存。

在用户态应用程序中,可以 LOOKUP、UPDATE 全局变量,只可 LOOKUP 常量(ARRAY bpf map 不支持 DELETE)。

eBPF 程序里推荐使用 __sync_fetch_and_XXX() 原子性地操作全局变量,以此保障内存安全。

全局变量的用法建议:

  • 要么用户态应用程序可读可写全局变量,而 eBPF 程序只读全局变量。
  • 要么用户态应用程序只读全局变量,而 eBPF 程序可读可写全局变量。