点击上方蓝字【囧囧妹】一起学习,一起成长!
在为 BPF 编写 C 程序时,与使用 C 进行通常的应用程序开发相比,有几个陷阱需要注意。以下描述了与 BPF 模型的一些差异:
一,一切都需要内联,没有函数调用(在旧 LLVM 版本上)或共享库调用可用。
bpf/lib/)。但是,这仍然允许包含例如来自内核或其他库的头文件,并重用它们的静态内联函数或宏/定义。
__inline,如下所示。建议使用
always_inline,因为编译器仍然可以决定取消内联仅注释为
inline.
#include <linux/bpf.h>#ifndef __section# define __section(NAME) \__attribute__((section(NAME), used))#endif#ifndef __inline# define __inline \inline __attribute__((always_inline))#endifstatic __inline int foo(void){return XDP_DROP;}__section("prog")int xdp_drop(struct xdp_md *ctx){return foo();}char __license[] __section("license") = "GPL";
二,多个程序可以驻留在不同部分的单个 C 文件中。
maps和
license作为默认部分名称来分别查找创建地图所需的元数据和 BPF 程序的许可证。在程序创建时,后者也被推送到内核中,并启用一些作为 GPL 公开的辅助函数,仅在程序还持有 GPL 兼容许可证的情况下,例如
bpf_ktime_get_ns(),
bpf_probe_read()等等。
ingress并且
egress. 玩具示例代码演示了两者都可以共享一个地图和常见的静态内联帮助器,例如
account_data()函数。
xdp-example.c示例已修改为
tc-example.c可以使用 tc 加载并附加到网络设备的入口和出口挂钩的示例。它将传输的字节记入一个名为 的映射
acc_map中,该映射有两个映射槽,一个用于入口钩子上的流量,一个用于出口钩子。
#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#endifstatic 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的头,其中包含struct bpf_elf_映射的定义。iproute2有一个通用的BPF ELF加载程序,因此,对于XDP和tc类型的程序,结构struct bpf_elf_map映射的定义是一样的。
一个struct bpf_elf_map在程序中定义了一个映射,并包含生成映射所需的所有相关信息(例如键/值大小等),该映射从两个 BPF 程序中使用。必须将结构放入maps部分中,以便加载程序可以找到它。这种类型的映射声明可以有多个具有不同变量名的声明,但都必须用.__section("maps")
结构struct bpf_elf_map被指定到 iproute2 。不同的 BPF ELF 加载器可以有不同的格式,例如内核源代码树中的 libbpf,该库主要被perf使用。 iproute2 向后兼容为了struct bpf_elf_map。Cilium 遵循 iproute2 模型。
该示例还演示了 BPF 辅助函数如何映射到 C 代码中并被使用。在这里,map_lookup_elem()
通过将此函数映射到BPF_FUNC_map_lookup_elem
作为助手公开的枚举值来定义uapi/linux/bpf.h
. 当程序稍后被加载到内核中时,验证器检查传递的参数是否是预期的类型,并将帮助程序调用重新指向真正的函数调用。此外,map_lookup_elem()
还演示了如何将映射传递给 BPF 辅助函数。在这里,&acc_map
来自 maps
部分作为第一个参数传递给map_lookup_elem()
.
由于定义的数组映射是全局的,需要使用原子操作,定义为lock_xadd()
. __sync_fetch_and_add()
作为内置函数映射到 BPF 原子添加指令。
三,不允许使用全局变量
由于第 1 点中已经提到的原因,BPF 不能像普通 C 程序中经常使用的那样具有全局变量。
但是,有一种变通方法,程序可以简单地使用类型为 BPF 的映射,BPF_MAP_TYPE_PERCPU_ARRAY
其中只有一个任意值大小的槽。这是可行的,因为在执行期间,BPF 程序保证永远不会被内核抢占,因此可以使用单个映射条目作为临时数据的暂存缓冲区,例如,扩展堆栈限制。这也适用于尾调用,因为它在抢占方面具有相同的保证。
否则,为了在多个 BPF 程序运行中保持状态,可以使用普通的 BPF 映射。
四,不允许使用 const 字符串或数组。
在 BPF C 程序中定义const
字符串或其他数组不起作用,原因与第 1 节和第 3 节中指出的相同,即,将在 ELF 文件中生成重定位条目,由于不是一部分,加载程序将拒绝这些条目加载器的 ABI(加载器也无法修复此类条目,因为它需要对已编译的 BPF 序列进行大量重写)。
将来,LLVM 可能会检测到这些事件并提前向用户抛出错误。
trace_printk()
可以按如下方式解决辅助功能:
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
五,将 LLVM 内置函数用于 memset()/memcpy()/memmove()/memcmp()
由于 BPF 程序除了对 BPF 助手的调用之外,不能执行任何函数调用,因此需要将公共库代码实现为内联函数。此外,LLVM 还提供了一些内置程序可用于固定大小(此处n
:),然后将始终内联:
#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()
内置有一些极端情况,由于后端的 LLVM 问题而没有发生内联,因此在问题得到解决之前不建议使用。
六,没有可用的循环
#pragma unroll指令,一种非常有限的循环形式可用于恒定的循环上限。编译为 BPF 的示例代码:
#pragma unrollfor (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);elselen += ipv6_optlen(&opthdr);break;default:*nexthdr = nh;return len;}}
七,使用尾调用对程序进行分区
BPF_MAP_TYPE_PROG_ARRAY),并将映射以及索引传递给下一个程序以跳转。执行跳转后不会返回旧程序,如果在给定的映射索引处不存在程序,则继续执行原始程序。
skb_event_output()调用位于被调用程序的尾部。因此,在正常操作期间,除非将程序添加到相关地图索引中,否则将始终执行直通路径,然后该程序准备元数据并触发事件通知给用户空间守护进程。
[...]#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 1static 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(BPF_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";
八,最大 512 字节的有限堆栈空间。
BPF 程序中的堆栈空间仅限于 512 字节,在 C 中实现 BPF 程序时需要仔细考虑。但是,如前面第 3 点所述,BPF_MAP_TYPE_PERCPU_ARRAY
可以使用具有单个条目的映射来扩大暂存空间缓冲空间。
九,可以使用 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 = 21: (7b) *(u64 *)(r10 -8) = r12: (b7) r1 = 33: (bf) r2 = r104: (07) r2 += -85: (db) lock *(u64 *)(r2 +0) += r16: (79) r0 = *(u64 *)(r10 -8)7: (95) exitprocessed 8 insns (limit 131072), stack depth 8
十,使用#pragma pack 移除带有对齐成员的结构填充。
在现代编译器中,数据结构默认对齐以有效地访问内存。结构成员被打包到内存地址并添加填充以与处理器字大小正确对齐(例如,64 位处理器为 8 字节,32 位处理器为 4 字节)。正因为如此,struct 的大小可能经常比预期的要大。
struct called_info {u64 start; // 8-byteu64 end; // 8-byteu32 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.
内核中的 BPF 验证器检查 BPF 程序不会访问边界或未初始化堆栈区域之外的堆栈边界。
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,.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;}
对应的输出bpf_load_program()
:
bpf_load_program() err=130: (bf) r6 = r1...19: (b7) r1 = 020: (7b) *(u64 *)(r10 -72) = r121: (7b) *(u64 *)(r10 -80) = r722: (63) *(u32 *)(r10 -64) = r1...30: (85) call bpf_map_update_elem#2invalid indirect read from stack off -80+20 size 24
十一,通过无效引用访问数据包数据
一些网络 BPF 辅助函数bpf_skb_store_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}
由于取消引用了 invalidated ,验证程序将拒绝该片段 ip4->protocol
:
R1=pkt_end(id=0,off=0,imm=0) R2=pkt(id=0,off=34,r=34,imm=0) R3=inv0R6=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#919: (7b) *(u64 *)(r10 -56) = r7R0=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=mmmmmmmm21: (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}




