运行第一个 eBPF 程序

674次阅读  |  发布于2年以前

上手 ebpf

一直想学习一下 ebpf 这个东东,最近买了本《Linux 内核观测技术 BPF》,准 备系统的研究一下。

原以为有了书之后学起来就相当轻松了,可以我发现书上的第一个例子就编译不 过。

书上只给了部分的源码,还需要去下载配套的 github 项目,这也没啥关系,不过下 载后编译也是编译不过。

编译不过的报错也不过是 types.h 头文件找不到,解决了头文件找不到的问题 后发现又有新的问题。

网上搜索了下发现基本上都是在介绍 ebpf xxx,没有看到一篇讲如何上手 ebpf, 只能自己搞搞喽,在这里记录下遇到的问题。

安装必要的程序

ebpf 程序的编译依赖 llvm 与 clang,需要安装这两个程序。debian like 系 统可以执行如下命令进行安装:

sudo apt-get install clang llvm

如果你要使用 python 作为 BCC 工具的前端来编写代码,你可能会遇到如下报 错信息:

ImportError: No module named bcc

可以执行如下命令安装之:

sudo apt-get install python-bpfcc

克隆项目代码

linux-observability-with-bpf 项目中能够找到《Linux内核观测技术 BPF》一 书中的配套代码,它的 git 项目地址如下:

https://github.com/bpftools/linux-observability-with-bpf.git

直接使用 git 进行克隆即可,克隆完成后进入到源码目录中,发现它有如下的 目录结构:

LICENSE  README.md  Vagrantfile  code  img

从 README.md 开始

在直接使用 code 目录前,一定要先阅读 README.md!一定要先阅读 README.md

README.md 文件中描述了使用这个项目需要预先执行的步骤,具体过程如下:

安装必要的工具

debian like 系统中可以执行如下命令:

$ sudo apt update
$ sudo apt install build-essential git make libelf-dev clang strace tar
$ bpfcc-tools linux-headers-$(uname -r) gcc-multilib llvm

获取内核源码

首先执行 uname -r 查看内核版本信息,然后下载相应的内核源码,这里需要注 意的是内核版本不能过低,至少要是 5.0.0 的内核。

进入到内核源码的 tools/lib/bpf 目录中,编译并安装

配置 libbpf.so

进入到项目源码的 code/chapter-x 中执行 make

注意这一步可能需要根据实际情况修改 Makefile 中的路径配置。

第一个问题:升级内核

我使用的是 debian 10 系统,它使用的内核版本是 4.19,要运行一些 ebpf 的例子 至少需要 5.0.0 的内核版本,这样我需要升级个内核先。

我已经预先克隆了内核的 git 仓库,这样我只需要检出 v5.0 版本的内核代码 就行了。

执行如下命令来完成:

$ git checkout -b v5.0 v5.0

这里的两个 v5.0,第一个 v5.0 表示的是新创建的分支名,第二个 v5.0 表示 的是项目的 tag 名称。

内核 config 仍然使用我之前裁剪过的 config 文件,其下载地址如下:

https://download.csdn.net/download/Longyu_wlz/12900957

直接拷贝为 .config 后,然后执行 make oldconfig,然后继续执行 make -j,发现需要重新设定其它的内核选项,一路 Enter 选择使用默认值。

编译完成后执行如下命令安装内核及内核模块:

$ sudo make modules_install
$ sudo make install

make install 的时候会自动生成 initrd 并更新引导。

重启系统,在 grub 中选择使用 5.0 版本的内核引导系统,进入系统后,执 行 uname -a 查看内核信息,确定使用的是 5.0 版本的内核。相关操作记录 如下:

$ linux-git $ uname -a
Linux debian-10 5.0.0+ #9 SMP Sun Nov 15 22:05:48 CST 2020 x86_64 GNU/Linux

第二个问题:编译 libbpf.so

libbpf.so 源码位于内核源码树的 tools/lib/bpf 目录中,直接进入到这个目 录中编译即可。

编译过程记录如下:

$ make 

Auto-detecting system features:
...                        libelf: [ on  ]
...                           bpf: [ on  ]

  CC       libbpf.o
  CC       bpf.o
  CC       nlattr.o
  CC       btf.o
  CC       libbpf_errno.o
  CC       str_error.o
  CC       netlink.o
  CC       bpf_prog_linfo.o
  LD       libbpf-in.o
  LINK     libbpf.a
  LINK     libbpf.so
  LINK     test_libbpf

编译完成后会生成 libbpf.so 文件,执行 sudo make install 安装此动态库。在我的系统中,它被安装到了 /usr/local/lib64 目录中,这个目录并不会被动 态库链接器搜索,这样直接运行使用了这个动态库的 ebpf 程序就会有如下报 错:

error while loading shared libraries: libbpf.so: cannot open shared object file: No such file or directory

解决方法如下:

/etc/ld.so.conf 中添加 /usr/local/lib64 这一行,运行 sudo ldconfig 重新生成动态库配置信息 成功执行的示例信息如下:

$ ld.so.conf.d $ sudo ldconfig -v  2>/dev/null | grep libbpf
    libbpf.so -> libbpf.so

编译运行 hello world ebpf 程序

完成上面的配置过程后就可以编译 hello world ebpf 程序了,它位于项目源码 的 code/chapter-2/hello_world 中,其源码如下:

#include <linux/bpf.h>
#define SEC(NAME) __attribute__((section(NAME), used))

static int (*bpf_trace_printk)(const char *fmt, int fmt_size,
                               ...) = (void *)BPF_FUNC_trace_printk;

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

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

这个程序,声明监控调用 execve 的事件,每监控到一个事件就调用 bpf_trace_printk 来打印 Hello, BPF World! 字符串,其细 节我就不进一步描述了,感兴趣的读者可以阅读《Linux内核观测技术BPF》的第 二章。

在编译之前还需要修改下 Makefile 中的内核源码路径,它默认是在 /kernel-src 目录下的,需要根据实际情况修改为真实的路径。

我执行如下 sed 命令将 Makefile 中的内核源码路径修改为我系统中的真实路 径:

$ sed -i 's;/kernel-src;/home/longyu/linux-git;' ./Makefile

修改完 Makefile 后直接执行 make 编译即可,相关过程记录如下:

$ make 
clang -O2 -target bpf -c bpf_program.c -I/home/longyu/linux-git/tools/testing/selftests/bpf -o bpf_program.o
clang  -o monitor-exec -lelf -I/home/longyu/linux-git/samples/bpf -I/home/longyu/linux-git/tools/lib -I/home/longyu/linux-git/tools/perf -I/home/longyu/linux-git/tools/include -L/usr/local/lib64 -lbpf \
        /home/longyu/linux-git/samples/bpf/bpf_load.c loader.c

编译完成后会生成一个 monitor-exec 程序,需要以 root 权限来运行。使用普 通用户执行将会报如下错误信息:

$ ./monitor-exec 
bpf_load_program() err=1
The kernel didn't load the BPF program

使用 root 用户执行后过一会就会打印 Hello,BPF World!,示例信息如下;

$ sudo ./monitor-exec 
  sogou-qimpanel-31885 [004] .... 11022.245597: 0: Hello, BPF World!
              sh-31886 [005] .... 11022.247254: 0: Hello, BPF World!
  sogou-qimpanel-31887 [005] .... 11022.249711: 0: Hello, BPF World!
              sh-31889 [004] .... 11022.251231: 0: Hello, BPF World!
              sh-31891 [007] .... 11022.251429: 0: Hello, BPF World!

这个程序使用了内核的 tracepoint 来监控执行 execve 的事件,当此事件发生 后,它会打印出 Hello,xxx 的信息,这个 execve 事件代表了一个新进程的执 行,我们在上面的示例中看到的第一列就是程序名与其 pid 号。

这个 hello world 程序的编译过程分为两个步骤:

使用 clang 编译生成 bpf 机器码

使用 clang 编译 loader.c 生成加载第一步生成的机器码的程序 loader.c 函数的源码如下所示:

#include "bpf_load.h"
#include <stdio.h>

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

  read_trace_pipe();

  return 0;
}

上述代码其实只调用了 load_bpf_file 来加载第一步编译生成的 ebpf 程序, load_bpf_filelibbpf.so 中提供的接口。

strace 跟踪 hello world ebpf 程序

使用 strace 跟踪 hello world ebpf 程序能够看到如下关键的系统调用:

bpf(BPF_PROG_LOAD, {prog_type=BPF_PROG_TYPE_TRACEPOINT, insn_cnt=14, insns=0xd04c80, license="GPL", log_level=0, log_size=0, log_buf=NULL, kern_version=KERNEL_VERSION(0, 0, 0), prog_flags=0, prog_name="", prog_ifindex=0, expected_attach_type=BPF_CGROUP_INET_INGRESS}, 112) = 4

其实 bpf 系统调用就是用户态程序与内核中的 ebpf 虚拟机交互的接口,libbpf.so 中提供的 api 实际上是对 bpf 系统调用的封装,可以想到 load_bpf_file 其实就是 指定 BPF_PROG_LOAD 等参数调用 bpf 系统调用来是实现的。

吐槽吐槽《Linux内核观测技术BPF》

一开始我并没有下载该书的配套源码,直接按照书中的描述编译 hello world 程序,命令行信息如下:

$ clang -O2 -target bpf -c hello.c -o bpf_program.o 

结果就报了 asm/types.h cannot find 的错误,整了一下发现它使用的是我系 统中 /usr/include/ 目录中的内核头文件,看来应该就是个版本问题。

我想应该可以通过安装内核头文件来解决这个问题。

首先在内核源码树根目录中执行 make help 中找到如下内容:

  headers_install - Install sanitised kernel headers to INSTALL_HDR_PATH
                    (default: ./usr)

可以看到它默认是使用 ./usr 目录,可以通过设定 INSTALL_HDR_PATH 来指定 其它安装目录。

我执行如下命令将这些头文件安装到指定目录中,示例过程如下:

$ make INSTALL_HDR_PATH="/home/longyu/ebpf/" headers_install
   INSTALL include/asm-generic (36 files)
   INSTALL include/drm (26 files)
   ......

安装后 ls 查看,确定 asm/types.h 存在,相关信息如下:

$ ls /home/longyu/ebpf/include/asm/types.h 
/home/longyu/ebpf/include/asm/types.h

执行 clang -O2 -target bpf -I /home/longyu/ebpf/include -c bpf_program.c -o bpf_program.o 后成功编译。

我觉得这个问题不应该遇到,但是确实遇到了,书里面也没有相关的描述信息, 不得不吐槽这本书的内容,这些最为基本的东西为啥不能写的清楚一些?也不差 那几页么?

第二、三章内容看描述还行,但是一尝试编译就会遇到问题,还是有点垃圾欧。

其它的 ebpf 组件

内核源码树的 tools/bpf 这个目录中存放了了 ebpf 的汇 编,反汇编,调试程序源码。

在编译的过程中我遇到了如下几个问题:

找不到 bfd.h

报错信息如下:

/home/longyu/linux-git/tools/bpf/bpf_jit_disasm.c:23:10: fatal error: bfd.h: 没有那个文件或目录
 #include <bfd.h>
          ^~~~~~~

解决方案如下:

$ apt-get install binutils-dev

找不到 radline/readline.h

报错信息如下:

/home/longyu/linux-git/tools/bpf/bpf_dbg.c:43:10: fatal error: readline/readline.h: 没有那个文件或目录
 #include <readline/readline.h>

解决方案如下:

$ sudo apt-get install libreadline-dev

安装了这几个开发包后能够成功编译,编译生成的文件信息如下:

bpf_asm  bpf_dbg  bpf_jit_disasm bpftool/bpftool

这几个程序目前还没有用起来,后面用起来了在描述吧。

内核源码树中 tools/testing/selftests/bpf 目录中中放了 ebpf 功能的相关测试 用例程序,在编译时遇到了如下问题:

test_verifier.c:28:10: fatal error: sys/capability.h: 没有那个文件或目录
 #include <sys/capability.h>

可以通过执行如下命令解决:

$ sudo apt-get install libcap-dev

这个目录中的测试用例程序可以作为编写 ebpf 程序的参考,这要比文档资料更 有价值。

samples/bpf 目录中也有一些 ebpf demo 程序,也是很好的参考资料。

内核源码树中与 ebpf 相关的一手资料

内核源码树中,除了上面说过的源码资料外,还有几个目录中有一些文档资料。

tools/bpf/bpftool/Documentation 中有 bpftool 命令的资料 Documentation/bpf 中有 bpf 的一些描述资料

总结

纸上得来终觉浅,绝知此事要躬行。书上的知识一定要经过实践的检验才能确定 它是否正确,学习从来都不只是阅读与记忆这么简单!

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8