简介
大概讲下对ebpf的一些概念跟原理的理解,其实ebpf就是对bpf一些指令的拓展,下面讲bpf原理部分其实跟ebpf是差不多的。分析的内核代码版本为5.4.0
BPF程序如何运行
这部分就讲BPF程序怎么运行的,顺着这个过程,可以把各种原理理顺下

BPF程序
比方说像sockex1_kern.c跟hello_kern.c这种程序,都会有个SEC这样的标志,这种就是BPF程序代码

字节码
BPF程序->CLANG->LLVM得到BPF字节码。编译过程中也可以看到CLANG-BPF的字样

而编译得到的hello_kern.o文件存储了BPF字节码


我们进行反汇编操作,可以看到具体的字节码以及对应的指令
比如BPF字节码bf a1 00 00 00 00 00 00 对应的BPF指令就是r1 = r10

系统调用
我们分析load_bpf_file函数,里面揭示了如何将字节码送入内核的
load_bpf_file
do_load_bpf_file
bpf_load_program /*tools/lib/bpf/bpf.c*/
bpf_load_program_xattr
sys_bpf_prog_load
sys_bpf(BPF_PROG_LOAD, attr, size)
syscall(__NR_bpf,cmd,attr,size)
sys_bpf函数如下,走的系统调用bpf

从另一个角度我们可以用strace工具跟踪下,也可以看到系统调用bpf(BPF_PROG_LOAD,XXXX,....)


kernel/bpf/syscall.c中定义了bpf的系统调用

我们使用的是BPF_PROG_LOAD类型的,所以对应的内核函数是bpf_prog_load

该函数的内容呢简单列一下大概是
/*kernel/syscall/bpf.c*/
static int bpf_prog_load(union bpf_attr *attr, union bpf_attr __user *uattr)
{
...
/*run eBPF verifier*/
/*使用verifer对BPF程序进行合法扫描*/
err = bpf_check(&prog, attr, uattr);
/*分配一个fd与prog关联,最终这个fd会返回用户空间*/
err = bpf_prog_new_fd(prog);
if (err < 0)
bpf_prog_put(prog);
return err;
...
}
校验器verifier
前面分析系统调用的时候,提到内核的bpf_prog_load函数中,有个bpf_check,其实就是verifier。函数定义在kernel/bpf/verifier.c文件中。这里就不去分析啦,反正如果校验器安全校验不过,也就注入不了啦。
解释器
首先先理解一个概念,BPF虚拟机。其实本质上就是内核自己模拟了一个小型的PC。

其组成有
- 11个
64bit的寄存器,寄存器被命名为r0- r10。操作模式默认为 64 位。64位的寄存器也可作32 位子寄存器使用,它们只能通过特殊的ALU(算术逻辑单元)操作访问,使用低32位,高32位使用零填充。
| 寄存器 | 使用 |
|---|---|
| r0 | 包含 BPF 程序退出值的寄存器。退出值的语义由程序类型定义。此外,当将执行交还给内核时,退出值作为 32 位值传递。 |
| r1-r5 | 保存从 BPF 程序到内核辅助函数的参数。其中 r1 寄存器指向程序的上下文(例如,网络程序可以将网络数据包 ( skb)的内核表示作为输入参数)。 |
| r6-r9 | 通用寄存器 |
| r10 | 唯一的只读寄存器,包含用于访问BPF堆栈空间的帧指针地址 |
- 一个程序计数器
- 存储模块,也就是图中的
MAP,可以被用户空间程序用来进行访问 - 解释器,将
BPF字节码转换为特定CPU架构能用的机器码。一共两个,看内核是否使能JIT,不使能则用Interpreter,使能则用JIT
机器码
就是解释器最后把BPF字节码变成特定架构能用的机器码啦。总的流程如下BPF程序-> clang-> llvm -> BPF字节码-> JIT/内核计时器-> 机器码
触发运行
前面其实相当于一个注册的流程,那怎么注入运行的呢,感觉很神奇,不过这里就不研究了。类比就是insmod了, 但是没有ioctl那样。
ebpf
前面很多概念都是BPF的,其实eBPF跟BPF差不多,只是extend了,扩展了一下而已,有两项。
常规项扩展
| 对比项 | BPF | eBPF |
|---|---|---|
| 寄存器数量 | 2个:寄存器A和寄存器X | 10个:R0-R9,此外R10是只读的帧指针寄存器 |
| 寄存器宽度 | 32位 | 64位 |
| 存储 | 16个内存槽位:M[0-15] | 512字节大小的栈空间,外加无限制的映射型存储Map |
| 受限的内核调用 | 非常受限,JIT专用 | 可用,通过BPF_CALL指令 |
| 支持的事件类型 | 网络数据包,seccomp-BPF | 网络数据包,内核函数,用户态函数,跟踪点,用户态标记,PMC |
在x86-64上,所有寄存器都一一映射到硬件寄存器,例如,x86-64的JIT编译器可以将它们映射为:R0 - rax ,R1 - rdi ,R2 - rsi ,R3 -rdx ,R4 - rcx ,R5 - r8 ,R6 - rbx ,R7 - r13 ,R8 - r14 ,R9 - r15 ,R10 - rbp
指令集扩展

编程方式
