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/aquasecurity/libbpfgo | 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 發布!