eBPF
程序是事件驱动,当内核或用户态程序经过一定的钩子点时运行。预定义的钩子包括系统调用、函数入口/出口、内核跟踪点、网络事件等。
如果特殊场景不存在预定义钩子,ebpf
程序基本上可以通过kprobe
挂钩到内核任何地址或uprobe
挂钩到用户态程序任何地址。
eBPF
程序如何编写在很多场景下,eBPF
并不是直接使用,而是通过一些项目如Cilium
,bcc
,bpftrace
来间接使用。这些项目在eBPF
之上进行抽象,使得不需要直接编写eBPF
程序,而是提供一些基于目标定义的能力,然后由eBPF
实现这些定义。
如果不存在上层抽象,eBPF
就需要直接编写。Linux
内核要求eBPF
程序以一种字节码的方式加载。这将是非常痛苦。如果确实是需要直接写eBPF
字节码,通常的开发实践是利用像LLVM
这样的编译器把伪C
代码编译成eBPF
字节码。
当需要的钩子已经选好了,eBPF
程序可以通过系统调用bpf
加载到Linux
内核。这一般是通过某个可用的eBPF
库来实现。下一节介绍一些可用的开发工具链。
当从eBPF
程序加载到Linux
内核到附加在指定的钩子点前,会经过下面两个步骤:
eBPF
安全运行。它检验程序符合某些条件,如:eBPF
程序到内核的进程拥有相应的权限。如果关闭非特权eBPF
开关,只有特权进程才允许加载eBPF
程序eBPF
程序不能弄崩或破坏系统eBPF
程序必须能够运行结束,意味着程序不能存在不终结的循环。JIT
编译JIT
编译步骤把eBPF
字节编译成机器码来优化运行速度,使得eBPF
程序运行效率和内核代码或内核模块代码一样。
eBPF
程序的一个重要特性是共享收集的信息和存储状态的能力。为此,eBPF
程序可以利用eBPF
映射的概念来存储和检索各种数据结构中的数据。eBPF
映射可以由eBPF
程序访问,也可以由用户态程序通过系统调用访问。
为了了解映射所支持数据结构的多样性,下面是一个不完整的列表,每种映射类型都适用于CPU
内或CPU
之间共享
hash
表,数组LRU
列表LPM
eBPF
程序不可以调用任意内核函数。因为这种方式会使得它和特定内核版本绑定,从而增加兼容性的复杂度。相反,eBPF
程序通过调用由内核提供通用稳定的帮助函数来实现这样能力。
可用的帮助函数系列一直在演变,相应的例子有:
eBPF
映射cgroup
的上下文eBPF
程序可以通过接龙和函数调用来组合。
eBPF
程序内定义和调用函数eBPF
程序调用另外一个eBPF
程序,并替换当前执行上下文,类似execve
系统调用eBPF
安全性《功夫》里阿鬼说的“能力越大,责任越大”
eBPF
是一种难以置信的强大技术,并且是运行在很多关键基础组件的核心。在eBPF
的演变中,当引入它到Linux
内核时,安全性是最关键的考虑因素。eBPF
安全通过某些层次来确保:
在关闭非特权eBPF
开关情况下,所有要把eBPF
程序加载到Linux
内核的进程必须要运行在特权模式(root
)或者要有CAP_BPF
能力。这意味着不可信程序不能加载eBPF
程序。
如果非特权eBPF
开关开启,非特权进程可以加载部分功能受限制的eBPF
程序,且只能有限访问内核
即使一个进程允许加载eBPF
程序,所有eBPF
程序还要经过检验器的检查。一个eBPF
检验器确保程序本身的安全性。
这意味着:
eBPF
程序校验结束后,无论eBPF
是由特权进程或非特权进程加载,都需要经过一个加固流程。步骤包括:
eBPF
程序的内核内存必须是保护和可读。在任何情况,无论是内核缺陷还是恶意操作来修改这块内存,都会引起内核崩溃,而不是继续执行。eBPF
程序会屏蔽内存访问,以便将临时指令访问重定向到受控区域,检验器也会跟踪那些只会在指令预测情况下的分支,并且在接龙调用无法转换成直接调用情况下,JIT
编译器会发现Retpolines
(一种避免指令幽灵漏洞的方法)eBPF
程序的内存区来执行代码。eBPF
程序无法直接访问任意内核内存。访问位于程序上下文之外的数据和对象,需要通过eBPF
帮助函数访问。这保证了数据一致性访问,也遵循了eBPF
的程序权限,比如,如果数据修改保证是安全的,一个eBPF
程序是允许去修改的。一个eBPF
程序是不允许任意修改内核的数据。
暗号:56015