Linux 内核观测技术BPF

2023-05-16

BPF简介

BPF,全称是Berkeley Packet Filter(伯克利数据包过滤器)的缩写。其诞生于1992年,最初的目的是提升网络包过滤工具的性能。后面,随着这个工具重新实现BPF的内核补丁和不断完善代码,BPF程序变成了一个更通用的执行引擎,可以完成多种任务。简单来说,BPF提供了一种在各种内核时间和应用程序事件发生时运行一小段程序的机制。其允许内核在系统和应用程序事件发生时运行一小段程序,这样就将内核变得完全可编程,允许用户定制和控制他们的系统。

BPF其有指令集、存储对象和辅助函数等几部分组成。由于它采取了虚拟指令集规范,因此也可将其视为一种虚拟机实现。当Linux指定的时候,其会提供两种执行机制:一个解释器和一个将BPF指令动态转换为本地化指令的即时编程器。在实际执行之前,BPF指令必须先通过验证器的安全性检查,以确保BPF程序自身不会崩溃或者损坏内核。

注:扩展后的BPF通常缩写为eBPF,但官方缩写仍然是BPF。在内核之中只有一个执行引擎,其同时支持eBPF和经典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 -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程序的依赖环境,主要可以分为三个部分:

  • 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 = -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 program\n");
    return -1;
  }

  read_trace_pipe();

  return 0;
}

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

make

# 运行以下程序
sudo ./loader

BPF映射

BPF映射以<key,value>的形式会被保存到内核之中,其可以被任何其他的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映射的最值方式就是使用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 <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!

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 <errno.h>
#include <linux/bpf.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <bpf.h>

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 元素进行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 <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: %s\n", optarg);
        key = atoi(optarg);
        break;
      case 'v':
        printf("set value: %s\n", 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

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 <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

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 <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!

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 <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;
}

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'

BPF跟踪

跟踪使一种为了进行分析和调试工作的数据收集行为,通过有效的利用BPF来使得我们可以以尽可能小的代价来访问Linux内核和应用程序的任何信息。

探针

探针使一种探测程序,其会传递程序执行时环境的相关信息,我们通过BPF探针收集系统之中的数据以方便我们后续进行探索分析。在BPF之中,主要会提供以下四种探针:

  1. 内核探针:提供对内核中内部组件的动态访问能力;
  2. 跟踪点:提供对内核中内部组件的静态访问能力;
  3. 用户空间探针:提供对用户空间运行的程序的动态访问能力;
  4. 用户静态定义跟踪点:提供对用户空间运行的程序的静态访问能力;

内核探针

内核探针提供了对几乎任何内核指令设置动态标记和中断的能力。当内核到达这些标志的时候,附加到探针的代码就会被执行,之后内核将恢复到正常运行的模式。

注:这里指的注意的是,内核探针没有稳定的应用程序二进制接口(ABI),其会随着内核版本的演进而更改。

内核探针可以分为两类:

  • kprobes:kprobes允许在执行任何内核指令之前插入BPF程序。我们首先可以指定一个要探测的程序,之后当内核执行到设置探针的指令的时候,它将会从代码处开始执行我们编写的BPF程序,在BPF程序执行完之后继续执行原有的程序。

下面的例子是个简单的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\\n", 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\\n", 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

这里我们可以看到由两个额外的文件:

  • enable:表示允许启用和禁用BPF子系统的所有跟踪点。如果该文件的内容为0,表示禁用跟踪点;如果该文件的内容为1,表示跟踪点已启用
    我们可以用以下命令去启用跟踪点:

  • filter:用来编写表达式,定义内核跟踪子系统过滤事件。
    下面是一个使用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的时候,内核会在附加的指令上创建陷阱。当程序执行到该指令的时候,内核将触发事件以回调函数的方式调用探针函数。

跟内核探针类似,用户探针也分为两类:

  • uprobes:其是内核在程序特定指令执行之前插入该指令集的钩子。下面是个示例代码:
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\\n", 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''
  • uretprobes:uretprobes是kretprobes并行探针,用于用户空间程序,其会将BPF程序附加到指令返回值上,允许通过BPF代码从寄存器中访问返回值,下面是这个程序示例:
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\\n", 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''

FQA

Q:使用python作为bcc前端的时候遇到报错:“ Option ‘openmp-ir-builder-optimistic-attributes’ registered more than once!”

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
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Linux 内核观测技术BPF 的相关文章

  • 树莓派无键盘安装步骤

    树莓派无键盘安装 下载系统烧录系统配置无线网络开机并连接树莓派更新源和系统安装xrdp xff08 远程访问 xff09 Windows连接远程桌面 下载系统 应该只有官方的Raspbian系统支持无键盘安装 xff0c 官网下载系统 xf
  • iic实现采集温湿度传感器值

    iic h ifndef IIC H define IIC H include 34 stm32mp1xx gpio h 34 include 34 stm32mp1xx rcc h 34 通过程序模拟实现I2C总线的时序和协议 GPIOF
  • Matlab在线运行网站

    桌面版的Matlab不仅安装包很大 xff0c 而且也很吃性能 xff0c 不如就用网页版 xff0c 来玩啊 xff01 https www tutorialspoint com execute matlab online php 点击c
  • An Introduction on Deep Learning for the Physical Layer

    An Introduction on Deep Learning for the Physical Layer 代码实现 xff1a https github com shengjian3476077 DLforPhy 一 文章的主要工作
  • motion planning 一起学习

    shenlan 学院 motion planning 一起学习 打算买深蓝的motion planning for Mobile robots xff0c 主要是讲规划算法的 xff0c 有无一起学习的小伙伴 xff1f 一起学习 xff0
  • 【java面试之Linux】Linux启动过程、

    一 Linux启动过程 启动第一步 xff0d xff0d 加载BIOS 启动第二步 xff0d xff0d 读取MBR 主引导记录 启动第三步 xff0d xff0d Boot Loader 启动第四步 xff0d xff0d 加载内核
  • Linux SPI 驱动示例

    一 Linux 下 SPI 驱动框架 SPI 驱动框架分为主机控制器驱动和设备驱动 xff0c 主机控制器也就是 SOC 的 SPI 控制器接口 1 1 SPI 主机驱动 SPI 主机驱动就是 SOC 的 SPI 控制器驱动 xff0c L
  • 使用 FFmpeg 推流,使用 VLC 软件进行拉流

    1 移植Nginx到开发板 xff0c 使用 Nginx 来搭建 RTMP 流媒体服务器 2 执行如下命令进行推流 xff1a ffmpeg re i run media mmcblk1p1 testVideo mp4 c av copy
  • MPC与LQR的详细对比分析

    从以下几个方面进行阐述 xff1a 一 xff0c 研究对象 xff1a 是否线性 二 xff0c 状态方程 xff1a 离散化 三 xff0c 目标函数 xff1a 误差和控制量的极小值 四 xff0c 工作时域 xff1a 预测时域 x
  • 类类型成员引用的问题

    一个类中的成员变量是另一个类的类类型 xff0c 赋值问题分为引用 xff0c 不引用两类 如先定义TESTB类 class TESTB public TESTB b 61 3 7 TESTB void change b 61 9 0 fl
  • FutureTask的用法及两种经常使用的使用场景

    FutureTask可用于异步获取执行结果或取消执行任务的场景 经过传入Runnable或者Callable的任务给FutureTask xff0c 直接调用其run方法或者放入线程池执行 xff0c 以后能够在外部经过FutureTask
  • 二维线性插值方法

    前几天在进行数据仿真的时候 对于将表格离散数据转化成连续数据一直是一件十分棘手的事情 xff0c 在网站上找了许多资源最后才找到可以利用二维线性插值的方法将数据进行转化 1 原理 是要将 m n m times n m n 的二维数据如下图
  • 【转载】Matlab中LMI(线性矩阵不等式)工具箱使用教程

    64 TOC 转载 原文地址 xff1a https www cnblogs com Hand Head articles 5412511 html 这一段被老板逼着论文开题 xff0c 自己找方向比较着急 xff0c 最后选择了供应链控制
  • Python各类常用库整理

    一 20个必不可少的Python库也是基本的第三方库 Requests Kenneth Reitz写的最富盛名的http库 每个Python程序员都应该有它 Scrapy 如果你从事爬虫相关的工作 xff0c 那么这个库也是必不可少的 用过
  • Matlab中LMI(线性矩阵不等式)工具箱使用例子

    我搜出来的都是一些简单的算例 xff0c 并且机会没有中文教程 xff0c 我在这里就斗胆把自己的体会写出来 xff0c 试着给大家提供一点参考 LMI xff1a Linear Matrix Inequality xff0c 就是线性矩阵
  • 转--ICEM CFD中合并多个网格

    原址 xff1a http www jishulink com content post 359975
  • 【转】无人机故障数据集ALFA: A Dataset for UAV Fault and Anomaly Detection

    这里写自定义目录标题 无人机故障数据集资源地址 xff1a https kilthub cmu edu articles dataset ALFA A Dataset for UAV Fault and Anomaly Detection
  • 【转】无人机小课堂:无人机的副翼、俯仰、偏航、油门代表什么?

    刚刚接触无人机的小伙伴 xff0c 经常会听到很多英文缩写 xff0c 如AIL ELE RUD THR等 xff0c 一不小心就会傻傻分不清 但它们却经常出现在各种遥控器 飞控 调参软件中 xff0c 因为它们是无人机 航模中最基础的四个
  • 多弹多约束协同制导问题

    参考文献 xff1a 张达 刘克新 李国飞 多约束条件下的协同制导研究进展 J 南京信息工程大学学报 自然科学版 2020 12 05 530 539 DOI 10 13878 j cnki jnuist 2020 05 002 多导弹协同
  • 浅谈设备驱动的作用与本质,有无操作系统Linux设备驱动的区别

    一 驱动的作用 任何一个计算机系统的运行都是系统中软硬件协作的结果 xff0c 没有硬件的软件是空中楼阁 xff0c 而没有软件的硬件则只是一堆废铁 硬件是底层基础 xff0c 是所有软件得以运行的平台 xff0c 代码最终会落实为硬件上的

随机推荐

  • 【转】多智能体系统一致性问题概述

  • 【转】从自然基金面上项目只许列10篇代表作说起

    作者 xff1a 喻海良 xff0c 字之亮 xff0c 2018年2月13日于北京沙河 关于建设 双一流大学 过程中 xff0c 我们作为大学教师该如何看待学术论文的讨论已经有很多了 个人觉得论文数量是基础 xff0c 一个大学教授的课题
  • 盘点 | 单目视觉3-D目标检测经典论文(附解读)

    2020年以来出现的一些单目视觉3 D目标检测的论文 本文针对部分典型的论文要点进行要点解读 xff0c 仅供参考 Towards Generalization Across Depth for Monocular 3D Object De
  • IBM的云平台Bluemix使用初体验-创建第一个容器

    概述 第一次使用IBM的云平台Bluemix xff0c 写一个blog记录一下 我注册Bluemix挺早的 xff0c 但是在工作中一直没有机会使用IBM的云平台 现在辞职创业 xff0c 做自己喜欢的互联网 xff0c 终于有机会用上了
  • 在Source Insight中添加对.cc的支持

    Options gt Document Options Document Type gt 下拉选择 xff1a C 43 43 Source File 在File Filter 中加入 cc
  • Android HFP流程记录

    DP 完成后 xff0c btif dm c文件中 xff0c btif dm search services evt函数 xff0c bond state changed BT STATUS SUCCESS amp bd addr BT
  • OPP文件传输

    在RFCOMM连接后 xff0c 进行Command Type Parameter Negotiation时 xff0c 会协商Credits初始值 建立OBEX连接时 xff0c 会将poll bit设置 xff0c 用于Given Cr
  • 算法——欧几里得算法

    目录 欧几里得算法算法原理欧几里得算法的代码表示 参考文献 欧几里得算法 欧几里得算法是用来求两个正整数最大公约数的算法 古希腊数学家欧几里得在其著作中 The Elements 中最早描述了这种算法 xff0c 所以叫欧几里得算法 a s
  • 算法——100瓶水,一瓶有毒,有一种试纸...

    问题描述 100瓶水 xff0c 一瓶有毒 xff0c 有一种试纸 xff0c 不过需要一个小时才能出结果 xff0c 问最少需要几片试纸才能在一小时内找到有毒的那一瓶 答案 span class token number 7 span 算
  • 基于muduo网络库的集群聊天系统(C++实现)

    文章目录 项目概述业务流程 数据模块表的设计数据库模块设计 通信格式网络和业务模块网络模块网络模块和业务模块解耦合业务模块注册业务登录业务加好友业务一对一聊天业务创建群业务加入群业务群聊业务注销业务 服务器集群跨服务器通信集群聊天服务器的思
  • linux设备驱动原理与本质

    任何计算机系统都是软件和硬件的结合体 xff0c 如果只有硬件而没有软件 xff0c 则硬件是没有灵魂的躯壳 xff1b 如果只有软件没有硬件 xff0c 则软件就是一堆无用的字符 在底层硬件的基础上 xff0c 操作系统覆盖一层驱动 xf
  • pycharm配置可视化界面流程简介

    一 安装QT Designer 在pycharm的终端里面输入如下命令 span class token comment 安装pyqt5 span pip install PyQt5 span class token comment 安装p
  • 内存——CPU、内存以及磁盘是如何交互的

    文章目录 内存的存储SRAMDRAMDRAM内部以及与内存控制模块的交互 xff08 重点 xff09 DRAM与内存存储CPU和内存的交互 xff08 重点 xff09 磁盘磁盘和CPU 内存的交互 局部性参考文献 之前在介绍linux
  • libco —— 安装与使用

    文章目录 libco的安装libco库的简单使用参考文献 libco的安装 可以直接从 Tencent 的 GitHub 仓库中拉取源码 xff1a ubuntu 64 VM 0 2 ubuntu libco span class toke
  • 为什么我的云服务器不能绑定公网 ip ?

    文章目录 云服务器的部署 xff1a 数据中心NAT协议开头问题的答案参考文献 写在前面 昨天呢 xff0c 在校招群里的小伙伴问了我们一个问题 xff0c 让我们帮给看看 xff1a 一开始呢 xff0c 博主按照经验呢跟他说是端口号被占
  • 汇编 —— 算术和逻辑操作

    文章目录 加载有效地址leaq 练习题练习题答案 一元操作符 amp 二元操作符一元 amp 二元 练习题练习题答案 移位操作移位练习题练习题答案 特殊的算数操作符练习题练习题答案 参考文献 写在前面 xff1a 从腾讯实习回来之后 xff
  • clang-format安装配置与vscode支持

    文章目录 calng format安装centos下clang format安装ubuntu下clang format的安装vscode支持clang format clang format使用参考文献 calng format安装 cen
  • 分布式共识算法 —— Raft详解

    文章目录 分布式共识算法顺序一致性线性一致性因果一致性 Raft 算法原理概览选举机制新节点加入leader 掉线处理多个 follower 同时掉线 日志复制 参考文献 分布式共识算法 首先我们先明确这个问题 xff1a 为什么需要分布式
  • container_of 根据成员变量获得包含其的对象的地址!

    写在前面 本系列文章的灵感出处均是各个技术书籍的读后感 xff0c 详细书籍信息见文章最后的参考文献 CONTAINER OF 在书中发现一个很有意思的宏 xff0c 以此可以衍生出来其很多的用法 xff0c 这个宏可以根据某个成员变量的地
  • Linux 内核观测技术BPF

    BPF简介 BPF xff0c 全称是Berkeley Packet Filter xff08 伯克利数据包过滤器 xff09 的缩写 其诞生于1992年 xff0c 最初的目的是提升网络包过滤工具的性能 后面 xff0c 随着这个工具重新