eBPF编程

Posted broler

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了eBPF编程相关的知识,希望对你有一定的参考价值。

简介

         如果读取数据包时eBPF程序想要读取超过数据包边界的内容,eBPF程序将会被停止执行。

硬件架构

寄存器

* R0       - return value from in-kernel function, and exit value for eBPF program

* R1 - R5  - arguments from eBPF program to in-kernel function

* R6 - R9  - callee saved registers that in-kernel function will preserve

* R10      - read-only frame pointer to access stack

 

R1-R5是函数调用的参数寄存器,每次调用了之后这几个寄存器的值可能被改变,所以需要每次调用其他函数后都要重新填充。这5个寄存器被映射到对应平台的实际的参数寄存器,在x86-64下,寄存器的映射方法如下:

    R0 - rax

   R1 - rdi

   R2 - rsi

   R3 - rdx

   R4 - rcx

   R5 - r8

   R6 - rbx

   R7 - r13

   R8 - r14

   R9 - r15

R10 – rbp

因为x86-64规定rdi, rsi,rdx, rcx, r8, r9用来做参数传递,rbx, r12 - r15用来做调用者保留。

 

         R6-R9会在eBPF调用其他函数前后保持一致,所以可以用来放eBPF变量。

函数调用

         一个eBPF函数被调用的时候会自动带一个参数ctx传递给eBPF程序,放在R1里,(在__bpf_prog_run()函数中实现),这个ctx对于用作filter的eBPF程序来说是skb,对于用作seccomp来说是seccomp_data。所以可以看出,一个使用xt_bpf模块的eBPF过滤程序的原理是在约定的hook点,eBPF被调用,skb被作为第一个参数传进eBPF程序,执行完毕,返回值R0作为判断这个包处理结果的返回值(是否丢弃等)。

指令编码

指令类型类型

         在指令编码上,使用8位进行编码,针对不同的指令,这8位的使用情况是不同的,但LSB的最后3位都是用来存储指令类型的。指令主要有如下几种类型:

BPF_LD   0x00

BPF_LDX  0x01

BPF_ST   0x02

BPF_STX  0x03

BPF_ALU  0x04

BPF_JMP  0x05

BPF_ALU64 0x07

         以上是表示指令编码的最后3位。BPF_JMP是跳转类型的指令,目前有10个。BPF_ALU和BPF_ALU64是运算类指令,目前有14个。而剩下的则是加载与存储类型的指令。也就是说eBPF一共有3大类指令:跳转、运算、加载与存储。

跳转:BPF_JMP

当最后3位是BPF_ALU或BPF_JMP时,8位的指令编码如上,中间一位有两种取值,表示这个指令使用的源寄存器:

 

         可以看出这一位如果是BPF_X(0),使用src_reg作为源寄存器,如果是BPF_K(1),则使用32位的立即数作为源寄存器。

最后3位是BPF_JMP时,操作符包括:

 BPF_JA    0x00

 BPF_JEQ   0x10

 BPF_JGT   0x20

 BPF_JGE   0x30

 BPF_JSET  0x40

 BPF_JNE   0x50  /* eBPF only: jump != */

 BPF_JSGT  0x60  /* eBPF only: signed ‘>‘ */

 BPF_JSGE  0x70  /* eBPF only: signed ‘>=‘ */

 BPF_CALL  0x80  /* eBPF only: function call */

 BPF_EXIT  0x90  /* eBPF only: function return */

运算:BPF_ALU和BPF_ALU64

当最后3位是BPF_ALU或者是BPF_ALU64时:

 BPF_ADD   0x00

 BPF_SUB   0x10

 BPF_MUL   0x20

 BPF_DIV   0x30

 BPF_OR    0x40

 BPF_AND   0x50

 BPF_LSH   0x60

 BPF_RSH   0x70

 BPF_NEG   0x80

 BPF_MOD   0x90

 BPF_XOR   0xa0

 BPF_MOV   0xb0  /* eBPF only: mov reg to reg */

 BPF_ARSH  0xc0  /* eBPF only: sign extending shift right */

 BPF_END   0xd0  /* eBPF only: endianness conversion */

举例

BPF_XOR | BPF_K| BPF_ALU 意味着:src_reg = src_reg ^ imm32

BPF_MOV | BPF_X| BPF_ALU :将src_reg的值移动到dst_reg

BPF_ADD | BPF_X| BPF_ALU64 :dst_reg = dst_reg + src_reg

加载与存储

加载与存储指令也有多个,每个可以操作的数据的大小是不一样的,这个大小的区别在中间2个位:

BPF_W   0x00:4个字节

  BPF_H  0x08 :2个字节

  BPF_B  0x10  :1个字节

  BPF_DW 0x18  :8个字节

前3个位的mode包括:

BPF_IMM 0x00  /* used for 32-bit mov inclassic BPF and 64-bit in eBPF */

 BPF_ABS  0x20

 BPF_IND  0x40

 BPF_MEM  0x60

 BPF_LEN  0x80  /* classic BPF only, reserved in eBPF */

 BPF_MSH  0xa0  /* classic BPF only, reserved in eBPF */

 BPF_XADD 0xc0  /* eBPF only,exclusive add */

         其中BPF_ABS和BPF_IND只能用在数据包处理上,在这时,R6里面是输入数据包sk_buff,R0是输出数据包。

 

 

 

  bpf_mov R6, R1 /* save ctx */

   bpf_mov R2, 2

   bpf_mov R3, 3

   bpf_mov R4, 4

   bpf_mov R5, 5

   bpf_call foo

   bpf_mov R7, R0 /* save foo() return value */

   bpf_mov R1, R6 /* restore ctx for next call */

   bpf_mov R2, 6

   bpf_mov R3, 7

   bpf_mov R4, 8

   bpf_mov R5, 9

   bpf_call bar

   bpf_add R0, R7

bpf_exit

 

翻译成x86-64是:

push %rbp

    mov %rsp,%rbp

    sub $0x228,%rsp

    mov %rbx,-0x228(%rbp)

    mov%r13,-0x220(%rbp)

    mov %rdi,%rbx

    mov $0x2,%esi

    mov $0x3,%edx

    mov $0x4,%ecx

    mov $0x5,%r8d

    callq foo

    mov %rax,%r13

    mov %rbx,%rdi

    mov $0x2,%esi

    mov $0x3,%edx

    mov $0x4,%ecx

    mov $0x5,%r8d

    callq bar

    add %r13,%rax

    mov -0x228(%rbp),%rbx

    mov -0x220(%rbp),%r13

    leaveq

    retq

在c就是:

    u64 bpf_filter(u64 ctx)

    {

        return foo(ctx, 2, 3, 4, 5) + bar(ctx,6, 7, 8, 9);

    }

 

可行的bpf开发方法:

使用tcpdump、使用iptables

iptables -A INPUT \

   -p udp --dport 53 \

   -m bpf --bytecode "14,0 0 0 20,177 0 0 0,12 0 0 0,7 0 0 0,64 0 00,21 0 7 124090465,64 0 0 4,21 0 5 1836084325,64 0 0 8,21 0 3 56848237,80 0 012,21 0 1 0,6 0 0 1,6 0 0 0," \

   -j DROP

llvm、内核提供的编译器、iovisitor的uBPF编译器

BPF CompilerCollection (BCC)这个工具集包含很多用来观测内核性能的工具,全部使用eBPF,并且提供了python的外部编程能力。其也是使用llvm用作底层编译器,并且整合了llvm中对bpf支持的最新进展。但是要求内核支持版本是4.1。

使用llvm编译并插入eBPF

使用llvm,使用llvm可以使用如下命令编译:

clang-3.7 -O2 -target bpf -c sockex1_kern.c-o soc1.o -I/lib/modules/3.19.0-15-generic/build/include-I/lib/modules/3.19.0-15-generic/build/arch/x86/include/uapi/

         这样可以编译一个包含了map和eBPF代码的elf文件。然而这个文件并不是用来直接插入内核的eBPf程序代码,它只是一个包含了需要插入内核的各种信息的一个集合体,内核代码在sample/bpf/里面有提供解析这个文件的代码逻辑,可以自动实现解析和插入内核。

         但是这种有个缺点,这种都是使用的bpf系统调用进行插入的eBPF代码,然而这个系统调用只支持插入到特定的内核位置:

BPF_PROG_TYPE_SOCKET_FILTER,    //附在某一个socket上,只对某一个socket产生影响。

BPF_PROG_TYPE_KPROBE,          //附在kprobe上

BPF_PROG_TYPE_SCHED_CLS,       //附在cls_bpf分类模块上

BPF_PROG_TYPE_SCHED_ACT,       //附在act_bpf模块上

         可以看到,无法插入到我们希望的hook点上(因为hook点是netfilter的基础设施,netfilter使用xt_bpf来支持bpf,所以就没有在系统调用层级支持)。而类型BPF_PROG_TYPE_SOCKET_FILTER的bpf,即使是使用raw,得到的数据也只是一份拷贝。这就注定了这个机制只能用来分析,不能用来做过滤。所以,目前这条路不通,过滤只能采用xt_bpf。

使用xt_bpf

         这是iptables的扩展,可以使用-m参数传递bpf代码进内核。我们可以使用内核提供的bpf编译程序编译代码,或者是tcpdump –ddd,或者是iptables的nfbpf_compile.c程序。但是这三个编译得到的都是cBPF代码。iptables的用户端程序也只支持cBPF代码。所以使用xt_bpf模块的合理选择应该是使用cBPF编程。

         如果要使用eBPF编程,可行的方法是在自己实现钩子函数中执行eBPF,或者是复用在xt_bpf的代码。

以上是关于eBPF编程的主要内容,如果未能解决你的问题,请参考以下文章

eBPF理解

eBPF理解

linuxio_uring 和 eBPF 将如何彻底改变 Linux 中的编程

XDP/eBPF — Overview

XDP/eBPF — Overview

ebpf的简单学习