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_connectTARGETS += kern/tcp_set_stateTARGETS += kern/dns_lookupTARGETS += kern/udp_lookup# Generate file name-scheme based on TARGETSKERN_SOURCES = ${TARGETS:=_kern.c}KERN_OBJECTS = ${KERN_SOURCES:.c=.o}LLC ?= llcCLANG ?= clangEXTRA_CFLAGS ?= -O2 -emit-llvm -glinuxhdrs ?= /lib/modules/`uname -r`/buildLINUXINCLUDE = \ -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/liball: $(KERN_OBJECTS) build @echo $(shell date).PHONY: cleanclean: 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 buildclean: -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 公布!