eBPF程序开发姿势总结

关于eBPF的讨论已经很火热了,在此不具体讲述什么是ebpf, 而是从ebpf程序开发的角度来直观感受ebpf的魅力。

1、eBPF程序执行流程

一个ebpf程序的执行流程分为以下几个步骤:

  • 第一步,使用 C 语言开发一个 eBPF 程序;

  • 第二步,借助 LLVM 把 eBPF 程序编译成 BPF 字节码;

  • 第三步,通过 bpf 系统调用,把 BPF 字节码提交给内核;

  • 第四步,内核验证并运行 BPF 字节码,并把相应的状态保存到 BPF 映射中;

  • 第五步,用户程序通过 BPF 映射查询 BPF 字节码的运行状态。

img就整体而言,ebpf程序的开发有三个主要步骤:内核态程序的开发、用户态程序、以及用户与内核进行数据交换。

( 1 ) 内核态程序

内核程序以ebpf字节码的方式运行在虚拟机中,虚拟机通过模拟CPU指令执行ebpf字节码效率较低,引入JIT来改善ebpf的执行效率。内核程序的编写可以使用C编写,也可以使用rust来编写,甚至可以手工编写epbf字节码。但是主流方式是使用C来开发ebpf程序。

内核程序的主要功能:挂钩内核函数进行数据采集。即当hook的系统调用等事件被触发后,执行ebpf程序进行数据采集。

( 2 ) 用户态程序

用户态程序通过系统bpf调用来与内核态程序进行交互。用户态可以采用很多流行的框架或者库来开发ebpf程序。比较著名的如BCC、libbpf、cilium等,根据不同的场景选择合适的方式进行开发, 这些库都对bpf系统调用进行了封装,简化了ebpf程序开发复杂度。

用户态程序的功能:用户态程序包含 eBPF 程序加载、挂载到内核函数和跟踪点,以及通过 BPF 映射获取和打印执行结果等部分。

( 3 ) 数据交换

在 eBPF 程序需要大块存储时,我们还需要根据应用场景,引入特定类型的 BPF 映射,并借助它向用户空间的程序提供运行状态的数据。

2、内核态程序开发

在使用eBPF开发程序之前,先要明确Linux系统中那些内核提供的API函数是可以被挂钩的,不同的内核版本对eBPF的支持会有所差异,这一点一定还要明确。在了解了eBPF可挂钩的内核函数之后,还需要了解目标函数的原型包括形式参数类型,个数,返回值类型等信息,然后才可以开始使用eBPF来实现相应的功能。

2.1 查询内核跟踪点信息

2.2.1 内核函数类型

内核态中的 eBPF 程序则需要通过 BPF 辅助函数完成所需的任务。但并不是所有的辅助函数都可以在 eBPF 程序中随意使用,不同类型的 eBPF 程序所支持的辅助函数是不同的。根据内核头文件 include/uapi/linux/bpf.h 中 bpf_prog_type 的定义,Linux 内核 v5.13 已经支持 30 种不同类型的 eBPF 程序。

image-20221119143235002

对于具体的内核版本,和编译配置选项不同,一个内核并不会支持所有的程序类型。可以使用工具bpftool 来查看当前系统中支持的程序类型。以下是ubuntu22 Server中支持的eBPF 程序类型。

image-20221119144707320

以上eBPF程序类型按照功能和使用场景,可以分为以下三种:

  • 第一类是跟踪:跟踪类 eBPF 程序主要用于从系统中提取跟踪信息,进而为监控、排错、性能优化等提供数据支撑。KPROBE、TRACEPOINT 以及 PERF_EVENT 都是最常用的跟踪类 eBPF 程序。
  • 第二类是网络:网络类 eBPF 程序主要用于对网络数据包进行过滤和处理,进而实现网络的观测、过滤、流量控制以及性能优化等各种丰富的功能,BPF_PROG_TYPE_XDP、BPF_PROG_TYPE_SCHED_CLS、BPF_PROG_TYPE_SCHED_ACT、BPF_PROG_TYPE_CGROUP_DEVICE等都属于网络类eBPF程序。
  • 第三类是其他类型:包括安全控制、BPF 扩展等等。这类程序类型包括如:BPF_PROG_TYPE_LSM、BPF_PROG_TYPE_LWT_IN、BPF_PROG_TYPE_LWT_OUT、BPF_PROG_TYPE_LWT_XMIT、BPF_PROG_TYPE_LIRC_MODE2等。

2.1.1 查看内核函数原型

内核跟踪点信息查询

  • 使用内核函数:对于非栈变量地址信息都被抽取到了/proc/kallsyms文件。但是内核函数是非稳定的API,在不断更新中经常变化,不适用该方法。且该文件下的函数只有显示导出的才可以被ebpf程序动态跟踪。

  • 调试系统文件:向用户空间提供了内核调试所需的基本信息,如内核符号列表、跟踪点、函数跟踪(ftrace)状态以及参数格式等。你可以在终端中执行 sudo ls /sys/kernel/debug 来查询内核调试文件系统的具体信息。

  • 使用perf来查询性能事件列表:sudo perf list [hw|sw|cache|tracepoint|pmu|sdt|metric|metricgroup]

  • 利用bpftrace来查询跟踪点:bpftrace 在 eBPF 和 BCC 之上构建了一个简化的跟踪语言,通过简单的几行脚本,就可以实现复杂的跟踪功能。在编写简单的 eBPF 程序,特别是编写的 eBPF 程序用于临时的调试和排错时,你可以考虑直接使用 bpftrace 。安装bpftrace(Ubuntu): sudo apt-get install -y bpftrace。

root@master:/home/flyfish01# sudo bpftrace -l 'tracepoint:syscalls:*'
tracepoint:syscalls:sys_enter_accept
tracepoint:syscalls:sys_enter_accept4
tracepoint:syscalls:sys_enter_access
tracepoint:syscalls:sys_enter_acct
tracepoint:syscalls:sys_enter_add_key
tracepoint:syscalls:sys_enter_adjtimex
tracepoint:syscalls:sys_enter_alarm
tracepoint:syscalls:sys_enter_arch_prctl
tracepoint:syscalls:sys_enter_bind
tracepoint:syscalls:sys_enter_bpf
tracepoint:syscalls:sys_enter_brk
....

使用bpftrace查看系统API入口参数和返回值

当我们掌握了eBPF 可以挂钩哪些内核函数之后,在编写eBPF之前我们还需要知道Hook函数的细节,如函数的参数和返回值信息,以下是如何使用bpftrace来观察系统函数的形参和返回值。

# 查询execve入口参数格式
$ sudo bpftrace -lv tracepoint:syscalls:sys_enter_execve
tracepoint:syscalls:sys_enter_execve
    int __syscall_nr
    const char * filename
    const char *const * argv
    const char *const * envp

# 查询execve返回值格式
$ sudo bpftrace -lv tracepoint:syscalls:sys_exit_execve
tracepoint:syscalls:sys_exit_execve
    int __syscall_nr
    long ret

2.2 编写内核程序

根据使用的用户态框架的不同,eBPF程序的可发稍微有所差别,但整体的思路是很清晰的。这里贴一个bcc/libbpf-tools/bashreadline.bpf.c的示例,该程序挂钩的uretprobe/readline函数,实现bash下用户输入探测功能。

/* SPDX-License-Identifier: GPL-2.0 */
/* Copyright (c) 2021 Facebook */
#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include "bashreadline.h"

#define TASK_COMM_LEN 16
struct {
	__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
	__uint(key_size, sizeof(__u32));
	__uint(value_size, sizeof(__u32));
} events SEC(".maps");

SEC("uretprobe/readline")
int BPF_KRETPROBE(printret, const void *ret) {
	struct str_t data;
	char comm[TASK_COMM_LEN];
	u32 pid;

	if (!ret)
		return 0;

	bpf_get_current_comm(&comm, sizeof(comm));
	if (comm[0] != 'b' || comm[1] != 'a' || comm[2] != 's' || comm[3] != 'h' || comm[4] != 0 )
		return 0;

	pid = bpf_get_current_pid_tgid() >> 32;
	data.pid = pid;
	bpf_probe_read_user_str(&data.str, sizeof(data.str), ret);

	bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &data, sizeof(data));

	return 0;
};

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

3、用户态程序开发

eBPF 工具的用户空间部分至少在理论上可以用任何语言编写,尽管实际上有一些库可以用部分语言来支持:C、Go、Rust 和 Python。eBPF 库主要协助实现两个功能:

  • 将 eBPF 程序和 Map 载入内核并执行重定位[9],通过其文件描述符将 eBPF 程序与正确的 Map 进行关联。
  • 与 eBPF Map 交互,允许对存储在 Map 中的键/值对进行标准的 CRUD 操作。

下面重点介绍以下BCC、libbpf、Cilium三个eBPF开发eBPF程序的使用。

3.1 BCC项目

3.1.1 项目简介

项目地址:https://github.com/iovisor/bcc

BCC 是一个 BPF 编译器集合,包含了用于构建 BPF 程序的编程框架和库,并提供了大量可以直接使用的工具。使用 BCC 的好处是,它把上述的 eBPF 执行过程通过内置框架抽象了起来,并提供了 Python、C++ 等编程语言接口。

3.1.1 BCC开发eBPF程序

开发准备工作: 必要的开发工具

# 将 eBPF 程序编译成字节码的 LLVM;
# C 语言程序编译工具 make;
# 最流行的 eBPF 工具集 BCC 和它依赖的内核头文件;
# 与内核代码仓库实时同步的 libbpf;
# 同样是内核代码提供的 eBPF 程序管理工具 bpftool。

# For Ubuntu20.10+
sudo apt-get install -y  make clang llvm libelf-dev libbpf-dev bpfcc-tools libbpfcc-dev linux-tools-$(uname -r) linux-headers-$(uname -r)

# For RHEL8.2+
sudo yum install libbpf-devel make clang llvm elfutils-libelf-devel bpftool bcc-tools bcc-devel

内核程序开发

// 包含头文件
#include <uapi/linux/openat2.h>
#include <linux/sched.h>
// 定义数据结构
struct data_t {
  u32 pid;
  u64 ts;
  char comm[TASK_COMM_LEN];
  char fname[NAME_MAX];
};

// 定义性能事件映射
BPF_PERF_OUTPUT(events);


// 定义kprobe处理函数
int hello_world(struct pt_regs *ctx, int dfd, const char __user * filename, struct open_how *how)
{
  struct data_t data = { };

  // 获取PID和时间
  data.pid = bpf_get_current_pid_tgid();
  data.ts = bpf_ktime_get_ns();

  // 获取进程名
  if (bpf_get_current_comm(&data.comm, sizeof(data.comm)) == 0)
  {
    bpf_probe_read(&data.fname, sizeof(data.fname), (void *)filename);
  }

  // 提交性能事件
  events.perf_submit(ctx, &data, sizeof(data));
  return 0;
}

用户程序开发

from bcc import BPF

# 1) load BPF program
b = BPF(src_file="trace-open.c")
b.attach_kprobe(event="do_sys_openat2", fn_name="hello_world")

# 2) print header
print("%-18s %-16s %-6s %-16s" % ("TIME(s)", "COMM", "PID", "FILE"))

# 3) define the callback for perf event
start = 0
def print_event(cpu, data, size):
    global start
    event = b["events"].event(data)
    if start == 0:
            start = event.ts
    time_s = (float(event.ts - start)) / 1000000000
    print("%-18.9f %-16s %-6d %-16s" % (time_s, event.comm, event.pid, event.fname))

# 4) loop with callback to print_event
b["events"].open_perf_buffer(print_event)
while 1:
    try:
        b.perf_buffer_poll()
    except KeyboardInterrupt:
        exit()

编写完成eBPF程序的开发之后,执行:sudo python hello.py运行程序。

1.001642440        b'node'          10878  b'/proc/12712/cmdline'
1.202377729        b'node'          10878  b'/proc/12712/cmdline'
1.264331274        b'node'          11049  b'/home/flyfish01/.vscode-server/data/User/workspaceStorage/e3195372342f7ed8e93e9c3d2ce8fecb/vscode.lock'
1.402635821        b'node'          10878  b'/proc/12712/cmdline'
1.569768264        b'node'          11041  b'/proc/meminfo'
1.602818505        b'node'          10878  b'/proc/12712/cmdline'
1.802893539        b'node'          10878  b'/proc/12712/cmdline'
2.003179522        b'node'          10878  b'/proc/12712/cmdline'
2.203337490        b'node'          10878  b'/proc/12712/cmdline'
2.264100392        b'node'          11050  b'/home/flyfish01/.vscode-server/data/User/workspaceStorage/e3195372342f7ed8e93e9c3d2ce8fecb/vscode.lock'
2.403939357        b'node'          10878  b'/proc/12712/cmdline'
2.604170876        b'node'          10878  b'/proc/12712/cmdline'
2.701696318        b'sh'            12757  b'/etc/ld.so.cache'
....

3.2 libbpf项目

3.2.1 项目简介

项目地址: https://github.com/libbpf/libbpf

libbpf 库其实是 linux 内核源码的一部分,位于 /tools/lib/bpf/ 路径下。为了使用这个库,把完整的源码下载下来实在不方便。所以内核的大佬们又新开了一个仓库,将 libbpf 的源码单独拎出来维护(https://github.com/libbpf/libbpf)。

在 eBPF 程序中,由于内核已经支持了 BTF,你不再需要引入众多的内核头文件来获取内核数据结构的定义。取而代之的是一个通过 bpftool 生成的 vmlinux.h 头文件,其中包含了内核数据结构的定义。libbpf 是从内核中抽离出来的标准库,用它开发的 eBPF 程序可以直接分发执行,这样就不需要每台机器都安装 LLVM 和内核头文件了。不过,它要求内核开启 BTF 特性,需要非常新的发行版才会默认开启(如 RHEL 8.2+ 和 Ubuntu 20.10+ 等)。

3.2.2 libpbf开发eBPF程序

使用 libbpf 开发 eBPF 程序也是分为两部分:第一,内核态的 eBPF 程序;第二,用户态的加载、挂载、映射读取以及输出程序等。

  • 使用 bpftool 生成内核数据结构定义头文件。BTF 开启后,你可以在系统中找到 /sys/kernel/btf/vmlinux 这个文件,bpftool 正是从它生成了内核数据结构头文件。

  • 开发 eBPF 程序部分。为了方便后续通过统一的 Makefile 编译,eBPF 程序的源码文件一般命名为 <程序名>.bpf.c。

  • 编译 eBPF 程序为字节码,然后再调用 bpftool gen skeleton 为 eBPF 字节码生成脚手架头文件(Skeleton Header)。这个头文件包含了 eBPF 字节码以及相关的加载、挂载和卸载函数,可在用户态程序中直接调用。

  • 最后就是用户态程序引入上一步生成的头文件,开发用户态程序,包括 eBPF 程序加载、挂载到内核函数和跟踪点,以及通过 BPF 映射获取和打印执行结果等。

前期准备工作:

  • 安装Golang,配置相关环境变量、安装clang和llvm编译器:

  • 安装libbpf:

    sudo apt install build-essential git make libelf-dev strace tar bpfcc-tools libbpf-dev linux-headers-$(uname -r)  linux-tools-common gcc-
    

libbpf开发编译eBPF程序

  • 生成内核头文件:sudo bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h

    #ifndef __VMLINUX_H__
    #define __VMLINUX_H__
    
    #ifndef BPF_NO_PRESERVE_ACCESS_INDEX
    #pragma clang attribute push (__attribute__((preserve_access_index)), apply_to = record)
    #endif
    typedef unsigned char __u8;
    struct hlist_node;
    
    struct hlist_head {
    	struct hlist_node *first;
    };
    
    struct hlist_node {
    	struct hlist_node *next;
    	struct hlist_node **pprev;
    };
    
    struct callback_head {
    	struct callback_head *next;
    	void (*func)(struct callback_head *);
    };
    .....
    
  • 开发内核态eBPF程序

    // 包含头文件
    #include "vmlinux.h"
    #include <bpf/bpf_helpers.h>
    
    // 定义进程基本信息数据结构
    struct event {
        char comm[TASK_COMM_LEN];
        pid_t pid;
        int retval;
        int args_count;
        unsigned int args_size;
        char args[FULL_MAX_ARGS_ARR];
    };
    
    // 定义哈希映射
    struct {
        __uint(type, BPF_MAP_TYPE_HASH);
        __uint(max_entries, 10240);
        __type(key, pid_t);
        __type(value, struct event);
    } execs SEC(".maps");
    
    // 定义性能事件映射
    struct {
        __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
        __uint(key_size, sizeof(u32));
        __uint(value_size, sizeof(u32));
    } events SEC(".maps");
    
    // sys_enter_execve跟踪点
    SEC("tracepoint/syscalls/sys_enter_execve")
    int tracepoint__syscalls__sys_enter_execve(struct trace_event_raw_sys_enter *ctx)
    {
      // 待实现处理逻辑
    }
    
    // 定义许可证(前述的BCC默认使用GPL)
    char LICENSE[] SEC("license") = "Dual BSD/GPL";
    
  • Clang将eBPF程序将源代码编译为.o对象文件, 并将可以将.o转化为.h文件,

    # 内核程序开发完成后,会利用 clang & llvm 将源程序编译成eBPF目标程序
    clang -O2 -g -target bpf -D__TARGET_ARCH_x86_64 -c exec.bpf.c -o exec.bpf.o
    # 将可以将.o转化为.h文件。
    # 在用户空间代码部分,通过include这个头文件,就可以很方便地访问内核程序中定义的变量,代码主体和 MAP 数据结构,并且提供了加载程序的抽象接口。
    bpftool gen skeleton execsnoop.bpf.o > execsnoop.skel.h
    
  • 使用Go编写用户态程序

    package main
    import (
      "bytes"
      "encoding/binary"
      "fmt"
      "os"
    
      bpf "github.com/aquasecurity/libbpfgo"
    )
    
    func main() {
      bpfModule, err := bpf.NewModuleFromFile("simple.bpf.o")
      if err != nil {
      	os.Exit(-1)
      }
    
      defer bpfModule.Close()
    
      bpfModule.BPFLoadObject()
      prog, err := bpfModule.GetProgram("kprobe_sys_execve")
      if err != nil {
      	os.Exit(-1)
      }
    
      _, err = prog.AttachKprobe("__x64_sys_execve")
      if err != nil {
      	os.Exit(-1)
      }
    
      eventsChannle := make(chan []byte)
      rb, err := bpfModule.InitRingBuf("events", eventsChannle)
      if err != nil {
      	os.Exit(-1)
      }
    
      rb.Start()
    
      for {
      	envent := <-eventsChannle
      	pid := int(binary.LittleEndian.Uint32(envent[0:4]))
      	comm := string(bytes.TrimRight(envent[4:], "\x00"))
      	fmt.Printf("%d: %v\n", pid, comm)
      }
    
      rb.Stop()
      rb.Close()
        }
    
  • 编译Go程序为二进制文件

    # 在go项目目录下执行
    go  mod  init  项目名
    #加载所需的module
    go  mod tidy
    #使用Go进行编译,生成二进制文件
    CC=gcc CGO_CFLAGS="-I /usr/include/bpf" CGO_LDFLAGS="/usr/lib/x86_64-linux-gnu/libbpf.a"  go build -o 程序名称
    # 测试运行
    sudo  ./程序名
    

3.3 cilium eBPF

3.3.1 项目介绍

项目地址: https://github.com/cilium/ebpf

cilium/ebpf库是 Cilium项目的一个子项目。仅使用 Go 语言编写的库,提供了加载、编译和调试 eBPF 程序的功能。它具有最小的外部依赖性,适合在长期运行的进程中使用。库主要有由 Cloudflare和 Cilium两家公司维护,由于 Cilium产品的火爆程度,该库的活跃度在社区层面还是会持续演进和发展。

3.3.2 cilium 开发eBPF程序

开发准备工作

准备开发环境:ubuntu22 server  腾讯云
安装GO: sudo apt  install golang-go    
安装llvm编译器:sudo  apt install clang llvm

编译cilium 自带的example程序

# 第一步:下载cilium源代码
git  clone https://github.com/cilium/ebpf.git
# 切换到example/kprobe目录下
cd  path/ebpf/excamle
//删除之前编译生成的文件, 只保留kprobe.c、 main.go
rm  *.o
rm  bpf_*.go
//执行go generate
//go generate命令是go 1.4版本里面新添加的一个命令,当运行go generate时,
//它将扫描与当前包相关的源代码文件,找出所有包含"//go:generate"的特殊注释,提取并执行该特殊注释后面的命令,命令为可执行程序
go generate

//执行go  build
go build

//成功编译后生成kprobe*可执行文件
sudo  ./kprobe*

测试运行结果:(统计系统调用kprobe/sys_execve的次数)

开发编译自定义的ebpf程序

# 第一步:下载cilium源代码
git  clone https://github.com/cilium/ebpf.git

# 在example目录下新建test目录,存放我们自己的代码。
cd  path/ebpf/excamle
mkdir test

# 复制头文件
cp -r ebpf/examples/headers test/headers

# 新建test/kprobe,复制ebpf/examples/krope中的kprobe.c和main.go到test/kprobe中
cp ebpf/examples/krope/kprobe.c  test/kprobe
cp ebpf/examples/krope/main.go test/kprobe

//执行go generate
go generate

//执行go  build
go build

//成功编译后生成kprobe*可执行文件
sudo  ./kprobe*

剩下的就是基于该测试进行二次开发,修改kprobe.c、 main.go文件以实现自己的需求。

3.4 总结

BCC:BCC编写程序采用c的子集作为前端,编写完毕后自动调用clang进行编译,llvm生成ebpf字节码。加载程序到内核,程序退出时自动从内核卸载bpf程序。

BCC还有各种前端语言辅助进行变成,前端语言主要是用户态用来处理加载到内核态bpf程序的输出和交互。python支持较好,也支持go。但是兼容性问题并不好,首先,bcc是类似一种动态语言的方式,每次执行都会进行编译,编译需要工具链和依赖内核头文件(需要安装kernel-header包),而编译依赖是最脆弱,容易失败的。

libbpf: libbpf的使得bpf程序像其它程序一样,编译好后,可以放在任何一台机器,任何一个kernel版本上运行(当然要对内核版本有一些要求)。此外libbpf前端支持C、Go语言,也有比较著名的libbpf-go、libbpf-bootstrap等框架可快速开发eBPF程序。libbpf的出现解决了以下问题:

  1. 头文件的问题,依赖内核态特性支持BTF,将内核的数据结构类型构建在内核中。用户态的程序可以导出BTF成一个单独的.h 头文件,bpf程序只要依赖这个头文件就行,不需要安装内核头文件的包了。
  2. 兼容性问题,使用clang-11的针对ebpf后端专门的特性:preserve_access_index,支持记录数据结构field相对位置信息,可实现relocation,从而解决了数据结构在不同内核版本间的变化带来的无法对应问题。
  3. 性能提升:内核中bpf模块做了一些增强,bpf verifier支持直接字段访问,不需要call bpf函数的方式来访问结构体字段,这样提升了性能。

cilium:Cilium是由革命性内核技术eBPF驱动,用于提供、保护和观察容器工作负载(云原生)之间的网络连接的网络组件。 Cilium使用eBPF的强大功能来加速网络,并在Kubernetes中提供安全性和可观测性。

posted @ 2022-11-19 19:49  丘山996  阅读(182)  评论(0)    收藏  举报