BCC eBPF内核函数挂载实践及特点

eBPF(尤其是使用 BCC 或 libbpf 等框架)可以挂载到几乎所有内核函数,但具体取决于你使用的机制(如 kprobe、tracepoint、LSM、XDP 等),下面是详细分类。

类型能挂载的位置适合用途
kprobe/kretprobe任意内核函数调试、数据收集
tracepoint预定义内核事件稳定追踪、安全分析
USDT用户程序插桩点用户态分析
LSM hook安全检查点强化访问控制
XDP/TC网络数据通路网络加速、过滤

本文重点介绍BCC实践,BCC为调试和观测内核行为而设计,主要使用kprobetracepointUSDT,为了方便追踪开发,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如果存在,就插入探针(通过 ftracekprobe 子系统),如果不存在(例如是 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_recvmsgTCP 数据收发
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 示例

子系统示例事件
syscallssys_enter_openat, sys_exit_read
schedsched_switch, sched_process_exit
netnet_dev_queue, netif_receive_skb
blockblock_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对比

特性kprobetracepoint
探测对象内核函数符号名内核事件预定义
稳定性随内核版本变化,容易失效接口稳定,推荐用于长期监控
灵活性高,可 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示例1USDT示例2enable_probe vs attach_uprobe核心函数区别:

项目enable_probeattach_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对比:

对比项BCClibbpf
语言python+C嵌入BPF代码C(分离 BPF和用户态代码)
用途内核追踪、性能分析、调试(如 trace/kprobe)高性能网络处理、系统安全策略、XDP、LSM、安全隔离
attach支持类型kprobe / tracepoint / uprobe / USDTXDP / 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 防御
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值