[笔记]BPF and XDP Reference Guide(cilium)
本文是Cilium中BPF and XDP Reference Guide的笔记。本文半翻译、半笔记,有些地方直接概况叙述。
学习BPF的过程:
- 阅读相关文献,记录两篇的笔记。另一篇的链接[笔记]Linux Socket Filtering aka Berkeley Packet Filter (BPF)
BPF类似于虚机指令集,允许在许多内核hook执行代码,主要用于网络、内核追踪以及安全(比如sandboxing)。
BPF的发展进过了cBPF与eBPF,tcpdump就是使用cBPF,但目前内核中只允许eBPF,兼容cBPF,cBPF的指令在内核中会自动替换为eBPF。
BPF结构
BPF除了指令集,还有bpf map、辅助函数(helper functions)、尾调机制(tail calls)、安全强化原语(security hardening primitives)、用于pinning对象(BPF map或是BPF program)的伪文件系统,另外还支持网卡offload的指令。
编译:C -> Clang -> LLVM backend -> BPF指令
BPF的两个主要子系统tc
和XDP
。
XDP
在最早的网络驱动程序阶段,高性能 ,但数据包还未进行元数据的解析。tc
位置在内核栈中,可以访问更多的数据包元数据以及内核函数。- 其他的子系统还包括:
kprobes
、uprobes
、tracepoints
等。
指令集
BPF是RISC指令集:
BPF指令-> JIT -> 机器码
BPF指令直接在内核中操作,无需进行内核/用户空间的切换。
灵活的可编程数据路径(data path),通过减少场景无需的特性,提升程序性能。
无缝的BPF程序更新,无需重启内核 ,也不会中断网络。
稳定的用户空间ABI,可跨不同体系的移植。
BPF是事件驱动的:例如数据包的接收触发ingress path上的BPF程序,内核代码的执行触发相应的kprobe BPF。
BPF包括11个64位的寄存器(包含32位的子寄存器)、程序计数器、512字节的栈。
寄存器分别是r0
到r10
,除了部分特殊的逻辑运算指令(ALU)外会使用其32位的子寄存器外,其他都是使用64位寄存器。32位到64位的扩展为高位补零。
r0
存放bpf helper函数的返回值。另外,整个BPF的返回值会以32位的形式存放在r0
中。r1-r5
存放调用bpf helper函数的实参,bpf helper函数之后,应作为未初始寄存器对待。因此在调用bpf helper函数之前,需要将r1-r5
转存到r6-r9
或是bpf的堆栈。r1
在bpf执行开始,会存储上下文变量(ctx),一般根据BPF程序的不同,ctx不同,比如tc/bpf中,ctx是sk_buff
的指针。r6-r9
在bpf helper执行前后数据不变,可用于存放需要保持的数据r10
唯一只读的寄存器,存储着bpf的堆栈地址。
BPF helper最多支持5个参数,在内核中BPF通过BPF_CALL_0()
到BPF_CALL_5()
进行声明。(注:后面数字表示参数数量,include/uapi/linux/bpf.h
中有各个helper函数的注解)
BPF程序的指令数量最大限制在4096条,5.1版本的内核后,限制扩到100万个,但仍不允许存在循环,但可以前后跳转的指令,比如尾调tail call
,尾调的上限为32层。
指令64位,目前有87条,格式(eBPF)如下,编码定义在linux/bpf.h
下。
op:8, dst_reg:4, src_reg:4, off:16, imm:32
op
字段表明了操作是基于寄存器还是立即数,基于立即数的op
,其目的操作数始终为寄存器。off
为偏移量,imm
为立即数,两者都是有符号类型。
OP
的结构为code:4,source:1,class:3
, source
值:
BPF_X
:基于寄存器的操作BPF_K
:基于立即数的操作
class
包括:
BPF_LD
、BPF_LDX
:BPF_LD
是个特殊指令,用于double word的立即数加载,受限imm:32
,会跨两个指令;BPF_LD
也可以用于数据包的读取(注:会有一些使用约定,比如r6
保存sk_buff
的指针)。BPF_LDX
从内存读取数据 ,内存可以是栈、map、数据包等。BPF_ST
、BPF_STX
:BPF_STX
将寄存器中的数据存储到内存或者是bpf堆栈、bpf map等,BPF_STX
还可以做word和double-word的原子累加操作。BPF_ST
与BPF_STX
类似,但原数据只能是立即数。BPF_ALU
、BPF_ALU64
:BPF_ALU
是32位操作,BPF_ALU64
是64位操作,都支持+ - & | << >> ^ * / % ~
。两者还包含特殊的ALU操作:mov(<X>:=<Y>
),BPF_ALU64
还包含一组有符号的右移操作,BPF_ALU
包含字节顺序转换的指令。BPF_JMP
:包括无条件与有条件跳转。跳转的下一指令为off+1
,因为off
为有符号类型,可以完成前跳,但不能产生循环或跳出程序范围。不同于cBPF(存在true或false两个偏移量),条件跳转机制为fall-through,更符合CPU的分支预测逻辑。另外包含三个特殊的跳转操作:退出指令exit instruction
;用于调用helper方法的调用指令call instruction
;用于调用其他BPF程序的尾调指令tail call instruction
。
目前下列架构都内置了内核 eBPF JIT 编译器:x86_64
、arm64
、ppc64
、s390x
、mips64
、sparc64
和 arm
。
通过系统调用bpf()
可以进行:
- 创建、加载、卸载BPF程序
- 创建、操作BPF map
- pinning BPF map和BPF程序到BPF文件系统中(做持久化)
辅助函数
不同类型的BPF 程序能够使用的辅助函数不完全相同。
u64 fn(u64 r1, u64 r2, u64 r3, u64 r4, u64 r5)
内核将辅助函数抽象为BPF_CALL_0()
到BPF_CALL_5()
几个宏,例如map update的调用(注:BPF_CALL_4
表示4个参数,后面依次是参数类型,参数;bpf_func_proto
给verifier进行调用的参数类型验证,另外map->ops->map_update_elem
不同bpf map类型有不同的实现,在kernel\bpf
下):
BPF_CALL_4(bpf_map_update_elem, struct bpf_map *, map, void *, key,
void *, value, u64, flags)
{
WARN_ON_ONCE(!rcu_read_lock_held());
return map->ops->map_update_elem(map, key, value, flags);
}
const struct bpf_func_proto bpf_map_update_elem_proto = {
.func = bpf_map_update_elem,
.gpl_only = false,
.ret_type = RET_INTEGER,
.arg1_type = ARG_CONST_MAP_PTR,
.arg2_type = ARG_PTR_TO_MAP_KEY,
.arg3_type = ARG_PTR_TO_MAP_VALUE,
.arg4_type = ARG_ANYTHING,
};
- 根据之前的约定(
r1-r5
传递参数),JIT只需要进行跳转,不需要进行寄存器的操作。 - 参数可以是任意类型,也可以是堆栈的指针。
- 内核
struct bpf_verifier_ops
包含回调函数get_func_proto
,能根据BPF程序类型(enum bpf_func_id
)来映射可用的BPF helper函数。
Maps
map驻留在内存中,可以通过用户空间的文件描述符访问,可以在任意BPF程序以及用户空间应用之间共享。共享map的BPF程序不需要是相同的程序类型,单个BPF程序最多可以访问64个map。
map有per-CPU
以及non-per-CPU
的通用map,另外还有提供给特定辅助函数的非通用map。通用map包括 BPF_MAP_TYPE_HASH
, BPF_MAP_TYPE_ARRAY
, BPF_MAP_TYPE_PERCPU_HASH
, BPF_MAP_TYPE_PERCPU_ARRAY
, BPF_MAP_TYPE_LRU_HASH
, BPF_MAP_TYPE_LRU_PERCPU_HASH
和 BPF_MAP_TYPE_LPM_TRIE
,他们都使用相同的一组BPF辅助函数来执行查找、更新或删除等。
非通用map包括 BPF_MAP_TYPE_PROG_ARRAY
, BPF_MAP_TYPE_PERF_EVENT_ARRAY
, BPF_MAP_TYPE_CGROUP_ARRAY
, BPF_MAP_TYPE_STACK_TRACE
, BPF_MAP_TYPE_ARRAY_OF_MAPS
, BPF_MAP_TYPE_HASH_OF_MAPS
。例如 BPF_MAP_TYPE_PROG_ARRAY
用于存储其他BPF程序,BPF_MAP_TYPE_ARRAY_OF_MAPS
用于存储其他map的指针。这些map都是为了与bpf help一起完成特定功能。
Object Pinning
BPF maps和BPF程序只能通过文件描述符访问,后端是匿名inode。用户空间的程序可以利用大多数文件描述符的APIs进行操作。但同时,文件描述符被限制在程序的生命周期内,使得map共享之类操作难以实现(比如tc的ingress和egress BPF程序希望共享一个bpf map,或是在BPF运行期间监视和更新BPF map)。
为克服此限制,实现了一个最小的内核空间的BPF文件系统,BPF map和BPF程序可以被Pin到此文件系统中。两个BPF系统调用BPF_OBJ_PIN
和BPF_OBJ_GET
分别用于pin操作和获取被pin的对象。
利用BPF文件系统,tc可以实现ingress和egress的map共享,BPF文件系统不是单例模式,支撑多挂载实例、硬链接、软链接等。
尾调用
尾调(tail calls)允许BPF程序调用其他BPF程序,但不再返回父程序。相比于通常的调用,尾调开销小,实现了远跳转(long jump),复用原来的栈帧(stack frame)(因为不用返回 ?)。
由于BPF程序是单独验证的,因此状态的传递要么是使用per-cpu
map,或是对于tc程序 ,使用skb
中的字段,例如cb[]
(Controller buff)。
尾调只能在相同类型的BPF程序之间,尾调的程序要么都是通过JIT编译的,要么都是通过解释器执行的,不能混合。
尾调涉及两个组件:一个是程序map BPF_MAP_TYPE_PROG_ARRAY
,可以由用户空间进行填充,其值为尾调的BPF程序的文件描述符;另一个是辅助函数bpf_tail_call()
,传入的参数包括一个程序map的引用以及需要查询的key。然后内核将这个辅助函数内联(inlines)到专门的BPF指令中。目前,程序数组在用户空间只写。
内核根据传入的文件描述符查找相关的 BPF 程序,然后自动替换。如果没有map对应的value,内核会fall through ,继续执行 bpf_tail_call()
后面的指令。尾调可用来解析报文头。另外,在运行时,尾调函数可自动添加替换,从而实现BPF程序行为的改变。
BPF到BPF的调用
除了BPF辅助函数、BPF尾调函数外,还有一个特性:BPF to BFP calls。在引入此特性之前,BPF C程序使用always_inline
声明方法,每次调用都进行内联,会增加代码的大小
#include <linux/bpf.h>
#ifndef __section
# define __section(NAME) \
__attribute__((section(NAME), used))
#endif
#ifndef __inline
# define __inline \
inline __attribute__((always_inline))
#endif
static __inline int foo(void)
{
return XDP_DROP;
}
__section("prog")
int xdp_drop(struct xdp_md *ctx)
{
return foo();
}
char __license[] __section("license") = "GPL";
从内核4.16和LLVM6.0开始,不再需要使用always_inline
:
#include <linux/bpf.h>
#ifndef __section
# define __section(NAME) \
__attribute__((section(NAME), used))
#endif
static int foo(void)
{
return XDP_DROP;
}
__section("prog")
int xdp_drop(struct xdp_md *ctx)
{
return foo();
}
char __license[] __section("license") = "GPL";
主流的BPF JIT都支持,例如x86_64
和arm64
。调用方式与BPF辅助函数类似,允许的最大嵌套调用为8。调用者可以将指针(比如指向堆栈的)向下传递给被调用者,但绝不能相反。
在内核5.9版本之前,BPF的尾调与子程序(BPF2BFP)是不兼容的,利用尾调的BPF程序无法减小应用image,无法加快加载。在内核5.10后,不再有此限制。但混合两个功能一起使用可能会导致内核栈溢出,下面是混合了BPF2BPF和尾调的情况:
尾调在跳到目标程序之前,只会回溯(unwind)它当前的栈。如上图所示,如果子函数(sub-function,图中subfunc1)中有尾调方法,那么在执行func2时(从subfunc1尾调过去),父函数func1的栈会保留在func2的栈中。一旦func3终止,之前所有的栈都会被回溯(unwind)。
内核引入如下机制:调用链中的每个子程序的栈大小不可超过256字节,当存在BPF2BPF时,主函数(调用BPF2BPF的函数)可被认为是子程序。根据这个限制,BPF程序最后的栈大小被限制在8KB(256*尾调上限32)。
当前,BPF尾调与BPF2BPF组合调用功能只在x86_64
上支持。
JIT
提供内核eBPF JIT编译器的架构包括:x84_64
、arm64
、ppc64
、s390x
、mips64
、sparc64
、32位的arm
、x86_32
。3开启JIT:
# echo 1 > /proc/sys/net/core/bpf_jit_enable
32位的mips
,ppc
和sparc
只支持cBPF JIT,其他的架构不支持BPF JIT,需要使用内核的解释器。内核源代码中,通过grep HAVE_EBPF_JIT
可以查询相应的支持。
# git grep HAVE_EBPF_JIT arch/
arch/arm/Kconfig: select HAVE_EBPF_JIT if !CPU_ENDIAN_BE32
arch/arm64/Kconfig: select HAVE_EBPF_JIT
arch/powerpc/Kconfig: select HAVE_EBPF_JIT if PPC64
arch/mips/Kconfig: select HAVE_EBPF_JIT if (64BIT && !CPU_MICROMIPS)
arch/s390/Kconfig: select HAVE_EBPF_JIT if PACK_STACK && HAVE_MARCH_Z196_FEATURES
arch/sparc/Kconfig: select HAVE_EBPF_JIT if SPARC64
arch/x86/Kconfig: select HAVE_EBPF_JIT if X86_64
JIT编译器加快了BPF程序的执行速度,因为与解释器(interpreter)比起来,JIT降低了每条指令的执行成本。通常,指令可以与基础架构的原生指令有一对一的映射,这也减少了最终可执行映像(executable image)的大小,因此对CPU更加友好。
加固(Hardening)
为了避免代码被损坏,BPF的解释器image(struct bpf_prog
)以及JIT编译器image(struct bpf_binary_header
)在内核中是只读。支持设置只读的架构:
$ git grep ARCH_HAS_SET_MEMORY | grep select
arch/arm/Kconfig: select ARCH_HAS_SET_MEMORY
arch/arm64/Kconfig: select ARCH_HAS_SET_MEMORY
arch/s390/Kconfig: select ARCH_HAS_SET_MEMORY
arch/x86/Kconfig: select ARCH_HAS_SET_MEMORY
其中CONFIG_ARCH_HAS_SET_MEMORY
是不可配置的,总是开启的。
在x86_64
中,开启CONFIG_RETPOLINE
后,BGP的尾调通过retpoline
实现间接跳转(retpoline是Google开发的针对Spectre变种2漏洞缓解利用技术),CONFIG_RETPOLINE
默认是开启的。
当时设置/proc/sys/net/core/bpf_jit_harden
为1时,对非特权用户的JIT编译会进行额外的加固过程,对性能有一定的影响,但仍比解释器要高。
盲化:开启加固可以对BPF的32位和64位常量盲化(blind),防止JIT喷射攻击。
JIT喷射攻击:攻击者利用包含立即数的脚本指令, 通过控制源程序将“常量” 作为立即数注入即时编译器的生成代码中(如指令 “XOR EAX 0x3c909090”), 然后通过一个控制流劫持漏洞跳转到“常量”的地址加一个偏移值, 此时“常量”就变成了可以被识别执行的代码片段。通过连续输入大量的常量就可以在代码缓存中生成攻击者需要的Shellcode。
盲化的方式是通过两步将基于立即数的指令转化成基于寄存器的指令,比如load指令的盲化:1)将盲化后的rnd^imm
立即数加载到寄存器;2)异或寄存器与rnd
。
盲化关闭的程序:
# echo 0 > /proc/sys/net/core/bpf_jit_harden
ffffffffa034f5e9 + <x>:
[...]
39: mov $0xa8909090,%eax
3e: mov $0xa8909090,%eax
43: mov $0xa8ff3148,%eax
48: mov $0xa89081b4,%eax
4d: mov $0xa8900bb0,%eax
52: mov $0xa810e0c1,%eax
57: mov $0xa8908eb4,%eax
5c: mov $0xa89020b0,%eax
[...]
在开启盲化后:
# echo 1 > /proc/sys/net/core/bpf_jit_harden
ffffffffa034f1e5 + <x>:
[...]
39: mov $0xe1192563,%r10d
3f: xor $0x4989b5f3,%r10d
46: mov %r10d,%eax
49: mov $0xb8296d93,%r10d
4f: xor $0x10b9fd03,%r10d
56: mov %r10d,%eax
59: mov $0x8c381146,%r10d
5f: xor $0x24c7200e,%r10d
66: mov %r10d,%eax
69: mov $0xeb2a830e,%r10d
6f: xor $0x43ba02ba,%r10d
76: mov %r10d,%eax
79: mov $0xd9730af,%r10d
7f: xor $0xa5073b1f,%r10d
86: mov %r10d,%eax
89: mov $0x9a45662b,%r10d
8f: xor $0x325586ea,%r10d
96: mov %r10d,%eax
[...]
两个程序是一样的,只是第二个不再有立即数的操作(注:通过寄存器操作与异或代替)。
JIT kallsyms:除盲化外,开启加固后还会关闭JIT kallsyms,从而JIT image的地址不再会暴露在/proc/kallsyms
中。
CONFIG_BPF_JIT_ALWAYS_ON
会总是开启JIT编译器,删除BPF解释器,也可用于Spectre变种2漏洞的缓解。在虚机的场景下,虚机内核不再复用主机内核的BPF解释器,以避免被攻击。在容器场景下,去除解释器可降低内核复杂度。因此这个属性通常建议开启。
最后/proc/sys/kernel/unprivileged_bpf_disabled
可以禁止非特权用户进行bpf()
系统调用,但此参数一旦设置,无法在更改,除非重启内核。开启后(设置为1
),在初始命名空间外,只有CAP_SYS_ADMIN
的特权进程才能调用。Cilium启动的时候会开启。
# echo 1 > /proc/sys/kernel/unprivileged_bpf_disabled
Offloads
BPF网络程序(tc、XDP)可以将程序offload到NIC上,通过NIC直接执行BPF程序。
Netronome的nfp驱动支持通过JIT编译器offload,即JIT会将BPF指令编译为NIC的指令(包括BPF map映射到NIC)。
工具
开发环境
一般发行版会自带iproute2
,除非需要测试或使用最新版本的BPF特性,则需要进行iproute2
和内核的编译。
编译内核
BPF新开发的特性在net-next
分支,而BPF最新的修正在net
分支。克隆net-next
分支:
git clone git://git.kernel.org/pub/scm/linux/kernel/git/netdev/net-next.git
克隆net
分支:
git clone git://git.kernel.org/pub/scm/linux/kernel/git/netdev/net.git
.config
的配置:
CONFIG_CGROUP_BPF=y
CONFIG_BPF=y
CONFIG_BPF_SYSCALL=y
CONFIG_NET_SCH_INGRESS=m
CONFIG_NET_CLS_BPF=m
CONFIG_NET_CLS_ACT=y
CONFIG_BPF_JIT=y
CONFIG_LWTUNNEL_BPF=y
CONFIG_HAVE_EBPF_JIT=y
CONFIG_BPF_EVENTS=y
CONFIG_TEST_BPF=m
有些条目无法通过make menuconfig
选择,比如CONFIG_HAVE_EBPF_JIT
(建议开启)在带有eBPF JIT的架构体系中自动选择。
验证
验证新内核的BPF功能:
$ cd tools/testing/selftests/bpf/
$ make
$ sudo ./test_verifier
Summary: 847 PASSED, 0 SKIPPED, 0 FAILED
对于4.16+以上的内核,BPF部分测试用例依赖LLVM 6.0+,提供无需inline的BPF2BPF功能,如果发行版无LLVM 6.0+,需要按照下面LLVM节中所述,编译LLVM。
运行所有的BPF自检测试:
sudo make run_tests
编译iproute2
与net
和net-next
类似,iproute2也分为master
和net-next
,并与内核的net
和net-next
相互对应。克隆master
:
git clone git://git.kernel.org/pub/scm/linux/kernel/git/iproute2/iproute2.git
克隆net-next
:
git clone git://git.kernel.org/pub/scm/linux/kernel/git/iproute2/iproute2.git
编译与安装:
$ cd iproute2/
$ ./configure --prefix=/usr
TC schedulers
ATM no
libc has setns: yes
SELinux support: yes
ELF support: yes
libmnl support: no
Berkeley DB: no
docs: latex: no
WARNING: no docs can be built from LaTeX files
sgml2html: no
WARNING: no HTML docs can be built from SGML
$ make
[...]
$ sudo make install
确保configure
脚本显示ELF support: yes
,以便iproute2可以处理LLVM后端的ELF文件。
编译bpftool
bpftool用于debug bpf程序与bpf map的工具,在内核的tools/bpf/bpftool
下。
$ cd <kernel-tree>/tools/bpf/bpftool/
$ make
Auto-detecting system features:
... libbfd: [ on ]
... disassembler-four-args: [ OFF ]
CC xlated_dumper.o
CC prog.o
CC common.o
CC cgroup.o
CC main.o
CC json_writer.o
CC cfg.o
CC map.o
CC jit_disasm.o
CC disasm.o
make[1]: Entering directory '/home/foo/trees/net/tools/lib/bpf'
Auto-detecting system features:
... libelf: [ on ]
... bpf: [ on ]
CC libbpf.o
CC bpf.o
CC nlattr.o
LD libbpf-in.o
LINK libbpf.a
make[1]: Leaving directory '/home/foo/trees/bpf/tools/lib/bpf'
LINK bpftool
$ sudo make install
LLVM
LLVM是当前唯一提供BPF编译后端的套件,gcc不支持。
LLVM 3.7加入BPF后端,一般默认开启,因此在安装clang和LLVM后就可以将C语言编译为BPF对象文件了。
典型的工作流程:
- 编写C语言文件
- LLVM编译为obj文件或ELF文件
- 用户空间的loader工具(iproute2)通过
bpf()
系统调用加载BPF到内核 - 内核验证BPF程序
- JIT编译为本机指令,并新建文件描述符,用于挂载到子系统中(e.g. networking)
- 如果支持,子系统进一步将BPF程序offload到NIC上。
检测LLVM是否支持BPF(llc是LLVM编译器,输入LLVM IR,输出汇编文件或者是目标文件):
$ llc --version
LLVM (http://llvm.org/):
LLVM version 3.8.1
Optimized build.
Default target: x86_64-unknown-linux-gnu
Host CPU: skylake
Registered Targets:
[...]
bpf - BPF (host endian)
bpfeb - BPF (big endian)
bpfel - BPF (little endian)
[...]
默认情况,bpf
类型的target是和CPU的字节序一致的。可以通过bpfeb
和bpfel
进行交叉编译,在大端机器上编译小端的BPF程序,在小端机器上运行。需要注意前端clang也需要运行在相同的字节序上。
一个XDP drop程序样例(xdp-example.c
):
#include <linux/bpf.h>
#ifndef __section
# define __section(NAME) \
__attribute__((section(NAME), used))
#endif
__section("prog")
int xdp_drop(struct xdp_md *ctx)
{
return XDP_DROP;
}
char __license[] __section("license") = "GPL";
编译并加载到内核:
$ clang -O2 -Wall -target bpf -c xdp-example.c -o xdp-example.o
# ip link set dev em1 xdp obj xdp-example.o
加载XDP程序到网络设备,需要linux 4.11版本以及对应的device支持,或是linux 4.12以上的版本。
$ file xdp-example.o
xdp-example.o: ELF 64-bit LSB relocatable, *unknown arch 0xf7* version 1 (SYSV), not stripped
0xf7
表示官方BPF机器值EM_BPF
,十进制247
。LSB
表示小端字节序。通过readelf -a xdp-example.o
命令可以看到更多的ELF文件信息,比如section头、重定向条目、symbol表。
编译安装LLVM
少数情况下可能需要编译安装clang和LLVM:
$ git clone https://git.llvm.org/git/llvm.git
$ cd llvm/tools
$ git clone --depth 1 https://git.llvm.org/git/clang.git
$ cd ..; mkdir build; cd build
$ cmake .. -DLLVM_TARGETS_TO_BUILD="BPF;X86" -DBUILD_SHARED_LIBS=OFF -DCMAKE_BUILD_TYPE=Release -DLLVM_BUILD_RUNTIME=OFF
$ make -j $(getconf _NPROCESSORS_ONLN)
$ ./bin/llc --version
LLVM (http://llvm.org/):
LLVM version x.y.zsvn
Optimized build.
Default target: x86_64-unknown-linux-gnu
Host CPU: skylake
Registered Targets:
bpf - BPF (host endian)
bpfeb - BPF (big endian)
bpfel - BPF (little endian)
x86 - 32-bit X86: Pentium-Pro and above
x86-64 - 64-bit X86: EM64T and AMD64
$ export PATH=$PWD/bin:$PATH # add to ~/.bashrc
确保--version
中会有Optimized build
,否则当以debug模式编译时,编译的时间会大大增加。
汇编输出
为了调试,clang可以直接产生汇编输出(通过-S
):
$ clang -O2 -S -Wall -target bpf -c xdp-example.c -o xdp-example.S
$ cat xdp-example.S
.text
.section prog,"ax",@progbits
.globl xdp_drop
.p2align 3
xdp_drop: # @xdp_drop
# BB#0:
r0 = 1
exit
.section license,"aw",@progbits
.globl __license # @__license
__license:
.asciz "GPL"
从LLVM 6.0开始,提供了BPF汇编解释器,通过llvm-mc
将上面的汇编输出编译为obj文件。
llvm-mc -triple bpf -filetype=obj -o xdp-example.o xdp-example.S
输出DWARF
在LLVM 4.0版本开始,可以通过-g
将debug信息以dwarf的格式编译到obj文件中。然后使用llvm-objdump
查看汇编以及对应的C语言的注释。其中编号0
和1
是与内核验证器的日志输出一致的,因此当未被内核的验证器通过时,可以通过llvm-objdump
查看具体报错的C语言代码。
$ clang -O2 -g -Wall -target bpf -c xdp-example.c -o xdp-example.o
$ llvm-objdump -S -no-show-raw-insn xdp-example.o
xdp-example.o: file format ELF64-BPF
Disassembly of section prog:
xdp_drop:
; {
0: r0 = 1
; return XDP_DROP;
1: exit
\# ip link set dev em1 xdp obj xdp-example.o verb
Prog section 'prog' loaded (5)!
- Type: 6
- Instructions: 2 (0 over limit)
- License: GPL
Verifier analysis:
0: (b7) r0 = 1
1: (95) exit
processed 2 insns
如果不加-no-show-raw-insn
,llvm-objdump
还会将struct bpf_insn
(BPF指令)以十六进制的形式显示。
$ llvm-objdump -S xdp-example.o
xdp-example.o: file format ELF64-BPF
Disassembly of section prog:
xdp_drop:
; {
0: b7 00 00 00 01 00 00 00 r0 = 1
; return foo();
1: 95 00 00 00 00 00 00 00 exit
LLVM IR
生成LLVM IR文件用于调试,并进一步通过LLVM IR文件生成obj文件:
$ clang -O2 -Wall -target bpf -emit-llvm -c xdp-example.c -o xdp-example.bc
$ llc xdp-example.bc -march=bpf -filetype=obj -o xdp-example.o
生成的LLVM IR还能转存为人类可读的格式:
clang -O2 -Wall -emit-llvm -S -c xdp-example.c -o -
BTF
除了默认的debug格式DWARF外,BPF还有提供BTF(BPF Type Format)。DWARF能转换成BTF,BTF通过BPF loader工具加载到内核,验证器可以验证BTF的正确性,并追踪BTF中的结构体。
将BPF map中的key、value类型的注释(annotate)到BTF中,dump时会同时显示map的类型。BTF是一个通用的debug格式,任意DWARF都可以转换为BTF,比如内核的vmlinux DWARF。
BTF与DWARF格式的转换,由于libdw(注:“libdw1 provides a library that provides access to DWARF debug information stored inside ELF files.”)不支持BPF的重定位,因此pahole
(注:“pahole shows data structure layouts encoded in debugging information formats, DWARF and CTF being supported.”)类似的工具无法正常使用,解决方法包括:
- 使用elfutils(>=0.173),elfutils实现了适当的BPF重定位支持。
llc
添加-mattr=dwarfris
参数,以关闭DWARF与ELF符号表之间的跨section重定位。$ llc -march=bpf -mattr=help |& grep dwarfris dwarfris - Disable MCAsmInfo DwarfUsesRelocationsAcrossSections. [...]
pahole
依赖LLVM输出的DWARF信息,后续将利用BTF信息。将DWARF转换为BTF,需要pahole
>=1.12:
git clone https://git.kernel.org/pub/scm/devel/pahole/pahole.git
pahole
通过-J
将DWARF转换为BTF,查看pahole
是否支持转换(pahole
还依赖llvm-objcopy
工具)
$ pahole --help | grep BTF
-J, --btf_encode Encode as BTF
如上面介绍的,debug信息的生成了依赖前端clang
的-g
选项,比如下面使用-mattr=dwarfris
的方式:
$ clang -O2 -g -Wall -target bpf -emit-llvm -c xdp-example.c -o xdp-example.bc
$ llc xdp-example.bc -march=bpf -mattr=dwarfris -filetype=obj -o xdp-example.o
或者也可以直接使用clang生成带有debug信息的obj文件。
clang -target bpf -O2 -g -c -Xclang -target-feature -Xclang +dwarfris -c xdp-example.c -o xdp-example.o
使用pahole
从DWARF信息中dump出上面BPF程序的数据结构:
$ pahole xdp-example.o
struct xdp_md {
__u32 data; /* 0 4 */
__u32 data_end; /* 4 4 */
__u32 data_meta; /* 8 4 */
/* size: 12, cachelines: 1, members: 3 */
/* last cacheline: 12 bytes */
};
pahole -J
可以将DWARF数据转换为BTF,但最终的obj文件里仍会同时包含DWARF与BTF两种数据。
$ clang -target bpf -O2 -Wall -g -c -Xclang -target-feature -Xclang +dwarfris -c xdp-example.c -o xdp-example.o
$ pahole -J xdp-example.o
通过readelf
工具可以看到.BTF
section。
$ readelf -a xdp-example.o
[...]
[18] .BTF PROGBITS 0000000000000000 00000671
[...]
指令集版本
默认情况,LLVM为了兼容低版本内核 ,会使用BPF基础指令集进行编译。但是也可以通过LLVM的-mcpu
来选择指令集的版本。-mcpu
可用于交叉编译的场景。
$ llc -march bpf -mcpu=help
Available CPUs for this target:
generic - Select the generic processor.
probe - Select the probe processor.
v1 - Select the v1 processor.
v2 - Select the v2 processor.
[...]
generic
是默认的选项,是v1版本的BPF基础指令集。probe
(cilium使用)会自动探测内核可用的BPF指令集。完整的是使用样例:
$ clang -O2 -Wall -target bpf -emit-llvm -c xdp-example.c -o xdp-example.bc
$ llc xdp-example.bc -march=bpf -mcpu=probe -filetype=obj -o xdp-example.o
clang target(64位/32位)
使用clang -target bpf
和不使用-target bpf
而使用默认的target,在不同的系统架构下有些不同的细微差别。
引用内核的Documentation/bpf/bpf_devel_QA.txt
:
- BPF可以递归引用(include)头文件,在文件范围内内联汇编代码。 默认target可以很好的处理这种情况,但target bpf大多数情况下无法正确处理。
- 如果没有加
-g
参数,对于编译后是否会产生额外的elf sections,默认target可能会生成,但target bpf不会。 - 对于C语言中的
switch
,默认target会生成switch表和跳转操作。但switch表在全局只读section中,bpf程序是无法读取的,因此target bpf不支持这种使用switch表的优化方式。clang可以通过-fno-jump-tables
关闭switch表的生成。 - 无论底层clang的版本和内核的版本(32位或64位),target bpf总是会保证指针、long 、unsigned long类型是64位的。但默认target是根据底层系统架构来的。
默认target(例如x86_64的架构上选择x86_64
的target)在追踪内核的struct pt_regs
结构或其他与寄存器宽带有关的内核结构时有用,struct pt_regs
结构用于映射cpu寄存器。但在其他情况下,比如网络的应用场景,建议使用target bpf
。
LLVM从7.0开始也支持32位的子寄存器和BPF ALU32,需要开启alu32
参数,开启后,LLVM会总是尝试使用32位的子寄存器以及BPF ALU32。
$ cat 32-bit-example.c
void cal(unsigned int *a, unsigned int *b, unsigned int *c)
{
unsigned int sum = *a + *b;
*c = sum;
}
默认情况下的汇编:
$ clang -target bpf -emit-llvm -S 32-bit-example.c
$ llc -march=bpf 32-bit-example.ll
$ cat 32-bit-example.s
cal:
r1 = *(u32 *)(r1 + 0)
r2 = *(u32 *)(r2 + 0)
r2 += r1
*(u32 *)(r3 + 0) = r2
exit
开启-mattr=+alu32
后:
$ llc -march=bpf -mattr=+alu32 32-bit-example.ll
$ cat 32-bit-example.s
cal:
w1 = *(u32 *)(r1 + 0)
w2 = *(u32 *)(r2 + 0)
w2 += w1
*(u32 *)(r3 + 0) = w2
exit
上面w
表示32位的子寄存器,r
表示64位的寄存器。
开启32位子寄存器可以减少类型扩展指令。对于32架构上的eBPF JIT编译器来说,会将一对寄存器作为64位的BPF寄存器使用,因此需要额外的指令去操作高位的32位寄存器。使用32位指令后,可以保证读取时,只操作低位的32位寄存器。但写时,仍需要将高位的32位寄存器清零。如果JIT编译器能知道逻辑上一个寄存器的定义就是指32位寄存器,那么高位的子寄存器操作就可以省略。
C语言开发注意事项
使用C语言开发BPF与通常的C语言开发不太一样,需要注意:
1)在旧版本的LLVM中不只支持函数调用以及共享库,因此所有方法都需要进行内联。
共享库是无法与BPF一起使用的,但对于一般的库来说,可以通过先放到头文件中,然后再引用到程序中的方式进行使用。比如Cilium大量使用了此方式(bpf/lib/
)。也可以直接引用头文件(比如内核或其他库的头文件),然后使用其中内联的方法或者是宏定义。
对于内核(4.16+)和LLVM(6.0+)来说,支持BPF2BPF,因此无需内联。其他情况下需要内联,在内联时推荐使用always_inline
方式,因为对于inline
属性,编译器仍能决定不进行内联。一旦编译器未进行内联,LLVM就会生成带有重定位项(relocation entry)的elf文件,BPF loaders(例如iproute2)只能识别处理BPF maps的重定位,因此会报错。
#include <linux/bpf.h>
#ifndef __section
# define __section(NAME) \
__attribute__((section(NAME), used))
#endif
#ifndef __inline
# define __inline \
inline __attribute__((always_inline))
#endif
static __inline int foo(void)
{
return XDP_DROP;
}
__section("prog")
int xdp_drop(struct xdp_md *ctx)
{
return foo();
}
char __license[] __section("license") = "GPL";
2)不同的程序可以在一个C文件的不同section中。
C语言编写的BPF程序中使用大量的section注释,BPF ELF loader会根据section的名称来加载相应的BPF程序和maps。比如iproute2默认使用maps
和license
作为section的名称,来查询BPF maps以及BPF程序的license信息。BPF程序启用部分helper函数,需要持有GPL兼容license,例如bpf_ktime_get_ns()
,bpf_probe_read()
。
下面的tc程序用于计数ingress和egress的流量,可以加载到某个网卡上 。程序定义了ingress
和egress
两个section,同时通过内联方式调用account_data()
,通过maps
section来利用bpf maps。
#include <linux/bpf.h>
#include <linux/pkt_cls.h>
#include <stdint.h>
#include <iproute2/bpf_elf.h>
#ifndef __section
# define __section(NAME) \
__attribute__((section(NAME), used))
#endif
#ifndef __inline
# define __inline \
inline __attribute__((always_inline))
#endif
#ifndef lock_xadd
# define lock_xadd(ptr, val) \
((void)__sync_fetch_and_add(ptr, val))
#endif
#ifndef BPF_FUNC
# define BPF_FUNC(NAME, ...) \
(*NAME)(__VA_ARGS__) = (void *)BPF_FUNC_##NAME
#endif
static void *BPF_FUNC(map_lookup_elem, void *map, const void *key);
struct bpf_elf_map acc_map __section("maps") = {
.type = BPF_MAP_TYPE_ARRAY,
.size_key = sizeof(uint32_t),
.size_value = sizeof(uint32_t),
.pinning = PIN_GLOBAL_NS,
.max_elem = 2,
};
static __inline int account_data(struct __sk_buff *skb, uint32_t dir)
{
uint32_t *bytes;
bytes = map_lookup_elem(&acc_map, &dir);
if (bytes)
lock_xadd(bytes, skb->len);
return TC_ACT_OK;
}
__section("ingress")
int tc_ingress(struct __sk_buff *skb)
{
return account_data(skb, 0);
}
__section("egress")
int tc_egress(struct __sk_buff *skb)
{
return account_data(skb, 1);
}
char __license[] __section("license") = "GPL";
- 上面程序引用了一些头文件,包括内核头文件、C语言标准头文件、iproute2头文件,其中iproute2头文件中定义了
struct bpf_elf_map
,用于创建BPF map。你可以用struct bpf_elf_map
创建多个不同名称的同类型map,但需要都在__section("maps")
下。另外不同的BPF ELF loader中也有不同的map定义结构,比如libbpf就和iproute2的struct bpf_elf_map
不同。 map_lookup_elem()
对应BPF_FUNC_map_lookup_elem
的helper函数,定义在uapi/linux/bpf.h
里。- 因为定义的map是全局的,因此需要使用原子操作。在程序中使用的是
lock_xadd()
,LLVM将__sync_fetch_and_add()
作为BPF原子加的内置函数,对应的指令为BPF_STX | BPF_XADD | BPF_W
。 .pinning = PIN_GLOBAL_NS,
表示将map pin到BPF的文件系统中,默认情况下,路径为/sys/fs/bpf/tc/globals/acc_map
,由于是global,表明各个obj文件都能对其进行访问。比如多个BPF程序都定义了相同(名字和属性都相同)的acc_map
bpf map,同时都使用了PIN_GLOBAL_NS
,那么多个BPF程序都会共享这个map。上面的ingress
启动后会查询是否存在acc_map
对应的文件系统,没有则创建。在egress
启动时,则直接使用。- 除了
PIN_GLOBAL_NS
外,还有PIN_OBJECT_NS
,将会创建object对应的目录,将map放在其下。PIN_NONE
表示不pin到文件系统中,那么tc结束后,map将不能被从用户空间进行访问,同时程序中的ingress和egress使用的将是两个map。 - 可以通过bpf系统调用中的
BPF_OBJ_GET
来创建指向相同的map的文件描述符,通过这个文件描述符可以对map进行lookup/update/delete的操作。
$ clang -O2 -Wall -target bpf -c tc-example.c -o tc-example.o
# tc qdisc add dev em1 clsact
# tc filter add dev em1 ingress bpf da obj tc-example.o sec ingress
# tc filter add dev em1 egress bpf da obj tc-example.o sec egress
# tc filter show dev em1 ingress
filter protocol all pref 49152 bpf
filter protocol all pref 49152 bpf handle 0x1 tc-example.o:[ingress] direct-action id 1 tag c5f7825e5dac396f
# tc filter show dev em1 egress
filter protocol all pref 49152 bpf
filter protocol all pref 49152 bpf handle 0x1 tc-example.o:[egress] direct-action id 2 tag b2fd5adc0f262714
# mount | grep bpf
sysfs on /sys/fs/bpf type sysfs (rw,nosuid,nodev,noexec,relatime,seclabel)
bpf on /sys/fs/bpf type bpf (rw,relatime,mode=0700)
# tree /sys/fs/bpf/
/sys/fs/bpf/
+-- ip -> /sys/fs/bpf/tc/
+-- tc
| +-- globals
| +-- acc_map
+-- xdp -> /sys/fs/bpf/tc/
4 directories, 1 file
3)不允许有全局的变量
BPF不允许有全局的变量,但可以通过只包含一个值的BPF_MAP_TYPE_PERCPU_ARRAY
来替代,因为BPF能确保不会被抢占。可以用于尾调或堆栈限制的扩充。
一般,不同BPF程序之间共享状态使用常规BPF map即可。
4)不支持const的字符串和数组
同样是由于loader不支持重定位导致。可通过trace_printk()
helper函数解决 。
static void BPF_FUNC(trace_printk, const char *fmt, int fmt_size, ...);
#ifndef printk
# define printk(fmt, ...) \
({ \
char ____fmt[] = fmt; \
trace_printk(____fmt, sizeof(____fmt), ##__VA_ARGS__); \
})
#endif
上面程序可以通过printk("skb len:%u\n", skb->len);
来使用宏,使用tc exec bpf dbg
来输出。但track_printk()
不推荐生产环境使用:一是由于helper函数最多5个参数,因此track_printk()
可输出的信息只能传递三个参数;二是skb len:%u\n
这种const string在每次调用track_printk()
时都需要重新加载到栈中。(另外track_printk()
是全局的)
推荐使用skb_event_output()
和xdp_event_output()
,你可以自定义event结构,将其存储在perf的事件环形缓冲区中,存储过程是无需锁的,比trace_printk()
更快。
5)使用LLVM内置的方法memset()/memcpy()/memmove()/memcmp()
BPF除了helper函数,一般使用库中的方法需要进行内联。此外,LLVM提供了内置的总会被内联的方法:
#ifndef memset
# define memset(dest, chr, n) __builtin_memset((dest), (chr), (n))
#endif
#ifndef memcpy
# define memcpy(dest, src, n) __builtin_memcpy((dest), (src), (n))
#endif
#ifndef memmove
# define memmove(dest, src, n) __builtin_memmove((dest), (src), (n))
#endif
memcmp()
在部分场景下可能不会被内联,暂时不推荐使用。
6)不支持循环
内核中的BPF验证器会通过深度优先的方式检测所有可能的执行路径,确保没有循环,以保证程序可以终止。
通过#pragma unroll
可以实现有限的循环。(#pragma unroll
的介绍)
#pragma unroll
for (i = 0; i < IPV6_MAX_HEADERS; i++) {
switch (nh) {
case NEXTHDR_NONE:
return DROP_INVALID_EXTHDR;
case NEXTHDR_FRAGMENT:
return DROP_FRAG_NOSUPPORT;
case NEXTHDR_HOP:
case NEXTHDR_ROUTING:
case NEXTHDR_AUTH:
case NEXTHDR_DEST:
if (skb_load_bytes(skb, l3_off + len, &opthdr, sizeof(opthdr)) < 0)
return DROP_INVALID;
nh = opthdr.nexthdr;
if (nh == NEXTHDR_AUTH)
len += ipv6_authlen(&opthdr);
else
len += ipv6_optlen(&opthdr);
break;
default:
*nexthdr = nh;
return len;
}
}
另一种方式是通过尾调同一函数,并使用BPF_MAP_TYPE_PERCPU_ARRAY
来作为局部缓存。但循环上限为34(本身一次调用,加上尾调上限33)。
7)使用尾调对程序进行划分
尾调可以实现多阶段的数据包分析,还可以实现事件通知。比如Cilium在运行时通知数据包的drop事件,通过尾调包含skb_event_output()
方法的函数。
尾调程序的map(BPF_MAP_TYPE_PROG_ARRAY
)可以定义灵活的功能,当尾调超过限制时,会进行fall-through。例如XDP或tc的程序,可以设置如下的程序map:
- 索引0中的程序执行流量采样(traffic sampling)
- 然后跳到索引1执行防火墙策略,然后DROP或者是继续执行索引2
- 索引2执行其他的修改,然后发出数据包上面的程序中,首先是创建了一个prog map
[...] #ifndef __stringify # define __stringify(X) #X #endif #ifndef __section # define __section(NAME) \ __attribute__((section(NAME), used)) #endif #ifndef __section_tail # define __section_tail(ID, KEY) \ __section(__stringify(ID) "/" __stringify(KEY)) #endif #ifndef BPF_FUNC # define BPF_FUNC(NAME, ...) \ (*NAME)(__VA_ARGS__) = (void *)BPF_FUNC_##NAME #endif #define BPF_JMP_MAP_ID 1 static void BPF_FUNC(tail_call, struct __sk_buff *skb, void *map, uint32_t index); struct bpf_elf_map jmp_map __section("maps") = { .type = BPF_MAP_TYPE_PROG_ARRAY, .id = BPF_JMP_MAP_ID, .size_key = sizeof(uint32_t), .size_value = sizeof(uint32_t), .pinning = PIN_GLOBAL_NS, .max_elem = 1, }; __section_tail(JMP_MAP_ID, 0) int looper(struct __sk_buff *skb) { printk("skb cb: %u\n", skb->cb[0]++); tail_call(skb, &jmp_map, 0); return TC_ACT_OK; } __section("prog") int entry(struct __sk_buff *skb) { skb->cb[0] = 0; tail_call(skb, &jmp_map, 0); return TC_ACT_OK; } char __license[] __section("license") = "GPL";
jmp_map
,并pin到global下的jmp_map
中。其次,通过__section_tail()
对looper设置id和key,设置的方式是对section进行特定的命名。可以看到jmp_map
的ID和__section_tail()
对looper()
函数设定的ID一致,都是JMP_MAP_ID
。在entry()
中,进行尾调tail_call
,尾调指定了key为0,正好是looper()
的key。$ llvm-objdump -S --no-show-raw-insn prog_array.o | less prog_array.o: file format ELF64-BPF Disassembly of section 1/0: looper: 0: r6 = r1 1: r2 = *(u32 *)(r6 + 48) 2: r1 = r2 3: r1 += 1 4: *(u32 *)(r6 + 48) = r1 5: r1 = 0 ll 7: call -1 8: r1 = r6 9: r2 = 0 ll 11: r3 = 0 12: call 12 13: r0 = 0 14: exit Disassembly of section prog: entry: 0: r2 = 0 1: *(u32 *)(r1 + 48) = r2 2: r2 = 0 ll 4: r3 = 0 5: call 12 6: r0 = 0 7: exi
注:这里看到尾调都是
call 12
,是因为BPF_FUN_tail_call
的ID为12。include/uapi/linux/bpf.h
/* integer value in 'imm' field of BPF_CALL instruction selects which helper * function eBPF program intends to call */ #define __BPF_ENUM_FN(x) BPF_FUNC_ ## x enum bpf_func_id { __BPF_FUNC_MAPPER(__BPF_ENUM_FN) __BPF_FUNC_MAX_ID, }; #undef __BPF_ENUM_FN
上面的section 1/0
表示是map id为1,key为0的函数,即looper()
。jmp_map
可以被用户空间的应用修改,也能被tc修改。更新是原子操作,比如:
# tc exec bpf graft m:globals/jmp_map key 0 obj new.o sec foo
上面使用了tc的graft
命令,更新了globals/jmp_map
key为0的方法,更新为new.o
中的section foo
。
8)栈空间最大限制为512bytes
BPF程序栈空间的限制为512bytes,但是可以如上面第三条所述的,通过BPF_MAP_TYPE_PERCPU_ARRAY
去扩大栈的空间。
9)可以使用BPF嵌入汇编
LLVM 6.0开始运行BPF程序使用嵌入汇编。比如下面是64位的原子加操作。相关文档在LLVM源码的lib/Target/BPF/BPFInstrInfo.td
以及test/CodeGen/BPF/
中。
#include <linux/bpf.h>
#ifndef __section
# define __section(NAME) \
__attribute__((section(NAME), used))
#endif
__section("prog")
int xdp_test(struct xdp_md *ctx)
{
__u64 a = 2, b = 3, *c = &a;
/* just a toy xadd example to show the syntax */
asm volatile("lock *(u64 *)(%0+0) += %1" : "=r"(c) : "r"(b), "0"(c));
return a;
}
char __license[] __section("license") = "GPL";
上面的程序对应的BPF指令为:
Verifier analysis:
0: (b7) r1 = 2
1: (7b) *(u64 *)(r10 -8) = r1
2: (b7) r1 = 3
3: (bf) r2 = r10
4: (07) r2 += -8
5: (db) lock *(u64 *)(r2 +0) += r1
6: (79) r0 = *(u64 *)(r10 -8)
7: (95) exit
processed 8 insns (limit 131072), stack depth 8
注:Cilium在tail call的实现上使用的是内嵌汇编,同样
call 12
表示BPF_FUN_tail_call
。
static __always_inline __maybe_unused void tail_call_static(const struct __ctx_buff *ctx, const void *map, const __u32 slot) { if (!__builtin_constant_p(slot)) __throw_build_bug(); /* Don't gamble, but _guarantee_ that LLVM won't optimize setting * r2 and r3 from different paths ending up at the same call insn as * otherwise we won't be able to use the jmpq/nopl retpoline-free * patching by the x86-64 JIT in the kernel. * * Note on clobber list: we need to stay in-line with BPF calling * convention, so even if we don't end up using r0, r4, r5, we need * to mark them as clobber so that LLVM doesn't end up using them * before / after the call. */ asm volatile("r1 = %[ctx]\n\t" "r2 = %[map]\n\t" "r3 = %[slot]\n\t" "call 12\n\t" :: [ctx]"r"(ctx), [map]"r"(map), [slot]"i"(slot) : "r0", "r1", "r2", "r3", "r4", "r5"); }
10)使用#pragma取消struct的填充
struct called_info {
u64 start; // 8-byte
u64 end; // 8-byte
u32 sector; // 4-byte
}; // size of 20-byte ?
printf("size of %d-byte\n", sizeof(struct called_info)); // size of 24-byte
// Actual compiled composition of struct called_info
// 0x0(0) 0x8(8)
// ↓________________________↓
// | start (8) |
// |________________________|
// | end (8) |
// |________________________|
// | sector(4) | PADDING | <= address aligned to 8
// |____________|___________| with 4-byte PADDING.
一般为了读取效率,struct会进行对齐填充,会导致BPF验证器的bpf_prog_load()
报错invalid indirect read from stack
。
struct called_info {
u64 start;
u64 end;
u32 sector;
};
struct bpf_map_def SEC("maps") called_info_map = {
.type = BPF_MAP_TYPE_HASH,
.key_size = sizeof(long),
.value_size = sizeof(struct called_info),
.max_entries = 4096,
};
SEC("kprobe/submit_bio")
int submit_bio_entry(struct pt_regs *ctx)
{
char fmt[] = "submit_bio(bio=0x%lx) called: %llu\n";
u64 start_time = bpf_ktime_get_ns();
long bio_ptr = PT_REGS_PARM1(ctx);
struct called_info called_info = {
.start = start_time,
.end = 0,
.bi_sector = 0
};
bpf_map_update_elem(&called_info_map, &bio_ptr, &called_info, BPF_ANY);
bpf_trace_printk(fmt, sizeof(fmt), bio_ptr, start_time);
return 0;
}
// On bpf_load_program
bpf_load_program() err=13
0: (bf) r6 = r1
...
19: (b7) r1 = 0
20: (7b) *(u64 *)(r10 -72) = r1
21: (7b) *(u64 *)(r10 -80) = r7
22: (63) *(u32 *)(r10 -64) = r1
...
30: (85) call bpf_map_update_elem#2
invalid indirect read from stack off -80+20 size 24
比如上面的代码中,bpf_prog_load()
会调用BPF验证器的bpf_check()
,其中会调用 check_func_arg() -> check_stack_boundary()
。由于called_info
被对齐,为24位大小,但报错中可以看到从+20处(实际上就是PADDING
开始的地方)读取是禁止的。check_stack_boundary()
内部循环遍历指针的每个access_size
(24字节),确保其在栈的范围内,并且已初始化。通过#pragma
去除填充:
#pragma pack(4)
struct called_info {
u64 start; // 8-byte
u64 end; // 8-byte
u32 sector; // 4-byte
}; // size of 20-byte ?
printf("size of %d-byte\n", sizeof(struct called_info)); // size of 20-byte
// Actual compiled composition of packed struct called_info
// 0x0(0) 0x8(8)
// ↓________________________↓
// | start (8) |
// |________________________|
// | end (8) |
// |________________________|
// | sector(4) | <= address aligned to 4
// |____________| with no PADDING.
上面的#pragma pack(4)
告诉编译器按照4字节进行对齐。但无PADDING会导致读取性能的下降,一般更好的解决方式是显示的填充。
struct called_info {
u64 start; // 8-byte
u64 end; // 8-byte
u32 sector; // 4-byte
u32 pad; // 4-byte
}; // size of 24-byte ?
printf("size of %d-byte\n", sizeof(struct called_info)); // size of 24-byte
// Actual compiled composition of struct called_info with explicit padding
// 0x0(0) 0x8(8)
// ↓________________________↓
// | start (8) |
// |________________________|
// | end (8) |
// |________________________|
// | sector(4) | pad (4) | <= address aligned to 8
// |____________|___________| with explicit PADDING.
11)通过无效的引用访问包中的数据
BPF helper函数中有部分(例如:bpf_skb_stort_bytes
)可能会改动数据包的大小。由于验证器服务跟踪这种变化,因此会认定之前的包引用为无效引用,因此在包变动后,访问包前需要更新引用。例如:
struct iphdr *ip4 = (struct iphdr *) skb->data + ETH_HLEN;
skb_store_bytes(skb, l3_off + offsetof(struct iphdr, saddr), &new_saddr, 4, 0);
if (ip4->protocol == IPPROTO_TCP) {
// do something
}
由于skb_store_bytes
后skb
发生变化,因此验证器会认定ip4
无效。
R1=pkt_end(id=0,off=0,imm=0) R2=pkt(id=0,off=34,r=34,imm=0) R3=inv0
R6=ctx(id=0,off=0,imm=0) R7=inv(id=0,umax_value=4294967295,var_off=(0x0; 0xffffffff))
R8=inv4294967162 R9=pkt(id=0,off=0,r=34,imm=0) R10=fp0,call_-1
...
18: (85) call bpf_skb_store_bytes#9
19: (7b) *(u64 *)(r10 -56) = r7
R0=inv(id=0) R6=ctx(id=0,off=0,imm=0) R7=inv(id=0,umax_value=2,var_off=(0x0; 0x3))
R8=inv4294967162 R9=inv(id=0) R10=fp0,call_-1 fp-48=mmmm???? fp-56=mmmmmmmm
21: (61) r1 = *(u32 *)(r9 +23)
R9 invalid mem access 'inv'
修复此问题需要在重新更新ip4
:
struct iphdr *ip4 = (struct iphdr *) skb->data + ETH_HLEN;
skb_store_bytes(skb, l3_off + offsetof(struct iphdr, saddr), &new_saddr, 4, 0);
ip4 = (struct iphdr *) skb->data + ETH_HLEN;
if (ip4->protocol == IPPROTO_TCP) {
// do something
}
iproute2
BPF loader有多种工具,例如bbc、perf、iproute2等。perf主要使用内核中的tools/lib/bpf/
库。bbc主要用于track,使用的是嵌有C的python编写BPF,编写逻辑和一般的BPF有细小区别(有专门函数,类似于BPF help函数)。另外还有如内核samples/bpf
下的程序,编译为obj文件后,通过系统调用来加载。
本章主要介绍iproute2对tc、XDP、lwt类型的BPF程序的加载。因为Cilium使用的是iproute2,Cilium后面可能会实现自己原生的loader,但仍会兼容iproute2。支持iproute2的BPF程序都有相同的loader逻辑。
1)加载XDP BPF
下面命令加载prog.o
的BPF到em1
设备上:
# ip link set dev em1 xdp obj prog.o
上面命令未指定section,默认使用prog
section。也可以指定section,比如指定加载foobar
section。
# ip link set dev em1 xdp obj prog.o sec foobar
也可以从.text
section中加载程序,例如下面为指定__section()
。
#include <linux/bpf.h>
#ifndef __section
# define __section(NAME) \
__attribute__((section(NAME), used))
#endif
int xdp_drop(struct xdp_md *ctx)
{
return XDP_DROP;
}
char __license[] __section("license") = "GPL";
可以通过如下方式加载:
# ip link set dev em1 xdp obj prog.o sec .text
默认情况下,通过ip
加载XDP到某个已加载XDP程序的设备上时,会报错。需要使用-force
选项进行替换。
# ip -force link set dev em1 xdp obj prog.o
大多数支持XDP的驱动在替换XDP时都是原子操作,不会中断流量。一个设备只会加载一个XDP,但你可以通过尾调或是BPF2BPF来实现复杂的逻辑。
通过ip link | grep xdp
可以看到设备上是否有XDP程序,通过ip -d link
以及bpftool
可以获取更多的信息。
关闭XDP程序:
# ip link set dev em1 xdp off
在non-XDP与native XDP切换过程中,设备会重新配置接收rings,确保接收的包线性的存储在单个页面,以供BPF程序读写。一旦配置完后,后续XDP程序的替换就是原子操作。
在iproute2中支持的XDP模式为xdpdrv
、xdpoffload
、xdpgeneric
。xdpdrv
需要驱动支持,其处理点在驱动的接收路径上,是软件层面能达到的最早处理点。
通过
lspci -vvv
可以查看网卡的驱动。比如我的驱动是e1000e
00:1f.6 Ethernet controller: Intel Corporation Ethernet Connection (7) I219-LM (rev 10) ... Kernel driver in use: e1000e Kernel modules: e1000e
在https://github.com/iovisor/bcc/blob/master/docs/kernel-versions.md 中可以看到e1000e
目前不支持XDP。
xdpgeneric
是针对不支持XDP的驱动的一种实验性模式。xdpgeneric
的hook点在包作为skb
进入协议栈后,因此性能不如xdpdrv
。一般xdpgeneric
只作为实验场景,不作为生产场景。xdpoffload
需要驱动支持SmartNICs
,例如Netronome
的nfp驱动。这种模式会将BPF程序加载到硬件上,由硬件处理数据包,并且xdpoffload
中,并非所有的BPF map和BPF helper函数都是支持的,如果使用了不支持的BPF功能,BPF验证器会拒绝并指出。
在通过ip link set dev em1 xdp obj [...]
命令进行XDP加载时,会先尝试xdpdrv
模式,如果驱动不支持,则使用xdpgeneric
模式。显示的使用xdpdrv
代替xdp
,会在驱动不支持时,直接报错,而避免使用xdpgeneric
模式。
下面的例子中,强制以xdpdrv
加载XDP程序,查看详情,卸载。
# ip -force link set dev em1 xdpdrv obj prog.o
# ip link show
[...]
6: em1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 xdp qdisc mq state UP mode DORMANT group default qlen 1000
link/ether be:08:4d:b6:85:65 brd ff:ff:ff:ff:ff:ff
prog/xdp id 1 tag 57cd311f2e27366b
[...]
# ip link set dev em1 xdpdrv off
下面的例子中,强制以xdpgeneric
加载XDP,查看详情,获取汇编指令,卸载。
# ip -force link set dev em1 xdpgeneric obj prog.o
# ip link show
[...]
6: em1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 xdpgeneric qdisc mq state UP mode DORMANT group default qlen 1000
link/ether be:08:4d:b6:85:65 brd ff:ff:ff:ff:ff:ff
prog/xdp id 4 tag 57cd311f2e27366b <-- BPF program ID 4
[...]
# bpftool prog dump xlated id 4 <-- Dump of instructions running on em1
0: (b7) r0 = 1
1: (95) exit
# ip link set dev em1 xdpgeneric off
# ip -force link set dev em1 xdpoffload obj prog.o
# ip link show
[...]
6: em1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 xdpoffload qdisc mq state UP mode DORMANT group default qlen 1000
link/ether be:08:4d:b6:85:65 brd ff:ff:ff:ff:ff:ff
prog/xdp id 8 tag 57cd311f2e27366b
[...]
# bpftool prog show id 8
8: xdp tag 57cd311f2e27366b dev em1 <-- Also indicates a BPF program offloaded to em1
loaded_at Apr 11/20:38 uid 0
xlated 16B not jited memlock 4096B
# ip link set dev em1 xdpoffload off
不同模式之间是不允许直接进行切换的,需要关闭之前的模式。
# ip -force link set dev em1 xdpgeneric obj prog.o
# ip -force link set dev em1 xdpoffload obj prog.o
RTNETLINK answers: File exists
# ip -force link set dev em1 xdpdrv obj prog.o
RTNETLINK answers: File exists
# ip -force link set dev em1 xdpgeneric obj prog.o <-- Succeeds due to xdpgeneric
#
# ip -force link set dev em1 xdpgeneric obj prog.o
# ip -force link set dev em1 xdpgeneric off
# ip -force link set dev em1 xdpoffload obj prog.o
# ip l
[...]
6: em1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 xdpoffload qdisc mq state UP mode DORMANT group default qlen 1000
link/ether be:08:4d:b6:85:65 brd ff:ff:ff:ff:ff:ff
prog/xdp id 17 tag 57cd311f2e27366b
[...]
# ip -force link set dev em1 xdpoffload off
2)加载tc BPF obj文件
tc BPF程序的加载不依赖驱动的支持:
# tc qdisc add dev em1 clsact
# tc filter add dev em1 ingress bpf da obj prog.o
clsact
不进行排队,只保存分类或操作,clsact
提供ingress
和egress
两个hooks。ingress
hook在__netif_receive_skb_core() -> sch_handle_ingress()
中调用,egress
hook在__dev_queue_xmit() -> sch_handle_egress()
中调用。添加egress
:
# tc filter add dev em1 egress bpf da obj prog.o
clsact
还可以挂载在虚拟的、无队列的设备上,例如容器的veth
设备。tc filter
使用da
(direct-action)模式加载BPF tc程序,因为BPF程序会处理数据包的转发或其他操作,不需要tc action模块。
section的指定类似于XDP:
# tc filter add dev em1 egress bpf da obj prog.o sec foobar
列出加载的tc程序:
# tc filter show dev em1 ingress
filter protocol all pref 49152 bpf
filter protocol all pref 49152 bpf handle 0x1 prog.o:[ingress] direct-action id 1 tag c5f7825e5dac396f
# tc filter show dev em1 egress
filter protocol all pref 49152 bpf
filter protocol all pref 49152 bpf handle 0x1 prog.o:[egress] direct-action id 2 tag b2fd5adc0f262714
prog.o:[ingress] direct-action
表示程序加载的是ingress section,使用da模式。后面的id
是tc程序的唯一标识符,可以与bpftool
一起使用来dump tc程序信息。tag
是tc程序指令编码的hash,可以与perf
的输出或obj文件进行对应。
虽然可以通过链式加载多个tc程序,但是一般使用一个BPF即可,因为da模式下,BPF程序本身就能返回tc active,比如TC_ACT_OK
、TC_ACT_SHOT
等。
上面的perf 49152
与handle 0x1
如果没显示的指定,就是自动生成的。perf
表示优先级的编号,如果附加了多个分类器,则按照升序来依次执行,hanler
用于表示在同一perf
下的某个分类器的多个实例。这两个值仅建议在计划自动替换BPF程序的时候指定,例如:
# tc filter add dev em1 ingress pref 1 handle 1 bpf da obj prog.o sec foobar
# tc filter show dev em1 ingress
filter protocol all pref 1 bpf
filter protocol all pref 1 bpf handle 0x1 prog.o:[foobar] direct-action id 1 tag c5f7825e5dac396f
原子的替换之前的tc:
# tc filter replace dev em1 ingress pref 1 handle 1 bpf da obj prog.o sec foobar
删除所有的tc程序:
# tc filter del dev em1 ingress
# tc filter del dev em1 egress
也可以通过删除clsact
qdisc,一次性删除所有的tc:
# tc qdisc del dev em1 clsact
如果网卡支持,tc也可以像XDP一样进行offload:
# tc qdisc add dev em1 clsact
# tc filter replace dev em1 ingress pref 1 handle 1 bpf skip_sw da obj prog.o
Error: TC offload is disabled on net device.
We have an error talking to the kernel
上面的报错表示需要开启设备的offload:
# ethtool -K em1 hw-tc-offload on
# tc qdisc add dev em1 clsact
# tc filter replace dev em1 ingress pref 1 handle 1 bpf skip_sw da obj prog.o
# tc filter show dev em1 ingress
filter protocol all pref 1 bpf
filter protocol all pref 1 bpf handle 0x1 prog.o:[classifier] direct-action skip_sw in_hw id 19 tag 57cd311f2e27366b
in_hw
表示tc程序已经offload到NIC上。另外,XDP与tc不能同时使用offload。
3)通过netdevsim测试BPF offload
netdevsim作为内核一部分实现了XDP与tc的offload接口,可以用于测试。
创建一个netdevsim设备:
# modprobe netdevsim
// [ID] [PORT_COUNT]
# echo "1 1" > /sys/bus/netdevsim/new_device
# devlink dev
netdevsim/netdevsim1
# devlink port
netdevsim/netdevsim1/0: type eth netdev eth0 flavour physical
# ip l
[...]
4: eth0: <BROADCAST,NOARP,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
link/ether 2a:d5:cd:08:d1:3f brd ff:ff:ff:ff:ff:ff
然后可以将XDP或tc offload到此设备上:
# ip -force link set dev eth0 xdpoffload obj prog.o
# ip l
[...]
4: eth0: <BROADCAST,NOARP,UP,LOWER_UP> mtu 1500 xdpoffload qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
link/ether 2a:d5:cd:08:d1:3f brd ff:ff:ff:ff:ff:ff
prog/xdp id 16 tag a04f5eef06a7f555
BPF的loader操作除了上面的命令外,还有一些高级选项,下面以XDP进行介绍,tc同样可用。
1)输出日志
通过verb
选项,可以输出验证器验证成功的日志或是验证失败的日志。
# ip link set dev em1 xdp obj xdp-example.o verb
Prog section 'prog' loaded (5)!
- Type: 6
- Instructions: 2 (0 over limit)
- License: GPL
Verifier analysis:
0: (b7) r0 = 1
1: (95) exit
processed 2 insns
2)加载BPF文件系统上的程序
上面加载都是使用的obj文件,可通过如下命令加载BPF文件系统上的程序:
# ip link set dev em1 xdp pinned /sys/fs/bpf/prog
也可以使用简写:
# ip link set dev em1 xdp pinned m:prog
当加载BPF程序时,iproute2会自动检测是否有BPF文件系统的实例(为了pinning BPF program或是BPF map),一旦未找BPF文件系统,就是自动在/sys/fs/bpf
下mount一个。如果找到,则不会在进行mount,例如:
# mkdir /var/run/bpf
# mount --bind /var/run/bpf /var/run/bpf
# mount -t bpf bpf /var/run/bpf
# tc filter add dev em1 ingress bpf da obj tc-example.o sec prog
# tree /var/run/bpf
/var/run/bpf
+-- ip -> /run/bpf/tc/
+-- tc
| +-- globals
| +-- jmp_map
+-- xdp -> /run/bpf/tc/
4 directories, 1 file
默认情况下,load tc程序将创建如上所示的初始化目录,各个子系统(XDP等)的目录通过链接的方式指向相同的globals空间,以便于BPF map在不同类型的BPF程序之间共享。但如果文件系统目录已经被创建,则loader不会重新修改已有的目录结构,这样可以分离lwt
、tc
和XDP
的BPF map。
iproute2在安装时会安装头文件,可以在BPF程序中引用:
#include <iproute2/bpf_elf.h>
头文件提供了map的API以及BPF程序默认的section,iproute2中通过struct bpf_elf_map
定义map。iproute2解析BPF obj文件的过程:
- 遍历所有section,获取
map
与license
。 - 验证
map
,并创建map对象。如果BPF文件系统中已存在pinned map,则使用,否则,创建新的map,并pin到BPF文件系统中。 - 处理包含ELF重定向(这里的重定向只包含map操作的)的section,将map的文件描述符编码为立即数。
- 通过系统调用创建BPF程序,如果提供了尾调的program map,更新其中程序的文件描述符。
bpftool
bpftool是在内核tools/bpf/bpftool/
下提供的debug工具。作用:
- dump已加载的BPF程序或map的信息,dump BPF程序使用的map信息。
- dump BPF map的键值对,查询、删除、修改某个键值对,查询某个key的下一个key。
- 可以通过BPF程序或map的ID进行操作,也可以指定pin到的文件系统路径来操作。
- pin BPF程序或map到某个文件系统上。
查询概况
查看当前机器上所有的BPF程序概况:
# bpftool prog
398: sched_cls tag 56207908be8ad877
loaded_at Apr 09/16:24 uid 0
xlated 8800B jited 6184B memlock 12288B map_ids 18,5,17,14
399: sched_cls tag abc95fb4835a6ec9
loaded_at Apr 09/16:24 uid 0
xlated 344B jited 223B memlock 4096B map_ids 18
400: sched_cls tag afd2e542b30ff3ec
loaded_at Apr 09/16:24 uid 0
xlated 1720B jited 1001B memlock 4096B map_ids 17
401: sched_cls tag 2dbbd74ee5d51cc8
loaded_at Apr 09/16:24 uid 0
xlated 3728B jited 2099B memlock 4096B map_ids 17
[...]
查询map概况:
# bpftool map
5: hash flags 0x0
key 20B value 112B max_entries 65535 memlock 13111296B
6: hash flags 0x0
key 20B value 20B max_entries 65536 memlock 7344128B
7: hash flags 0x0
key 10B value 16B max_entries 8192 memlock 790528B
8: hash flags 0x0
key 22B value 28B max_entries 8192 memlock 987136B
9: hash flags 0x0
key 20B value 8B max_entries 512000 memlock 49352704B
[...]
可以添加参数--json
来输出json格式,参数--pretty
来翻译为可读的信息。
# bpftool prog --json --pretty
dump BPF程序
dump已经验证的BPF程序的image,先通过tc获取信息:
# tc filter show dev cilium_host egress
filter protocol all pref 1 bpf chain 0
filter protocol all pref 1 bpf chain 0 handle 0x1 bpf_host.o:[from-netdev] \
direct-action not_in_hw id 406 tag e0362f5bd9163a0a jited
可以看出,BPF程序来着bpf_host.o
文件,section为from-netdev
,ID为406
。使用bpftool获取更多元信息:
# bpftool prog show id 406
406: sched_cls tag e0362f5bd9163a0a
loaded_at Apr 09/16:24 uid 0
xlated 11144B jited 7721B memlock 12288B map_ids 18,20,8,5,6,14
可以看到BPF程序类型为sched_cls
(BPF_PROG_TYPE_SCHED_CLS
),器tag为e0362f5bd9163a0a
,由uid 0(root
)在Apr 09/16:24
加载,BPF指令大小为11144B
,JIT编译后的image为7721B
,程序本身(不包括map)占用了12288B
的内存空间,在map方面,使用了id为18,20,8,5,6,14
的这几个BPF map。
dump BPF程序的指令(输出对BPF map和BPF helper做了专门的注释):
# bpftool prog dump xlated id 406
0: (b7) r7 = 0
1: (63) *(u32 *)(r1 +60) = r7
2: (63) *(u32 *)(r1 +56) = r7
3: (63) *(u32 *)(r1 +52) = r7
[...]
47: (bf) r4 = r10
48: (07) r4 += -40
49: (79) r6 = *(u64 *)(r10 -104)
50: (bf) r1 = r6
51: (18) r2 = map[id:18] <-- BPF map id 18
53: (b7) r5 = 32
54: (85) call bpf_skb_event_output#5656112 <-- BPF helper call
55: (69) r1 = *(u16 *)(r6 +192)
[...]
交叉输出指令对应的opcodes的编码:
# bpftool prog dump xlated id 406 opcodes
0: (b7) r7 = 0
b7 07 00 00 00 00 00 00
1: (63) *(u32 *)(r1 +60) = r7
63 71 3c 00 00 00 00 00
2: (63) *(u32 *)(r1 +56) = r7
63 71 38 00 00 00 00 00
3: (63) *(u32 *)(r1 +52) = r7
63 71 34 00 00 00 00 00
4: (63) *(u32 *)(r1 +48) = r7
63 71 30 00 00 00 00 00
5: (63) *(u32 *)(r1 +64) = r7
63 71 40 00 00 00 00 00
[...]
dump JIT编译过的image汇编信息:
# bpftool prog dump jited id 406
0: push %rbp
1: mov %rsp,%rbp
4: sub $0x228,%rsp
b: sub $0x28,%rbp
f: mov %rbx,0x0(%rbp)
13: mov %r13,0x8(%rbp)
17: mov %r14,0x10(%rbp)
1b: mov %r15,0x18(%rbp)
1f: xor %eax,%eax
21: mov %rax,0x20(%rbp)
25: mov 0x80(%rdi),%r9d
[...]
交叉输出opcodes的编码:
# bpftool prog dump jited id 406 opcodes
0: push %rbp
55
1: mov %rsp,%rbp
48 89 e5
4: sub $0x228,%rsp
48 81 ec 28 02 00 00
b: sub $0x28,%rbp
48 83 ed 28
f: mov %rbx,0x0(%rbp)
48 89 5d 00
13: mov %r13,0x8(%rbp)
4c 89 6d 08
17: mov %r14,0x10(%rbp)
4c 89 75 10
1b: mov %r15,0x18(%rbp)
4c 89 7d 18
[...]
可以通过bpftool与graphviz进行指令的可视化,生成相应的png:
# bpftool prog dump xlated id 406 visual &> output.dot
$ dot -Tpng output.dot -o output.png
需要注意的是xlated
是验证器验证后的程序,因此dump的指令与通过解释器的BPF指令一样。在内核中,验证器会对BPF loader提供的原生指令进行重写。比如对内联的BPF helper函数进行重写,以map lookup为例:
# bpftool prog dump xlated id 3
0: (b7) r1 = 2
1: (63) *(u32 *)(r10 -4) = r1
2: (bf) r2 = r10
3: (07) r2 += -4
4: (18) r1 = map[id:2] <-- BPF map id 2
6: (85) call __htab_map_lookup_elem#77408 <-+ BPF helper inlined rewrite
7: (15) if r0 == 0x0 goto pc+2 |
8: (07) r0 += 56 |
9: (79) r0 = *(u64 *)(r0 +0) <-+
10: (15) if r0 == 0x0 goto pc+24
11: (bf) r2 = r10
12: (07) r2 += -4
[...]
bpftool通过kallsyms来关联bpf helper函数以及bpf2bpf,因此需要确保如下参数开启:
# echo 0 > /proc/sys/kernel/kptr_restrict
# echo 1 > /proc/sys/net/core/bpf_jit_kallsyms
对于BPF2BPF,JIT编译后的dump信息,与使用解释器的dump信息是一样的,只不过JIT中使用tag来代替请求的目标函数,如下下面,pc+2
表示子程序的偏移量:
# bpftool prog dump xlated id 1
0: (85) call pc+2#__bpf_prog_run_args32
1: (b7) r0 = 1
2: (95) exit
3: (b7) r0 = 2
4: (95) exit
上面是通过解释器加载的BPF程序,下面是通过JIT编译后的:
# bpftool prog dump xlated id 1
0: (85) call pc+2#bpf_prog_3b185187f1855c4c_F
1: (b7) r0 = 1
2: (95) exit
3: (b7) r0 = 2
4: (95) exit
对于尾调,与BPF helper函数类似,会映射为一条call指令:
# bpftool prog dump xlated id 2
[...]
10: (b7) r2 = 8
11: (85) call bpf_trace_printk#-41312
12: (bf) r1 = r6
13: (18) r2 = map[id:1]
15: (b7) r3 = 0
16: (85) call bpf_tail_call#12
17: (b7) r1 = 42
18: (6b) *(u16 *)(r6 +46) = r1
19: (b7) r0 = 0
20: (95) exit
# bpftool map show id 1
1: prog_array flags 0x0
key 4B value 4B max_entries 1 memlock 4096B
dump BPF map
可以通过map dump
子命令来dump map中的所有数据:
# bpftool map dump id 5
key:
f0 0d 00 00 00 00 00 00 0a 66 00 00 00 00 8a d6
02 00 00 00
value:
00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
key:
0a 66 1c ee 00 00 00 00 00 00 00 00 00 00 00 00
01 00 00 00
value:
00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
[...]
Found 6 elements
结合BTF,还能输出map的结构体。例如结合BTF与iproute2中的BPF_ANNOTATE_KV_PAIR()
宏:
程序源码:
# cat tools/testing/selftests/bpf/test_xdp_noinline.c
[...]
struct ctl_value {
union {
__u64 value;
__u32 ifindex;
__u8 mac[6];
};
};
struct bpf_map_def __attribute__ ((section("maps"), used)) ctl_array = {
.type = BPF_MAP_TYPE_ARRAY,
.key_size = sizeof(__u32),
.value_size = sizeof(struct ctl_value),
.max_entries = 16,
.map_flags = 0,
};
BPF_ANNOTATE_KV_PAIR(ctl_array, __u32, struct ctl_value);
[...]
BPF_ANNOTATE_KV_PAIR()
宏用于生成一个map专用的ELF section,其中保存了一组空的键值对。通过这个section,iproute2可以与BTF中的数据对应起来,从而从BTF中选择相应的类型,来进行映射输出。
通过clang进行编译,并通过pahole生成BTF信息:
# clang [...] -O2 -target bpf -g -emit-llvm -c test_xdp_noinline.c -o - |
llc -march=bpf -mcpu=probe -mattr=dwarfris -filetype=obj -o test_xdp_noinline.o
# pahole -J test_xdp_noinline.o
加载到内核中,然后dump map:
# ip -force link set dev lo xdp obj test_xdp_noinline.o sec xdp-test
# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 xdpgeneric/id:227 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
[...]
# bpftool prog show id 227
227: xdp tag a85e060c275c5616 gpl
loaded_at 2018-07-17T14:41:29+0000 uid 0
xlated 8152B not jited memlock 12288B map_ids 381,385,386,382,384,383
# bpftool map dump id 386
[{
"key": 0,
"value": {
"": {
"value": 0,
"ifindex": 0,
"mac": []
}
}
},{
"key": 1,
"value": {
"": {
"value": 0,
"ifindex": 0,
"mac": []
}
}
},{
[...]
还可以通过bpftool对map指定key进行查询、更改或删除。如果BPF程序成功添加了BTF信息,在prog show
中可以查看到btf_id
:
# bpftool prog show id 72
72: xdp name balancer_ingres tag acf44cabb48385ed gpl
loaded_at 2020-04-13T23:12:08+0900 uid 0
xlated 19104B jited 10732B memlock 20480B map_ids 126,130,131,127,129,128
btf_id 60
通过btf show
命令也能查看:
# bpftool btf show
60: size 12243B prog_ids 72 map_ids 126,130,131,127,129,128
通过btf dump
可以输出btf的信息,可以是c或是raw:
# bpftool btf dump id 60 format c
[...]
struct ctl_value {
union {
__u64 value;
__u32 ifindex;
__u8 mac[6];
};
};
typedef unsigned int u32;
[...]
BPF sysctls
BPF相关的内核参数:
/proc/sys/net/core/bpf_jit_enable
:开启或关闭BPF JIT
Value | Description |
---|---|
0 | 关闭JIT,使用内核解释器(默认值) |
1 | 开启JIT |
2 | 开启JIT,并产生debug信息到内核日志中 |
当设置为2时,可以使用bpf_jit_disasm 进行调试。 |
/proc/sys/net/core/bpf_jit_harden
:开启或关闭BPF JIT加固。注意开启后会损耗性能,并且会盲化BPF中的立即数。对于通过解释器处理的BPF程序,无需加固。
Value | Description |
---|---|
0 | 关闭JIT加固(默认值) |
1 | 对非特权用户启用JIT加固 |
2 | 对所有用户启动JIT加固 |
/proc/sys/net/core/bpf_jit_kallsyms
:控制是否将JIT编译的BPF程序作为内核符号输出到/proc/kallsyms
中,以便与perf
一起使用,或是提供堆栈展开的功能。符号名称包含BPF程序的tag(bpf_prog_<tag>
)。如果开启bpf_jit_harden
,则此特性需要关闭:
Value | Description |
---|---|
0 | 关闭JIT的kallsyms输出(默认值) |
1 | 对特权用户开启JIT的kallsyms输出 |
/proc/sys/kernel/unprivileged_bpf_disabled
:开启或关闭非特权用户的bpf(2)
系统调用。默认是开启了非特权用户的使用的,这个值是一次性开关,一旦切换,会永久禁用,除非重启。开关不影响其他使用非bpf(2)
进行加载的BPF程序,比如seccomp和传统套接字过滤。
Value | Description |
---|---|
0 | 允许非特权用户进行bpf系统调用 (默认值) |
1 | 禁止非特权用户进行bpf系统调用 |
内核测试
内核提供了BPF的自测,在tools/testing/selftests/bpf/
中:
$ cd tools/testing/selftests/bpf/
$ make
# make run_tests
测试涉及:
- BPF验证器
- BPF程序tags
- 各种BPF map接口与各类型的BPF map
- 检测LLVM后端的C代码
- 检测解释器与JIT的eBPF与cBPF汇编代码
JIT debug
通过修改bpf_jit_enable
可以将每次编译的JIT image信息发送到内核日志中:
# echo 2 > /proc/sys/net/core/bpf_jit_enable
当BPF程序load后,可以通过dmesg
输出:
[ 3389.935842] flen=6 proglen=70 pass=3 image=ffffffffa0069c8f from=tcpdump pid=20583
[ 3389.935847] JIT code: 00000000: 55 48 89 e5 48 83 ec 60 48 89 5d f8 44 8b 4f 68
[ 3389.935849] JIT code: 00000010: 44 2b 4f 6c 4c 8b 87 d8 00 00 00 be 0c 00 00 00
[ 3389.935850] JIT code: 00000020: e8 1d 94 ff e0 3d 00 08 00 00 75 16 be 17 00 00
[ 3389.935851] JIT code: 00000030: 00 e8 28 94 ff e0 83 f8 01 75 07 b8 ff ff 00 00
[ 3389.935852] JIT code: 00000040: eb 02 31 c0 c9 c3
flen
表示程序BFP指令的数量(6条BPF指令),proglen
表示JIT生成image的大小(70字节),pass=3
表示image经过了3个编译器passes(例如x86_64
中为了减小image的大小,会有多种进行优化的passes)。image
表示JIT image的地址,from
与pid
表示触发此次编译的用户空间的程序与PID。下面的JIT code可以使用tools/bpf/
中的bpf_jit_disasm
来反汇编。
# ./bpf_jit_disasm
70 bytes emitted from JIT compiler (pass:3, flen:6)
ffffffffa0069c8f + <x>:
0: push %rbp
1: mov %rsp,%rbp
4: sub $0x60,%rsp
8: mov %rbx,-0x8(%rbp)
c: mov 0x68(%rdi),%r9d
10: sub 0x6c(%rdi),%r9d
14: mov 0xd8(%rdi),%r8
1b: mov $0xc,%esi
20: callq 0xffffffffe0ff9442
25: cmp $0x800,%eax
2a: jne 0x0000000000000042
2c: mov $0x17,%esi
31: callq 0xffffffffe0ff945e
36: cmp $0x1,%eax
39: jne 0x0000000000000042
3b: mov $0xffff,%eax
40: jmp 0x0000000000000044
42: xor %eax,%eax
44: leaveq
45: retq
还可以插上opcode:
# ./bpf_jit_disasm -o
70 bytes emitted from JIT compiler (pass:3, flen:6)
ffffffffa0069c8f + <x>:
0: push %rbp
55
1: mov %rsp,%rbp
48 89 e5
4: sub $0x60,%rsp
48 83 ec 60
8: mov %rbx,-0x8(%rbp)
48 89 5d f8
c: mov 0x68(%rdi),%r9d
44 8b 4f 68
10: sub 0x6c(%rdi),%r9d
44 2b 4f 6c
14: mov 0xd8(%rdi),%r8
4c 8b 87 d8 00 00 00
1b: mov $0xc,%esi
be 0c 00 00 00
20: callq 0xffffffffe0ff9442
e8 1d 94 ff e0
25: cmp $0x800,%eax
3d 00 08 00 00
2a: jne 0x0000000000000042
75 16
2c: mov $0x17,%esi
be 17 00 00 00
31: callq 0xffffffffe0ff945e
e8 28 94 ff e0
36: cmp $0x1,%eax
83 f8 01
39: jne 0x0000000000000042
75 07
3b: mov $0xffff,%eax
b8 ff ff 00 00
40: jmp 0x0000000000000044
eb 02
42: xor %eax,%eax
31 c0
44: leaveq
c9
45: retq
c3
最新的bpftool
已经可以通过指定BPF程序的ID来dump JITed BPF程序(上面bpftool中介绍了)。
可以使用perf
对JITed BPF程序进行性能分析,需要kallsyms信息的支持:
# echo 1 > /proc/sys/net/core/bpf_jit_enable
# echo 1 > /proc/sys/net/core/bpf_jit_kallsyms
切换bpf_jit_kallsyms
不需要重新加载BPF程序。下面的例子中:perf记录bpf_clone_redirect()
helper函数中的分配失败。因为直接写,bpf_clone_redirect()
中调用的bpf_try_make_head_writable()
会失败,然后会释放克隆的skb
,并返回错。perf
记录了所有的kfree_skb
事件。
# tc qdisc add dev em1 clsact
# tc filter add dev em1 ingress bpf da obj prog.o sec main
# tc filter show dev em1 ingress
filter protocol all pref 49152 bpf
filter protocol all pref 49152 bpf handle 0x1 prog.o:[main] direct-action id 1 tag 8227addf251b7543
# cat /proc/kallsyms
[...]
ffffffffc00349e0 t fjes_hw_init_command_registers [fjes]
ffffffffc003e2e0 d __tracepoint_fjes_hw_stop_debug_err [fjes]
ffffffffc0036190 t fjes_hw_epbuf_tx_pkt_send [fjes]
ffffffffc004b000 t bpf_prog_8227addf251b7543
# perf record -a -g -e skb:kfree_skb sleep 60
# perf script --kallsyms=/proc/kallsyms
[...]
ksoftirqd/0 6 [000] 1004.578402: skb:kfree_skb: skbaddr=0xffff9d4161f20a00 protocol=2048 location=0xffffffffc004b52c
7fffb8745961 bpf_clone_redirect (/lib/modules/4.10.0+/build/vmlinux)
7fffc004e52c bpf_prog_8227addf251b7543 (/lib/modules/4.10.0+/build/vmlinux)
7fffc05b6283 cls_bpf_classify (/lib/modules/4.10.0+/build/vmlinux)
7fffb875957a tc_classify (/lib/modules/4.10.0+/build/vmlinux)
7fffb8729840 __netif_receive_skb_core (/lib/modules/4.10.0+/build/vmlinux)
7fffb8729e38 __netif_receive_skb (/lib/modules/4.10.0+/build/vmlinux)
7fffb872ae05 process_backlog (/lib/modules/4.10.0+/build/vmlinux)
7fffb872a43e net_rx_action (/lib/modules/4.10.0+/build/vmlinux)
7fffb886176c __do_softirq (/lib/modules/4.10.0+/build/vmlinux)
7fffb80ac5b9 run_ksoftirqd (/lib/modules/4.10.0+/build/vmlinux)
7fffb80ca7fa smpboot_thread_fn (/lib/modules/4.10.0+/build/vmlinux)
7fffb80c6831 kthread (/lib/modules/4.10.0+/build/vmlinux)
7fffb885e09c ret_from_fork (/lib/modules/4.10.0+/build/vmlinux)
从上面perf的输出可以看出,bpf_prog_8227addf251b7543
是调用栈中的一层,表示tag为8227addf251b7543
的BPF程序与kfree_skb
事件相关。
内省(Introspection)
内核提供了关于BPF和XDP的各种跟踪点,可以用于内省。BPF的跟踪点:
# perf list | grep bpf:
bpf:bpf_map_create [Tracepoint event]
bpf:bpf_map_delete_elem [Tracepoint event]
bpf:bpf_map_lookup_elem [Tracepoint event]
bpf:bpf_map_next_key [Tracepoint event]
bpf:bpf_map_update_elem [Tracepoint event]
bpf:bpf_obj_get_map [Tracepoint event]
bpf:bpf_obj_get_prog [Tracepoint event]
bpf:bpf_obj_pin_map [Tracepoint event]
bpf:bpf_obj_pin_prog [Tracepoint event]
bpf:bpf_prog_get_type [Tracepoint event]
bpf:bpf_prog_load [Tracepoint event]
bpf:bpf_prog_put_rcu [Tracepoint event]
比如使用perf(这里可以用tc程序代替sleep)
# perf record -a -e bpf:* sleep 10
# perf script
sock_example 6197 [005] 283.980322: bpf:bpf_map_create: map type=ARRAY ufd=4 key=4 val=8 max=256 flags=0
sock_example 6197 [005] 283.980721: bpf:bpf_prog_load: prog=a5ea8fa30ea6849c type=SOCKET_FILTER ufd=5
sock_example 6197 [005] 283.988423: bpf:bpf_prog_get_type: prog=a5ea8fa30ea6849c type=SOCKET_FILTER
sock_example 6197 [005] 283.988443: bpf:bpf_map_lookup_elem: map type=ARRAY ufd=4 key=[06 00 00 00] val=[00 00 00 00 00 00 00 00]
[...]
sock_example 6197 [005] 288.990868: bpf:bpf_map_lookup_elem: map type=ARRAY ufd=4 key=[01 00 00 00] val=[14 00 00 00 00 00 00 00]
swapper 0 [005] 289.338243: bpf:bpf_prog_put_rcu: prog=a5ea8fa30ea6849c type=SOCKET_FILTER
对于BPF程序,会显示单独的程序tag。
perf还能捕获XDP引发的异常事件:
# perf list | grep xdp:
xdp:xdp_exception [Tracepoint event]
异常触发的场景包括:
- BPF程序返回了无效的XDP action code
- BPF程序返回
XDP_ABORTED
表示为能优雅退出 - BPF程序返回
XDP_TX
,但传输过程中出现错误,比如端口未运行、分配失败、传输ring已满等
还可以将BPF程序加载到一个或多个跟踪点(tracepoints),从而进一步收集信息存储在map中,或是通过bpf_pert_event_output()
helper函数来发送到用户空间。
跟踪管道(tracing pipe)
BPF可以使用bpf_trace_printk()
将输出发送到内核的tracing pipe中,用户可以通过如下命令获取其中的内容:
# tail -f /sys/kernel/debug/tracing/trace_pipe
...
bpf_trace_printk()
方法是全局的,并且操作是加锁的,生产环境一般使用。
其他
与perf类似,BPF程序和map也是受RLIMIT_MEMLOCK
限制的,可以通过ulimit -l
查看当前可用的系统页数量。
默认的限制可能不足以加载复杂的BPF程序或是较大的BPF map,会导致BPF系统调用返回EPERM
的errno
。可以设置更大的limit或通过RLIMIT_MEMLOCK
取消限制。RLIMIT_MEMLOCK
通常是针对非特权用户的,一般可以为特权用户设置更高的限制。
程序类型
目前,BPF共有18种程序类型,其中网络主要是两类:XDP与tc。
XDP
XDP是eXpress Data Path的缩写,处理点在软件层面能达到的最早的地方,即驱动收到数据包的时候。因此,XDP处理的数据包未进行skb
的构建,也未经过GRO的处理。
XDP将数据留在内核中处理的优点(相比于DPDK):
- XDP可以利用上游的内核网络驱动程序、用户空间的工具,以及BPF helper函数(可以访问路由表、socket这些内核网络基础设施)。
- 由于在内核中,因此可以和内核其余模块一样,使用类似的安全模型来访问硬件设备。
- 无需进行内核与用户空间的跨越,另外由于处理的数据包不出内核,可以灵活地转发到另外的Namespace中。
- 可以从XDP中向内核转发数据包,利用内核的TCP/IP协议栈。
- 完全的可编程,并且ABI的稳定性可以得到保证。相比于一般的内核模块,由于使用了BPF验证器,安全更能得到保证。
- 可以在运行时原子的替换BPF程序,而不中断流量。
- XDP可以灵活得到集成到内核中,比如可以运行在“busy polling”与”interrupt driven”两种模式 (前者轮询,后者中断),不需要占用单独的CPU,也不需要特殊的硬件支持或是hugepages。
- 无需第三方的内核模块或许可。
- 主流发行版中,内核4.8+的版本都支持XDP,支持主流的万兆网络驱动。
作为驱动程序中运行的BPF,XDP确保数据包以线性的方式存储在单个DMA页上,以供BPF程序读写。XDP还能使用256字节的额外的headroom,可以通过bpf_xdp_adjust_head()
实现自定义的数据包封装,或是通过bpf_xdp_adjust_meta()
来在数据包前添加自定义的元数据。
XDP允许直接对数据包进行访问,程序可以将指向数据包的指针保存在寄存器中,加载数据包的数据到寄存器,将寄存器中的数据写入数据包。
XDP的上下文
XDP的上下文(ctx):
struct xdp_buff {
void *data;
void *data_end;
void *data_meta;
void *data_hard_start;
struct xdp_rxq_info *rxq;
};
data
指向页中数据包的开始位置,data_end
指向数据包结束位置,data_hard_start
指向可能的最大headroom开始的地方,封装后通过bpf_xdp_adjust_head()
调整data
,解封后也一样。
headroom与tailroom分别是包data前后分配的空间,封装时,会在data前添加封装头,data指针向headroom靠近,反之远离。
data_meta
开始时与data
指向相同的地方,可以通过bpf_xdp_adjust_meta()
来将指针向data_hard_start
方向进行调整,来保存自定义的元数据。data_meta
与data
之间保存的自定义元数据在常规内核网络协议栈中是不可见的,只可用于tc BPF读取。相同的,使用bpf_xdp_adjust_meta()
将指针向data
方向移动,能删除自定义的元数据。data_meta
可以用于在尾调的函数之间传递状态,类似于tc BPF用skb->cb[]
传递状态。
上面指针之间的关系:data_hard_start
<= data_meta
<= data
< data_end
。rxq
保存单个接收队列的元数据。
struct xdp_rxq_info {
struct net_device *dev;
u32 queue_index;
u32 reg_state;
} ____cacheline_aligned;
BPF程序可以通过netdevice
获取queue_index
或其他信息,比如ifindex
等。
XDP的返回值
XDP的返回值用于指示下一步如何处理数据包,定义在linux/bpf.h
中:
enum xdp_action {
XDP_ABORTED = 0,
XDP_DROP,
XDP_PASS,
XDP_TX,
XDP_REDIRECT,
};
XDP_DROP
丢弃不再浪费资源进行处理。DDoS中常用。XDP_PASS
继续进入内核协议栈进行处理,是没有XDP程序时,数据包的默认行为。XDP_PASS
意味着当前CPU会转而去处理数据包的skb
分配,传递数据包到GRO中。XDP_TX
从接收的网卡中发出,可用于实现负载均衡(在XDP BPF中对数据包进行重写)XDP_REDIRECT
类似于XDP_TX
,但是是从其他NIC中发出,类似于转发。另外XDP_REDIRECT
可以将数据包转发到BPF cpumap中,意味着用于处理XDP的CPU会仍然处理XDP,而将需要进入内核协议栈的数据包交由其它CPU处理,这种情况类似于XDP_PASS
,但CPU不会处理发送数据包到内核协议栈的工作,因此XDP处理性能会更高。XDP_ABORTED
用于表示程序中的异常状态,与XDP_DROP
的行为相同,但会传递到trace_xdp_exception
的tracepoint中。
XDP主要使用场景
DDoS防御,防火墙
由于可以通过XDP_DROP
在早期就丢弃数据包,因此很适合处理DDoS攻击。更普遍的情况是使用BPF实现任何类型的防火墙。比如作为独立式设备(stand alone appliance),通过XDP_TX
进行流量“清理”,或是广泛部署节点上来保护终端设备。offload XDP通过NIC线性速率的处理,提供更高的性能。转发或负载均衡
可以通过XDP_TX
与XDP_REDIRECT
来转发或进行负载均衡。XDP BPF可以对数据包进行任意的处理,可以在发送之前进行封装或解封。XDP_REDIRECT
可以与BPF cpumap使用,将数据包负载均衡到本地的协议栈中。协议栈之前的过滤或处理
可以在协议栈之前过滤掉无关的数据包,比如我们已知此节点只处理TCP,那么就能DROP掉UDP、SCTP以及其他4层协议的包。此外,如果在内核接收路径上存在某个潜在的BUG可能会导致“ping of death”,可以通过XDP来过滤掉这些可能触发BUG的数据包。
另一个使用场景是在数据包进入协议栈前对数据包进行修改。比如这种自定义封装协议的场景下,可能由于GRO对封装协议的不支持而导致无法进行数据包的聚合,则可以通过XDP进行先解封。或者是通过写入元数据(对内核协议栈不可见),来与tc BPF协调处理。流量的采样与监控
XDP可以用于流量的采样、监控或者网络分析。可以将数据包(截断的或是完整的payload)或者是用户自定义的元数据发送到无锁、per CPU的内存映射缓冲区,缓冲区由linux perf基础设施提供,供用户空间的应用读取。
此外还可以对流量的初始数据包进行分析,一旦判定是正常流量后,对此流量不再进行监控。
XDP生产环境使用案例:Facebooks的SHIV和Droplet,前者用于4层负载,后者用于抗DDoS,相比IPVS性能提升了10倍以上。
- Slides: https://netdevconf.info/2.1/slides/apr6/zhou-netdev-xdp-2017.pdf
- Video:https://youtu.be/YEU2ClcGqts
另一个案例:Cloudflare的抗DDoS使用了XDP,原来使用的是cBPF与iptables的xt_bpf
,由于使用了iptables,在面临DDoS时,会有性能的下降。
- Slides:https://netdevconf.info/2.1/slides/apr6/bertin_Netdev-XDP.pdf
- Video:https://youtu.be/7OuOukmuivg
XDP的操作模式
Native XDP
默认模式,运行在网络驱动的早期接收路径上。需要网卡的支持。Offloaded XDP
将XDP offload到NIC上,使用NIC处理,而非本机的CPU,性能最高。需要支持多线程、多核处理的SmartNIC支持。可能不支持部分BPF helper。一般支持此模式的NIC,都支持Native XDP。Generic XDP
一般用于不支持Native XDP的场景,代码运行的位置在协议栈靠后部分。
驱动支持
从内核4.17支持的情况:
** 支持native XDP**
Broadcom
- bnxt
Cavium
- thunderx
Intel
- ixgbe
- ixgbevf
- i40e
Mellanox
- mlx4
- mlx5
Netronome
- nfp
Others
- tun
- virtio_net
Qlogic
- qede
Solarflare
- sfc [1]
支持offloaded XDP
- Netronome
- nfp [2]
TC
除了XDP外,BPF还可以在tc层运行 ,两者的区别在于:
上下文不一样,tc的是
sk_buff
,而XDP是xdp_buff
。内核在收到数据包,经过XDP那一层后,需要为数据包分配空间、解析并存储到sk_buff
结构中。因此tc可以利用sk_buff
中包含的数据包的元数据,但sk_buff
的创建也会带来性能的损耗。XDP由于在sk_buff
生成之前,因此无法利用sk_buff
中的数据包元数据。
tc BPF可以读写sk_buff
中的mark
,pkt_type
,protocol
,priority
,queue_mapping
,napi_id
,cb[]
,hash
,tc_classid
或者tc_index
,vlan数据,XDP写入的自定义元数据等。sk_buff
的定义在linux/bpf.h
下。
sk_buff
与xdp_buff
各有优劣势:sk_buff
可以方便的处理相关的数据包元数据,而xdp_buff
则不行。但sk_buff
很难通过重写数据包字段来切换协议,因为协议栈处理数据包时依据其元数据,而非每次都读取数据包来判断协议。因此需要额外的BPF helper函数来处理元数据。而xdp_buff
则可以直接对数据包进行重写 ,因为此时还不存在sk_buff
以及其中的元数据。一般可以XDP与tc配合使用(通过XDP的自定义元数据来与tc进行数据传递)。XDP只能在ingress路径上触发,tc能在ingress和egress路径上触发。tc中的两个hook:
sch_handle_ingress()
和sch_handle_egress()
分别由内核中的__netif_receive_skb_core()
和__dev_queue_xmit()
触发。不考虑XDP,后面两个函数是每个数据包进入和发出都会经过的。tc不需要NIC的支持。对于ingress,tc在内核的处于GRO之后,但在任何协议处理、iptable防火墙(例如PREROUTING、nftable ingress hook、或其他数据包处理)之前;对于egress,tc在数据包交于driver传输之前的最后一段,即在iptables防火墙(例如POSTROUTING)之后,但在GSO之前。唯一需要驱动支持的tc是offload的场景,与XDP offload类似,但两者的上下文、支持的helper函数以及返回值不一样。
tc bpf的分类器
tc BPF运行在cls_bpf
分类器中,虽然cls_bpf
也称为分类器,但它可以做到完全的可编程的数据包处理,包括对skb
元数据和包数据进行读取、更改、返回tc action。cls_bpf
可被认为是一个用于管理tc BPF程序的独立的实体。
cls_bpf
可以包含一个或多个tc bpf程序。Cilium中cls_bpf
在direct-action
模式下,只包含一个tc bpf程序。传统的,分类器classifier与动作action是分开的,一个分类器可以有多个action,即当分类器匹配成功后实施多个action。这种模式不适合扩展复杂的处理逻辑 ,而tc bpf可以将解析(类似分类匹配)和action结合在一起,并且在cls_bpf
的direct-action
模式下,只会获取tc bpf返回的tc action,然后终止程序,这样可以通过编程来扩展复杂的处理逻辑。cls_bpf
是唯一支持这种模式的“分类器”。
与XDP BPF相同,tc BPF也可以无缝切换,无需重启或中断流量。
cls_bpf
可以被attach到ingress或egress hook上,而ingress hook与egress hook是由sch_clsact
qdisc管理的,它可以直接替换ingress qdisc,是ingress qdisc的超集。对于__dev_queue_xmit()
中的tc egresss hook,它不在qdisc root lock下执行,因此tc的ingress和egress都是在无锁下执行,且禁止抢占,在RCU读侧运行。
在典型的egress qdiscs中,比如 sch_mq
, sch_fq
, sch_fq_codel
或 sch_htb
,有些是分类型qdiscs,他们包含一些子类,利用数据包的分类机制来对数据包进行分类,这个过程是由tcf_classify()
调用相应的分类器来实现的。cls_bpf
也可作为分类器被调用,但这种过程一般是在qdisc root lock下执行的,会产生锁的竞争。而sch_clsact
的egress hook处于处理流程中更早的阶段,不在qdisc root lock的范围内。因此对于sch_htb
来说,sch_clsact
可以无锁下通过tc BPF进行复杂的数据包分类,将分类结果记录到skb->mark
或是skb->priority
中,而sch_htb
只需要在qdisc root lock下,实现简单的映射,从而减少了锁的竞争。
sch_clsact
与cls_bpf
结合使用的场景也支持offload tc bpf,bpf会通过JIT编译,然后运行在NIC上。只有cls_bpf
的direct-action
支持offload,并且只支持包含单个bpf程序,只支持ingress。
当一个cls_bpf
内包含多个bpf程序时,当bpf返回TC_ACT_UNSPEC
,则会继续执行下一个tc bpf。但这样做的缺点是各个BPF程序需要各自解析数据包 ,从而导致性能下降。
TC BPF的返回值
ingress和egress共用返回值,在linux/pkg_cls.h
下定义:
#define TC_ACT_UNSPEC (-1)
#define TC_ACT_OK 0
#define TC_ACT_SHOT 2
#define TC_ACT_STOLEN 4
#define TC_ACT_REDIRECT 7
在头文件中还提供了一些其他的TC_ACT_*
的返回值,也可以用于ingress和egress。但他们语义相同,TC_ACT_OK
和TC_ACT_RECLASSIFY
的语义相同,TC_ACT_STOLEN
、TC_ACT_QUEUED
和TC_ACT_TRAP
的语义相同。
TC_ACT_UNSPEC
,表示”unspecified action”,用于以下三种情况:i)加载了offload tc BPF,且运行了tc ingress hook,则代表offload tc BPF的
cls_bpf
将返回TC_ACT_UNSPEC
ii)在
cls_bpf
包含多个bpf程序的情况下,为了执行下一个bpf程序。这种情况可以与第一点中的offload tc bpf一起使用。执行完offload tc BPF后,再执行下一个非offload的tc BPF。iii)在单bpf程序下,表示数据包
skb
继续进行内核的处理。此时TC_ACT_UNSPEC
类似于TC_ACT_OK
,都是将skb
发送到上层内核协议栈进行进一步处理,或是发送给网络驱动来发出数据包。不同的地方在于TC_ACK_OK
会根据tc BPF程序的classid设置skb->tc_index
,而TC_ACT_UNSPEC
是在tc BPF程序外,根据BPF ctx中的skb->tc_classid
设置。TC_ACT_SHOT
表示Drop。TC_ACT_SHOT
与TC_ACT_STOLEN
相似,但有区别:前者通过kfree_skb()
来释放skb
,并返回NET_XMIT_DROP
。后者通过consume_skb()
来释放skb
,然后返回NET_XMIT_SUCCESS
来假装传输成功。因此监视kfree_skb()
的perf不会记录到TC_ACK_STOLEN
引起的drop,因为语义上,skb
不是”dropped”,而是”consumed”。TC_ACT_REDIRECT
用于转发,结合bpf_redirect()
helper函数,将skb
转发到相同的、或是不同的设备的ingress、或是egress。对于转发的目标设备来说,不需要设备上运行BPF或是有其他限制要求,只需要是个网络设备
常见问题
作为tc action模块的
act_bpf
是否可用?不常用。虽然
act_bpf
与cls_bpf
对tc BPF来说功能是一样的,但cls_bpf
作为act_bpf
的超集,使用起来更灵活。tc action需要挂载到某个tc classifiers上,为了和cls_bpf
一样,act_bpf
需要挂载到cls_matchall
的分类器上,这种分类器会匹配任何流量,发送到action中进行处理。这种使用方式性能不如cls_bpf
,如果act_bpf
使用除cls_bpf
和cls_matchall
以外的分类器,则会更糟。因为数据包可能需要经过多个分类器后才会被匹配,从而发送给act_bpf
处理。因此act_bpf
没有太大的使用场景,另外,act_bpf
没有实现tc offload的接口。
推荐使用
cls_bpf
的非direct-action
模式吗?不推荐。主要是考虑复杂处理逻辑的扩展。tc BPF本身已经能够高效地完成各种处理,因此没有必要使用除了
direct-action
以外的其他action。
offload
cls_bpf
与offload XDP的性能差异没有性能差异。两者都是通过内核JIT进行编译,offload到SmartNIC中,加载机制也类似。BPF程序会被转换成相同的目标指令集,以便在NIC上本地运行。两者有不同的特性,可能会为了在offload场景中使用特定的helper函数,而相互替换。
使用场景
虽然tc的许多使用场景与XDP是重合的,但是两者大多时候是互补的作用。
容器网络策略的执行
tc BPF可以用于容器或pod的安全策略、防火墙。通常,容器是使用namespace进行资源隔离,通过veth pair将容器的namespace与宿主机的namespace相连,因此进出的流量都会通过宿主机端的veth,可以在veth设备的ingress hook和egress hook上加载tc BPF,则发送给容器的流量会触发egress,容器发出的流量会触发ingress(注意这里是相反的)。
对于veth设备使用XDP是不合适的,因为这里内核的操作仅仅是对
skb
进行的,而XDP会有限制,不能操作克隆的skb
。而在TCP/IP协议栈中,为了重传会持有数据片(data segments),使用克隆的skb
,这种情况下会直接绕过XDP。其次,XDP处理时需要将skb
线性化(linearize,将分页的skb
线性化为一个对象),而导致性能下降。tc BPF则是专门用于处理skb
的,没有XDP的那些限制。
转发或负载均衡
转发或负载均衡的使用案例和XDP相似,但tc bpf会更倾向于东西向的容器流量,而非南北向流量,虽然两种流量都可以使用XDP和tc。XDP只能作用于ingress,tc则可以作用于egress。例如可以在宿主机上通过BPF对容器的egress流量进行NAT与负载均衡。由于内核网络栈的属性,egress流量是基于
sk_buff
的,因此tc适合对数据包进行重写或重定向。通过利用bpf_redirect()
helper函数,BPF可以将数据包转发到其他设备上,因此也无需使用网桥类的设备。
流量的采样与监控
类似于XDP,采样与监控也是通过一个高性能的、无锁的、per-CPU的内存映射缓冲区实现,tc BPF程序会通过
bpf_skb_event_output()
helper函数将数据push到缓冲区中,bpf_skb_event_output()
功能与bpf_xdp_event_output()
是相同的。同样的,tc也能加载到ingress和egress上 ,这样就能监控节点双向的所有流量。这与tcpdump和Wireshark有些类似,但不需要克隆skb
,而且更加灵活的处理skb
。例如BPF可以在内核里进行聚合,而不是将所有内容都推送到用户空间,或者是推送自定义的注释。在Cilium中使用了大量的自定义注释,用于关联容器标签以及为何需要Drop数据包等,以提供更丰富的信息。
数据包调度前的预处理
sch_clsact
的egress hook被称为sch_handle_egress()
,它在获取qdisc root lock之前调用,因此在数据包进入到真正的例如sch_htb
之类的qdisc前,可以通过tc BPF来执行复杂繁重的数据包分类与处理。因为tc BPF的执行是无锁的,通过sch_clsact
与真正的qdisc结合使用(比如上面提到的sch_htb
),可以提高处理性能。
生产环境的使用案例:Cilium。
- Slides: https://www.slideshare.net/ThomasGraf5/dockercon-2017-cilium-network-and-application-security-with-bpf-and-xdp
- Video: https://youtu.be/ilKlmTDdFgk
- Github: https://github.com/cilium/cilium
驱动支持
因为tc BPF是由内核网络协议栈触发的,而非网络驱动触发,因此不依赖与网络驱动。唯一的例外是offload tc BPF。
支持offload tc BPF的驱动
- Netronome
- nfp
参考
JIT技术的介绍:深入浅出 JIT 编译器
bbc介绍:https://github.com/iovisor/bcc/blob/master/docs/reference_guide.md
https://github.com/iovisor/bcc/blob/master/docs/tutorial_bcc_python_developer.md
介绍内核各种track技术的文章:https://jvns.ca/blog/2017/07/05/linux-tracing-systems/#kprobes