动态控制eBPF程序加载:检查 Tracepoint、Kprobe是否存在

发布于:2024-06-03 ⋅ 阅读:(235) ⋅ 点赞:(0)

前言

在 eBPF 程序开发中,确保程序能够在各种不同的系统配置中兼容运行是至关重要的。本文将详细介绍一个方案,通过动态检查Tracepoint、Kprobe是否存在,并结合libbpf的API接口控制 eBPF 程序的加载。这种方法不仅可以提升程序的灵活性,还能提高其在不同内核版本和系统配置中的兼容性。

方案概述

本方案包括以下步骤:

  1. 检查指定的 Tracepoint 是否存在;
  2. 检查指定的 Kprobe 是否存在;
  3. 若不存在,则通过API函数bpf_object__find_program_by_name查找到 eBPF 程序,再使用 API函数bpf_program__set_autoload 动态设置 eBPF 程序的autoload属性使其不加载。

检查Tracepoint挂载点是否存在

Tracepoint是内核提供的一种跟踪机制,通过访问特定目录可以检查其是否存在。不同的Linux系统,查找的目录不同,当前有以下两种路径:

  1. /sys/kernel/debug/tracing/events
  2. /sys/kernel/tracing/events

设计思路是顺序地在这两个路径下使用access系统调用检查是否存在某tracepoint文件,任意一个路径下检查到了,结果就是存在;两个路径下都没检查到,则结果是不存在。
注:这两个路径一般需要root权限,普通用户无法访问。

示例代码

#include <iostream>
#include <unistd.h>

bool tracepoint_exists(const char* tp_category, const char* tp_name)
{
    char path[256];
    snprintf(path, sizeof(path), "/sys/kernel/debug/tracing/events/%s/%s",
             tp_category, tp_name);
    auto ret = access(path, F_OK);
    if (ret == 0) {
        return true;
    }
    snprintf(path, sizeof(path), "/sys/kernel/tracing/events/%s/%s", tp_category, tp_name);
    ret = access(path, F_OK);
    if (ret == 0) {
        return true;
    }
    return false;
}

int main(int argc, char** argv)
{
    if (argc != 3) {
        std::cout << "Usage: ./test category tracepoint_name" << std::endl;
        return -1;
    }
    auto ret = tracepoint_exists(argv[1], argv[2]);
    if (ret) {
        std::cout << "Tracepoint tp/" << argv[1] << "/" << argv[2]
            << " exists!"<< std::endl;
        return 0;
    }
    std::cout << "Tracepoint tp/" << argv[1] << "/" << argv[2]
        << " does not exist!"<< std::endl;
    return -1;
}

测试结果

使用以上程序检测syscalls:sys_enter_read是否存在。

[root@VM-8-2-centos check_tracepoint_kprobe_uprobe]# ./test syscalls sys_enter_read
Tracepoint tp/syscalls/sys_enter_read exists!

再检测一个不存在的,syscalls:sys_enter_readd。

[root@VM-8-2-centos check_tracepoint_kprobe_uprobe]# ./test syscalls sys_enter_readd
Tracepoint tp/syscalls/sys_enter_readd does not exist!

通过测试结果可知,功能符合预期。

检查Kprobe挂载点是否存在

Kprobes允许动态挂载内核函数进行调试,通过读取以下系统文件可以检查特定的 Kprobe 是否存在。

  1. /sys/kernel/debug/tracing/available_filter_functions
  2. /sys/kernel/tracing/available_filter_functions

注:这两个文件一般需要root权限,普通用户无法访问。

示例代码

#include <iostream>
#include <stdio.h>
#include <string.h>

bool kprobe_exists(const char *kprobe_name)
{
    FILE* file = fopen("/sys/kernel/debug/tracing/available_filter_functions", "r");
    if (!file) {
        file = fopen("/sys/kernel/tracing/available_filter_functions", "r");
        if (!file) {
            return false;
        }
    }
    char line[256];
    while (fgets(line, sizeof(line), file)) {
        line[strcspn(line, "\n")] = 0;
        if (strcmp(line, kprobe_name) == 0) {
            return true;
        }
    }
    return false;
}

int main(int argc, char** argv)
{
    if (argc != 2) {
        std::cout << "Usage: ./test kprobe_name" << std::endl;
        return -1;
    }
    auto ret = kprobe_exists(argv[1]);
    if (ret) {
        std::cout << "kprobe " << argv[1] << " exists!"<< std::endl;
        return 0;
    }
    std::cout << "kprobe " << argv[1] << " does not exist!"<< std::endl;
    return -1;
}

测试结果

使用以上程序检测__alloc_pages_direct_compact这个kprobe挂载点是否存在。

[root@VM-8-2-centos check_kprobe]# ./test __alloc_pages_direct_compact
kprobe __alloc_pages_direct_compact exists!

再检测一个不存在的,__alloc_pages_direct_compact_abcdefg。

[root@VM-8-2-centos check_kprobe]# ./test __alloc_pages_direct_compact_abcdefg
kprobe __alloc_pages_direct_compact_abcdefg does not exist!

通过测试结果可知,功能符合预期。

动态设置 eBPF 程序的 autoload 属性

当我们以libbpf-bootstrap为参考写ebpf程序时,可以使用前文的方法提前判断函数挂载点是否存在,若不存在,则使用libbpf的API将eBPF 程序的 autoload 属性设置为false。使用到的API如下:

bpf_object__find_program_by_name

bpf_object__find_program_by_name接口是一个用于在 eBPF 对象中查找特定 eBPF 程序的函数,在bpf/libbpf.h头文件中。
函数功能
在 eBPF 项目中,一个 BPF 对象可能包含多个 eBPF 程序(如一个bpf c语言源文件中有多个SEC),每个程序都可以有一个唯一的名称。bpf_object__find_program_by_name接口的主要功能是根据给定的程序名称,从 BPF 对象中查找并返回相应的 eBPF 程序。如果找到匹配的程序,则返回指向该程序的指针;否则,返回 NULL。
在前文中,我们能够通过程序动态判断是否支持某Tracepoint或Kprobe的挂载点,若不存在,需要使用这个API查找到对应的ebpf程序(struct bpf_program *),以便后续设置其autoload属性。
函数原型

struct bpf_program * bpf_object__find_program_by_name(const struct bpf_object *obj, const char *name);

参数说明

  1. obj: 指向 BPF 对象的指针。该对象通常是通过加载 BPF ELF 文件生成的,包含多个 eBPF 程序。在libbpf-bootstrap框架中,可以通过skel->obj来获取。
  2. name: 要查找的 eBPF 程序的名称。名称是一个字符串,应与 BPF 程序在源代码中的定义名称相匹配。

返回值
成功时,返回指向找到的 eBPF 程序的指针(struct bpf_program *);
如果未找到匹配的程序,则失败,返回 NULL。

bpf_program__set_autoload

bpf_program__set_autoload接口定义在bpf/libbpf.h头文件中,用于设置 eBPF 程序在加载 BPF 对象时是否自动加载。这在需要根据运行时条件动态控制 eBPF 程序的加载和执行时非常有用。
函数功能
bpf_program__set_autoload 接口的主要功能是启用或禁用特定 eBPF 程序的自动加载。通过调用此函数,开发者可以决定某个 eBPF 程序在调用 bpf_object__load 时是否应被加载到内核中。
函数原型

int bpf_program__set_autoload(struct bpf_program *prog, bool autoload);

参数说明

  1. prog: 指向要设置自动加载属性的 eBPF 程序的指针(struct bpf_program *)。
  2. autoload: 一个布尔值,指示是否应自动加载该程序。设为 true 表示启用自动加载,设为 false 表示禁用自动加载。

返回值
成功时返回 0;
失败时返回一个负值的错误码。

代码示例

以下代码展示了如何使用前文的两个API实现动态禁用某ebpf程序的功能。

void disable_autoload(struct bpf_object *obj, const char *prog_name) {
    struct bpf_program *prog = bpf_object__find_program_by_name(obj, prog_name);
    if (!prog) {
        fprintf(stderr, "Program %s not found in object\n", prog_name);
        return;
    }
    if (bpf_program__set_autoload(prog, false) != 0) {
        fprintf(stderr, "Failed to disable autoload for program %s\n", prog_name);
    } else {
        printf("Autoload disabled for program %s\n", prog_name);
    }
}

完整的动态禁用eBPF程序autoload属性的小demo可免费下载资源《禁用某eBPF程序源代码 》。Demo中在eBPF程序的open和load之间调用了disable_autoload,将不存在的tracepoint禁用了。

结束语

通过本文的学习,在开发eBPF程序时可以实现对 eBPF 程序加载的动态控制,确保其在不同的系统配置和内核版本中都能正常运行。我们详细探讨了如何检查 Tracepoint、Kprobe是否存在,并结合 bpf_program__set_autoload 和 bpf_object__find_program_by_name 控制 eBPF 程序的加载。这不仅提升了 eBPF 程序的灵活性,还增强了系统兼容性。希望本文的内容能为 eBPF 程序开发提供有价值的指导,帮助读者更有效地利用 eBPF 技术实现内核级监控和调试。
后续会对Uprobe和USDT的检测进行研究,欢迎关注和订阅。


网站公告

今日签到

点亮在社区的每一天
去签到