简介
大概讲下对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