Linux内核在2022年主要发布了5.16-5.19以及6.0和6.1这几个版本,每个版本都为eBPF引入了大量的新特性。本文将对这些新特性进行一点简要的介绍,更详细的资料请参考对应的链接信息。总体而言,eBPF在内核中依然是最活跃的模块之一,它的功能特性也还在高速发展中。某种意义上说,eBPF正朝着一个完备的内核态可编程接口快速进化。
eBPF 进阶: 内核新特性进展一览
- BPF kfuncs
- Bloom Filter Map:5.16
- Compile Once – Run Everywhere:Linux 5.17
- bpf_loop() 辅助函数:5.17
- BPF_LINK_TYPE_KPROBE_MULTI:5.18
- 动态指针和类型指针:5.19
- USDT:5.19
- bpf panic:6.1
- BPF 内存分配器、链表:6.1
- user ring buffer 6.1
精选文章推荐阅读:
- 掌握GDB调试工具,轻松排除bug
- 解密Linux内核神器:内存屏障的秘密功效与应用方法
- 牛客网论坛考研计算机组成原理笔记,GitHub已下载量已过百万
- 探索网络通信核心技术,手写TCP/IP用户态协议栈,让性能飙升起来!
- 万字总结简化跨平台编译利器CMake,从入门到项目实战演练!
- 牛客网论坛最具争议的Linux内核成神笔记,GitHub已下载量已过百万
一、eBPF概述
1.1eBPF是什么
eBPF 是一个基于寄存器的虚拟机,使用自定义的 64 位 RISC 指令集,能够在 Linux 内核内运行即时本地编译的 “BPF 程序”,并能访问内核功能和内存的一个子集。这是一个完整的虚拟机实现,不要与基于内核的虚拟机(KVM)相混淆,后者是一个模块,目的是使 Linux 能够作为其他虚拟机的管理程序。eBPF 也是主线内核的一部分,所以它不像其他框架那样需要任何第三方模块(LTTng 或 SystemTap),而且几乎所有的 Linux 发行版都默认启用。熟悉 DTrace 的读者可能会发现 DTrace/BPFtrace 对比非常有用。
在内核内运行一个完整的虚拟机主要是考虑便利和安全。虽然 eBPF 程序所做的操作都可以通过正常的内核模块来处理,但直接的内核编程是一件非常危险的事情 - 这可能会导致系统锁定、内存损坏和进程崩溃,从而导致安全漏洞和其他意外的效果,特别是在生产设备上(eBPF 经常被用来检查生产中的系统),所以通过一个安全的虚拟机运行本地 JIT 编译的快速内核代码对于安全监控和沙盒、网络过滤、程序跟踪、性能分析和调试都是非常有价值的。部分简单的样例可以在这篇优秀的 eBPF 参考中找到。
基于设计,eBPF 虚拟机和其程序有意地设计为不是图灵完备的:即不允许有循环(正在进行的工作是支持有界循环【译者注:已经支持有界循环,#pragma unroll 指令】),所以每个 eBPF 程序都需要保证完成而不会被挂起、所有的内存访问都是有界和类型检查的(包括寄存器,一个 MOV 指令可以改变一个寄存器的类型)、不能包含空解引用、一个程序必须最多拥有 BPF_MAXINSNS 指令(默认 4096)、“主"函数需要一个参数(context)等等。当 eBPF 程序被加载到内核中,其指令被验证模块解析为有向环状图,上述的限制使得正确性可以得到简单而快速的验证。
主要区别如下:
- 允许使用C 语言编写代码片段,并通过LLVM编译成eBPF 字节码;
- cBPF 只实现了SOCKET_FILTER,而eBPF还有KPROBE 、PERF等。
- BPF使用socket 实现了用户态与内核交互,eBPF 则定义了一个专用于eBPF 的新的系统调用,用于装载BPF 代码段、创建和读取BPF map,更加通用。
- BPF map 机制,用于在内核中以key-value 的方式临时存储BPF 代码产生的数据。
对于eBPF可以简单的理解成kernel实现了一个虚拟机机制,将类C代码编译成字节码(后文有详细解释),挂在到内核的钩子上,当钩子被触发时,kernel在虚拟机的"沙盒"中运行字节码,这样既能方便的实现很多功能,也能通过沙箱保证内核的安全性。
-------------------------------感谢大家的支持------------------------------------
【文章福利】小编推荐自己的Linux内核技术交流群:【 865977150】整理了一些个人觉得比较好的学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!!!
[内核资料领取,](https://docs.qq.com/doc/DTmFTc29xUGdNSnZ2) [Linux内核源码学习地址。](https://ke.qq.com/course/4032547?flowToken=1044435)
1.2eBPF的演进
最初的[Berkeley Packet Filter (BPF) PDF]是为捕捉和过滤符合特定规则的网络包而设计的,过滤器为运行在基于寄存器的虚拟机上的程序。
在内核中运行用户指定的程序被证明是一种有用的设计,但最初BPF设计中的一些特性却并没有得到很好的支持。例如,虚拟机的指令集架构(ISA)相对落后,现在处理器已经使用64位的寄存器,并为多核系统引入了新的指令,如原子指令XADD。BPF提供的一小部分RISC指令已经无法在现有的处理器上使用。
因此Alexei Starovoitov在eBPF的设计中介绍了如何利用现代硬件,使eBPF虚拟机更接近当代处理器,eBPF指令更接近硬件的ISA,便于提升性能。其中最大的变动之一是使用了64位的寄存器,并将寄存器的数量从2提升到了10个。由于现代架构使用的寄存器远远大于10个,这样就可以像本机硬件一样将参数通过eBPF虚拟机寄存器传递给对应的函数。另外,新增的BPF_CALL指令使得调用内核函数更加便利。
将eBPF映射到本机指令有助于实时编译,提升性能。3.15内核中新增的eBPF补丁使得x86-64上运行的eBPF相比老的BPF(cBPF)在网络过滤上的性能提升了4倍,大部分情况下会保持1.5倍的性能提升。很多架构 (x86-64, SPARC, PowerPC, ARM, arm64, MIPS, and s390)已经支持即时(JIT)编译。
1.3ebpf环境搭建
编译运行源码samples/bpf中的代码
- 下载内核源码并解压
- /bin/sh: scripts/mod/modpost: No such file or directory 遇到这种错误,需要make scripts
- make M=samples/bpf 需要.config文件,需要保证这些项存在
- 遇到错误libcrypt1.so.1 not found,执行如下代码(https://www.mail-archive.com/debian-bugs-dist@lists.debian.org/msg1818037.html)
$ cd /tmp
$ apt -y download libcrypt1
$ dpkg-deb -x libcrypt1_1%3a4.4.25-2_amd64.deb .
$ cp -av lib/x86_64-linux-gnu/* /lib/x86_64-linux-gnu/
$ apt -y --fix-broken install
5.编译成功,可以执行samples/bpf中的可执行文件。
编译运行自己开发的代码
1. 下载linux source code,编译内核并升级
git clone https://github.com/torvalds/linux.git
cd linux/
git checkout -b v5.0 v5.0
配置文件
cp -a /boot/config-4.14.81.bm.15-amd64 ./.config
echo '
CONFIG_BPF=y
CONFIG_BPF_SYSCALL=y
CONFIG_BPF_JIT=y
CONFIG_HAVE_EBPF_JIT=y
CONFIG_BPF_EVENTS=y
CONFIG_FTRACE_SYSCALLS=y
CONFIG_FUNCTION_TRACER=y
CONFIG_HAVE_DYNAMIC_FTRACE=y
CONFIG_DYNAMIC_FTRACE=y
CONFIG_HAVE_KPROBES=y
CONFIG_KPROBES=y
CONFIG_KPROBE_EVENTS=y
CONFIG_ARCH_SUPPORTS_UPROBES=y
CONFIG_UPROBES=y
CONFIG_UPROBE_EVENTS=y
CONFIG_DEBUG_FS=y
CONFIG_DEBUG_INFO_BTF=y
' >> ./.config
需要添加sid源安装dwarves
apt install dwarves
make oldconfig
apt install libssl-dev
make
make modules_install
make install
reboot
此时:
uname -a
Linux n231-238-061 5.0.0 #1 SMP Mon Dec 13 05:38:52 UTC 2021 x86_64 GNU/Linux
编译bpf helloworld
切换到https://github.com/bpftools/linux-observability-with-bpf的helloworld目录
sed -i 's;/kernel-src;/root/linux;' Makefile
make
有报错:
参考:http://www.helight.info/blog/2021/build-kernel-ebpf-sample/解决
cp /root/linux/include/uapi/linux/bpf.h /usr/include/linux/bpf.h
执行./monitor-exec,有报错
./monitor-exec: error while loading shared libraries: libbpf.so: cannot open shared object file: No such file or directory
解决方法
cd /root/linux/tools/lib/bpf/
make
make install
在 /etc/ld.so.conf 中添加 /usr/local/lib64这一行,运行 sudo ldconfig 重新生成动态库配置信息。
~/linux/tools/lib/bpf# ldconfig -v 2>/dev/null | grep libbpf
libbpf.so.0 -> libbpf.so.0.5.0
libbpf.so -> libbpf.so
最终执行情况:
可能需要安装apt-get install gcc-multilib g++-multilib
安装bpftrace
(1)debian 添加sid源 https://github.com/iovisor/bcc/blob/master/INSTALL.md#debian---source
deb http://deb.debian.org/debian sid main contrib non-free
deb-src http://deb.debian.org/debian sid main contrib non-free
(2)apt install bpftrace https://github.com/iovisor/bpftrace/blob/master/INSTALL.md
1.4使用eBPF可以做什么?
一个eBPF程序会附加到指定的内核代码路径中,当执行该代码路径时,会执行对应的eBPF程序。鉴于它的起源,eBPF特别适合编写网络程序,将该网络程序附加到网络socket,进行流量过滤,流量分类以及执行网络分类器的动作。eBPF程序甚至可以修改一个已建链的网络socket的配置。XDP工程会在网络栈的底层运行eBPF程序,高性能地进行处理接收到的报文。
从下图可以看到eBPF支持的功能:
BPF对网络的处理可以分为tc/BPF和XDP/BPF,它们的主要区别如下(参考该文档):
XDP的钩子要早于tc,因此性能更高:tc钩子使用sk_buff结构体作为参数,而XDP使用xdp_md结构体作为参数,sk_buff中的数据要远多于xdp_md,但也会对性能造成一定影响,且报文需要上送到tc钩子才会触发处理程序。由于XDP钩子位于网络栈之前,因此XDP使用的xdp_buff(即xdp_md)无法访问sk_buff元数据。
structxdp_buff{
/* Linux 5.8*/
void*data;
void*data_end;
void*data_meta;
void*data_hard_start;
structxdp_rxq_info*rxq;
structxdp_txq_info*txq;
u32 frame_sz;/* frame size to deduce data_hard_end/reserved tailroom*/
};
structxdp_rxq_info{
structnet_device*dev;
u32 queue_index;
u32 reg_state;
structxdp_mem_info mem;
} ____cacheline_aligned;/* perf critical, avoid false-sharing */
structxdp_txq_info{
structnet_device*dev;};
data指向page中的数据包的其实位置,data_end指向数据包的结尾。由于XDP允许headroom(见下文),data_hard_start指向page中headroom的起始位置,即,当对报文进行封装时,data会通过bpf_xdp_adjust_head()向data_hard_start移动。
相同的BPF辅助函数也可以用以解封装,此时data会远离data_hard_start。data_meta一开始指向与data相同的位置,但bpf_xdp_adjust_meta() 能够将其朝着 data_hard_start 移动,进而给用户元数据提供空间,这部分空间对内核网络栈是不可见的,但可以被tc BPF程序读取( tc 需要将它从 XDP 转移到 skb)。
反之,可以通过相同的BPF程序将data_meta远离data_hard_start来移除或减少用户元数据大小。data_meta 还可以单纯地用于在尾调用间传递状态,与tc BPF程序访问的skb->cb[]控制块类似。 对于struct xdp_buff中的报文指针,有如下关系 :data_hard_start <= data_meta <= data < data_end。rxq字段指向在ring启动期间填充的额外的与每个接受队列相关的元数据。BPF程序可以检索queue_index,以及网络设备上的其他数据(如ifindex等)。
tc能够更好地管理报文:tc的BPF输入上下文是一个sk_buff,不同于XDP使用的xdp_buff,二者各有利弊。当内核的网络栈在XDP层之后接收到一个报文时,会分配一个buffer,解析并保存报文的元数据,这些元数据即sk_buff。
该结构体会暴露给BPF的输入上下文,这样tc ingress层的tc BPF程序就能够使用网络栈从报文解析到的元数据。使用sk_buff,tc可以更直接地使用这些元数据,因此附加到tc BPF钩子的BPF程序可以读取或写入skb的mark,pkt_type, protocol, priority, queue_mapping, napi_id, cb[] array, hash, tc_classid 或 tc_index, vlan metadata等,而XDP能够传输用户的元数据以及其他信息。tc BPF使用的 struct __sk_buff定义在linux/bpf.h头文件中。xdp_buff 的弊端在于,其无法使用sk_buff中的数据,XDP只能使用原始的报文数据,并传输用户元数据。
XDP的能够更快地修改报文:sk_buff包含很多协议相关的信息(如GSO阶段的信息),因此其很难通过简单地修改报文数据达到切换协议的目的,原因是网络栈对报文的处理主要基于报文的元数据,而非每次访问数据包内容的开销。因此,BPF辅助函数需要正确处理内部sk_buff的转换。而xdp_buff 则不会有这种问题,因为XDP的处理时间早于内核分配sk_buff的时间,因此可以简单地实现对任何报文的修改(但管理起来要更加困难)。
tc/ebpf和xdp可以互补:如果用户需要修改报文,同时对数据进行比较复杂的管理,那么,可以通过运行两种类型的程序来弥补每种程序类型的局限性。XDP程序位于ingress,可以修改完整的报文,并将用户元数据从XDP BPF传递给tc BPF,然后tc可以使用XDP的元数据和sk_buff字段管理报文。
tc/eBPF可以作用于ingress和egress,但XDP只能作用于ingress:与XDP相比,tc BPF程序可以在ingress和egress的网络数据路径上触发,而XDP只能作用于ingress。
tc/BPF不需要改变硬件驱动,而XDP通常会使用native驱动模式来获得更高的性能。但tc BPF程序的处理仍作用于早期的内核网络数据路径上(GRO处理之后,协议处理和传统的iptables防火墙的处理之前,如iptables PREROUTING或nftables ingress钩子等)。而在egress上,tc BPF程序在将报文传递给驱动之前进行处理,即在传统的iptables防火墙(如iptables POSTROUTING)之后,但在内核的GSO引擎之前进行处理。一个特殊情况是,如果使用了offloaded的tc BPF程序(通常通过SmartNIC提供),此时Offloaded tc/eBPF接近于Offloaded XDP的性能。
从下图可以看到TC和XDP的工作位置,可以看到XDP对报文的处理要先于TC:
内核执行的另一种过滤类型是限制进程可以使用的系统调用。通过seccomp BPF实现。
eBPF也可以用于通过将程序附加到tracepoints, kprobes,和perf events的方式定位内核问题,以及进行性能分析。因为eBPF可以访问内核数据结构,开发者可以在不编译内核的前提下编写并测试代码。对于工作繁忙的工程师,通过该方式可以方便地调试一个在线运行的系统。此外,还可以通过静态定义的追踪点调试用户空间的程序(即BCC调试用户程序,如Mysql)。
使用eBPF有两大优势:快速,安全。为了更好地使用eBPF,需要了解它是如何工作的。
1.5内核的eBPF校验器
在内核中运行用户空间的代码可能会存在安全和稳定性风险。因此,在加载eBPF程序前需要进行大量校验。首先通过对程序控制流的深度优先搜索保证eBPF能够正常结束,不会因为任何循环导致内核锁定。严禁使用无法到达的指令;任何包含无法到达的指令的程序都会导致加载失败。
第二个阶段涉及使用校验器模拟执行eBPF程序(每次执行一个指令)。在每次指令执行前后都需要校验虚拟机的状态,保证寄存器和栈的状态都是有效的。严禁越界(代码)跳跃,以及访问越界数据。
校验器不会检查程序的每条路径,它能够知道程序的当前状态是否是已经检查过的程序的子集。由于前面的所有路径都必须是有效的(否则程序会加载失败),当前的路径也必须是有效的,因此允许验证器“修剪”当前分支并跳过其模拟阶段。
校验器有一个"安全模式",禁止指针运算。当一个没有CAP_SYS_ADMIN特权的用户加载eBPF程序时会启用安全模式,确保不会将内核地址泄露给非特权用户,且不会将指针写入内存。如果没有启用安全模式,则仅允许在执行检查之后进行指针运算。例如,所有的指针访问时都会检查类型,对齐和边界冲突。
无法读取包含未初始化内容的寄存器,尝试读取这类寄存器中的内容将导致加载失败。R0-R5的寄存器内容在函数调用期间被标记未不可读状态,可以通过存储一个特殊值来测试任何对未初始化寄存器的读取行为;对于读取堆栈上的变量的行为也进行了类似的检查,确保没有指令会写入只读的帧指针寄存器。
最后,校验器会使用eBPF程序类型(见下)来限制可以从eBPF程序调用哪些内核函数,以及访问哪些数据结构。例如,一些程序类型可以直接访问网络报文。、
1.6pf()系统调用
使用bpf()系统调用和BPF_PROG_LOAD命令加载程序。该系统调用的原型为:
int bpf(int cmd, union bpf_attr *attr, unsigned int size);
bpf_attr允许数据在内核和用户空间传递,具体类型取决于cmd参数。
cmd可以是如下内容:
BPF_MAP_CREATE
Create a map and return a file descriptor that refers to the
map. The close-on-exec file descriptor flag (see fcntl(2)) is
automatically enabled for the new file descriptor.
BPF_MAP_LOOKUP_ELEM
Look up an element by key in a specified map and return its
value.
BPF_MAP_UPDATE_ELEM
Create or update an element (key/value pair) in a specified
map.
BPF_MAP_DELETE_ELEM
Look up and delete an element by key in a specified map.
BPF_MAP_GET_NEXT_KEY
Look up an element by key in a specified map and return the
key of the next element.
BPF_PROG_LOAD
Verify and load an eBPF program, returning a new file descrip‐
tor associated with the program. The close-on-exec file
descriptor flag (see fcntl(2)) is automatically enabled for
the new file descriptor.
size参数给出了bpf_attr联合体对象的字节长度。BPF_PROG_LOAD加载的命令可以用于创建和修改eBPF maps,maps是普通的key/value数据结构,用于在eBPF程序和内核空间或用户空间之间通信。其他命令允许将eBPF程序附加到一个控制组目录或socket文件描述符上,迭代所有的maps和程序,以及将eBPF对象固定到文件,这样在加载eBPF程序的进程结束后不会被销毁(后者由tc分类器/操作代码使用,因此可以将eBPF程序持久化,而不需要加载的进程保持活动状态)。完整的命令可以参考bpf()帮助文档。
虽然可能存在很多不同的命令,但大体可以分为两类:与eBPF程序交互的命令,与eBPF maps交互的命令,或同时与程序和maps交互的命令(统称为对象)。
1.7eBPF程序类型
使用BPF_PROG_LOAD加载的程序类型确定了四件事:附加的程序的位置,验证器允许调用的内核辅助函数,是否可以直接访问网络数据报文,以及传递给程序的第一个参数对象的类型。实际上,程序类型本质上定义了一个API。创建新的程序类型甚至纯粹是为了区分不同的可调用函数列表(例如,BPF_PROG_TYPE_CGROUP_SKB 和BPF_PROG_TYPE_SOCKET_FILTER)。
当前内核支持的eBPF程序类型为:
- BPF_PROG_TYPE_SOCKET_FILTER: a network packet filter
- BPF_PROG_TYPE_KPROBE: determine whether a kprobe should fire or not
- BPF_PROG_TYPE_SCHED_CLS: a network traffic-control classifier
- BPF_PROG_TYPE_SCHED_ACT: a network traffic-control action
- BPF_PROG_TYPE_TRACEPOINT: determine whether a tracepoint should fire or not
- BPF_PROG_TYPE_XDP: a network packet filter run from the device-driver receive path
- BPF_PROG_TYPE_PERF_EVENT: determine whether a perf event handler should fire or not
- BPF_PROG_TYPE_CGROUP_SKB: a network packet filter for control groups
- BPF_PROG_TYPE_CGROUP_SOCK: a network packet filter for control groups that is allowed to modify socket options
- BPF_PROG_TYPE_LWT_*: a network packet filter for lightweight tunnels
- BPF_PROG_TYPE_SOCK_OPS: a program for setting socket parameters
- BPF_PROG_TYPE_SK_SKB: a network packet filter for forwarding packets between sockets
- BPF_PROG_CGROUP_DEVICE: determine if a device operation should be permitted or not
随着新程序类型的增加,内核开发人员也会发现需要添加新的数据结构。
1.8eBPF数据结构
eBPF使用的主要的数据结构是eBPF map,这是一个通用的数据结构,用于在内核或内核和用户空间传递数据。其名称"map"也意味着数据的存储和检索需要用到key。
使用bpf()系统调用创建和管理map。当成功创建一个map后,会返回与该map关联的文件描述符。关闭相应的文件描述符的同时会销毁map。每个map定义了4个值:类型,元素最大数目,数值的字节大小,以及key的字节大小。eBPF提供了不同的map类型,不同类型的map提供了不同的特性。
- BPF_MAP_TYPE_HASH: a hash table
- BPF_MAP_TYPE_ARRAY: an array map, optimized for fast lookup speeds, often used for counters
- BPF_MAP_TYPE_PROG_ARRAY: an array of file descriptors corresponding to eBPF programs; used to implement jump tables and sub-programs to handle specific packet protocols
- BPF_MAP_TYPE_PERCPU_ARRAY: a per-CPU array, used to implement histograms of latency
- BPF_MAP_TYPE_PERF_EVENT_ARRAY: stores pointers to struct perf_event, used to read and store perf event counters
- BPF_MAP_TYPE_CGROUP_ARRAY: stores pointers to control groups
- BPF_MAP_TYPE_PERCPU_HASH: a per-CPU hash table
- BPF_MAP_TYPE_LRU_HASH: a hash table that only retains the most recently used items
- BPF_MAP_TYPE_LRU_PERCPU_HASH: a per-CPU hash table that only retains the most recently used items
- BPF_MAP_TYPE_LPM_TRIE: a longest-prefix match trie, good for matching IP addresses to a range
- BPF_MAP_TYPE_STACK_TRACE: stores stack traces
- BPF_MAP_TYPE_ARRAY_OF_MAPS: a map-in-map data structure
- BPF_MAP_TYPE_HASH_OF_MAPS: a map-in-map data structure
- BPF_MAP_TYPE_DEVICE_MAP: for storing and looking up network device references
- BPF_MAP_TYPE_SOCKET_MAP: stores and looks up sockets and allows socket redirection with BPF helper functions
所有的map都可以通过eBPF或在用户空间的程序中使用 bpf_map_lookup_elem() 和bpf_map_update_elem()函数进行访问。某些map类型,如socket map,会使用其他执行特殊任务的eBPF辅助函数。eBPF的更多细节可以参见官方帮助文档。
注: 在Linux4.4之前,bpf()要求调用者具有CAP_SYS_ADMIN capability权限,从Linux 4.4.开始,非特权用户可以使用BPF_PROG_TYPE_SOCKET_FILTER类型和相应的map创建受限的程序,然而这类程序无法将内核指针保存到map中,仅限于使用如下辅助函数: * get_random * get_smp_processor_id * tail_call * ktime_get_ns 可以通过sysctl禁用非特权访问: /proc/sys/kernel/unprivileged_bpf_disabled eBPF对象(maps和程序)可以在不同的进程间共享。例如,在fork之后,子进程会继承引用eBPF对象的文件描述符。此外,引用eBPF对象的文件描述符可以通过UNIX域socket传输。引用eBPF对象的文件描述符可以通过dup(2)和类似的调用进行复制。当所有引用对象的文件描述符关闭后,才会释放eBPF对象。eBPF程序可以使用受限的C语言进行编写,并使用clang编译器编译为eBPF字节码。受限的C语言会禁用很多特性,如循环,全局变量,浮点数以及使用结构体作为函数参数。可以在内核源码的samples/bpf/*_kern.c 文件中查看例子。 内核中的just-in-time (JIT)可以将eBPF字节码转换为机器码,提升性能。在Linux 4.15之前,默认会禁用JIT,可以通过修改/proc/sys/net/core/bpf_jit_enable启用JIT。
- 0 禁用JIT
- 1 正常编译
- 2 dehub模式。
从Linux 4.15开始,内核可能会配置CONFIG_BPF_JIT_ALWAYS_ON 选项,这种情况下,会启用JIT编译器,bpf_jit_enable 会被设置为1。
如下架构支持eBPF的JIT编译器:
- * x86-64 (since Linux 3.18; cBPF since Linux 3.0);
- * ARM32 (since Linux 3.18; cBPF since Linux 3.4);
- * SPARC 32 (since Linux 3.18; cBPF since Linux 3.5);
- * ARM-64 (since Linux 3.18);
- * s390 (since Linux 4.1; cBPF since Linux 3.7);
- * PowerPC 64 (since Linux 4.8; cBPF since Linux 3.1);
- * SPARC 64 (since Linux 4.12);
- * x86-32 (since Linux 4.18);
- * MIPS 64 (since Linux 4.18; cBPF since Linux 3.16);
- * riscv (since Linux 5.1)
1.9eBPF辅助函数
可以参考官方帮助文档查看libbpf库提供的辅助函数。
官方文档给出了现有的eBPF辅助函数。更多的实例可以参见内核源码的samples/bpf/和tools/testing/selftests/bpf/目录。
在官方帮助文档中有如下补充:
由于在编写帮助文档的同时,也同时在进行eBPF开发,因此新引入的eBPF程序或map类型可能没有及时添加到帮助文档中,可以在内核源码树中找到最准确的描述: include/uapi/linux/bpf.h:主要的BPF头文件。包含完整的辅助函数列表,以及对辅助函数使用的标记,结构体和常量的描述 net/core/filter.c:包含大部分与网络有关的辅助函数,以及使用的程序类型列表 kernel/trace/bpf_trace.c:包含大部分与程序跟踪有关的辅助函数 kernel/bpf/verifier.c:包含特定辅助函数使用的用于校验eBPF map有效性的函数 kernel/bpf/:该目录中的文件包含了其他辅助函数(如cgroups,sockmaps等)
如何编写eBPF程序
历史上,需要使用内核的bpf_asm汇编器将eBPF程序转换为BPF字节码。幸运的是,LLVM Clang编译器支持将C语言编写的eBPF后端编译为字节码。bpf()系统调用和BPF_PROG_LOAD命令可以直接加载包含这些字节码的对象文件。
可以使用C编写eBPF程序,并使用Clang的 -march=bpf参数进行编译。在内核的samples/bpf/ 目录下有很多eBPF程序的例子。大多数文件名中都有一个_kern.c后缀。Clang编译出的目标文件(eBPF字节码)需要由一个本机运行的程序进行加载(通常为使用_user.c开头的文件)。为了简化eBPF程序的编写,内核提供了libbpf库,可以使用辅助函数来加载,创建和管理eBPF对象。
例如,一个eBPF程序和使用libbpf的用户程序的大体流程为:
- 在用户程序中读取eBPF字节流,并将其传递给bpf_load_program()。
- 当在内核中运行eBPF程序时,将会调用bpf_map_lookup_elem()在一个map中查找元素,并保存一个新的值。
- 用户程序会调用 bpf_map_lookup_elem() 读取由eBPF程序保存的内核数据。
然而,大部分的实例代码都有一个主要的缺点:需要在内核源码树中编译自己的eBPF程序。幸运的是,BCC项目解决了这类问题。它包含了一个完整的工具链来编写并加载eBPF程序,而不需要链接到内核源码树。
二、eBPF框架
在开始说明之前先解释下eBPF上的名词,来帮忙更好的理解:
eBPF bytecode:将C语言写的钩子代码,通过clang编译成二进制字节码,通过程序加载到内核中,钩子触发后在kernel "虚拟机"中运行。
JIT: Just-in-time compilation,将字节码编译成本地机器码来提升运行速度,和Java中的概念类似。
Maps:钩子代码可以将一些统计类信息保存在键值对的map中,来与用户空间程序进行通信,传递数据。
关于eBPF机制详细的讲解网上有很多,这里就不展开了,这里先上一张图,这里包括了使用或者编写ebpf涉及到的所有东西,下面会对这个图进行详细的讲解。
foo_kern.c 钩子实现代码,主要负责:
- 声明使用的Map节点
- 声明钩子挂载点及处理函数
通过LLVM/clang编译成字节码
- 编译命令:clang --target=bpf
- android平台有集成eBPF的编译,后文会提到
foo_user.c 用户空间处理函数,主要负责:
- 将foo_kern.c 编译成的字节码加载到kenel中
- 读取Map中的信息并处理输出给用户
kernel当收到eBPF的加载请求时,会先对字节码进行验证,并通过JIT编译为机器码,当钩子事件来临后,调用钩子函数,kernel会对加载的字节码进行验证,来保证系统的安全性,主要验证规则如下:
- a. 检查是否声明了GNU GPL,检查kernel的版本是否支持
- b. 函数调用规则:
允许bpf函数之间的相互调用
只允许调用kernel允许的BPF helper函数,具体可以参考linux/bpf.h文件
上述以外的函数及动态链接都是不允许的。
- c. 流程处理规则:
不允许使用loop循环以防止进入死循环卡死kernel
不允许有不可到达的分支代码
- d. 堆栈大小被限制在MAX_BPF_STACK范围内。
- e. 编译的字节码大小被限制在BPF_COMPLEXITY_LIMIT_INSNS范围内。
钩子挂载点,主要包括:
另外在kernel的源代码中samples/bpf目录下有大量的示例,感兴趣的可以阅读下。