0x1: 技术背景
bpf:
BPF 的全称是 Berkeley Packet Filter,是一个用于过滤 (filter) 网络报文 (packet) 的架构。(例如 tcpdump),目前称为 Cbpf(Classical bpf)
Ebpf:
eBPF 全称 extended BPF,Linux Kernel 3.15 中引入的全新设计, 是对既有 BPF 架构进行了全面扩大,一方面,反对了更多畛域的利用,比方:内核追踪 (Kernel Tracing)、利用性能调优 / 监控、流控(Traffic Control) 等;另一方面,在接口的设计以及易用性上,也有了较大的改良。
eBPF 反对在用户态将 C 语言编写的一小段“内核代码”注入到内核中运行,注入时要先用 llvm 编译失去应用 BPF 指令集的 ELF 文件,而后从 ELF 文件中解析出能够注入内核的局部,最初用 bpf_load_program() 办法实现注入。用户态程序和注入到内核中的程序通过共用一个位于内核的 eBPF MAP 实现通信。为了避免注入的代码导致内核解体,eBPF 会对注入的代码进行严格查看,回绝不合格的代码的注入。
- eBPF prog load 的严格的 verify 机制
- eBPF 拜访内核资源需借助各种 eBPF 的 helper func,helper func 函数能在最坏的状况下保障平安
- 当初,Linux 内核只运行 eBPF,内核会将加载的 cBPF 字节码 通明地转换成 eBPF 再执行
0x2: 技术比照
| 优劣 | eBPF | 源码开发 | 热补丁 |
| :– | :– | :–| :– |
| 劣势 | 1. 平安,不会引起宕机
2. 自主,可控
2. 热加载(良好的加载 / 卸载流程)
3. 开启 CO-RE 后,移植性高,适配量小
4. 能够在注入的代码中写入业务逻辑,优化 hids 性能
5. 开发难度低,上手快 | 1. 体积小
2. 自由度高
3. 性能高
4. 功能强大
| 1. 体积小
2. 自由度高
3. 性能高
4. 热加载,不须要重启
|
| 毛病 | 1. 性能受限(验证器)
2. 强依赖于内核版本
3. 不反对内核函数调用
4. 单函数最大 512byte 栈空间,通过尾调用扩大到 8K
5. 性能不如其余两者
|1. 须要从新编译内核
2. 须要重启业务主机
3. 须要开发者相熟内核
4. 适配工作量微小
5.netlink 上发数据有性能瓶颈
|1. 须要开发者相熟内核
2. 适配工作量大
0x3: 运行流程
用 C 编写 BPF 程序
用 LLVM 将 C 程序编译成对象文件(ELF)用户空间 BPF ELF 加载器(例如 libbpf)解析对象文件
加载器通过 bpf() 零碎调用将解析后的对象文件注入内核
内核验证 BPF 指令,而后对其执行即时编译(JIT),返回程序的一个新文件描述符
利用文件描述符 attach 到内核子系统(例如网络子系统)某些子系统还反对将 BPF 程序 offload 到硬件(例如网卡)。
0x4: 库选型
bcc | libbpf | ebpfgo | cilium eBPF | |
---|---|---|---|---|
https://github.com/iovisor/bcc | https://github.com/libbpf/libbpf | https://github.com/aquasecuri… | https://github.com/cilium/ebpf | |
劣势 | 1. 开发沉闷 2. 示例多 |
1.linux 官网提供,牢靠 2. 反对 CO-RE |
1.Go 库,合乎技术栈 2. 反对 CO-RE |
1. 纯 Go 库,大厂背书 2. 开发沉闷 3. 局部反对 CO-RE |
毛病 | 1. 须要在指标机器编译,对业务影响大 | 1. 前端语言为 C | 1. 须要开启 CGO | 1. 对 CO-RE 反对的不全面 |
0x5:BTF & CO-RE
当 eBPF 被用来做信息收集性能时,就得和内核中各种构造体打交道,家喻户晓,linux 内核改变一贯比拟随 (keng) 意(die),不会像 windows 那样还思考兼容性,所以咱们得本人解决不同内核版本间接字段不统一问题。
惯例内核代码写法是通过宏定义来判断内核版本,在编译的时候走不同的代码分支,解决差异性,办法尽管不难,然而适配却十分吃力,当须要反对的内核版本多时,光是适配就得消耗大量精力。
static __always_inline u32 get_task_ns_pid(struct task_struct *task)
{#if LINUX_VERSION_CODE < KERNEL_VERSION(4, 19, 0)
// kernel 4.14-4.18:
return task-> [PIDTYPE_PID].pid->numbers[task->nsproxy->pid_ns_for_children->level].nr;
#else
// kernel 4.19 onwards:
return task->thread_pid->numbers[task->nsproxy->pid_ns_for_children->level].nr;
#endif
}
这也就是 BTF 呈现之前的很长一段时间里,bcc + clang + llvm 被人们诟病的中央,程序在运行的时候,才进行编译,指标机器还得装置 clang llvm kernel-header 头文件,同时编译也会耗费大量 cpu 资源,这在某些高负载机器上是不能被承受的。
因而 BTF & CO-RE 横空呈现,BTF 能够了解为一种 debug 符号形容形式,此前传统形式 debug 信息会十分微小,linux 内核个别会敞开 debug 符号,btf 的呈现解决了这一问题,大幅度缩小 debug 信息的大小,使得生产场景内核携带 debug 信息成为可能。
CO-RE 正是基于这一技术开发的,原理相似于 pe/elf 构造中的重定位表,核心思想就是采纳非硬编码模式对成员在构造中的偏移地位进行形容,解决不同版本间构造体差异性。
可喜的是通过使用这项技术,的确能够帮忙开发者节俭大量精力在版本适配上,然而这项技术目前还是在开发中,还有许多解决不了的场景,比方构造体成员被迁入子结构体中,这时候还是须要手动解决问题,BTF 的开发者也写了一篇文章,解说不同场景的解决计划 bpf-core-reference-guide
tips:目前 cilium 提供的 eBPF 库对 CO-RE 的反对也不全面,期待社区继续更新。
0x6: 开发流程
思考到本身业务技术栈,因而选用 cilium 提供的 go 库作为前端库,同时默认开启 btf,加强程序可移植性。
开发环境:
Mac + Vscode(装置 remote develop 插件) 强烈推荐
ubuntu 20.10 server(5.8 之后开启 BTF 的内核都能够)
OS:
倡议装置最新 5.16 内核版本且开启 BTF,不喜爱折腾就间接装置 ubuntu20.10 server 版本,默认开启了 BTF
应用层:
无特殊要求,引入 github.com/cilium/ebpf 库即可。
内核层:
- 装置 libbpf 库
- 装置 clang llvm
- 查看是否开启 btf
cat /boot/config-uname -r
| grep BTF 其中 CONFIG_DEBUF_INFO_BTF 开启即可,未开启则须要从新编译内核,开启 BTF。 - 生成 vmlinux.h 文件(CO-RE)外围。
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h -
编写 eBPF c 代码
#include "vmlinux.h" //linux 内核头文件大汇合 #include <bpf/bpf_helpers.h> #include <bpf/bpf_endian.h> #include <bpf/bpf_core_read.h> #include <bpf/bpf_tracing.h> // 蕴含这些头文件,就能够用 CORE 编程了(这里没啥好说的,就和写内核代码一样,只是留神能用的函数比拟少,同时如果遇到编译问题,请参考笔者踩坑记录【eBPF 开发记录】)
- 应用 bpf_printk 进行代码调试即可
cat /sys/kernel/debug/tracing/trace_pipe 输入在这里 -
创立一个 Makefile,外围就是用 clang 对上一个步骤的 c 文件进行编译即可。
办法一:手动编写,自主可控,实现 TARGETS := kern/sec_socket_connect TARGETS += kern/tcp_set_state TARGETS += kern/dns_lookup TARGETS += kern/udp_lookup # Generate file name-scheme based on TARGETS KERN_SOURCES = ${TARGETS:=_kern.c} KERN_OBJECTS = ${KERN_SOURCES:.c=.o} LLC ?= llc CLANG ?= clang EXTRA_CFLAGS ?= -O2 -emit-llvm -g linuxhdrs ?= /lib/modules/`uname -r`/build LINUXINCLUDE = \ -I$(linuxhdrs)/arch/x86/include \ -I$(linuxhdrs)/arch/x86/include/generated \ -I$(linuxhdrs)/include \ -I$(linuxhdrs)/arch/x86/include/uapi \ -I$(linuxhdrs)/arch/x86/include/generated/uapi \ -I$(linuxhdrs)/include/uapi \ -I$(linuxhdrs)/include/generated/uapi \ -I/usr/include \ -I/home/cfc4n/download/linux-5.11.0/tools/lib all: $(KERN_OBJECTS) build @echo $(shell date) .PHONY: clean clean: rm -rf kern/*.o rm -rf user/bytecode/*.o rm -rf network-monitoring $(KERN_OBJECTS): %.o: %.c $(CLANG) $(EXTRA_CFLAGS) \ $(LINUXINCLUDE) \ -include kern/chim_helpers.h \ -Wno-deprecated-declarations \ -Wno-gnu-variable-sized-type-not-at-end \ -Wno-pragma-once-outside-header \ -Wno-address-of-packed-member \ -Wno-unknown-warning-option \ -fno-unwind-tables \ -fno-asynchronous-unwind-tables \ -Wno-unused-value -Wno-pointer-sign -fno-stack-protector \ -c $< -o -|$(LLC) -march=bpf -filetype=obj -o $(subst kern/,user/bytecode/,$@) build: go build .
办法二:采纳 cilium 提供的 bpf2go 库
- 在 main.go 中退出 //go:generate go run github.com/cilium/ebpf/cmd/bpf2go -cc clang ProcInfo src/procinfo.bpf.c — -nostdinc -I/usr/include 这外面的 ProcInfo 是上一步 c 文件的名字,本人手动批改即可
-
编写如下 makefile
all: go generate go build clean: -rm *_bpfe*.o -rm *_bpfe*.go -rm eBPF-*
0x7: 新个性 & 内核要求
以下信息来自笔者查看 Linux Kernel Release 文档总结得出 Kernel Release Note
4.7 反对 tracepoint
4.16 且 LLVM 6.0 不再应用宏 always_inline 润饰函数,反对 bpf 程序调用非 bpf 程序
4.18 反对 btf jit 反对 32 位 cpu
5.1 Add __sk_buff->sk, struct bpf_tcp_sock, BPF_FUNC_sk_fullsock and BPF_FUNC_tcp_sock | 加强 btf 能力 | 指令数量从 4096 进步到 100w 条
5.2 反对全局变量
5.3 反对无限 for 循环
5.5 Add probe_read_user, probe_read_kernel and probe_read_user_str, probe_read_kernel_str | 反对 BPF_CORE_READ
5.7 退出 bpf-lsm 框架(selinux appamor)
5.8 退出 CAP_BPF and CAP_PERFMON | 引入 Ring buffer
5.10 反对尾调用(long jump) 和一般函数调用 (func call) 混用
总结:内核组能反对的越新越好,如果能反对 Ring buffer 那就能解决数据乱序问题, 且传输性能优于 Perf Buffer。
0x8:eBPF 限度
- 一个 BPF 程序的代码数量不能超过 BPF_MAXINSNS (4K),它的总运行步数不能超过 32K (4.9 内核中这个值改成了 96k);
- BPF 代码只反对无限循环,这也是为了保障出错时不会呈现死循环来 hang 死内核。一个 BPF 程序总的可能的分支数也被限度到 1K;(反对无限循环)
- 为了限度它的作用域,BPF 代码不能拜访全局变量,只能拜访局部变量。一个 BPF 程序只有 512 字节的堆栈。在开始时会传入一个 ctx 指针,BPF 程序的数据拜访就被限度在 ctx 变量和堆栈局部变量中;
- 如果 BPF 须要拜访全局变量,它只能拜访 BPF map 对象。BPF map 对象是同时能被用户态、BPF 程序、内核态独特拜访的,BPF 对 map 的拜访通过 helper function 来实现;
- 旧版本 BPF 代码中不反对 BPF 对 BPF 函数的调用,所以所有的 BPF 函数必须申明成 always_inline。在 Linux 内核 4.16 和 LLVM 6.0 当前,才反对 BPF to BPF Calls;
- BPF 尽管不能函数调用,然而它能够应用 Tail Call 机制从一个 BPF 程序间接跳转到另一个 BPF 程序。它须要通过 BPF_MAP_TYPE_PROG_ARRAY 类型的 map 来晓得另一个 BPF 程序的指针。这种跳转的次数也是有限度的,32 次(8k 栈空间)
-
内核还能够通过一些额定的伎俩来加固 BPF 的安全性 (Hardening)。次要包含:把 BPF 代码映像和 JIT 代码映像的 page 都锁成只读,JIT 编译时把常量致盲(constant blinding),以及对 bpf() 零碎调用的权限限度;
0x9:Perf Buffer & Ring Buffer
Perf Buffer
Ring Buffer
总结:
共同点:
- Perf/Ring Buffer 绝对于其余品种 map(被动轮询)来说,提供专用 api,告诉应用层事件就绪,缩小 cpu 耗费,进步性能。
- 采纳共享内存,节俭复制数据开销。
- Perf/Ring Buffer 反对传入可变长构造。
差别: - Perf Buffer 每个 CPU 外围一个缓存区,不保证数据程序(fork exec exit),会对咱们应用层生产数据造成影响。Ring Buffer 多 CPU 共用一个缓存区且外部实现了自旋锁,保证数据程序。
- Perf Buffer 有着两次数据拷贝动作,当空间有余时,效率低下。Ring Buffer 采纳先申请内存,再操作模式,提高效率。
- perfbuf 的 buffer size 是在用户态定义的,而 ringbuf 的 size 是在 bpf 程序中预约义的。
- max_entries 的语义,perfbuf 是 buffer 数量(社区举荐设置为 cpu 个数),ringbuf 中是单个 buffer 的 size。
-
Ring Buffer 性能强于 Perf Buffer。参考 patch【ringbuf perfbuf 性能比照】
Perf/Ring Buffer 用法请参考另一篇 km【Perf/Ring buffer 用法 & 性能比照】0x10:eBPF 配置
1. 加固
/proc/sys/net/core/bpf_jit_harden 设置为 1 会为非特权用户(unprivileged users)的 JIT 编译做一些额定的加固工作。比方常量致盲,损失局部性能。
2. 限度零碎调用
/proc/sys/kernel/unprivileged_bpf_disabled 设置为 1 会禁止非特权用户应用 bpf(2) 零碎调用,将它设为 1,就没有方法再改为 0 了,除非重启内核。一旦设置为 1 之后,只有初始命名空间中有 CAP_SYS_ADMIN 特权的过程才能够调用 bpf(2) 零碎调用。Cilium 启动后也会将这个配置项设为 1
3.eBPF 须要开启的编译参数(不蕴含 BTF 相干)
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
本文由博客一文多发平台 OpenWrite 公布!