eBPF编程入门学习笔记--C编程


eBPF编程入门学习笔记–C编程

前言

目前的BPF编程方式:

其中ply目前也应该已经比较完善了,他的仓库是:https://github.com/iovisor/ply

可以看出目前BPF编程主要分成3种方式,指令集编程、C编程和BPF前端。
指令集编程就是直接写BPF的指令集,我们通过单个指令的组合进行编程,比较困难。可参考这篇文章
C编程 编出就是本文下面的内容,我们需要实现 kern.c(bpf程序) 和 user.c(加载器)。
BPF前端,更友好的编程方式,提供了编程的框架,将一些操作封装成了函数,方便开发。详细的区别将在后续学习中进行记录。

参考资料

环境配置

使用ubuntu20版本

1.1.下载内核源码
下载的内核版本应与你系统的版本一致,查看当前内核版本 uname -r

然后在源码镜像站点(http://ftp.sjtu.edu.cn/sites/ftp.kernel.org/pub/linux/kernel)下载对应版本的内核源码

我是 5.13.0版本,所以下载对应的源码:

http://ftp.sjtu.edu.cn/sites/ftp.kernel.org/pub/linux/kernel/v5.x/linux-5.13.tar.gz

解压到/usr/src/目录下

1.2.安装依赖项

apt install libncurses5-dev flex bison libelf-dev binutils-dev libssl-dev

1.3.安装Clang和LLVM
然后使用以下两条命令分别安装 clang 和 llvm

apt install clang
apt install llvm

这个下面几个过程中可能会遇到各种 fatal error 错误,基本上是依赖缺失导致的,关键报错搜索就有解决办法,安装对应缺失的文件即可

1.4.配置内核
在源码根目录下使用make defconfig生成.config<c/ode>文件

1.5.解决modpost: not found错误
因为直接make M=samples/bpf时,会报错缺少modules的错误。修复modpost的错误,以下两种解决方案二选一

make modules_prepare
make script

1.6.关联内核头文件

make headers_install

1.7.编译内核程序样例
在源码根目录下执行make M=samples/bpf,

此时进入linux-source-4.15.0/smaples/bpf中,会看到生成了BPF字节码文件*_kern.o和用户态的可执行文件

我们可以运行demo试一下:

开始编写程序

hello-world

第一个实例程序
编写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 World\n";
        bpf_trace_printk(msg, sizeof(msg));
        return 0;
}

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

这个程序的作用就是当发生系统调用(sys_enter_execve)时在终端输出”Hello World”,其实bpf_trace_printk只是将msg写到一个管道文件中

编写hello_user.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;
}

这个程序的作用是将包含BPF的文件hello_kern.o通过系统调用的方式加载进内核,read_trace_pipe()读取管道文件并打印到终端

修改Makefile文件,这里有点坑,不同内核版本的Makefile文件有些差异,网上不同文章的版本可能不同,不能完全按照进行修改,所以依据我当前的Makefile文件进行修改。

# List of programs to build
tprogs-y += hello

xxx

编译
返回源码根目录用 make M=samples/bpfmake samples/bpf/ 编译

或者直接在当前目录(samples/bpf) 执行make 编译

这里遇到如下报错:

/usr/src/linux-5.13/samples/bpf/hello_user.c:3:10: fatal error: bpf_load.h: No such file or directory
    3 | #include "bpf_load.h"
      |          ^~~~~~~~~~~~
compilation terminated.
make[2]: *** [/usr/src/linux-5.13/samples/bpf/Makefile.target:75: /usr/src/linux-5.13/samples/bpf/hello_user.o] Error 1
make[1]: *** [Makefile:1847: /usr/src/linux-5.13/samples/bpf] Error 2
make[1]: Leaving directory '/usr/src/linux-5.13'
make: *** [Makefile:269: all] Error 2

可以看到,提示缺失 bpf_load.h 头文件,这个头文件是内核给做的示范文件中一个,在较新版本的内核中没有了。 bpf_load 被认为是bpf loader并在2020年一次更新中删除了:https://www.spinics.net/lists/xdp-newbies/msg01866.html

不过还有很多可以学习的东西值得记录。

Makefile文件分析

来学习一下Makefile文件有什么内容,什么作用,make怎么工作的。

不同内核版本的Makefile内容会有差异,但总体逻辑是一致的。

  • Make命令教程
  • make命令,利用的是根目录下的Makefile,完成“生成头文件”和“生成.config文件”

关键内容分析:

  • hostprogs -y 或者新版本中是 tprogs -y

它是Makefile文件中最开始的一段变量,官方的注释是List of programs to build,就是说明要构建的目标可执行程序,比如我们要构建新的hello程序,就添加一条 hostprogs-y += hello。kbuild默认会去同一个目录下查找名为hello.c作为构建这个可执行文件的源文件。
之后将显式依赖关系添加到可执行文件中,一种是为Makefile中某个target添加这个可执行文件,作为prerequisites,形成依赖关系,这样就可以触发这个可执行文件的构建任务,另一种是直接利用变量 always,即无需指定第一种方式中的依赖关系,只要Makefile被执行,变量always中包含的可执行文件都会被构建。文件中有如下内容:

always := $(hostprogs-y)
  • 变量 <executeable>-objs
LIBBPF := ../../tools/lib/bpf/bpf.o
CGROUP_HELPERS := ../../tools/testing/selftests/bpf/cgroup_helpers.o

test_lru_dist-objs := test_lru_dist.o $(LIBBPF)
sock_example-objs := sock_example.o $(LIBBPF)
fds_example-objs := bpf_load.o $(LIBBPF) fds_example.o
sockex1-objs := bpf_load.o $(LIBBPF) sockex1_user.o
sockex2-objs := bpf_load.o $(LIBBPF) sockex2_user.o
sockex3-objs := bpf_load.o $(LIBBPF) sockex3_user.o

如上代码,前两行是声明变量,在后续会进行引用。后续的变量名称和定义都有着这样的规律:<executeable>-objs,右边是多个.o文件, 可执行文件可以由多个其他文件复合组成,通过<executeable>-objs这样的语法,可以列出并指定所有用于生成最终可执行文件(命名为executeable)的文件清单。以如下代码为例,可执行文件sockex1是由bpf_load.o、bpf.o和sockex1_usr.o链接生成的。

sockex1-objs := bpf_load.o $(LIBBPF) sockex1_user.o
  • 变量HOSTCFLAGS和HOSTLOADLIBES
HOSTCFLAGS += -I$(objtree)/usr/include
HOSTCFLAGS += -I$(srctree)/tools/lib/
HOSTCFLAGS += -I$(srctree)/tools/testing/selftests/bpf/
HOSTCFLAGS += -I$(srctree)/tools/lib/ -I$(srctree)/tools/include
HOSTCFLAGS += -I$(srctree)/tools/perf

HOSTCFLAGS_bpf_load.o += -I$(objtree)/usr/include -Wno-unused-variable
HOSTLOADLIBES_fds_example += -lelf
HOSTLOADLIBES_sockex1 += -lelf
HOSTLOADLIBES_sockex2 += -lelf
HOSTLOADLIBES_sockex3 += -lelf
...
HOSTLOADLIBES_tracex4 += -lelf -lrt
...
  1. 变量HOSTCFLAGS, 它是在编译host program(即可执行文件)时,为编译操作指定的特殊选项,如上面代码中使用-I参数指定依赖的头文件所在目录。默认情况下,这个变量的配置会作用到当前Makefile涉及的所有host program。如果你想为某个host program单独指定一个编译选项,可以像上文的这行代码:(只为bpf_load.o这个object文件指定特殊选项。)
     HOSTCFLAGS_bpf_load.o += -I$(objtree)/usr/include -Wno-unused-variable
  2. HOSTLOADLIBES是用于链接(link)操作时指定的特殊选项,如上面代码中使用两个library(因为代码中使用了相关的函数),通过选项-l加到最终生成的可执行文件中:
    • libelf,这个库用来管理elf格式的文件,bpf程序一般都会使用elf作为最终格式,因此需要加载这个library。
    • librt,这个库其实很常用,一般含有#include<time.h>头文件的代码,都需要加载这个library,用来支持real time相关功能。
  • 如何编译BPF程序源文件
# Trick to allow make to be run from this directory
all:
    $(MAKE) -C ../../ $(CURDIR)/

...
$(obj)/%.o: $(src)/%.c
    $(CLANG) $(NOSTDINC_FLAGS) $(LINUXINCLUDE) $(EXTRA_CFLAGS) -I$(obj) \
              -I$(srctree)/tools/testing/selftests/bpf/ \
              -D__KERNEL__ -Wno-unused-value -Wno-pointer-sign \
              -D__TARGET_ARCH_$(ARCH) -Wno-compare-distinct-pointer-types \
              -Wno-gnu-variable-sized-type-not-at-end \
              -Wno-address-of-packed-member -Wno-tautological-compare \
              -Wno-unknown-warning-option $(CLANG_ARCH_ARGS) \
              -O2 -emit-llvm -c $< -o -| $(LLC) -march=bpf -filetype=obj -o $@

其中有两个系统变量:第一个 $@ 代表的是target所指的文件名;第二个 $< 代表的是第一个prerequisite的文件名。这里看过前面提到的阮一峰的Make教程的话就知道这两个变量的意思,他们属于自动变量:

$@ 指代当前目标,就是Make命令当前构建的那个目标。比如,make foo的 $@ 就指代fo

a.txt b.txt: 
touch $@

等同于下面的写法:

a.txt:
touch a.txt
b.txt:
touch b.txt

$< 指代第一个前置条件。比如,规则为 t: p1 p2,那么 $< 就指代p1。

综上:

a.txt: b.txt c.txt
cp $< $@ 

等同于:

a.txt: b.txt c.txt
cp b.txt a.txt 

上面的编译代码抽取出核心部分如下:

$(obj)/%.o: $(src)/%.c
    clang -I $(srctree)/tools/testing/selftests/bpf/ \ 
            -O2 -emit-llvm -c $< -o -| \ 
            llc -march=bpf -filetype=obj -o $@

可以看出最后一行make命令的本质,就是把所有.c源代码文件,通过clang全部编译成.o目标文件。其中通配符匹配的 .o 文件和 .c 文件分别是Makefile规则 <target> : <prerequisites> 中的两个参数。

小结

对samples/bpf/Makefile这个文件执行make命令的本质就是:

  1. 为运行在内核空间的示例源代码(一般文件名称后缀为kern.c),编译生成.o后缀的目标文件,以便加载到对应BPF提供的hook中去。
  2. 为运行在用户空间的示例源代码(一般文件文件后缀为user.c),编译生成可以在本机直接运行的可执行文件,以便用户可以直接运行测试。

总结

在学习这部分内容的文章和课程的时候,发现新版本的内核源码已经有了一些变化,其中 load.h 的缺失也是在发展过程被抛弃的,虽然导致了编译的问题。 但是也学习了关于编译过程进行了哪些工作和 Makefile 的规范,以及 BPF C编程的一些入门。

这里后续我转为使用libbpf-bootstrap进行编程工作。本章节学习结束,后续会在libbpf-bootstrap的学习基础上进行文章分享。


文章作者: LANVNAL
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 LANVNAL !
 本篇
eBPF编程入门学习笔记--C编程 eBPF编程入门学习笔记--C编程
记录针对eBPF学习的笔记内容,本文是对eBPF编程学习的记录,第一篇文章包括eBPF C编程和Makefile文件分析
2022-09-28
下一篇 
MAC定期需要更改密码的问题的解决 MAC定期需要更改密码的问题的解决
一般来说,消费或者个人用户的机器上不会有用户账户密码时效规则,而企业用户中比较常见。主要原因是之前实习的时候为了入域,安装了ioa,但是卸载的话没使用正确的方式,直接拖垃圾桶了,当时软件设置的密码策略就还在,所以就会定期提示更改密码
2022-02-11
  目录