BPF,全称是Berkeley Packet Filter(伯克利数据包过滤器)的缩写。其诞生于1992年,最初的目的是提升网络包过滤工具的性能。后面,随着这个工具重新实现BPF的内核补丁和不断完善代码,BPF程序变成了一个更通用的执行引擎,可以完成多种任务。简单来说,BPF提供了一种在各种内核时间和应用程序事件发生时运行一小段程序的机制。其允许内核在系统和应用程序事件发生时运行一小段程序,这样就将内核变得完全可编程,允许用户定制和控制他们的系统。
BPF其有指令集、存储对象和辅助函数等几部分组成。由于它采取了虚拟指令集规范,因此也可将其视为一种虚拟机实现。当Linux指定的时候,其会提供两种执行机制:一个解释器和一个将BPF指令动态转换为本地化指令的即时编程器。在实际执行之前,BPF指令必须先通过验证器的安全性检查,以确保BPF程序自身不会崩溃或者损坏内核。
注:扩展后的BPF通常缩写为eBPF,但官方缩写仍然是BPF。在内核之中只有一个执行引擎,其同时支持eBPF和经典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程序在4系内核之后就已经成为了内核的顶级子系统,但是为了让我们的系统能够稳定运行BPF程序,还是推荐安装5系内核。首先,我们可以使用如下的命令获取当前系统的版本:
uname -a
Linux 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程序的依赖环境,主要可以分为三个部分:
sudo dnf install make glibc-devel.i686 elfutils-libelf-devel wget tar clang bcc strace kernel-devel -y
在安装好上述程序之后,我们使用如下的代码可以来测试我们的环境是否配置完成。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 = -lbpf
CFLAGS += $(shell grep -q "define HAVE_ATTR_TEST 1" /kernel-src/tools/perf/perf-sys.h
&& echo "-DHAVE_ATTR_TEST=0")
.PHONY: clean $(CLANG) bpfload build
clean:
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 < 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, 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 < stdio.h >
int main(int argc, char **argv) {
if (load_bpf_file("bpf_program.o") != 0) {
printf("The kernel didn't load the BPF programn");
return -1;
}
read_trace_pipe();
return 0;
}
之后我们执行如下的命令去编译上述的代码:
make
# 运行以下程序
sudo ./loader
BPF映射以的形式会被保存到内核之中,其可以被任何其他的BPF程序访问。用户空间的程序也可以通过文件描述符访问BPF映射。BPF映射之中可以保存事先指定大小的任何类型的数据。内核会将数据看作二进制块,这意味着内核并不关系BPF映射保存的具体内容。
此内容会存在较多的代码,这里会将相关所需要的MakeFile文件内容展示出来:
CLANG = clang
INCLUDE_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_create_map
系统调用。这个函数需要传入五个参数:
BPF_MAP_CREATE
,则表示创建一个新的映射。int bpf_create_map(bpf_map_type map_type, int key_size, int value_size, int max_entries, int map_flags);
如果创建成功,这个接口会返回一个指向这个map的文件描述符。如果创建失败,将返回-1。失败会有三种原因,我们可以通过errno
来进行区分。
EINVAL
;EPERM
;ENOMEM
;#include < errno.h >
#include < linux/bpf.h >
#include < stdio.h >
#include < stdlib.h >
#include < unistd.h >
int main(int argc, char **argv) {
//# create
int 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!
在Demo之中我们使用到了BPF_MAP_TYPE_HASH
这个map类型,其表示在内核空间之中创建一个哈希表映射。除此之外,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
,其函数原型如下:
int bpf_obj_pin(int file_fd, const char* file_path)
#include < errno.h >
#include < linux/bpf.h >
#include < stdio.h >
#include < string.h >
#include < unistd.h >
#include < stdlib.h >
#include
static const char *file_path = "/sys/fs/bpf/my_hash";
int main(int argc, char **argv) {
//# create
int 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_map_update_elem系统调用去插入元素到刚创建的map之中。内核程序需要从bpf/bpf_helpers.h文件加载此函数,而用户空间程序则需要从tools/lib/bpf/bpf.h文件加载,所以内核程序访问的函数签名和用户空间之不同的。当然,访问的行为也是不同的:内核程序可以原子的执行更新操作,用户空间则需要发送消息到内核,之后先复制值,然后再进行更新映射。这意味着更新操作不是原子性的。
下面使这个函数的函数原型,如果执行成功,该函数返回0;如果失败,则将返回复数并且把失败的原因写入全局变量errno之中。
int bpf_map_update_elem(int file_fd, void* key, void* value, int type);
#include < errno.h >
#include < linux/bpf.h >
#include < stdio.h >
#include < stdlib.h >
#include < string.h >
#include < unistd.h >
#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: %sn", optarg);
key = atoi(optarg);
break;
case 'v':
printf("set value: %sn", optarg);
value = atoi(optarg);
break;
}
}
int fd, added, pinned;
//# open
fd = 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
当新元素写入到map之后,我们可以使用bpf_map_lookup_elem
系统调用来读取map之中的元素,其函数原型如下:
下面使这个函数的函数原型,如果执行成功,该函数返回0;如果失败,则将返回复数并且把失败的原因写入全局变量errno之中。
int bpf_map_lookp_elem(int file_fd, void* key, void* value);
#include < errno.h >
#include < linux/bpf.h >
#include < stdio.h >
#include < string.h >
#include "bpf.h"
#include < unistd.h >
#include < stdlib.h >
#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
当新元素写入到map之后,我们可以使用bpf_map_delete_elem
系统调用来删除map之中的元素,其函数原型如下:
下面使这个函数的函数原型,如果执行成功,该函数返回0;如果失败,则将返回复数并且把失败的原因写入全局变量errno之中。
int bpf_map_delete_elem(int file_fd, void* key);
#include < errno.h >
#include < linux/bpf.h >
#include < stdio.h >
#include < string.h >
#include "bpf.h"
#include < unistd.h >
#include < stdlib.h >
#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!
假设我们写入了很多元素到map之后,我们可以使用bpf_map_get_next_key
系统调用来遍历map之中的元素,其函数原型如下:
下面使这个函数的函数原型,如果执行成功,该函数返回0;如果失败,则将返回复数并且把失败的原因写入全局变量errno之中。
int bpf_map_get_next_key(int file_fd, void* key, void* next_key);
#include < errno.h >
#include < linux/bpf.h >
#include < stdio.h >
#include < stdlib.h >
#include < string.h >
#include < unistd.h >
#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;
}
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'
跟踪使一种为了进行分析和调试工作的数据收集行为,通过有效的利用BPF来使得我们可以以尽可能小的代价来访问Linux内核和应用程序的任何信息。
探针使一种探测程序,其会传递程序执行时环境的相关信息,我们通过BPF探针收集系统之中的数据以方便我们后续进行探索分析。在BPF之中,主要会提供以下四种探针:
内核探针提供了对几乎任何内核指令设置动态标记和中断的能力。当内核到达这些标志的时候,附加到探针的代码就会被执行,之后内核将恢复到正常运行的模式。
注:这里指的注意的是,内核探针没有稳定的应用程序二进制接口(ABI),其会随着内核版本的演进而更改。
内核探针可以分为两类:
下面的例子是个简单的Demo:
我们首先在python之中插入C代码,其主要工作就是获取当前内核正在运行的命令名称。之后使用python 的BPF加载此C代码,并将此代码和execve
系统调用相关联起来,也就是当execve系统调用被触发之后,会先去执行我们指定的用户代码。
from bcc import BPF
bpf_source = """
#include < uapi/linux/ptrace.h >
int do_sys_execve(struct pt_regs *ctx) {
char comm[16];
//获得当前内核正在运行的命令名
bpf_get_current_comm(&comm, sizeof(comm));
bpf_trace_printk("executing program: %s
", comm);
return 0;
}
"""
# 加载BPF程序到内核
bpf = BPF(text=bpf_source)
# 将BPF程序和execve系统调用关联
execve_function = bpf.get_syscall_fnname("execve")
# 由于不同内核版本提供的ABI不同,bcc工具包提供了获得函数签名的接口
bpf.attach_kprobe(event=execve_function, fn_name="do_sys_execve")
# 输出跟踪日志
bpf.trace_print()
上面的代码最终执行效果如下:
sudo python3 example.py
b' node-35560 [005] d..31 26011.217315: bpf_trace_printk: executing program: node'
b''
b' sh-35562 [007] d..31 26011.219055: bpf_trace_printk: executing program: sh'
b''
b' node-35563 [006] d..31 26011.221001: bpf_trace_printk: executing program: node'
b''
b' sh-35563 [007] d..31 26011.222363: bpf_trace_printk: executing program: sh'
b''
b' node-35564 [007] d..31 26011.233929: bpf_trace_printk: executing program: node'
b''
b' sh-35564 [007] d..31 26011.235267: bpf_trace_printk: executing program: sh'
b''
b' cpuUsage.sh-35565 [002] d..31 26011.236663: bpf_trace_printk: executing program: cpuUsage.sh'
kretprobes:kretprobes是在内核指令有返回值时插入BPF程序
下面是一个使用kretprobs的例子,其会在execve
系统调用之后开始执行我们的指定的BPF程序。
from bcc import BPF
bpf_source = """
#include < uapi/linux/ptrace.h >
int ret_sys_execve(struct pt_regs *ctx) {
int return_value;
char comm[16];
bpf_get_current_comm(&comm, sizeof(comm));
//获取返回值 PT_REGS_RC 获取上下文之中寄存器的返回值
return_value = PT_REGS_RC(ctx);
bpf_trace_printk("program: %s, return: %d
", comm, return_value);
return 0;
}
"""
bpf = BPF(text=bpf_source)
execve_function = bpf.get_syscall_fnname("execve")
bpf.attach_kretprobe(event=execve_function, fn_name="ret_sys_execve")
bpf.trace_print()
上面的程序执行效果如下:
sudo python3 example.py
b' sh-35856 [000] d..31 26366.112370: bpf_trace_printk: program: sh, return: 0'
b''
b' which-35858 [007] d..31 26366.114034: bpf_trace_printk: program: which, return: 0'
b''
b' sh-35859 [007] d..31 26366.116329: bpf_trace_printk: program: sh, return: 0'
b''
b' ps-35859 [007] d..31 26366.117328: bpf_trace_printk: program: ps, return: 0'
b''
b' sh-35860 [007] d..31 26366.129422: bpf_trace_printk: program: sh, return: 0'
b''
b' cpuUsage.sh-35860 [007] d..31 26366.130579: bpf_trace_printk: program: cpuUsage.sh, return: 0'
跟踪点时内核代码的静态标记,可用于将代码附加在运行的内核中。跟踪点和kprobes的主要区别在于跟踪点由内核开发人员在内核中编写和修改。由于其是静态存在的,所以跟踪点的ABI会更加的稳定。我们可以查看/sys/kernel/debug/tracing/events目录下的内容,这里是系统之中所有可用的跟踪点,在笔者的电脑上,跟踪点如下:
[ik@localhost kretprobes]$ sudo ls /sys/kernel/debug/tracing/events
alarmtimer devlink gvt iomap mdio nmi rcu sunrpc workqueue
avc dma_fence hda iommu mei oom regmap swiotlb writeback
block drm hda_controller io_uring migrate page_isolation resctrl syscalls x86_fpu
bpf_test_run enable hda_intel irq mmap pagemap rpm task xdp
bpf_trace error_report header_event irq_matrix mmap_lock page_pool rseq tcp xen
bridge exceptions header_page irq_vectors mmc percpu rtc thermal xfs
cfg80211 fib huge_memory kmem module power sched timer xhci-hcd
cgroup fib6 hwmon kvm mptcp printk scsi tlb
clk filelock hyperv kvmmmu msr pwm signal ucsi
compaction filemap i2c kyber napi qdisc skb udp
context_tracking fs_dax i915 libata neigh random smbus vmscan
cpuhp ftrace initcall mac80211 net ras sock vsyscall
dev gpio intel_iommu mce netlink raw_syscalls spi wbt
这里我们可以看到由两个额外的文件:
我们可以用以下命令去启用跟踪点:
下面是一个使用BPF程序跟踪系统加载其他BPF程序的Demo。我们定义我们的BPF程序,其会在执行到跟踪点的时候,执行我们的BPF程序,这里我们指定了跟踪点为net_dev_xmit,其会在执行这个跟踪点的之后,执行我们的BPF程序trace_net_dev_xmit
from bcc import BPF
bpf_source = """
int trace_net_dev_xmit(struct pt_regs *ctx) {
char comm[16];
bpf_get_current_comm(&comm, sizeof(comm));
bpf_trace_printk("%s is loading a BPF program", comm);
return 0;
}
"""
bpf = BPF(text = bpf_source)
bpf.attach_tracepoint(tp = "net:net_dev_xmit", fn_name = "trace_net_dev_xmit")
bpf.trace_print()
注:这里的net表示跟踪子系统,net_dev_xmit 才是具体的跟踪点
上面的函数执行结果如下:
sudo python3 example.py
b' node-34494 [005] d..31 27609.874798: bpf_trace_printk: node is loading a BPF program'
b' sshd-34382 [007] d..31 27609.874937: bpf_trace_printk: sshd is loading a BPF program'
b' node-34494 [005] d..31 27609.876698: bpf_trace_printk: node is loading a BPF program'
b' sshd-34382 [007] d..31 27609.876769: bpf_trace_printk: sshd is loading a BPF program'
b' irq/129-iwlwifi-847 [006] d.s61 27609.877073: bpf_trace_printk: irq/129-iwlwifi is loading a BPF program'
b' irq/129-iwlwifi-847 [006] d.s61 27609.877078: bpf_trace_printk: irq/129-iwlwifi is loading a BPF program'
b' irq/129-iwlwifi-847 [006] d.s61 27609.877079: bpf_trace_printk: irq/129-iwlwifi is loading a BPF program'
用户空间探针允许也在用户空间运行的程序中设置动态标志。它们等同于内核探针,用户空间探针是运行在用户空间的监测程序。当我们定义uprobe
的时候,内核会在附加的指令上创建陷阱。当程序执行到该指令的时候,内核将触发事件以回调函数的方式调用探针函数。
跟内核探针类似,用户探针也分为两类:
package main
import "fmt"
func main() {
fmt.Println("Hello, BPF")
}
from bcc import BPF
bpf_source = """
int trace_go_main(struct pt_regs *ctx) {
u64 pid = bpf_get_current_pid_tgid();
bpf_trace_printk("New main process running with PID: %d
", pid);
return 0;
}
"""
bpf = BPF(text = bpf_source)
bpf.attach_uprobe(name = "./main", sym = "main.main", fn_name = "trace_go_main")
bpf.trace_print()
在这里我们用go语言写了个程序用于打印"Hello, BPF",之后我们指定BPF程序,其会在执行main函数的时候打印一个提示信息。下面是这个程序执行的示例:
sudo python3 example.py
b' main-38680 [004] d..31 31093.647465: bpf_trace_printk: New main process running with PID: 38680'
b''
from bcc import BPF
bpf_source = """
BPF_HASH(cache, u64, u64);
int trace_start_time(struct pt_regs *ctx) {
u64 pid = bpf_get_current_pid_tgid();
u64 start_time_ns = bpf_ktime_get_ns();
cache.update(&pid, &start_time_ns);
return 0;
}
"""
bpf_source += """
int print_duration(struct pt_regs *ctx) {
u64 pid = bpf_get_current_pid_tgid();
u64 *start_time_ns = cache.lookup(&pid);
if (start_time_ns == 0) {
return 0;
}
u64 duration_ns = bpf_ktime_get_ns() - *start_time_ns;
bpf_trace_printk("Function call duration: %d
", duration_ns);
return 0;
}
"""
bpf = BPF(text = bpf_source)
bpf.attach_uprobe(name = "./main", sym = "main.main", fn_name = "trace_start_time")
bpf.attach_uretprobe(name = "./main", sym = "main.main", fn_name = "print_duration")
bpf.trace_print()
上面的程序会统计man函数开始和结束的时间,其会将开始时间放到BPF映射之中,然后再结束的时候从映射之中读取这个一开始的值,得到程序的执行时间:
sudo python3 example.py
b' main-39066 [005] d..31 31384.927590: bpf_trace_printk: Function call duration: 52049'
b''
A: 重新编译一遍BCC,使用如下命令:
# 编译bcc模块
git clone https://github.com/iovisor/bcc.git
mkdir bcc/build; cd bcc/build
sudo cmake ..
sudo make
sudo make install
# 解决上述报错
sudo cmake -DENABLE_LLVM_SHARED=1 ..
sudo make
sudo make install
# 编译python3依赖
sudo cmake -DPYTHON_CMD=python3 .. # build python3 binding
pushd src/python/
sudo make
sudo make install
popd
全部0条评论
快来发表一下你的评论吧 !