eBPF的工作原理:从历史演变到内部机制详解
eBPF的工作原理:从历史演变到内部机制详解
eBPF(扩展可编程伯克利数据包过滤器)是一种在Linux内核中安全高效执行用户定义程序的通用环境。它最初是为加速TCP dump和包过滤而设计的,现已演变为一种强大的内核扩展机制,广泛应用于网络、安全、性能监控等领域。本文将深入探讨eBPF的工作原理,包括其历史演变、内部机制、编译过程、事件附着方式以及数据交互等核心概念。
eBPF的历史
BPF(Berkeley Packet Filter)最早出现在BSD系统中,主要用于加速TCP dump和包过滤。它通过让用户空间定义过滤器并将其编译成最高效的指令,在包接收流程的最短路径上运行,从而实现高性能的包过滤。
2013年,BPF得到了重大改进,演变为eBPF(扩展BPF)。eBPF在规格上进行了扩展,使用64位字长,增加了更多的寄存器,并提供了几乎无限的存储空间(通过map结构体)。更重要的是,eBPF不再局限于包过滤,而是可以附着在各种不同的事件源上,在Linux内核内部运行用户定义的程序。
eBPF的内部机制
上图展示了一个追踪工具的简化工作流程。在用户空间,BPF工具包含一个BPF程序,该程序被编译为BPF字节码。字节码随后被传输到内核部分的验证器进行验证。验证通过后,程序可以附着到不同的事件源。事件源在用户空间就已经确定,并且在执行过程中,可以通过perf buffer或map进行内核态到用户空间的数据传输。
BPF程序的转换
下面将详细解释BPF程序从源代码到字节码的转换过程。
AST(抽象语法树)
BPF程序首先被解析为抽象语法树(AST)。这个过程使用lex和yacc工具完成。例如,当程序中使用内置变量pid
时,lex会将其识别为builtin
,然后在yacc中匹配并分配相应的AST节点。类似地,printf
、字符串以及kprobe
等元素也会被解析为AST节点。
Clang解析器
接下来,使用Clang解析器处理结构体信息。这一步主要在内核中发挥作用,当需要遍历结构体或获取追踪点参数信息时,Clang解析器会解引用并获取正确的内存偏移量。然而,在本例中,由于没有使用结构体,因此可以跳过这一步。
语义分析
语义分析阶段检查语法错误,并调用内部函数创建map和probe。在本例中,分析器检测到了错误的pidd
输入。
LLVM IR
通过语义分析后,程序被视为合法(尽管尚未通过验证)。接下来,使用LLVM编译器生成BPF字节码。LLVM支持BPF目标,因此可以先将程序预处理为LLVM IR,然后再生成字节码。
BPF字节码
最终生成的BPF字节码遵循Linux内核中定义的格式。eBPF使用64位指令宽度。例如,上面的示意图展示了call get_current_pid_tgid
指令的字节码表示。
内核虚拟机
BPF字节码生成后,需要传递给内核进行进一步处理。
bpf()系统调用
使用strace
工具可以追踪BPF字节码的加载过程,这通常涉及一个bpf()
系统调用。
验证器
字节码传递给内核后,首先由验证器确认其安全性。验证器检查指令的合法性,包括内存读写范围是否合法、代码路径是否有限(可达)等。例如,验证器会检查函数ID的范围,确保没有无限循环或回跳。
JIT编译
通过验证后,字节码可以被JIT(即时编译)编译为机器码。JIT编译不仅提高了执行速度,还增强了安全性。新版内核已经不再使用解释器,而是推荐使用JIT编译。
事件附着
接下来介绍BPF程序如何附着到特定事件上。
kprobe实现
kprobe利用ftrace机制,在函数调用时触发探测点。ftrace在内核启动时将__fentry__
调用替换为nop指令,在需要时再替换回fentry。BPF/kprobe只需将处理例程添加到fentry上即可。
tracepoint实现
tracepoint是一种静态追踪手段,预先定义在内核源代码中。与kprobe相比,tracepoint更稳定,适合长期使用的工具。tracepoint通过在编译时预留nop指令,在使能时替换为jmp指令,停用时再恢复为nop指令。
数据交互
最后介绍数据的汇总和输出机制。
输出缓冲区
printf()
使用perf buffer进行数据输出。perf buffer为每个CPU核心提供一个缓冲区,用户空间通过epoll_wait()
收集数据。由于数据可能乱序,需要在应用层进行排序处理。
map
map是一种键值对存储结构,用于在内核和用户空间之间传递数据。常见的类型包括hash map,可以使用bpf()
系统调用创建,并通过BPF辅助函数进行操作。需要注意的是,遍历map会产生大量系统调用,因此通常在程序退出时一次性输出数据。
通过以上讨论的一系列流程,我们得到了最终的eBPF程序执行结果。