01
简介
-
ebpf是一种Linux应用程序在内核空间执行代码的机制,经常被用于网络,调试,函数调用追踪,防火墙等领域。
-
ebpf可以在Linux内核里面运行沙箱程序而不需要修改内核代码或者加载内核模块。
-
ebpf程序的功能及其执行涉及一些复杂的组件。
ebpf技术本意是扩展bpf技术,本来bpf是用来对于数据包进行过滤,但是经过不断的发展,现在是一种可以从用户态向内核态注入代码从而可以使得用户态的代码可以在内核态执行,为了防止内核破坏,ebpf技术对注入的代码有verifier验证和jit即时编译保护,整体的架构如下:
02
架构设计
事件以及hook
-
Syscall
-
Function entry and Exit
-
Network events
-
Kprobe and uprobes
eBPF Maps
BPF_MAP_CREATE
的参数的bpf_cmd
syscall 可以创建eBPF Maps,syscall返回一个文件描述符用于索引eBPF MAP,至于如何和MAP进行交互,可以参考 这里[4]。运行eBPF 程序
BTF&CO-RE
03
开发指南
开发环境
cat /boot/config-uname -r (这里''加不上,看下图) | grep BTF
-
libbfp Library
-
clang and llvm
-
通过vmlinux生成.h头文件以方便进行CO-CR(一次编译多处使用)
Bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
-
编写C程序
//包含这些头文件,就可以用CORE编程了
-
使用bpf_printk进行代码调试
long bpf_trace_printk(const char *fmt, __u32 fmt_size, ...);
cat /sys/kernel/debug/tracing/trace_pipe输出在这里(记得使用root用户)。
-
创建Makefile
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)
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 .
go generate
命令,当运行该命令的时候会扫描当前包相关的源代码文件,找出所有包含//go:generate
的特殊注释,提取并执行该特殊注释后面的命令。具体可以参考这里[7]。当然,不同的内核可能还有很多不同的功能特性,踩坑是必然的。
开发demo
这里使用cilium作为应用层依赖库,其中的例子也来源于cilium提供的Examples[8]。
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -cc clang bpf cgroup_skb.c -- -I../headers
// Allow the current process to lock memory for eBPF resources.
if err := rlimit.RemoveMemlock(); err != nil {
log.Fatal(err)
setrlimit()
函数的RLIMIT_MEMLOCK
参数实现的。RLIMIT_MEMLOCK
指的是可以锁定的最大RAM内存,它并不特定于用户空间内存地址范围的分配,在5.11版本之前的内核,内存被用于eBPF对象,这意味着你有可能短时间创建大量对象消耗内存,通过该参数限制内存来防止出现错误。-
loadBpfObjects
:加载预编译的程序和maps到内核。 -
link
库,这个主要是将eBPF程序attach到不同的hook位置。
SEC("cgroup_skb/egress")
SEC("fentry/tcp_connect")
SEC("kprobe/sys_execve")
SEC
宏,这些宏表示在当前的obj文件中新增一个段(section)。char __license[] SEC("license") = "Dual MIT/GPL";
struct bpf_map_def SEC("maps") pkt_count = {
.type = BPF_MAP_TYPE_ARRAY,
.key_size = sizeof(u32),
.value_size = sizeof(u64),
.max_entries = 1,
};
BPF_MAP_TYPE_ARRAY
在之前环境准备中生成的vmlinux.h
中定义。SEC("cgroup_skb/egress")
int count_egress_packets(struct __sk_buff *skb) {
u32 key = 0;
u64 init_val = 1;
u64 *count = bpf_map_lookup_elem(&pkt_count, &key);
if (!count) {
bpf_map_update_elem(&pkt_count, &key, &init_val, BPF_ANY);
return 1;
}
__sync_fetch_and_add(count, 1);
return 1;
}
bpf_map_look_elem
函数的原型为bpf_map_lookup_elem(map_fd, void *key)
,表明在map_fd中寻找key,并返回对应的value。-
bpf_map_update_elem(map_fd, void *key, void *value)
: 更新key或者value。 -
bpf_map_delete_elem(map_fd, void *key)
在map_fd中删除一个键。 -
__sync_fetch_and_add (T* p, U v, ...)
该函数自动增加v到指针p指向的内容。
04
关键词
-
ebpf映射是为了保存多类型数据的通用数据结构,用户可以创建多个映射通过fd访问,以便进行数据共享。
-
Helper functions可以帮助eBPF程序操作数据。
-
因此一个BPF程序是否在内核中存活取决于 通过bpf()载入内核后如何进一步附加在别的子系统上
-
llvm 作为BPF的后端使得clang等可以直接编译c到bpf object file
-
XDP BPF程序在最早的网络驱动阶段被attch,然后再收到数据包的时候触发网络执行。
-
内核里面的BPF程序都是事件驱动
-
BPF 由 11 个 64 位寄存器和 32 位子寄存器、一个程序计数器和一个 512 字节的大 BPF 堆栈空间组成。寄存器命名为 r0 - r10。操作模式默认为 64 位,32 位子寄存器只能通过特殊的 ALU(算术逻辑单元)操作访问。低 32 位子寄存器在被写入时零扩展为 64 位。
-
当前不支持具有 6 个或更多参数的调用。内核中专用于 BPF 的辅助函数(BPF_CALL_0() 到 BPF_CALL_5() 函数)是专门为这种约定而设计的。
-
在进入 BPF 程序执行时,寄存器 r1 最初包含程序的上下文。上下文是程序的输入参数(类似于典型 C 程序的 argc/argv 对)。BPF 仅限于在单个上下文中工作。上下文由程序类型定义,例如,网络程序可以将网络数据包 (skb) 的内核表示作为输入参数。
-
尽管指令集包含前向和后向跳转,但内核 BPF 验证器将禁止循环,以便始终保证终止
-
还有一个尾调用的概念,它允许一个 BPF 程序跳转到另一个 BPF 程序。这也带有 33 个调用的嵌套上限,通常用于将程序逻辑的部分解耦,例如,分解为阶段。
-
Bpf call 和 bpf helper功能混合在5.9 kernel ,回环调用可能引起堆栈溢出
-
BPF locks the entire BPF interpreter image (
struct bpf_prog
) as well as the JIT compiled image (struct bpf_binary_header
) in the kernel as read-only during the program’s lifetime in order to prevent the code from potential corruptions.
05
参考链接
引用连接
-
[1] https://github.com/iovisor/bcc
-
[2] https://github.com/torvalds/linux/blob/master/kernel/bpf/verifier.c
-
[3] https://man7.org/linux/man-pages/man7/bpf-helpers.7.html
-
[4] https://man7.org/linux/man-pages/man2/bpf.2.html
-
[5] https://nakryiko.com/posts/bpf-core-reference-guide/
-
[6] https://github.com/cilium/ebpf
-
[7] http://c.biancheng.net/view/4442.html
-
[8] https://github.com/cilium/ebpf/tree/master/examples
其他连接
-
https://www.infoq.com/articles/gentle-linux-ebpf-introduction/ -
https://developpaper.com/ebpf-development-guide/ -
https://segmentfault.com/a/1190000041178939#item-6 -
https://www.anquanke.com/post/id/263803 -
https://www.seekret.io/blog/a-practical-guide-to-capturing-production-traffic-with-ebpf/
原文始发于微信公众号(RainSec):《eBPF 系列(一): 初探eBPF》
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论