Linux 内核观测技术BPF
创始人
2024-06-02 07:39:32
0

BPF简介

BPF验证器

BPF允许任何人在Linux内核之中执行任意的代码,这听起来的十分危险,但是由于有着BPF验证器使得这一过程变的相当的安全。BPF时内核的一个模块,所有的BPF程序都必须经过它的审查才能够被加载到内核之中去运行。

验证器执行的第一项检查就是对BPF虚拟机加载的代码进行静态分析。这一步的目的是保证程序可以按照预期去结束,而不会产生死循环拜拜浪费系统资源。验证器会创建一个DAG(有向无环图),将BPF程序的每个执行首位相连之后去执行DFS(深度优先遍历),当且仅当每个路径都能达到DAG的底部才会通过验证。

之后其会执行第二项检查,也就是对BPF程序执行预执行处理。这个时候验证器会去分析程序执行的每条指令,确保不会执行无效的指令。同时也会检查所有内存指针是否可以正确访问和解引用。

尾部调用

BPF程序可以使用尾部调用来调用其他BPF程序,这是个强大的功能。其允许通过组合比较小的BPF功能来实现更为复杂的程序。当从一个BPF程序调用另外一个BPF程序的时候,内核会完全重置程序上下文。这意味着如果想要在多个BPF程序之中共享信息这是做不到的。为了解决程序间共享信息的问题,BPF引入了BPF映射的机制来解决这个问题,我们会在后面详细的介绍BPF映射机制。

注:内核5.2 版本之前BPF只允许执行4096条指令,所以才有了尾部调用这个特性。从5.2开始,指令限制扩展到了100w条,尾部调用的递归层次也有了32次的限制。

BPF 环境配置

内核升级

BPF程序在4系内核之后就已经成为了内核的顶级子系统,但是为了让我们的系统能够稳定运行BPF程序,还是推荐安装5系内核。首先,我们可以使用如下的命令获取当前系统的版本:

uname -aLinux localhost 5.0.9 #2 SMP PREEMPT Mon Feb 27 00:00:23 CST 2023 x86_64 x86_64 x86_64 GNU/Linux

笔者这里的系统已经经过升级了,如果没有经历过升级,可以按照如下的命令获取系统的源码:

# 获取相应版本的内核源码
cd /tmp
wget -c https://mirrors.aliyun.com/linux-kernel//v5.x/linux-5.0.9.tar.gz -O - | tar -xz

之后的过程,同学们可以百度相应的教程获取安装,本文章将专注于BPF技术的使用。

安装好相应内核之后,为了让我们在开发的时候更为容易,推荐这里将内核源码单独编译一下,方便我们链接:

tar -xvf linux-5.0.9.tar.gz
sudo mv linux-5.0.9 /kernel-src
cd /kernel-src/tools/lib/bpf
sudo make && sudo make install prefix=/

依赖环境安装

升级好内核环境之后,我们还需要安装BPF程序的依赖环境,主要可以分为三个部分:

  • BCC 工具包:通过github 获取相应的源码进行安装
  • LLVM 编译器:访问官网可获取安装教程
  • 其他依赖程序:
sudo dnf install make glibc-devel.i686 elfutils-libelf-devel wget tar clang bcc strace kernel-devel -y

运行第一个BPF程序

在安装好上述程序之后,我们使用如下的代码可以来测试我们的环境是否配置完成。BPF程序可以由C语言来编写,之后由LLVM编译,其可以将C语言写的程序编译成能够加载到内核执行的汇编代码。

# 指定编译器为clang
CLANG = clang
# 编译完后的程序名称
EXECABLE = monitor-exec
# 源码名称
BPFCODE = bpf_program
# BPF依赖地址
BPFTOOLS = /kernel-src/samples/bpf
BPFLOADER = $(BPFTOOLS)/bpf_load.c
# 指定头文件
CCINCLUDE += -I/kernel-src/tools/testing/selftests/bpf
LOADINCLUDE += -I/kernel-src/samples/bpf
LOADINCLUDE += -I/kernel-src/tools/lib
LOADINCLUDE += -I/kernel-src/tools/perf
LOADINCLUDE += -I/kernel-src/tools/include
LIBRARY_PATH = -L/usr/local/lib64
BPFSO = -lbpfCFLAGS += $(shell grep -q "define HAVE_ATTR_TEST 1" /kernel-src/tools/perf/perf-sys.h \&& echo "-DHAVE_ATTR_TEST=0").PHONY: clean $(CLANG) bpfload buildclean:rm -f *.o *.so $(EXECABLE)build: ${BPFCODE.c} ${BPFLOADER}$(CLANG) -O2 -target bpf -c $(BPFCODE:=.c) $(CCINCLUDE) -o ${BPFCODE:=.o}bpfload: build# 编译程序clang $(CFLAGS) -o $(EXECABLE) -lelf $(LOADINCLUDE) $(LIBRARY_PATH) $(BPFSO) \$(BPFLOADER) loader.c$(EXECABLE): bpfload.DEFAULT_GOAL := $(EXECABLE)

程序源码有两个,一个是bpf_program.c这里面存放的是要执行的BPF源码,其会被编译成为一个.o文件。
在这里我们使用BPF提供的SEC属性告知BPF虚拟机在何时运行此程序。下面的代码会在execve系统调用跟踪点被执行的时候运行BPF程序。当内核检测到execve的时候,BPF程序被执行时,我们会看到输出消息"Hello, World, BPF!"

#include 
#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, World, BPF!";bpf_trace_printk(msg, sizeof(msg));return 0;
}// 程序许可证,linux内核只允许加载GPL许可的程序
char _license[] SEC("license") = "GPL";

上面的.o文件会被下面的这个由loader.c编译成为的moniter-exec程序去执行。其会把BPF程序加载到内核之中去运行,这里依赖的就是我们使用的load_bpf_file,其将会获取一个二进制文件并把它加载到内核之中。

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

之后我们执行如下的命令去编译上述的代码:

make# 运行以下程序
sudo ./loader

BPF映射

BPF映射以的形式会被保存到内核之中,其可以被任何其他的BPF程序访问。用户空间的程序也可以通过文件描述符访问BPF映射。BPF映射之中可以保存事先指定大小的任何类型的数据。内核会将数据看作二进制块,这意味着内核并不关系BPF映射保存的具体内容。

此内容会存在较多的代码,这里会将相关所需要的MakeFile文件内容展示出来:

CLANG = clangINCLUDE_PATH += -I/kernel-src/tools/lib/bpf
INCLUDE_PATH += -I/kernel-src/tools/**
LIBRARY_PATH = -L/usr/local/lib64
BPFSO = -lbpf
.PHONY: clean clean:rm -f # 要删除的BPF模块build: # 填写要编译的 BPF程序模块.DEFAULT_GOAL := build

创建BPF映射

创建BPF映射的最值方式就是使用bpf_create_map系统调用。这个函数需要传入五个参数:

  • map_type:map的类型,如果设置为BPF_MAP_CREATE,则表示创建一个新的映射。
  • key_size: key的字节数
  • value_size:value的字节数
  • max_entries:最大的键值对数量
  • map_flags:map创建行为的参数,0表示不预先分配内存
int bpf_create_map(bpf_map_type map_type, int key_size, int value_size, int max_entries, int map_flags);

如果创建成功,这个接口会返回一个指向这个map的文件描述符。如果创建失败,将返回-1。失败会有三种原因,我们可以通过errno来进行区分。

  1. 如果属性无效,内核将errnor变量设置为EINVAL
  2. 如果用户权限不够,内核将errno变量设置为EPERM
  3. 如果没有足够的内存来保存映射的话,内核将errno变量设置为ENOMEM

Demo

#include 
#include 
#include 
#include 
#include int main(int argc, char **argv) {//# createint fd = bpf_create_map(BPF_MAP_TYPE_HASH, sizeof(int), sizeof(int), 100, 0);if (fd < 0) {printf("Failed to create map: %d (%s)\n", fd, strerror(errno));return -1;}printf("Create BPF map success!\n");
}

我们在一开始提到的MakeFile文件之中添加如下信息即可编译上述代码:

create: map_create.c clang -o create -lelf $(INCLUDE_PATH) $(LIBRARY_PATH) $(BPFSO) $?
...
build: create

最后运行编译后的程序:

sudo ./create 
Create BPF map success!

BPF映射类型

在Demo之中我们使用到了BPF_MAP_TYPE_HASH这个map类型,其表示在内核空间之中创建一个哈希表映射。除此之外,BPF还支持如下的Map类型:

  • BPF_MAP_TYPE_HASH: 哈希表映射,和我们熟知的哈希表是类似的。该映射可以使用任意大小的Key和Value,内核会按照需求分配和释放他们。当在哈希表映射上使用更新操作的时候,内核会自动的更新元素。
  • BPF_MAP_TYPE_ARRAY:数据映射,在对数据初始化的时候,所有元素在内存之中将预分配空间并且设置为0。数据映射的Key必须是4字节的,而且使用数组映射的一个缺点是映射之中的元素不能够被删除,这使得无法使数据变小。如果在数组上执行删除操作,那么用户将得到一个EINVAL错误。
  • BPF_MAP_TYPE_PROG_ARRAY:程序数组映射,这种类型保存对BPF程序的引用(其他BPF程序的文件描述符),程序数据映射类型可以使用bpf_tail_call来执行刚刚提到的尾部调用。
  • BPF_MAP_TYPE_PERF_EVENT_AYYAY:Perf事件数组映射,该映射将perf_events数据存储在环形缓存区,用于BPF程序和用户空间程序进行实时通信。其可以将内核跟踪工具发出的事件转发给用户空间程序,使很多可观测工具的基础。
  • BPF_MAP_TYPE_PERCUP_HASH:哈希表映射的改进版本,我们可以将此哈希表分配给单个独立的CPU(每个CPU都有自己独立的哈希表),而不是多个CPU共享一个哈希表。
  • BPF_MAP_TYPE_PRECPU_ARRAY:数据映射的改进版本,也是每个CPU拥有自己独立的数组。
  • BPF_MAP_TYPE_STACK_TRACE:栈跟踪信息,可以结合内核开发人员添加的帮助函数bpf_get_stackid将栈跟踪信息写入到该映射。

持久化BPF MAP

BPF映射的基本特征使基于文件描述符的,这意味着关闭文件描述符后,映射及其所保存的所有信息都会消失。这意味着我们无法获取已经结束的BPF程序保存在映射之中的信息,在Linux 内核4.4 版本之后,引入了两个新的系统调用,bpf_obj_pin用来固定(固定后不可更改)和bpf_obj_get获取来自BPF虚拟文件系统的映射和BPF程序。

BPF虚拟文件系统的默认目录使/sys/fs/bpf,如果Linux系统内核不支持BPF,可以使用mount命令挂载此文件系统:

mount -t bpf /sys/fs/bpf /sys/fs/bpf

BPF固定的系统调用为bpf_obj_pin,其函数原型如下:

  • file_fd:表示map的文件描述符
  • file_path:要固定到的文件路径
int bpf_obj_pin(int file_fd, const char* file_path)

Demo

#include 
#include 
#include 
#include 
#include 
#include 
#include static const char *file_path = "/sys/fs/bpf/my_hash";int main(int argc, char **argv) {//# createint fd = bpf_create_map(BPF_MAP_TYPE_HASH, sizeof(int), sizeof(int), 100, 0);if (fd < 0) {printf("Failed to create map: %d (%s)\n", fd, strerror(errno));return -1;}int pinned = bpf_obj_pin(fd, file_path);if (pinned < 0) {printf("Failed to pin map to the file system: %d (%s)\n", pinned,strerror(errno));return -1;}return 0;
}

我们在一开始提到的MakeFile文件之中添加如下信息即可编译上述代码:

save: map_save.c clang -o save -lelf $(INCLUDE_PATH) $(LIBRARY_PATH) $(BPFSO) $?
...
build: save

之后,我们可以查看这个目录查看是否固定成功了:

sudo ls  /sys/fs/bpf/
my_hash

对BPF 元素进行CRUD

Update

我们可以使用bpf_map_update_elem系统调用去插入元素到刚创建的map之中。内核程序需要从bpf/bpf_helpers.h文件加载此函数,而用户空间程序则需要从tools/lib/bpf/bpf.h文件加载,所以内核程序访问的函数签名和用户空间之不同的。当然,访问的行为也是不同的:内核程序可以原子的执行更新操作,用户空间则需要发送消息到内核,之后先复制值,然后再进行更新映射。这意味着更新操作不是原子性的。

下面使这个函数的函数原型,如果执行成功,该函数返回0;如果失败,则将返回复数并且把失败的原因写入全局变量errno之中。

  • file_fd:map的文件描述符表示
  • key:指向key的指针
  • value:指向value的指针
  • type:表示更新映射的方式。
    1. 如果传入0,表示元素存在则更新,不存在则创建;
    2. 如果传入1,表示在元素不存在的时候,内核创建元素
    3. 如果传入2,表示元素存在的时候,内核更新元素
int bpf_map_update_elem(int file_fd, void* key, void* value, int type);

Demo

#include 
#include 
#include 
#include 
#include 
#include #include "bpf.h"extern char *optarg;extern int optind;extern int opterr;extern int optopt;static const char *file_path = "/sys/fs/bpf/my_hash";int main(int argc, char **argv) {char ch;int key;int value;while ((ch = getopt(argc, argv, "k:v:")) != -1) {switch (ch) {case 'k':printf("set key: %s\n", optarg);key = atoi(optarg);break;case 'v':printf("set value: %s\n", optarg);value = atoi(optarg);break;}}int fd, added, pinned;//# openfd = bpf_obj_get(file_path);if (fd < 0) {printf("Failed to fetch the map: %d (%s)\n", fd, strerror(errno));return -1;}added = bpf_map_update_elem(fd, &key, &value, BPF_ANY);if (added < 0) {printf("Failed to update map: %d (%s)\n", added, strerror(errno));return -1;}return 0;
}

我们在一开始提到的MakeFile文件之中添加如下信息即可编译上述代码:

update: map_update.c clang -o update -lelf $(INCLUDE_PATH) $(LIBRARY_PATH) $(BPFSO) $?
...
build: update

最后运行编译后的程序:

sudo ./update -k 1 -v 9
set key: 1
set value: 9

Fetch

当新元素写入到map之后,我们可以使用bpf_map_lookup_elem系统调用来读取map之中的元素,其函数原型如下:

下面使这个函数的函数原型,如果执行成功,该函数返回0;如果失败,则将返回复数并且把失败的原因写入全局变量errno之中。

  • file_fd:map的文件描述符表示
  • key:指向key的指针
  • value:指向value的指针
int bpf_map_lookp_elem(int file_fd, void* key, void* value);

Demo

#include 
#include 
#include 
#include 
#include "bpf.h"
#include 
#include 
#include "bpf.h"extern char* optarg;extern int optind;extern int opterr;extern int optopt;static const char *file_path = "/sys/fs/bpf/my_hash";int main(int argc, char **argv) {char ch;int key;int value;while ((ch = getopt(argc, argv, "k:v:")) != -1){switch (ch){case 'k':key = atoi(optarg);break;}}int fd, result;fd = bpf_obj_get(file_path);if (fd < 0) {printf("Failed to fetch the map: %d (%s)\n", fd, strerror(errno));return -1;}result = bpf_map_lookup_elem(fd, &key, &value);if (result < 0) {printf("Failed to read value from the map: %d (%s)\n", result,strerror(errno));return -1;}printf("Value read from the key %d: '%d'\n", key,value);return 0;
}

我们在一开始提到的MakeFile文件之中添加如下信息即可编译上述代码:

fetch: map_fetch.c clang -o fetch -lelf $(INCLUDE_PATH) $(LIBRARY_PATH) $(BPFSO) $?
...
build: fetch

最后运行编译后的程序:

sudo ./update -k 1 -v 9
set key: 1
set value: 9

Delete

当新元素写入到map之后,我们可以使用bpf_map_delete_elem系统调用来删除map之中的元素,其函数原型如下:

下面使这个函数的函数原型,如果执行成功,该函数返回0;如果失败,则将返回复数并且把失败的原因写入全局变量errno之中。

  • file_fd:map的文件描述符表示
  • key:指向key的指针
int bpf_map_delete_elem(int file_fd, void* key);

Demo

#include 
#include 
#include 
#include 
#include "bpf.h"
#include 
#include 
#include "bpf.h"extern char* optarg;extern int optind;extern int opterr;extern int optopt;static const char *file_path = "/sys/fs/bpf/my_hash";int main(int argc, char **argv) {char ch;int key;int value;while ((ch = getopt(argc, argv, "k:v:")) != -1){switch (ch){case 'k':key = atoi(optarg);break;}}int fd,result;fd = bpf_obj_get(file_path);if (fd < 0) {printf("Failed to fetch the map: %d (%s)\n", fd, strerror(errno));return -1;}key = 1;result = bpf_map_delete_elem(fd, &key);if (result < 0) {printf("Failed to delete value from the map: %d (%s)\n", fd,strerror(errno));return -1;}printf("delte key:%d success!\n", key);return 0;
}

我们在一开始提到的MakeFile文件之中添加如下信息即可编译上述代码:

delete: map_delete.c clang -o delete -lelf $(INCLUDE_PATH) $(LIBRARY_PATH) $(BPFSO) $?
...
build: delete

最后运行编译后的程序:

sudo ./delete -k 1
delte key:1 success!

Iter

假设我们写入了很多元素到map之后,我们可以使用bpf_map_get_next_key系统调用来遍历map之中的元素,其函数原型如下:

下面使这个函数的函数原型,如果执行成功,该函数返回0;如果失败,则将返回复数并且把失败的原因写入全局变量errno之中。

  • file_fd:map的文件描述符表示
  • key:指向key的指针
  • next_key:指向下个key的指针
int bpf_map_get_next_key(int file_fd, void* key, void* next_key);

Demo

#include 
#include 
#include 
#include 
#include 
#include #include "bpf.h"extern char *optarg;extern int optind;extern int opterr;extern int optopt;static const char *file_path = "/sys/fs/bpf/my_hash";int main(int argc, char **argv) {int fd, value, result;fd = bpf_obj_get(file_path);if (fd < 0) {printf("Failed to fetch the map: %d (%s)\n", fd, strerror(errno));return -1;}int start_key = -1;int next_key;while (bpf_map_get_next_key(fd, &start_key, &next_key) == 0) {start_key = next_key;printf("Key read from the map: '%d'\n", next_key);}return 0;
}

Demo

iter: map_iter.c clang -o iter -lelf $(INCLUDE_PATH) $(LIBRARY_PATH) $(BPFSO) $?
...
build: iter

最后运行编译后的程序:

[ik@localhost chapter-3]$ sudo ./iter 
Key read from the map: '2'
Key read from the map: '8'
Key read from the map: '10'
Key read from the map: '5'
Key read from the map: '6'
Key read from the map: '3'
Key read from the map: '4'
Key read from the map: '9'
Key read from the map: '7'
Key read from the map: '11'

相关内容

热门资讯

生活描写太阳的诗句 生活描写太阳的诗句  在学习、工作、生活中,大家肯定对各类诗句都很熟悉吧,不同的诗句,其语言艺术所表...
出伏的古诗词 出伏的古诗词  在日复一日的'学习、工作或生活中,大家都收藏过令自己印象深刻的古诗吧,汉魏以后的古诗...
形容野外雪天的诗句 形容野外雪天的诗句  冬天,大雪纷飞。人们好像来到了一个幽雅恬静的境界,来到了一个晶莹透剔的童话般的...
“一川明月疏星,浣纱人影娉婷... 一川明月疏星,浣纱人影娉婷出自辛弃疾《清平乐·博山道中即事》柳边飞鞚,露湿征衣重。宿鹭窥沙孤影动,应...
描写舞姿优美的古诗句 描写舞姿优美的古诗句  在平时的学习、工作或生活中,大家或多或少都接触过一些经典的诗句吧,诗句富于音...
张爱玲散文《雨伞下》 张爱玲散文《雨伞下》  引导语:张爱玲的散文《雨伞下》讲述了一个道理穷人结交富人,往往要赔本,下文是...
浪花淘尽英雄 “浪花淘尽英雄”出处 出自 明代 杨慎 的《临江仙》“浪花淘尽英雄”平仄韵脚 拼音:làng huā...
袁枚所见古诗 袁枚所见古诗  在学习、工作、生活中,大家都接触过很多优秀的古诗吧,古诗包括唐律形成以前所有体式的诗...
“还将石溜调琴曲,更取峰霞入... “还将石溜调琴曲,更取峰霞入酒杯。”这两句是形容南庄幽美的景色——用岩石间流水淙淙的声音,来调谐琴曲...
时光只解催人老,不信多情,长... “时光只解催人老,不信多情,长恨离亭,泪滴春衫酒易醒。”出处 出自 宋代 晏殊 的《采桑子·时光只解...
赞美苏州的诗句 赞美苏州的诗句  无论是身处学校还是步入社会,大家都看到过许多经典的诗句吧,诗句一般饱含丰富的想象、...
中秋节团圆古诗 中秋节团圆古诗  在生活、工作和学习中,大家都看到过许多经典的古诗吧,汉魏以后的古诗一般以五七言为基...
新年的诗句解读 新年的诗句赏析  引导语:诗句中的新年是怎样一副景象?小编为大家带来关于新年的'诗句,希望对大家有所...
现代七夕情诗 现代七夕情诗(精选9首)  在我国农历七月初七这一天是人们俗称的七夕节,以下是小编为大家推荐的现代七...
歌颂母爱的诗句 歌颂母爱的诗句  在现实生活或工作学习中,大家最不陌生的就是诗句了吧,诗句以强烈的节奏、美妙的韵律、...
山枇杷 白居易 山枇杷 白居易  山枇杷 白居易,此诗是一首七言律诗,是一首描绘山枇杷的诗作,作者是唐代著名的`诗人...
思念家乡时的诗句 思念家乡时的诗句  引导语:门外若无南北路,人间应免别离愁。下面就是一些思念家乡时的诗句,欢迎大家阅...
描写燕子的古诗 描写燕子的古诗  燕子身袭一身黑衣,虽是乌黑却能散发出柔和的光亮。它那一双灵动的小眼睛让人即便在黑夜...
描写春雨后景色的诗句 描写春雨后景色的诗句  导语:中国最早的诗句为律诗结构,格律要求严格,比如先秦时期的诗一般每句四言律...
朱自清作品欣赏 《背影》 朱自清作品欣赏 《背影》  我与父亲不相见已二年余了,我最不能忘记的是他的背影 。  那年冬天,祖母...