前言
在 eBPF 程序開發中,確保程序能夠在各種不同的系統配置中兼容運行是至關重要的。本文將詳細介紹一個方案,通過動態檢查Tracepoint、Kprobe是否存在,并結合libbpf的API接口控制 eBPF 程序的加載。這種方法不僅可以提升程序的靈活性,還能提高其在不同內核版本和系統配置中的兼容性。
方案概述
本方案包括以下步驟:
- 檢查指定的 Tracepoint 是否存在;
- 檢查指定的 Kprobe 是否存在;
- 若不存在,則通過API函數bpf_object__find_program_by_name查找到 eBPF 程序,再使用 API函數bpf_program__set_autoload 動態設置 eBPF 程序的autoload屬性使其不加載。
檢查Tracepoint掛載點是否存在
Tracepoint是內核提供的一種跟蹤機制,通過訪問特定目錄可以檢查其是否存在。不同的Linux系統,查找的目錄不同,當前有以下兩種路徑:
- /sys/kernel/debug/tracing/events
- /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 是否存在。
- /sys/kernel/debug/tracing/available_filter_functions
- /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);
參數說明
- obj: 指向 BPF 對象的指針。該對象通常是通過加載 BPF ELF 文件生成的,包含多個 eBPF 程序。在libbpf-bootstrap框架中,可以通過skel->obj來獲取。
- 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);
參數說明
- prog: 指向要設置自動加載屬性的 eBPF 程序的指針(struct bpf_program *)。
- 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的檢測進行研究,歡迎關注和訂閱。