BPF BPF(Berkeley Packet Filter,伯克利数据包过滤)于1992年在论文“The BSD Packet Filter: A New Architecture for User-level Packet Capture”中被提出。当时设计的BPF具有以下两个特性:
内核态引入一个新的虚拟机,所有的指令都在内核虚拟机中运行;
用户态使用BPF字节码来定义过滤表达式,然后传递给内核,由内核虚拟机解释执行。
BPF技术可在无需编译内核或加载内核模块的情况下,安全地高效地附加到内核的各种事件上,对内核事件进行监控、跟踪和可观测性,避免了向用户态复制每个数据,极大提升了包过滤性能。
在Linux 2.1.75中,首次引入了BPF技术;在Linux 3.0中,增加了BPF即时编译器,它替换掉了原本性能更差的解释器,优化了BPF指令运行的效率,但BPF只是一种数据包过滤技术。
eBPF 2014年,Alexei Starovoitov将BPF扩展为一个通用的虚拟机,即eBPF(Extended Berkeley Packet Filter,扩展的伯克利包过滤器)。eBPF不仅扩展了寄存器的数量,引入了全新的BPF映射存储,同时还在Linux 4.x中将原本单一的数据包过滤事件逐步扩展到了内核态函数、用户态函数、跟踪点、性能事件以及安全控制等。
eBPF的诞生是BPF技术的一个转折点,使得BPF不在局限于网络栈,而是成为内核的一个顶级子系统。它与传统方法需要修改内核源代码或加载新模块不同,eBPF使得动态定制和优化网络行为成为可能,且不会中断系统操作。eBPF的整个发展流程如下图所示,目前eBPF广泛用于网络、安全、可观测等领域。
内核原理 eBPF是一个运行在内核中的虚拟机,与系统虚拟化(例如kvm)中的虚拟机存在本质不同。系统虚拟化基于x86或arm64等通用指令集,这些指令集足以完成完整计算机的所有功能。而为了确保在内核中安全地执行,eBPF只提供了非常有限的指令集。这些指令集可用于完成一部分内核的功能,但却远不足以模拟完整的计算机。如下图所示,eBPF在内核中的运行时主要由5个模块组成:
eBPF辅助函数。它提供了一系列用于eBPF程序与内核其他模块进行交互的函数,这些函数并不是任意一个eBPF程序都可以调用的,具体可用的函数集由BPF程序类型决定。
eBPF验证器。它用于确保eBPF程序的安全,验证器会将待执行的指令创建为一个有向无环图(DAG),确保程序中不包含不可达指令;接着再模拟指令的执行过程,确保不会执行无效指令。
存储模块。该模块是由11个64位寄存器、一个程序计数器和一个512字节的栈组成的存储模块。这个模块用于控制eBPF程序的执行。其中,R0寄存器用于存储函数调用和eBPF程序的返回值,这意味着函数调用最多只能有一个返回值;R1-R5寄存器用于函数调用的参数,因此函数调用的参数最多不能超过5个;而R10则是一个只读寄存器,用于从栈中读取数据。
即时编译器。它将eBPF字节码编译成本地机器指令,以便更高效地在内核中执行。
BPF映射。它用于提供大块的存储,这些存储可被用户空间程序用来进行访问,进而控制eBPF程序的运行状态。
内核交互 eBPF程序是事件驱动的,当内核或应用程序通过某个钩子点时运行。预定义的钩子包括系统调用、函数入口/退出、内核跟踪点、网络事件等。如果预定义的钩子不能满足特定需求,则可以创建内核探针(kprobe)或用户探针(uprobe),以便在内核用户或应用程序的几乎任何位置附加eBPF程序。 确定需要的钩子后,就可以使用bpf系统调用将eBPF程序加载到Linux内核中。当程序被加载到Linux内核时,它在被附加到所请求的钩子上之前需要经过两个步骤:
验证:验证步骤用于保证eBPF程序可以安全运行。它可以验证程序是否满足几个条件:
加载eBPF程序的进程必须有必需的能力(特权),除非启用非特权eBPF,否则只有特权进程可以加载eBPF程序;
eBPF程序不会崩溃或者对系统造成损害;
eBPF程序一定会运行至结束(即程序不会处于循环状态中,否则会阻碍后续的处理)。
JIT编译:JIT(Just-in-Time)编译步骤将程序的通用字节码高效转换为机器的特定指令集,从而优化程序的执行。这使得eBPF程序可以像本地编译的内核代码或作为内核模块加载的代码一样地运行。
eBPF程序的其中一个重要方面是共享和存储所收集的信息和状态的能力。为此,eBPF程序可以利用eBPF Maps的概念来存储和检索各种数据结构中的数据。如下图所示,eBPF Maps既可以从eBPF程序访问,也可以通过系统调用从用户空间中的应用程序访问。
这其中,最关键的是设置映射的类型。内核头文件bpf.h 中的bpf_map_type定义了所有支持的映射类型,也可以使用bpftool命令bpftool feature probe | grep map_type来查询当前系统支持哪些映射类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 enum bpf_map_type { BPF_MAP_TYPE_UNSPEC, BPF_MAP_TYPE_HASH, BPF_MAP_TYPE_ARRAY, BPF_MAP_TYPE_PROG_ARRAY, BPF_MAP_TYPE_PERF_EVENT_ARRAY, BPF_MAP_TYPE_PERCPU_HASH, BPF_MAP_TYPE_PERCPU_ARRAY, BPF_MAP_TYPE_STACK_TRACE, BPF_MAP_TYPE_CGROUP_ARRAY, BPF_MAP_TYPE_LRU_HASH, BPF_MAP_TYPE_LRU_PERCPU_HASH, BPF_MAP_TYPE_LPM_TRIE, BPF_MAP_TYPE_ARRAY_OF_MAPS, BPF_MAP_TYPE_HASH_OF_MAPS, BPF_MAP_TYPE_DEVMAP, BPF_MAP_TYPE_SOCKMAP, BPF_MAP_TYPE_CPUMAP, BPF_MAP_TYPE_XSKMAP, BPF_MAP_TYPE_SOCKHASH, BPF_MAP_TYPE_CGROUP_STORAGE, BPF_MAP_TYPE_REUSEPORT_SOCKARRAY, BPF_MAP_TYPE_PERCPU_CGROUP_STORAGE, BPF_MAP_TYPE_QUEUE, BPF_MAP_TYPE_STACK, BPF_MAP_TYPE_SK_STORAGE, BPF_MAP_TYPE_DEVMAP_HASH, BPF_MAP_TYPE_STRUCT_OPS, BPF_MAP_TYPE_RINGBUF, BPF_MAP_TYPE_INODE_STORAGE, BPF_MAP_TYPE_TASK_STORAGE, BPF_MAP_TYPE_BLOOM_FILTER, };
如下图所示,一个完整的eBPF程序通常包含用户态(*_kern.c)和内核态(*_user.c)两部分。其中,用户态负责eBPF程序的加载、事件绑定以及eBPF程序运行结果的汇总输出;内核态运行在eBPF虚拟机中,负责定制和控制系统的运行状态。
内核空间代码定义逻辑
功能
说明
定义事件处理逻辑
编写当某个内核事件触发时要执行的代码(如系统调用被调用、网络包到达、函数进入/退出等)
数据收集与过滤
在内核态直接捕获数据,进行初步过滤和聚合,减少用户态开销
Map操作
将处理结果写入BPF Map(内核与用户空间共享的键值存储)
触发通知
通过bpf_ringbuf_submit()或perf_event向用户空间发送事件
1 2 3 4 5 6 7 8 SEC("tracepoint/syscalls/sys_enter_openat" ) int trace_openat (struct trace_event_raw_sys_enter *ctx) { bpf_printk("PID %d opening file" , bpf_get_current_pid_tgid() >> 32 ); return 0 ; }
用户空间代码负责加载和与内核交互
功能
说明
加载eBPF程序
将编译好的*_kern.o字节码加载进内核,经过验证器检查
附着到钩子点
把程序挂到具体的事件源(如tracepoint、kprobe、XDP等)
管理BPF Map
创建、读取、更新Map,与内核程序交换数据
处理数据输出
从Map或Ring Buffer读取结果,格式化输出(打印、存文件、发网络等)
生命周期控制
信号处理、优雅退出、资源清理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 int main () { struct bpf_object *obj = bpf_object_open_file("program_kern.o" , NULL ); bpf_object_load(obj); struct bpf_program *prog = bpf_object_find_program_by_name(obj, "trace_openat" ); bpf_program_attach(prog); struct bpf_map *map = bpf_object_find_map_by_name(obj, "events" ); while (running) { } bpf_object_close(obj); }
虽然eBPF的功能很强大,但是为了保证内核安全,对eBPF的程序也有很多的限制:
eBPF程序必须被验证器校验通过之后才能执行,且不能包含无法到达的指令;
eBPF程序不能随意调用内核函数,只能调用在API中定义的辅助函数;
eBPF程序栈空间最多只有512字节,可以通过映射保存;
在内核5.2之前,eBPF字节码最多只支持4096条指令,而5.2内核提升至100万条;
由于内核的快速变化,在不同版本内核中运行时,需要访问内核数据结构的eBPF程序就需要修改源代码重新编译。
开发工具链 在实际开发中,eBPF并不是直接使用,而是通过像Cilium、bcc或bpftrace这样的项目间接使用,这些项目提供了eBPF上面的抽象,不需要直接编写定义程序,而是提供了基于意图的来实现的能力,然后用eBPF实现。以下是一些eBPF开发工具:
BCC :一个基于Python的工具链,简化了eBPF程序的编写、编译和加载。它提供了许多预构建的追踪工具,但在依赖和兼容性方面存在一些限制。
eBPF Go库 :一个Go库,解耦了获取eBPF字节码的过程与加载和管理eBPF程序的过程。
libbpf-bootstrap :基于libbpf的现代脚手架,提供了高效的工作流用于编写eBPF程序,提供简单的一次性编译过程以生成可重用的字节码。
eunomia-bpf :一个用于编写仅包含内核空间代码的eBPF程序的工具链,它通过动态加载eBPF程序简化了eBPF程序的开发。
Aya :一个纯Rust实现的eBPF开发框架,提供了完全基于Rust的工具链来同时编写内核空间和用户空间代码。
环境搭建 由于eBPF是内核技术,因此需要具备较新版本的Linux内核(推荐Ubuntu24.04),以支持eBPF功能。以ebpf-go框架为例,安装如下依赖与相关包:
1 2 3 4 5 6 7 sudo apt-get install -y make clang llvm libelf-dev libbpf-dev bpfcc-tools libbpfcc-dev linux-tools-$(uname -r) linux-headers-$(uname -r)go env -w GOPROXY=https://goproxy.cn,direct go install github.com/cilium/ebpf/cmd/bpf2go@latest go install github.com/aya-rs/bpf-linker@latest echo 'export PATH=$PATH:$(go env GOPATH)/bin' >> ~/.bashrcsource ~/.bashrc
或者可以搭建一个Docker环境来进行代码的编写和编译。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 FROM --platform=linux/amd64 ubuntu:24.04 ENV DEBIAN_FRONTEND=noninteractiveENV CLANG_VERSION=18 ENV SSH_USER=rootENV SSH_PASSWORD=rootRUN set -eux; \ sed -i 's|http://archive.ubuntu.com/ubuntu|https://mirrors.aliyun.com/ubuntu|g' /etc/apt/sources.list && \ sed -i 's|http://security.ubuntu.com/ubuntu|https://mirrors.aliyun.com/ubuntu|g' /etc/apt/sources.list && \ apt-get update && \ apt-get install -y --no-install-recommends \ ca-certificates build-essential git make cmake pkg-config \ curl wget vim openssh-server sudo \ clang-${CLANG_VERSION} \ lld-${CLANG_VERSION} \ llvm-${CLANG_VERSION} \ llvm-${CLANG_VERSION} -dev \ libclang-${CLANG_VERSION} -dev \ libbpf-dev \ libelf-dev \ zlib1g-dev \ libssl-dev \ linux-headers-generic \ libc6-dev-amd64-cross \ gcc-x86-64-linux-gnu \ linux-tools-generic \ libncurses-dev \ libcap-dev \ libbfd-dev \ trace-cmd tcpdump strace \ iproute2 iputils-ping net-tools ethtool \ python3 python3-pip python3-dev \ python3-setuptools python3-wheel \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* RUN update-alternatives --install /usr/bin/clang clang /usr/bin/clang-${CLANG_VERSION} 100 \ && update-alternatives --install /usr/bin/clang++ clang++ /usr/bin/clang++-${CLANG_VERSION} 100 \ && update-alternatives --install /usr/bin/llc llc /usr/bin/llc-${CLANG_VERSION} 100 \ && update-alternatives --install /usr/bin/llvm-strip llvm-strip /usr/bin/llvm-strip-${CLANG_VERSION} 100 \ && update-alternatives --install /usr/bin/llvm-objdump llvm-objdump /usr/bin/llvm-objdump-${CLANG_VERSION} 100 RUN set -eux; \ git clone --recurse-submodules --depth=1 https://github.com/libbpf/bpftool.git /tmp/bpftool && \ cd /tmp/bpftool/src && \ make -j$(nproc ) && \ make install && \ bpftool version && \ rm -rf /tmp/bpftool ENV GOPATH=/root/goENV PATH=$PATH:/usr/local/go/bin:$GOPATH/binRUN set -eux; \ arch ="$(dpkg --print-architecture) " ; \ wget -q "https://go.dev/dl/go1.24.0.linux-${arch} .tar.gz" -O /tmp/go.tar.gz && \ tar -C /usr/local -xzf /tmp/go.tar.gz && \ rm /tmp/go.tar.gz && \ go version RUN set -eux; \ go env -w GOPROXY=https://goproxy.cn,direct && \ go env -w GOSUMDB=sum.golang.google.cn && \ go install github.com/cilium/ebpf/cmd/bpf2go@latest && \ which bpf2go ENV CARGO_HOME=/root/.cargoENV RUSTUP_HOME=/root/.rustupENV PATH=$PATH:$CARGO_HOME/binRUN set -eux; \ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain stable && \ . $CARGO_HOME /env && \ rustup toolchain install nightly && \ rustup component add rust-src --toolchain nightly && \ rustup component add rustfmt clippy && \ cargo install cargo-generate RUN set -eux; \ cat >> /root/.bashrc <<'EOF' export GOPATH=/root/go export CARGO_HOME=/root/.cargo export RUSTUP_HOME=/root/.rustup export PATH=/usr/local/go/bin:$GOPATH/bin:$CARGO_HOME/bin:$PATH EOF RUN set -eux; \ mkdir -p /var/run/sshd && \ echo "root:${SSH_PASSWORD} " | chpasswd && \ mkdir -p /root/.ssh && \ chmod 700 /root/.ssh && \ sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config && \ sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config && \ sed -i 's/^#\?UsePAM.*/UsePAM no/' /etc/ssh/sshd_config && \ ssh-keygen -A WORKDIR /workspace EXPOSE 22 CMD ["/usr/sbin/sshd" , "-D" , "-e" ]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 version: "3.8" services: ebpf-dev: build: context: . dockerfile: Dockerfile platforms: - linux/amd64 image: ebpf-dev:latest container_name: ebpf-dev hostname: ebpf-dev privileged: false ports: - "2222:22" volumes: - ./workspace:/workspace - ~/.ssh:/root/.ssh:ro deploy: resources: limits: cpus: '4' memory: 8G environment: - GO_ENV=development - GOPROXY=https://goproxy.cn,direct - GOSUMDB=sum.golang.google.cn restart: unless-stopped stdin_open: true tty: true
测试代码 先编写一个eBPF程序,并使用bpf2go进行编译,该程序运行在内核态。它挂到sock/inet_sock_set_state tracepoint,筛选IPv4、TCP、状态变为已建立、且本地端口是22的连接,然后把客户端IP、客户端端口和服务端端口写进ring buffer,上报给用户态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 #define BPF_NO_GLOBAL_DATA #include "vmlinux.h" #include <bpf/bpf_helpers.h> #include <bpf/bpf_tracing.h> #define AF_INET 2 #define IPPROTO_TCP 6 #define TCP_ESTABLISHED 1 #define SSH_PORT 22 struct event { __u8 src_ip[4 ]; __u16 src_port; __u16 dst_port; }; struct { __uint(type, BPF_MAP_TYPE_RINGBUF); __uint(max_entries, 1 << 24 ); } events SEC (".maps" ) ; SEC("tracepoint/sock/inet_sock_set_state" ) int handle_ssh_conn (struct trace_event_raw_inet_sock_set_state *ctx) { if (ctx->family != AF_INET) { return 0 ; } if (ctx->protocol != IPPROTO_TCP) { return 0 ; } if (ctx->newstate != TCP_ESTABLISHED) { return 0 ; } if (ctx->sport != SSH_PORT) { return 0 ; } struct event *e = bpf_ringbuf_reserve(&events, sizeof (*e), 0 ); if (e) { e->src_ip[0 ] = ctx->daddr[0 ]; e->src_ip[1 ] = ctx->daddr[1 ]; e->src_ip[2 ] = ctx->daddr[2 ]; e->src_ip[3 ] = ctx->daddr[3 ]; e->src_port = ctx->dport; e->dst_port = ctx->sport; bpf_ringbuf_submit(e, 0 ); } return 0 ; } char LICENSE[] SEC("license" ) = "Dual BSD/GPL" ;
接着编写用户态程序,负责加载eBPF对象、把程序挂到tracepoint、打开ring buffer读事件,并把读到的源IP打印出来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 package mainimport ( "bytes" "context" "encoding/binary" "errors" "fmt" "log" "net" "os/signal" "syscall" "github.com/cilium/ebpf/link" "github.com/cilium/ebpf/ringbuf" "github.com/cilium/ebpf/rlimit" ) type event struct { SrcIP [4 ]byte SrcPort uint16 DstPort uint16 } func main () { ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() if err := rlimit.RemoveMemlock(); err != nil { log.Fatalf("remove memlock limit failed: %v" , err) } var objs helloObjects if err := loadHelloObjects(&objs, nil ); err != nil { log.Fatalf("load eBPF objects failed: %v" , err) } defer objs.Close() tp, err := link.Tracepoint("sock" , "inet_sock_set_state" , objs.HandleSshConn, nil ) if err != nil { log.Fatalf("attach tracepoint failed: %v" , err) } defer tp.Close() rd, err := ringbuf.NewReader(objs.Events) if err != nil { log.Fatalf("open ring buffer failed: %v" , err) } defer rd.Close() go func () { <-ctx.Done() _ = rd.Close() }() log.Println("eBPF program attached. Printing SSH source IP on new connections. Press Ctrl+C to exit." ) for { rec, err := rd.Read() if err != nil { if errors.Is(err, ringbuf.ErrClosed) { log.Println("stopping..." ) return } log.Printf("read ring buffer failed: %v" , err) continue } var e event if err := binary.Read(bytes.NewReader(rec.RawSample), binary.LittleEndian, &e); err != nil { log.Printf("decode event failed: %v" , err) continue } srcIP := net.IPv4(e.SrcIP[0 ], e.SrcIP[1 ], e.SrcIP[2 ], e.SrcIP[3 ]) fmt.Printf("new SSH connection: src=%s:%d dst_port=%d\n" , srcIP.String(), e.SrcPort, e.DstPort) } }
参考 eBPF 文档
eBPF 核心技术与实战