ebpf-使用内核编译开发一个程序(ubuntu20.04)

发布时间 2023-09-09 15:37:17作者: 依然依然的a

前不久正好工作中使用到了这个方面的知识,这里写一下我的总结

我对ebpf的理解

ebpf(extended Berkeley Packet Filter)是一种虚拟机,通常我们使用的vmware是一种大型的虚拟机,vmware里面可以模拟cpu、显卡、网卡、硬盘等硬件,而ebpf这种的虚拟机是只模拟栈的小型的虚拟机,jvm也是一种栈虚拟机,栈虚拟机的优点就是可以跨平台,代码简单,但是不好优化,与其相比寄存器虚拟机就很好优化,但是代码会复杂一点,跨平台不方便。
ebpf的使用方式和java有点类似,只是ebpf需要两个文件,一个在用户态运行,一个在内核态运行,两者可以相互通信,运行时由用户态将内核态程序载入内核中,然后再和内核态程序通信。

发展的历史

Ebpf(extended Berkeley Packet Filter)起源于bpf,最初的目的是用于网络报文的过滤,而ebpf不再仅仅用于网络报文,而是可以用于多种场景,1992 年,Steven McCanne 和 Van Jacobson 写了一篇名为 The BSD Packet Filter: A New Architecture for User-level Packet Capture的论文。在文中,作者描述了他们如何在 Unix 内核实现网络数据包过滤,这种新的技术比当时最先进的数据包过滤技术快 20 倍。2014 年初,Alexei Starovoitov 实现了 eBPF(extended Berkeley Packet Filter)。经过重新设计,eBPF 演进为一个通用执行引擎,可基于此开发性能分析工具、软件定义网络等诸多场景。eBPF 最早出现在 3.18 内核中,此后原来的 BPF 就被称为经典 BPF,缩写 cBPF(classic BPF),cBPF 现在已经基本废弃。现在,Linux 内核只运行 eBPF,内核会将加载的 cBPF 字节码透明地转换成 eBPF 再执行。2014 年 6 月,eBPF 扩展到用户空间,这也成为了 BPF 技术的转折点。 正如 Alexei 在提交补丁的注释中写到:“这个补丁展示了 eBPF 的潜力”。当前,eBPF 不再局限于网络栈,已经成为内核顶级的子系统。

ebpf编程的实现方式

  • 基于 bcc 开发:bcc 提供了对 eBPF 开发,前段提供 Python API,后端 eBPF 程序通过 C 实现。特点是简单易用,但是性能较差。
  • 基于 libebpf-bootstrap 开发:libebpf-bootstrap 提供了一个方便的脚手架
  • 基于内核源码开发:内核源码开发门槛较高,但是也更加切合 eBPF 底层原理。

一个hello程序的编译运行过程

这里使用基于内核源码开发,实现一个简单的hello程序来演示编译和运行过程。
步骤是:

  1. 创建一个linux操作环境(Ubuntu 20.04.2-Ubuntu)
    这里用的操作系统镜像我是在国内mirror找的,我试过centos8,rocky linux,ubuntu22版本,都没成功,最后这个版本试了一下就可以了,

进入root状态,输入

apt-cache search linux-source #会显示一系列内核版本,尽量选择和本系统靠近的内核版本
apt install linux-source-5.4.0
cd /usr/src
tar -jxvf linux-source-5.4.0.tar.bz2#解压内核代码
cd linux-source-5.4.0
make script
cp -v /boot/config-$(uname -r) .configs 
make headers_install
make M=samples/bpf #这里不出现error即可

执行命令后的情况

root@ubuntu:/usr/src/linux-source-5.4.0# make M=samples/bpf
make -C /usr/src/linux-source-5.4.0/samples/bpf/../../tools/lib/bpf/ RM='rm -rf' LDFLAGS= srctree=/usr/src/linux-source-5.4.0/samples/bpf/../../ O=
Warning: Kernel ABI header at 'tools/include/uapi/linux/netlink.h' differs from latest version at 'include/uapi/linux/netlink.h'
Warning: Kernel ABI header at 'tools/include/uapi/linux/if_link.h' differs from latest version at 'include/uapi/linux/if_link.h'

  WARNING: Symbol version dump ./Module.symvers
           is missing; modules will have no dependencies and modversions.

  Building modules, stage 2.
  MODPOST 0 modules

会有一些警告,只要没有红色的error显示,就可以了,有时会出现这样一些红色的error,这些错误的解决方法可以在常见error解决方法里面找到
2. 编写内核态代码和用户态代码
这里用户态代码和内核态代码我参考知乎上的,这里给出链接 https://zhuanlan.zhihu.com/p/492185920

cd /samples/bpf
vim hello_kern.c
#include <linux/bpf.h>
#include "bpf_helpers.h"

#define SEC(NAME) __attribute__((section(NAME), used))

SEC("tracepoint/syscalls/sys_enter_execve")
int bpf_prog(void *ctx)
{
	char msg[] = "Hello BPF!\n";
	bpf_trace_printk(msg, sizeof(msg));
	return 0;
}

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

execve是用于运行用户程序(a.out)或shell脚本的函数,是linux编程中常用的一个系统调用类函数。在linux命令行下运行用户程序本质其实就是执行execve系统调用,这里是将整个函数挂载到tracepoint/syscalls/sys_enter_execve这个section,这里的入口函数使用的是*ctx,bpf_trace_printk() 是一个最常用的 BPF 辅助函数,它的作用是输出一段字符串,这个程序的意义就是系统调用用户程序时打印字符串 Hello BPF

然后再写一个user_hello.c文件

#include <stdio.h>
#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;
}

再修改Makefile文件,在对应位置加上
这里要仿照不同内核版本下的格式,可能会有一点不同,主要要和其他内容对应上,

hostprogs-y += hello
hello-objs := bpf_load.o hello_user.o
always-y += hello_kern.o

hostprogs-y += hello 的含义是指定生成一个名为hello的可执行文件
hello-objs :=bpf_load.o hello_user.o 来指明生成可执行文件hello所需的文件清单就是bpf_load.o和hello_user.o
always-y += hello_kern.o这个要和Makefile文件中的always := $(hostprogs-y)结合起来看,意义是告诉kbuild需要的依赖有hello_kern.o这个文件

  1. 运行

回到内核根目录下执行 make M=samples/bpf,这个命令的作用就是将xxx_kern.c、xxx_user.c等ebpf源文件进行编译链接,最后变成可以在内核执行的字节码文件和用户态运行的可执行文件。

root@ubuntu:/usr/src/linux-source-5.4.0#cd /samples/bpf
root@ubuntu:/usr/src/linux-source-5.4.0/samples/bpf# ./hello

运行./hello,加载 bpf 程序实质上是加载 ELF 格式文件,Linux 加载普通 ELF 格式的文件在通过 load_elf_binary 来实现,而 Linux 加载 bpf elf 其实在用户态实现的,使用的是开源的 libelf 库实现的,调用过程不太一样,而且只是把 ELF 格式的指令 dump 出来,接下来还需要 JIT 编译器翻译出机器汇编码才能执行,这个调用过程比 Linux 加载普通 ELF 格式文件简单。通过 load_bpf_file 加载 .o 文件。这个时候界面上是没有任何输出的。
我们打开另外一个终端,输入ls,终端上面出现字符。

root@ubuntu:/usr/src/linux-source-5.4.0/samples/bpf# ./hello 
            bash-8586    [003] d...1 37473.713482: bpf_trace_printk: Hello BPF from houmin!


       (install)-8588    [002] d...1 37476.396070: bpf_trace_printk: Hello BPF from houmin!


          (find)-8590    [002] d...1 37476.400707: bpf_trace_printk: Hello BPF from houmin!


           <...>-8587    [001] d...1 37476.406077: bpf_trace_printk: Hello BPF from houmin!



当ls程序运行时,会调用syscall,那么被JIT编译后的挂载在tracepoint中的程序int bpf_prog(void *ctx)会运行,通过bpf helper函数bpf_trace_printk打印字符。