eBPF(尤其是使用 BCC 或 libbpf 等框架)可以挂载到几乎所有内核函数,但具体取决于你使用的机制(如 kprobe、tracepoint、LSM、XDP 等),下面是详细分类。
类型 | 能挂载的位置 | 适合用途 |
---|---|---|
kprobe/kretprobe | 任意内核函数 | 调试、数据收集 |
tracepoint | 预定义内核事件 | 稳定追踪、安全分析 |
USDT | 用户程序插桩点 | 用户态分析 |
LSM hook | 安全检查点 | 强化访问控制 |
XDP/TC | 网络数据通路 | 网络加速、过滤 |
本文重点介绍BCC实践,BCC为调试和观测内核行为而设计,主要使用kprobe
、tracepoint
、USDT,
为了方便追踪开发,BCC 封装了许多自动转换、格式化输出逻辑。其原理为,将eBPF 程序嵌入python中使用Clang+LLVM编译C代码并加载到内核。
kprobe / kretprobe(动态追踪任意内核函数)
-
可以挂载到 任意符号表中可见的内核函数。只能 hook 非静态函数,因为静态函数不会出现在内核符号表中,静态函数局部于其定义的 C 文件,它们在编译后不会导出到内核的全局符号表(
kallsyms
),静态函数好比私有函数,看不到它的名字无法hook。kprobe 依赖于全局符号解析BCC 和 kprobe 的流程大致是:用户写的b.attach_kprobe(event="target_func", ...)
,BCC 在/proc/kallsyms
中查找是否存在target_func
如果存在,就插入探针(通过ftrace
或kprobe
子系统),如果不存在(例如是 static),则失败。 -
使用
attach_kprobe
/attach_kretprobe
进行函数入口和返回值追踪。
内核函数可以event作为参数传给 attach_kprobe(
event,fn_name)函数,
运行以下命令列出当前系统可用于挂载kprobe
的内核函数名。
cat /proc/kallsyms | awk '$2 == "T" { print $3 }' | grep -E 'tcp_|udp_|ip_|skb|netif' | sort | uniq
例如:
函数名 | 描述 |
---|---|
sys_read / sys_write | 跟踪系统调用读取/写入 |
tcp_sendmsg / tcp_recvmsg | TCP 数据收发 |
do_sys_open / do_sys_close | 文件打开与关闭 |
vfs_read / vfs_write | 虚拟文件系统层读写 |
do_exit | 进程退出 |
如果非要 hook 静态函数可采取下面方法:
1.启用调试内核符号(CONFIG_KALLSYMS_ALL)+ 内联符号导出一些内核发行版支持更多符号导出。但仍有很多静态函数编译时被优化、内联了。
2.使用 BTF + fentry/fexit(较新内核),如果目标函数有 BTF(BPF Type Format)描述符,即使static,也可能 hook 到。工具如 bpftool 和 fentry 支持这种方式。fentry/fexit hook 可精确探测 C 函数,无需 kallsyms。
3.patch 源码或修改重新编译内核,最暴力但自由度最高:把目标 static 改成非 static重新编译内核模块。
eBPF kprobe实例
BCC + eBPF 显式挂载内核函数 unix_stream_recvmsg
,以追踪 UNIX 域套接字(AF_UNIX
)接收的报文信息。hook unix_stream_recvmsg()
函数,打印接收到的消息长度、当前进程名和 PID。
from bcc import BPF
prog = """
#include <linux/socket.h>
#include <linux/net.h>
int trace_unix_recv(struct pt_regs *ctx, struct socket *sock, struct msghdr *msg, size_t size, int flags) {
u32 pid = bpf_get_current_pid_tgid() >> 32;
char comm[16];
bpf_get_current_comm(&comm, sizeof(comm));
bpf_trace_printk("unix_stream_recvmsg: PID=%d COMM=%s SIZE=%lu\\n", pid, comm, size);
return 0;
}
"""
b = BPF(text=prog)
b.attach_kprobe(event="unix_stream_recvmsg", fn_name="trace_unix_recv")
print("%-6s %-16s %-6s" % ("PID", "COMM", "SIZE"))
while True:
try:
(_, pid, _, _, _, msg) = b.trace_fields()
print(msg)
except KeyboardInterrupt:
break
代码解释
bpf_get_current_pid_tgid()
是 eBPF 程序中常用的内核helper辅助函数,用于获取当前进程 PID 和 TID,
bpf_get_current_comm()
是 eBPF 程序中常用的内核helper辅助函数,用于获取当前正在执行的进程名即 comm
字段,通常用于调试或日志记录。
eBPF 提供了一组内核helper 辅助函数,用于在 eBPF 程序中完成如访问上下文信息、与 maps 交互、调试等功能。它们由内核实现,以 bpf_
为前缀。
文章末尾会详细介绍eBPF 程序中常用的内核helper辅助函数。
tracepoint内核事件
-
是内核内置的稳定接口,适合生产环境。常用于网络事件观测和套接字状态追踪。
-
不容易因为内核版本变化而失效。
-
使用
attach_tracepoint()
方法。
常用 tracepoint 示例
子系统 | 示例事件 |
---|---|
syscalls | sys_enter_openat , sys_exit_read |
sched | sched_switch , sched_process_exit |
net | net_dev_queue , netif_receive_skb |
block | block_rq_issue , block_rq_complete |
查看支持的 tracepoint
ls /sys/kernel/debug/tracing/events/
tracepoint 实例
应用程序频繁 write()
,写入效率低下,怀疑系统调用阻塞或频繁调用,观测哪个进程最频繁调用 write
,写了多少次?数据量大与否?使用 b.attach_tracepoint()
监控 sys_enter_write函数,使用
用 tracepoint
挂在系统调用入口点 sys_enter_write
,统计每个进程的调用频率和写入字节数。
from bcc import BPF
#BPF 程序挂载在sys_enter_write)
prog = """
#include <uapi/linux/ptrace.h>
#include <linux/sched.h>
struct data_t {
u32 pid;
char comm[16];
int fd;
size_t count;
};
BPF_PERF_OUTPUT(events);
int trace_write(struct trace_event_raw_sys_enter *ctx) {
struct data_t data = {};
data.pid = bpf_get_current_pid_tgid() >> 32;
data.fd = ctx->args[0];
data.count = ctx->args[2];
bpf_get_current_comm(&data.comm, sizeof(data.comm));
events.perf_submit(ctx, &data, sizeof(data));
return 0;
}
"""
b = BPF(text=prog)
b.attach_tracepoint(tp="syscalls:sys_enter_write", fn_name="trace_write")
def print_event(cpu, data, size):
event = b["events"].event(data)
print(f"[PID {event.pid}] {event.comm.decode()} -> fd={event.fd}, count={event.count}")
print("Tracing syscalls: sys_enter_write... Hit Ctrl-C to end.")
b["events"].open_perf_buffer(print_event)
try:
while True:
b.perf_buffer_poll()
except KeyboardInterrupt:
print("Done.")
输出
Tracing syscalls: sys_enter_write... Hit Ctrl-C to end.
[PID 1234] nginx -> fd=10, count=1024
[PID 4321] python -> fd=1, count=49
[PID 1001] redis-server -> fd=6, count=2048
可以通过 tracepoint 来解决日常开发中遇到的许多问题,比如:
问题 | 使用的 tracepoint |
---|---|
哪个进程频繁写文件 | sys_enter_write |
文件打开失败 | sys_enter_openat |
程序卡在哪 | sched:sched_switch |
频繁创建线程/进程 | sched:sched_process_fork |
IO wait 过高 | block:block_rq_issue |
kprobe和tracepoint对比
特性 | kprobe | tracepoint |
---|---|---|
探测对象 | 内核函数符号名 | 内核事件预定义 |
稳定性 | 随内核版本变化,容易失效 | 接口稳定,推荐用于长期监控 |
灵活性 | 高,可 hook 任意导出函数 | 中等,但更安全 |
性能 | 较高 | 更高tracepoint 有内部优化 |
---------------------------------------------------------分割线---------------------------------------------------------------
USDT用户态静态追踪点
-
针对用户空间应用如nginx、mysql提供的追踪点。
-
需要应用程序支持(如编译了 DTrace probes)。
示例:
-
nginx:
ngx_http_process_request_line -
mysql:
USDT实例1
追踪nginx ngx_http_process_request_line函数,一个http请求是从ngx_http_init_request函数开始,然后设置读事件为ngx_http_process_request_line,接下来的网络事件,会由该函数来执行。NGINX 编译需要引用USDT探针 编译加入--with-dtrace-probes参数。
from bcc import BPF
import time
import subprocess
NGINX_PATH = "/usr/sbin/nginx"
bpf_text = """
int trace_proc_line(struct pt_regs *ctx) {
bpf_trace_printk("ngx_http_process_request_line() called\\n");
return 0;
}
"""
#加载 BPF 程序并 attach 到 nginx 符号
b = BPF(text=bpf_text)
b.attach_uprobe(name=NGINX_PATH, sym="ngx_http_process_request_line", fn_name="trace_proc_line")
print("开始追踪 ngx_http_process_request_line...")
#以非守护模式启动 nginx
print(" 启动 nginx...")
subprocess.call([NGINX_PATH])
#发起一个 curl 请求触发函数
print("发送请求触发 nginx 行为")
time.sleep(1)
subprocess.call(["curl", "-s", "http://localhost"])
#从 BPF 缓冲区打印内核日志内容
print(" BPF 输出如下:")
try:
b.trace_print(timeout=5)
except KeyboardInterrupt:
print("中断退出")
使用eBPF 的 uprobe
技术追踪用户态函数时,它依赖可执行文件中函数的符号表(即函数名与地址的映射)。如果 nginx 被剥离了符号信息,你将无法通过函数名ngx_http_process_request_line
来 attach。openresty带完整符号,集成 DTrace/USDT 探针,可直接用 USDT
模块追踪。
USDT实例2
应用程序频繁调用 query_db()
函数访问数据库,怀疑这个函数调用耗时不稳定。在生产环境不中断业务地监控这个函数,获取每次调用的耗时(微秒)。
应用程序
//gcc -g -o appdb appdb.c -ldl
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <time.h>
#include <sys/sdt.h> //USDT探针头文件
void query_db(int id) {
DTRACE_PROBE1(appdb, db_query_start, id); //追踪点开始
usleep(1000 + rand() % 9000); //模拟延迟
DTRACE_PROBE1(appdb, db_query_end, id); //追踪点结束
}
int main() {
srand(time(NULL));
for (int i = 0; i < 1000; i++) {
query_db(i);
}
return 0;
}
USDT调试程序
from bcc import BPF, USDT
usdt = USDT(path="./appdb")
usdt.enable_probe(probe="db_query_start", fn_name="trace_start")
usdt.enable_probe(probe="db_query_end", fn_name="trace_end")
bpf_text = """
#include <uapi/linux/ptrace.h>
BPF_HASH(start, u32);
BPF_HISTOGRAM(dist);
int trace_start(struct pt_regs *ctx) {
u32 tid = bpf_get_current_pid_tgid();
u64 ts = bpf_ktime_get_ns();
start.update(&tid, &ts);
return 0;
}
int trace_end(struct pt_regs *ctx) {
u32 tid = bpf_get_current_pid_tgid();
u64 *tsp = start.lookup(&tid);
if (tsp) {
u64 delta = bpf_ktime_get_ns() - *tsp;
dist.increment(bpf_log2l(delta / 1000)); //转换为微秒
start.delete(&tid);
}
return 0;
}
"""
b = BPF(text=bpf_text, usdt_contexts=[usdt])
print("等待appdb中的USDT探针触发(db_query_start / db_query_end)...")
try:
b["dist"].print_log2_hist("Latency (us)")
except KeyboardInterrupt:
exit()
输出
等待myapp中的USDT探针触发(db_query_start / db_query_end)...
Latency(us)
value ------------- count
8 | |
16 |*********** | 10
32 |**************************************** | 89
64 | |
USDT示例1和USDT示例2中enable_probe
vs attach_uprobe
核心函数区别:
项目 | enable_probe | attach_uprobe |
---|---|---|
适用对象 | USDT 用户静态探针 | 普通函数入口/偏移地址 |
原理 | 解析程序中 SDT 符号表信息(.note.stapsdt ) | 直接在用户态 ELF 可执行文件中插入 uprobes |
使用对象 | 明确插入了 DTRACE_PROBE() 的程序 | 没有显式探针也可追踪任意符号或地址 |
附加方式 | USDT().enable_probe(...) 后传入 BPF(..., usdt_contexts=[...]) | b.attach_uprobe(...) |
参数支持 | 支持读取 DTRACE_PROBE 的参数 | 仅能抓寄存器(无参数元信息) |
是否依赖源码修改 | 是,需要程序嵌入 USDT 宏 | 否,可对第三方程序如 nginx/mysql 直接追踪 |
LSM linux安全模块 hook
-
LSM(Linux Security Modules) 是 Linux 内核框架,用于实现各种安全模块,比如 SELinux、AppArmor、Tomoyo 等。内核定义了很多hook点,例如比如打开文件、执行进程、发送网络包等操作发生时,内核会调用对应的hook。
-
使用eBPF LSM hook实现自定义的访问控制逻辑,例如:拦截访问操作,allow或deny处理,eBPF 程序挂载LSM hook无须写内核模块,开发更快,动态加载,安全性好。适合安全审计、轻量级限制。
-
linux 5.7+内核,支持eBPF LSM hook。
在实际开发中的应用:
-
沙箱:限制应用访问文件系统和网络,避免泄密和破坏。
-
强制访问控制(MAC):比传统 Unix 权限更细粒度的权限管理,比如 SELinux。
-
安全审计:记录关键操作日志,如谁访问了哪个文件,谁建立了网络连接。
-
容器安全:给容器限制系统调用权限、文件访问范围。
LSM hook示例
仅允许指定进程(例如 PID = 5566)打开 /etc/passwd
文件,其他进程全部禁止访问该文件。
BCC+LSM 代码
from bcc import BPF
bpf_text = """
#include <uapi/linux/ptrace.h>
#include <linux/sched.h>
#include <linux/fs.h>
#include <linux/dcache.h>
#include <linux/lsm_hooks.h>
#include <linux/bpf.h>
#define ALLOWED_PID 5566
SEC("lsm/file_open")
int BPF_PROG(check_file_open, struct file *file)
{
u32 pid = bpf_get_current_pid_tgid() >> 32;
struct dentry *dentry = file->f_path.dentry;
char fname[64];
bpf_probe_read_kernel_str(fname, sizeof(fname), dentry->d_name.name);
const char target[] = "passwd";
int match = 1;
for (int i = 0; i < sizeof(target); i++) {
if (fname[i] != target[i]) {
match = 0;
break;
}
if (target[i] == '\\0')
break;
}
if (match && pid != ALLOWED_PID) {
bpf_printk("LSM DENY: pid %d tried to open /etc/passwd\\n", pid);
return -13; // -EACCES
}
return 0;
}
"""
#加载并挂载 LSM hook
b = BPF(text=bpf_text)
print("LSM hook installed. Only PID 1234 can open /etc/passwd.")
#实时打印日志
b.trace_print()
测试
#允许访问
sleep 10000 & # 假设PID为5566
nsenter -t 5566-n cat /etc/passwd
#其他PID进程访问拒绝
cat /etc/passwd
注意:LSM类型的eBPF 程序不容易卸载,设计用于强制策略而非临时调试,不能像 tracepoint/kprobe那样随时插拔。每个 LSM hook点(如 file_open
)只能挂一个 eBPF 程序。挂载之后该 eBPF 程序和钩子绑定,直到进程退出或系统重启。这里需要注意卸载问题。
XDP高速可编程网络数据包处理
-
用于网络数据包处理。XDP挂载在网络驱动的最早阶段。运行在驱动层,甚至在skb结构体创建之前就拦截网络包。使用
bpf_prog_load()
并通过bpf_set_link_xdp_fd()
绑定到 NIC 设备。不能使用bpf_trace_printk()等调试API,要求BPF verifer更严格检查。 -
XDP 程序强依赖BTF和 CO-RE(Compile Once Run Everywhere)。
特性 | 描述 |
---|---|
超高速 | 在内核网络协议栈之前处理数据包,效率高于Netfilter、tc等 |
零拷贝 | 结合AF_XDP 使用,支持零拷贝直通用户态 |
可编程 | 使用eBPF实现数据包处理,例如:防火墙/DDoS攻击包过滤/负载均衡等 |
接口绑定 | 程序绑定到网卡的ingress 路径 |
BCC不支持网卡attach
BCC包含BPF.attach_xdp()
方法,仅支持最基础的XDP使用能力,适用于调试或验证的场景。
XDP attach 是对网络接口设备级别attach,BCC架构不支持队XDP这类高速路径的程序加载和编译。BCC 编译模型是运行时使用 Clang 编译,不具备独立的 CO-RE 支持机制。
BCC 的核心函数BPF.attach_kprobe()
、BPF.attach_uprobe()
仅支持 attach 到内核函数kprobe/kretprobe、tracepoint内核事件、uprobe/uretprobe用户探针、静态探针USDT静态探针。
最后
libbpf原生支持XDP程序加载与绑定,适用于高性能、低延迟的网络数据包处理场景。它提供完整的加载、验证、attach(如 bpf_set_link_xdp_fd
)、map管理以及 BTF/CO-RE 支持,使得 XDP 程序能安全、高效地绑定至网卡设备入口。BCC和libbpf对比:
对比项 | BCC | libbpf |
---|---|---|
语言 | python+C嵌入BPF代码 | C(分离 BPF和用户态代码) |
用途 | 内核追踪、性能分析、调试(如 trace/kprobe) | 高性能网络处理、系统安全策略、XDP、LSM、安全隔离 |
attach支持类型 | kprobe / tracepoint / uprobe / USDT | XDP / TC / LSM / fentry / kprobe / tracepoint 等 |
依赖 | BCC 运行时依赖python、LLVM、Clang、内核头等 | 仅依赖libbpf ,无python和LLVM依赖 |
是否CO-RE | 否,依赖运行时加载匹配 | 是,BTF+编译期解析,跨内核版本兼容 |
路径解析(如 bpf_d_path) | 支持有限,部分helper可能不工作 | 全支持,只要内核提供helper和BTF |
部署方式 | 仅适合调试环境或有python的系统 | 适合生产部署,可打包为单独可执行文件 |
构建工具 | 不需要复杂makefile | 需要makefile+clang+pahole+libbpf 编译流程 |
性能表现 | 中等,适合开发和调试 | 更高性能,生产环境首选 |
生态工具集成 | bpftrace、trace.py、funclatency 等便捷 | 灵活但需要自己封装CLI工具或结合 bpftool |
性能 | 较高 | 极低,适合生产环境部署 |
使用场景 | 调试、性能分析、运行时观测 | 实时数据包处理、网络防护、入侵检测、DDOS 防御 |