透视Linux内核,BPF 深度分析与案例讲解

本次主要对BPF的部分原理、应用案例上进行一次分析记录。

BPF介绍

当内核触发事件时,BPF虚拟机能够运行相应的BPF程序指令,但是并不是意味着BPF程序能访问内核触发的所有事件。将BPF目标文件加载到BPF虚拟机时,需要确定特定的程序类型,内核根据BPF程序类型决定BPF程序的上下文参数、在什么事件发生的时候触发,如BPF_PROG_TYPE_SOCKET_FILTER型程序(套接字过滤器程序)的上下文参数是struct __sk_buff(注意并不是struct sk_buff,其只是对struct sk_buff结构体的关键成员变量的封装),套接字过滤器程序在嗅探到网络收发包事件上触发;BPF_PROG_TYPE_KPROBE(kprobe类型BPF程序)的上下文参数是struct pt regs *ctx,可以从中访问寄存器,BPF附件都kprobe后,启动并命中时触发。

上面的描述中涉及到的几个重要的点:

什么是BPF虚拟机?什么是BPF程序指令?BPF文件目标格式是什么?BPF程序类型有哪些?用户编写的程序是如何加载的?下面进行分析:

BPF指令与BPF虚拟机

BPF指令解析:

核心的结构体如下,每一条eBPF指令都以一个bpf_insn来表示,在cBPF中是其他的一个结构体(struct sock_filter ),不过最终都会转换成统一的格式,这里我们只研究eBPF。

struct bpf_insn {
 __u8 code;  /* opcode */
 __u8 dst_reg:4; /* dest register */
 __u8 src_reg:4; /* source register */
 __s16 off;  /* signed offset */
 __s32 imm;  /* signed immediate constant */
};

由结构体中__u8 code;可以知道,一条bpf指令是8个字节长。这8位的0,1,2位表示的是该操作指令的类别,共8种:

BPF_LD(0X00) 、BPF_LDX(0x01) 、BPF_ST(0x02)、BPF_STX(0x03) 、BPF_ALU(0x04)、BPF_JMP(0x05)、BPF_RET(0x06) 、BPF_MISC(0x07)

BPF(默认指eBPF非cBPF) 程序指令都是64位的,使用了 11 个 64位寄存器,32位称为半寄存器(subregister)和一个程序计数器(program counter),一个大小为 512 字节的 BPF 栈。所有的 BPF 指令都有着相同的编码方式。eBPF虚拟指令系统属于RISC,拥有11个虚拟寄存器,r0-r10,在实际运行时,虚拟机会把这11个寄存器一 一对应于硬件CPU的物理寄存器。如下是x64为例,虚拟寄存器与真实的寄存器的对应关系以及BPF指令的编码方式:

   //R0 - 保存返回值
    //R1-R5 参数传递
    //R6-R9 保存临时变量
    //R10 只读,用做栈指针
    R0 – rax
    R1 - rdi
    R2 - rsi
    R3 - rdx
    R4 - rcx
    R5 - r8
    R6 - rbx
    R7 - r13
    R8 - r14
    R9 - r15
    R10 – rbp(帧指针,frame pointer)
msb                                                        lsb
+------------------------+----------------+----+----+--------+
|immediate               |offset          |src |dst |opcode  |
+------------------------+----------------+----+----+--------+

从最低位到最高位分别是:

  • 8 位的 opcode,有 BPF_X 类型的基于寄存器的指令,也有 BPF_K 类型的基于立即数的指令

  • 4 位的目标寄存器 (dst)

  • 4 位的原始寄存器 (src)

  • 16 位的偏移(有符号),是相对于栈、映射值(map values)、数据包(packet data)等的相对偏移量

  • 32 位的立即数 (imm)(有符号)

大多数指令都不会使用所有的域,未被使用的部分就会被置为0。编程时只需要将我们想要程序放到一个bpf_insn数组中,也就代表了整个BPF程序的所有指令,如下所示:

struct bpf_insn insn[] = {
   BPF_MOV64_IMM(BPF_REG_1, 0xa21), 
      ......
    BPF_EXIT_INSN(),
};

如上面的 BPF_MOV64_IMM与 BPF_EXIT_INSN在内核include\linux\filter.h中的宏定义:

#define BPF_MOV64_IMM(DST, IMM)     \
 ((struct bpf_insn) {     \
  .code  = BPF_ALU64 | BPF_MOV | BPF_K,  \
  .dst_reg = DST,     \
  .src_reg = 0,     \
  .off   = 0,     \
  .imm   = IMM })
  
#define BPF_EXIT_INSN()      \
 ((struct bpf_insn) {     \
  .code  = BPF_JMP | BPF_EXIT,   \
  .dst_reg = 0,     \
  .src_reg = 0,     \
  .off   = 0,     \
  .imm   = 0 })  
再例如,编写程序kprobe类型的BPF程序,当触发相应事件时输出helloword,。

将相应的BPF内核代码,写入结构体bpf_insn_prog中如下:

struct bpf_insn prog[] = {
  BPF_MOV64_IMM(BPF_REG_1, 0xa21),        /* '!\n' */
        BPF_STX_MEM(BPF_H, BPF_REG_10, BPF_REG_1, -4),
        BPF_MOV64_IMM(BPF_REG_1, 0x646c726f),   /* 'orld' */
        BPF_STX_MEM(BPF_W, BPF_REG_10, BPF_REG_1, -8),
        BPF_MOV64_IMM(BPF_REG_1, 0x57202c6f),   /* 'o, W' */
        BPF_STX_MEM(BPF_W, BPF_REG_10, BPF_REG_1, -12),
        BPF_MOV64_IMM(BPF_REG_1, 0x6c6c6548),   /* 'Hell' */
        BPF_STX_MEM(BPF_W, BPF_REG_10, BPF_REG_1, -16),
        BPF_MOV64_IMM(BPF_REG_1, 0),   
        BPF_STX_MEM(BPF_B, BPF_REG_10, BPF_REG_1, -2),
        BPF_MOV64_REG(BPF_REG_1, BPF_REG_10),
        BPF_ALU64_IMM(BPF_ADD, BPF_REG_1, -16),
        BPF_MOV64_IMM(BPF_REG_2, 15),   
        BPF_RAW_INSN(BPF_JMP|BPF_CALL, 0,0,0, BPF_FUNC_trace_printk),
        BPF_MOV64_IMM(BPF_REG_0, 0),
        BPF_EXIT_INSN(),
 };

但是采用这种方式进行BPF编程需要熟悉BPF汇编,不建议使用,开发者往往使用C语言、python等高级语言(如BCC)编写BPF程序,然后通过llvm、clang等编译器将其编译成BPF字节码。

例如下面的BPF Hello word程序:

hello_kern.c:

#include 
#include 
#include "bpf_helpers.h"
SEC("tracepoint/sched/sched_switch")
int bpf_prog(void *ctx){
   char msg[] = "hello BPF!\n";
   bpf_trace_printk(msg, sizeof(msg));
   return 0;
}

char _license[] SEC("license") = "GPL";

hello_user.c:

#include 
#include "bpf_load.h"

int main(int argc, char **argv)
{
 if( load_bpf_file("hello_kern.o") != 0)
 {
  printf("The kernel didn't load BPF program\n");
  return -1;
 }

 read_trace_pipe();
 return 0;
}

编译运行该程序,可参考该链接(https://cloudnative.to/blog/compile-bpf-examples/) 主要是修改内核案例中写好的Makefile文件。

BPF目标文件

将内核给出的BPF案例进行编译后(BPF编程环境、编译参考BPF编程环境搭建),编译后生成的ELF文件格式的目标文件,随便选择其中一个(sockex1_kern.o)进行分析:

查看文件头信息

#如下Machine字段,显示:Linux BPF
dx@ubuntu:/usr/src/linux-4.15.0/samples/bpf$ readelf -h sockex1_kern.o
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              REL (Relocatable file)
  Machine:                           Linux BPF
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          0 (bytes into file)
  Start of section headers:          528 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           0 (bytes)
  Number of program headers:         0
  Size of section headers:           64 (bytes)
  Number of section headers:         10
  Section header string table index: 1

相关视频推荐

神奇的linux技术:Linux观测技术bpf,用bpf来观测tcp网络

bpf开启内核探测的另一种方案+bpftrace

tcpip,accept,11个状态,细枝末节的秘密,还有哪些你不知道

免费学习地址:c/c++ linux服务器开发/后台架构师

需要C/C++ Linux服务器架构师学习资料加qun812855908获取(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享

透视Linux内核,BPF 深度分析与案例讲解_第1张图片

 

查看文件的节头信息

如下所示,13行就是指明此BPF程序的类型,“socket”就是BPF_PROG_TYPE_SOCKET_FILTER类型,下面章节会进行介绍,其他的节头如"Map"、"license"是用户编程通过SEC()宏进行自定义的节头。

SEC宏的定义:#define SEC(NAME) attribute((section(NAME), used))

dx@ubuntu:/usr/src/linux-4.15.0/samples/bpf$ readelf -S sockex1_kern.o
There are 10 section headers, starting at offset 0x210:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .strtab           STRTAB           0000000000000000  000001b8
       0000000000000056  0000000000000000           0     0     1
  [ 2] .text             PROGBITS         0000000000000000  00000040
       0000000000000000  0000000000000000  AX       0     0     4
  [ 3] socket            PROGBITS         0000000000000000  00000040
       0000000000000078  0000000000000000  AX       0     0     8
  [ 4] .relsocket        REL              0000000000000000  00000198
       0000000000000010  0000000000000010           9     3     8
  [ 5] maps              PROGBITS         0000000000000000  000000b8
       000000000000001c  0000000000000000  WA       0     0     4
  [ 6] license           PROGBITS         0000000000000000  000000d4
       0000000000000004  0000000000000000  WA       0     0     1
  [ 7] .eh_frame         PROGBITS         0000000000000000  000000d8
       0000000000000030  0000000000000000   A       0     0     8
  [ 8] .rel.eh_frame     REL              0000000000000000  000001a8
       0000000000000010  0000000000000010           9     7     8
  [ 9] .symtab           SYMTAB           0000000000000000  00000108
       0000000000000090  0000000000000018           1     3     8
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  p (processor specific)

如程序sockex1_kern.c:

#include 
#include 
#include 
#include 
#include "bpf_helpers.h"
//定义了一个MAP头,声名要创建一个MAP,内核识别到"map"后会创建相应的内存
struct bpf_map_def SEC("maps") my_map = {
 .type = BPF_MAP_TYPE_ARRAY,
 .key_size = sizeof(u32),
 .value_size = sizeof(long),
 .max_entries = 256,
};
//定义了一个socket头,声明了该BPF的程序类型,而程序类型决定了什么时候触发BPF内核程序
SEC("socket1")
int bpf_prog1(struct __sk_buff *skb)
{
 int index = load_byte(skb, ETH_HLEN + offsetof(struct iphdr, protocol));
 long *value;

 if (skb->pkt_type != PACKET_OUTGOING)
  return 0;

 value = bpf_map_lookup_elem(&my_map, &index);
 if (value)
  __sync_fetch_and_add(value, skb->len);

 return 0;
}
//定义了一个license头,声名遵循的GPL协议
char _license[] SEC("license") = "GPL";

而声名的这些section,内核是如何识别、处理这些section的(也就是说BPF虚拟机是如何根据SEC属性进行执行相应程序的)将在后面章节进行分析。

下面将分析一下,BPF虚拟机是如何根据程序定义的section进行处理的:

上面的sockex1_kern.c通过编译成sockex1_kern.o的目标文件,并最终用户在用户态通过BPF辅助函数将sockex1_kern.o插入到内核,在BPF虚拟机上处理。

其中最重要的函数就是:load_bpf_file (samples/bpf/bpf_load.c)

用户态:main->load_bpf_file ->do_load_bpf_file

在do_load_bpf_file:

static int do_load_bpf_file(const char *path, fixup_map_cb fixup_map)
{
    ......
        
// 循环读取elf中所有的sections,获取license和map相关信息
 for (i = 1; i < ehdr.e_shnum; i++) {
  if (get_sec(elf, i, &ehdr, &shname, &shdr, &data))//shname为“section”的名字
   continue;

  if (0) /* helpful for llvm debugging */
   printf("section %d:%s data %p size %zd link %d flags %d\n",
          i, shname, data->d_buf, data->d_size,
          shdr.sh_link, (int) shdr.sh_flags);
        //如果license字段
  if (strcmp(shname, "license") == 0) {
   processed_sec[i] = true;
   memcpy(license, data->d_buf, data->d_size);//把data->d_buf拷贝到license数组中
  } else if (strcmp(shname, "version") == 0) {   //如果是"version"字段
   processed_sec[i] = true;
   if (data->d_size != sizeof(int)) {
    printf("invalid size of version section %zd\n",
           data->d_size);
    return 1;
   }
   memcpy(&kern_version, data->d_buf, sizeof(int));//把data-d_buf拷贝到kern_version变量中
  } else if (strcmp(shname, "maps") == 0) { //如果是maps字段
   int j;

   maps_shndx = i;
   data_maps = data;
   for (j = 0; j < MAX_MAPS; j++)
    map_data[j].fd = -1;
  } else if (shdr.sh_type == SHT_SYMTAB) {
   strtabidx = shdr.sh_link;
   symbols = data;
  }
 }

   ......
       
 //解析map section,并创建bpf系统调用创建map
 if (data_maps) {
        //使用elf中的map section填充map_data
  nr_maps = load_elf_maps_section(map_data, maps_shndx,
      elf, symbols, strtabidx);
  if (nr_maps < 0) {
   printf("Error: Failed loading ELF maps (errno:%d):%s\n",
          nr_maps, strerror(-nr_maps));
   ret = 1;
   goto done;
  }
        /*
         使用系统调用syscall(__NR_bpf,0,attr,size);创建各个map,此时用户空间可以
         使用两个全局变量:map_fd,map_data来定位创建的map,全局变量map_data_count记录着
         该程序创建的map数量
        */
  if (load_maps(map_data, nr_maps, fixup_map))
   goto done;
  map_data_count = nr_maps;

  processed_sec[maps_shndx] = true;
 }

 /* process all relo sections, and rewrite bpf insns for maps */
 for (i = 1; i < ehdr.e_shnum; i++) {
  if (processed_sec[i])
   continue;

  if (get_sec(elf, i, &ehdr, &shname, &shdr, &data))
   continue;

  if (shdr.sh_type == SHT_REL) {
   struct bpf_insn *insns;

   /* locate prog sec that need map fixup (relocations) */
   if (get_sec(elf, shdr.sh_info, &ehdr, &shname_prog,
        &shdr_prog, &data_prog))
    continue;

   if (shdr_prog.sh_type != SHT_PROGBITS ||
       !(shdr_prog.sh_flags & SHF_EXECINSTR))
    continue;

   insns = (struct bpf_insn *) data_prog->d_buf;
   processed_sec[i] = true; /* relo section */

   if (parse_relo_and_apply(data, symbols, &shdr, insns,
       map_data, nr_maps))
    continue;
  }
 }

 /* load programs */
 for (i = 1; i < ehdr.e_shnum; i++) {

  if (processed_sec[i])
   continue;

  if (get_sec(elf, i, &ehdr, &shname, &shdr, &data))
   continue;
        /*判断定义的程序类型并加载,可以看到判断的时候,给出了判断的字符串的大小,也就是编程时,
        我们可以定义Section("xdp1")、Section("xdp2")、Section("socket1")...去区别相同程序类型的不同Section
        */
  if (memcmp(shname, "kprobe/", 7) == 0 ||
      memcmp(shname, "kretprobe/", 10) == 0 ||
      memcmp(shname, "tracepoint/", 11) == 0 ||
      memcmp(shname, "xdp", 3) == 0 ||
      memcmp(shname, "perf_event", 10) == 0 ||
      memcmp(shname, "socket", 6) == 0 ||
      memcmp(shname, "cgroup/", 7) == 0 ||
      memcmp(shname, "sockops", 7) == 0 ||
      memcmp(shname, "sk_skb", 6) == 0) {
   ret = load_and_attach(shname, data->d_buf,
           data->d_size);
   if (ret != 0)
    goto done;
  }
 }
......

上面的do_load_bpf_file函数判断完,程序类型后调用了load_and_attach函数:

static int load_and_attach(const char *event, struct bpf_insn *prog, int size)
{   /*
     根据判断的程序类型,给以下变量赋值0、1
    */
 bool is_socket = strncmp(event, "socket", 6) == 0;
 bool is_kprobe = strncmp(event, "kprobe/", 7) == 0;
 bool is_kretprobe = strncmp(event, "kretprobe/", 10) == 0;
 bool is_tracepoint = strncmp(event, "tracepoint/", 11) == 0;
 bool is_xdp = strncmp(event, "xdp", 3) == 0;
 bool is_perf_event = strncmp(event, "perf_event", 10) == 0;
 bool is_cgroup_skb = strncmp(event, "cgroup/skb", 10) == 0;
 bool is_cgroup_sk = strncmp(event, "cgroup/sock", 11) == 0;
 bool is_sockops = strncmp(event, "sockops", 7) == 0;
 bool is_sk_skb = strncmp(event, "sk_skb", 6) == 0;
 size_t insns_cnt = size / sizeof(struct bpf_insn);
 enum bpf_prog_type prog_type;
 char buf[256];
 int fd, efd, err, id;
 struct perf_event_attr attr = {};

 attr.type = PERF_TYPE_TRACEPOINT;
 attr.sample_type = PERF_SAMPLE_RAW;
 attr.sample_period = 1;
 attr.wakeup_events = 1;
    /*
    开始创建SEC头和实际prog_type之间的关联
    */
     if (is_socket) {
  prog_type = BPF_PROG_TYPE_SOCKET_FILTER;
 } else if (is_kprobe || is_kretprobe) {
  prog_type = BPF_PROG_TYPE_KPROBE;
 } else if (is_tracepoint) {
  prog_type = BPF_PROG_TYPE_TRACEPOINT;
 } else if (is_xdp) {
  prog_type = BPF_PROG_TYPE_XDP;
 } else if (is_perf_event) {
  prog_type = BPF_PROG_TYPE_PERF_EVENT;
 } else if (is_cgroup_skb) {
  prog_type = BPF_PROG_TYPE_CGROUP_SKB;
 } else if (is_cgroup_sk) {
  prog_type = BPF_PROG_TYPE_CGROUP_SOCK;
 } else if (is_sockops) {
  prog_type = BPF_PROG_TYPE_SOCK_OPS;
 } else if (is_sk_skb) {
  prog_type = BPF_PROG_TYPE_SK_SKB;
 } else {
  printf("Unknown event '%s'\n", event);
  return -1;
 }
    fd = bpf_load_program(prog_type, prog, insns_cnt, license, kern_version,
         bpf_log_buf, BPF_LOG_BUF_SIZE);
    ......
}

通过分析上面的do_load_bpf_file函数,大致就明白了在写BPF程序时定义的Section如何进行判断然后进一步加载的了。

BPF程序类型

BPF 相关的程序,首先需要设置为相对应的的程序类型,截止Linux 内核 5.8 程序类型定义有 29 个,而且还是持续增加中,BPF 程序类型(prog_type)决定了程序可以调用的内核辅助函数的子集。BPF 程序类型也决定了程序输入上下文 -- bpf_context 结构的格式,其作为 BPF 程序中的第一个输入参数(数据blog),每类程序都有自己独特的上下文参数。例如,跟踪程序与套接字过滤器程序的辅助函数子集并不完全相同(尽管它们可能具有一些共同的帮助函数)。同样,跟踪程序的输入上下文(context)是一组寄存器值,如套接字过滤器的输入是一个网络数据包。

特定类型的eBPF程序可用的功能集将来可能会增加。

include/uapi/linux/bpf.h

 161 enum bpf_prog_type {
 162         BPF_PROG_TYPE_UNSPEC,
 163         BPF_PROG_TYPE_SOCKET_FILTER,
 164         BPF_PROG_TYPE_KPROBE,
 165         BPF_PROG_TYPE_SCHED_CLS,
 166         BPF_PROG_TYPE_SCHED_ACT,
 167         BPF_PROG_TYPE_TRACEPOINT,
 168         BPF_PROG_TYPE_XDP,
 169         BPF_PROG_TYPE_PERF_EVENT,
 170         BPF_PROG_TYPE_CGROUP_SKB,
 171         BPF_PROG_TYPE_CGROUP_SOCK,
 172         BPF_PROG_TYPE_LWT_IN,
 173         BPF_PROG_TYPE_LWT_OUT,
 174         BPF_PROG_TYPE_LWT_XMIT,
 175         BPF_PROG_TYPE_SOCK_OPS,
 176         BPF_PROG_TYPE_SK_SKB,
 177         BPF_PROG_TYPE_CGROUP_DEVICE,
 178         BPF_PROG_TYPE_SK_MSG,
 179         BPF_PROG_TYPE_RAW_TRACEPOINT,
 180         BPF_PROG_TYPE_CGROUP_SOCK_ADDR,
 181         BPF_PROG_TYPE_LWT_SEG6LOCAL,
 182         BPF_PROG_TYPE_LIRC_MODE2,
 183         BPF_PROG_TYPE_SK_REUSEPORT,
 184         BPF_PROG_TYPE_FLOW_DISSECTOR,
 185         BPF_PROG_TYPE_CGROUP_SYSCTL,
 186         BPF_PROG_TYPE_RAW_TRACEPOINT_WRITABLE,
 187         BPF_PROG_TYPE_CGROUP_SOCKOPT,
 188         BPF_PROG_TYPE_TRACING,
 189         BPF_PROG_TYPE_STRUCT_OPS,
 190         BPF_PROG_TYPE_EXT,
 191         BPF_PROG_TYPE_LSM,
 192 };

为了直观看出来这些程序类型,在bpftool源码中摘取了这些名字和类型对应的关系

const char * const prog_type_name[] = {
        [BPF_PROG_TYPE_UNSPEC]                  = "unspec",
        [BPF_PROG_TYPE_SOCKET_FILTER]           = "socket_filter",
        [BPF_PROG_TYPE_KPROBE]                  = "kprobe",
        [BPF_PROG_TYPE_SCHED_CLS]               = "sched_cls",
        [BPF_PROG_TYPE_SCHED_ACT]               = "sched_act",
        [BPF_PROG_TYPE_TRACEPOINT]              = "tracepoint",
        [BPF_PROG_TYPE_XDP]                     = "xdp",
        [BPF_PROG_TYPE_PERF_EVENT]              = "perf_event",
        [BPF_PROG_TYPE_CGROUP_SKB]              = "cgroup_skb",
        [BPF_PROG_TYPE_CGROUP_SOCK]             = "cgroup_sock",
        [BPF_PROG_TYPE_LWT_IN]                  = "lwt_in",
        [BPF_PROG_TYPE_LWT_OUT]                 = "lwt_out",
        [BPF_PROG_TYPE_LWT_XMIT]                = "lwt_xmit",
        [BPF_PROG_TYPE_SOCK_OPS]                = "sock_ops",
        [BPF_PROG_TYPE_SK_SKB]                  = "sk_skb",
        [BPF_PROG_TYPE_CGROUP_DEVICE]           = "cgroup_device",
        [BPF_PROG_TYPE_SK_MSG]                  = "sk_msg",
        [BPF_PROG_TYPE_RAW_TRACEPOINT]          = "raw_tracepoint",
        [BPF_PROG_TYPE_CGROUP_SOCK_ADDR]        = "cgroup_sock_addr",
        [BPF_PROG_TYPE_LWT_SEG6LOCAL]           = "lwt_seg6local",
        [BPF_PROG_TYPE_LIRC_MODE2]              = "lirc_mode2",
        [BPF_PROG_TYPE_SK_REUSEPORT]            = "sk_reuseport",
        [BPF_PROG_TYPE_FLOW_DISSECTOR]          = "flow_dissector",
        [BPF_PROG_TYPE_CGROUP_SYSCTL]           = "cgroup_sysctl",
        [BPF_PROG_TYPE_RAW_TRACEPOINT_WRITABLE] = "raw_tracepoint_writable",
        [BPF_PROG_TYPE_CGROUP_SOCKOPT]          = "cgroup_sockopt",
        [BPF_PROG_TYPE_TRACING]                 = "tracing",
        [BPF_PROG_TYPE_STRUCT_OPS]              = "struct_ops",
        [BPF_PROG_TYPE_EXT]                     = "ext",
        [BPF_PROG_TYPE_LSM]                     = "lsm",
        [BPF_PROG_TYPE_SK_LOOKUP]               = "sk_lookup",
}

这些程序类型,我们可以认为是有两大类,其一是用来跟踪,编写程序来更好地了解系统正在发生什么,这类程序提供了系统行为以及系统硬件的直接信息,同时可以访问特定程序的内存区域,从运行进程中提取执行跟踪信息。其二是网络,这类程序可以监测和控制系统的网络流量,注意:“监测”、“控制”,“监测”也就是说我们可以观测,如套接字过滤器程序、“控制”可以去修改网络一些信息,如XDP程序做过滤、拒绝数据包、重定向数据包。

上面提到了每类BPF程序都有自己独特的上下文参数和自己触发的事件,下面将分析其中一种:BPF_PROG_TYPE_SOCKET_FILTER类型的上下文参数和触发的事件,其他的BPF程序类型将在今后的文章进行分析和讲解。

套接字过滤器程序

BPF_PROG_TYPE_SOCKET_FILTER

这类程序可以说是刚才提到的两种大类中:网络类,刚才提到这类程序可以监测和控制系统的网络流量,套接字过滤器程序就仅仅可以进行监测,不允许修改数据包内容或更改其目的地等。每类程序都有自己的独特的上下文参数,该类程序的上下文入口参数是struct __sk_buff,struct __sk_buff的定义在include/uapi/linux/bpf.h>,如下所示:

struct __sk_buff {
 __u32 len;
 __u32 pkt_type;
 __u32 mark;
 __u32 queue_mapping;
 __u32 protocol;
 __u32 vlan_present;
 __u32 vlan_tci;
 __u32 vlan_proto;
 __u32 priority;
 __u32 ingress_ifindex;
 __u32 ifindex;
 __u32 tc_index;
 __u32 cb[5];
 __u32 hash;
 __u32 tc_classid;
 __u32 data;
 __u32 data_end;
 __u32 napi_id;

 /* Accessed by BPF_PROG_TYPE_sk_skb types from here to ... */
 __u32 family;
 __u32 remote_ip4; /* Stored in network byte order */
 __u32 local_ip4; /* Stored in network byte order */
 __u32 remote_ip6[4]; /* Stored in network byte order */
 __u32 local_ip6[4]; /* Stored in network byte order */
 __u32 remote_port; /* Stored in network byte order */
 __u32 local_port; /* stored in host byte order */
 /* ... here. */

 __u32 data_meta;
};

从上面的struct __sk_buff 可以看到,这个结构体来自于struct sk_buff中的一些关键字段。那么会有一个疑问,该结构体只有struct sk_buff的关键字段而且与struct sk_buff中的字段并没有做到对齐(位置对应),是怎样做到访问到这些字段的?

参考:https://lwn.net/Articles/636647/ They were few other ideas considered. At the end the best seems to be to introduce a user accessible mirror of in-kernel sk_buff structure: struct __sk_buff { __u32 len; __u32 pkt_type; __u32 mark; __u32 ifindex; __u32 queue_mapping; ....... }; bpf programs will do: int bpf_prog1(struct __sk_buff *skb) { __u32 var = skb->pkt_type; which will be compiled to bpf assembler as: dst_reg = *(u32 *)(src_reg + 4) // 4 == offsetof(struct __sk_buff, pkt_type) bpf verifier will check validity of access and will convert it to: dst_reg = *(u8 *)(src_reg + offsetof(struct sk_buff, __pkt_type_offset)) dst_reg &= 7 since 'pkt_type' is a bitfield.

上面这段参考,大概就是在说struct __sk_buff包含了这个一些sk_buff结构体的关键字段,内核的BPF程序将对这些关键字段的访问转换成“真实”sk_buff的偏移量,这一点很关键!!

该程序类型用于过滤进出口网络报文,功能上和 cBPF 类似,也就是和在tcpdump中所运用的cBPF相似,可以在tcpdump系统文章中进行查看相关的实现。

关于套接字过滤器程序的案例,还是选择sockex1:

sockex1_kern.c

#include 
#include 
#include 
#include 
#include "bpf_helpers.h"
/*
预先定义好的MAP对象,然而MAP的真正创建时机,是用户态程序调用load_bpf_file时
解析到section后进行创建;这里SEC宏会在obj文件中新增一个段(section)
这里定义的是一个数组型的MAP,索引是数组下标,值是数组对应的值
*/
struct bpf_map_def SEC("maps") my_map = {
 .type = BPF_MAP_TYPE_ARRAY,
 .key_size = sizeof(u32),
 .value_size = sizeof(long),
 .max_entries = 256,
};

SEC("socket1")
    //struct __sk_buff就是该程序类型的上下文参数,封装了struct sk_buff中的一些关键的成员变量
int bpf_prog1(struct __sk_buff *skb)
{   
    //load_byte实际指向了llvm的buit-in函数asm(llvm.bpf.load.byte),该语句是读取输入报文的包头中的协议头
 int index = load_byte(skb, ETH_HLEN + offsetof(struct iphdr, protocol));
 long *value;
     //pkt_type表明了数据包的方向,PACKET_OUTGOING表示发往其他主机的报文和本地主机的环回报文
 if (skb->pkt_type != PACKET_OUTGOING)
  return 0;

 value = bpf_map_lookup_elem(&my_map, &index);
 if (value)
  __sync_fetch_and_add(value, skb->len);//统计流量包的大小

 return 0;
}
char _license[] SEC("license") = "GPL";

sockex1_user.c

#include 
#include 
#include 
#include "libbpf.h"
#include "bpf_load.h"
#include "sock_example.h"
#include 
#include 

int main(int ac, char **argv)
{
 char filename[256];
 FILE *f;
 int i, sock;

 snprintf(filename, sizeof(filename), "%s_kern.o", argv[0]);
    //加载sockex1_kern.c编译后的sockex1_kern.o,解析出定义的section,完成创建MAP,向内核注册插入BPF代码。
 if (load_bpf_file(filename)) {
  printf("%s", bpf_log_buf);
  return 1;
 }
     //主要是完成:sock = socket(PF_PACKET, SOCK_RAW | SOCK_NONBLOCK | SOCK_CLOEXEC, htons(ETH_P_ALL));
 sock = open_raw_sock("lo");
    /*因为 sockex1_kern.o 中 bpf 程序的类型为 BPF_PROG_TYPE_SOCKET_FILTER,所以这里需要用用 SO_ATTACH_BPF 来指明程序的 sk_filter 要挂载到哪一个套接字上,其中prof_fd为注入到内核的BPF程序的描述符*/
 assert(setsockopt(sock, SOL_SOCKET, SO_ATTACH_BPF, prog_fd,
     sizeof(prog_fd[0])) == 0);

 f = popen("ping -c5 localhost", "r");
 (void) f;
    //循环5次查询相应协议对应的数据包大小
 for (i = 0; i < 5; i++) {
  long long tcp_cnt, udp_cnt, icmp_cnt;
  int key;
  key = IPPROTO_TCP;
  assert(bpf_map_lookup_elem(map_fd[0], &key, &tcp_cnt) == 0);

  key = IPPROTO_UDP;
  assert(bpf_map_lookup_elem(map_fd[0], &key, &udp_cnt) == 0);

  key = IPPROTO_ICMP;
  assert(bpf_map_lookup_elem(map_fd[0], &key, &icmp_cnt) == 0);

  printf("TCP %lld UDP %lld ICMP %lld bytes\n",
         tcp_cnt, udp_cnt, icmp_cnt);
  sleep(1);
 }

 return 0;
}

sockex1_user.c中的:

 if (load_bpf_file(filename)) {
  printf("%s", bpf_log_buf);
  return 1;
 }

用户空间通过辅助函数load_bpf_file加载BPF的elf文件后,将返回使用MAP两个全局变量:map_fd数组,map_data,同时也会创建 struct bpf_prog存储指令,struct bpf_prog内容如下所示,每一个load到内核的BPF程序都有一个fd(prog_fd)返回给用户态,它对应一个bpf_prog。将指令Load内核,创建struct bpf_prog存储指令外,只是第一步,成功运行还需要:

  • 将bpf_prog与内核中的特定Hook点关联起来,也就是将程序挂到钩子上

  • 在Hook点被访问到,取出bpf_prog,执行这些指令。

那socket类型的BPF程序,也就是BPF_PROG_TYPE_SOCKET_FILTER类型的程序什么时候会执行注入到内核的指令?

其实正如tcpdump原理一样,可以参考tcpdump1和tcpdump2,当绑定的指定的网卡在发包或者收包时会进行嗅探并执行注入的指令。

    struct bpf_prog {
 u16   pages;  // Number of allocated pages 
 u16   jited:1, // Is our filter JIT'ed? 
    locked:1, // Program image locked? 
    gpl_compatible:1, // Is filter GPL compatible? 
    cb_access:1, // Is control block accessed? 
    dst_needed:1; // Do we need dst entry? 
 enum bpf_prog_type type;  // Type of BPF program 
 u32   len;  // Number of filter blocks 
 u32   jited_len; // Size of jited insns in bytes 
 u8   tag[BPF_TAG_SIZE];
 struct bpf_prog_aux *aux;  // Auxiliary fields 
 struct sock_fprog_kern *orig_prog; // Original BPF program 
 unsigned int  (*bpf_func)(const void *ctx,
         const struct bpf_insn *insn);
 // Instructions for interpreter 
 union {
  struct sock_filter insns[0];   //对应cBPF的指令集
  struct bpf_insn  insnsi[0];  //对应eBPF的指令集
 };
};

BPF_PROG_TYPE_SOCKET_FILTER用来干啥?

filter操作包括丢弃数据包(如果程序返回0)或修改数据包(如果程序返回的长度小于原始长度)。请参见sk_filter_trim_cap()及其对bpf_prog_run_save_cb()的调用。请注意,我们不是修改或者丢弃原始的数据包,这些数据包都会到达原始的套接字。我们处理的是原始数据包元数据的copy,这些copy的数据包可以被raw socket访问到。除此之外,我们还可以做一些其他的事情。例如进行流量统计在BPF 的Maps中。

如何attach我们的BPF program?

它可以通过SO_ATTACH_BPF setsockopt()进行attach。

该类型上下文参数?

struct __sk_buff 包含这个packet的metadata/data。他的结构在include/linux/ bpf.h中定义,并包含来自实际sk_buff的关键字段。bpf验证程序将对有效的__sk_buff字段的访问转换为“真实”sk_buff的偏移量。

它什么时候被执行?

绑定到指定的网卡后,在发包或者收包时会进行嗅探并执行注入的程序,该类型程序被各种协议(TCP,UDP,ICMP,原始套接字等)调用,可用于过滤inbound的流量。

 

你可能感兴趣的:(linux,网络,linux内核,bpf)