第2章 基于eBPF的进程崩溃实时拦截

2.1 eBPF 崩溃拦截基础原理

eBPF(extended Berkeley Packet Filter)提供了一种在内核态安全执行用户定义程序的能力。在进程崩溃实时拦截场景中,eBPF 的核心优势在于:

  • 零侵入性:无需修改目标进程或加载内核模块
  • 低延迟:事件触发到拦截处理的延迟通常在微秒级
  • 完整上下文:可捕获寄存器、堆栈、系统调用参数等崩溃现场信息

其工作原理是:通过挂载到内核 tracepoint 或 syscall 探针,当目标进程触发 SIGSEGV、SIGABRT、SIGBUS 等致命信号时,eBPF 程序在信号投递前被触发,采集崩溃上下文并通知用户态监听器。

2.2 核心架构:探针挂载路径

两种主流挂载路径各有适用场景:

探针类型挂载点优势限制
tracepointsignal:signal_generate精确捕获信号生成事件;兼容性好无法获取完整陷入内核前的寄存器
syscalltracepoint/syscalls/sys_enter_kill
tracepoint/syscalls/sys_enter_tkill
可拦截主动发起的致命信号被动崩溃信号(如段错误)无法捕获
kprobe/hw_breakpoint(高级)内存访问异常入口最低延迟;完整寄存器上下文内核版本依赖性强;调试复杂性高
工程选型建议:生产环境首选 tracepoint 方案。若需要捕获未处理信号导致的崩溃,可结合 signal:signal_generate 与 signal:signal_deliver 两个 tracepoint 实现闭环。

2.3 实现流程与关键代码

2.3.1 BPF 程序定义

以下 BPF C 程序挂载到 signal:signal_generate tracepoint,采集崩溃信号上下文:

// crash_intercept.bpf.c
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>

// 定义 perf event 输出映射
struct {
    __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
    __uint(key_size, sizeof(int));
    __uint(value_size, sizeof(int));
    __uint(max_entries, 64);
} crash_events SEC(".maps");

// 定义崩溃上下文结构
struct crash_context {
    __u32 pid;
    __u32 tgid;
    __u32 signal;       // 信号编号
    __u32 cause_sicode;
    __u64 kstack_id;    // 内核态堆栈 ID
    __u64 timestamp_ns;
};

// tracepoint: signal:signal_generate 入口
SEC("tracepoint/signal/signal_generate")
int trace_signal_generate(struct trace_event_raw_signal_generate *ctx)
{
    struct task_struct *task = (struct task_struct *)bpf_get_current_task();

    // 只捕获致命信号: SIGSEGV(11), SIGABRT(6), SIGBUS(7), SIGFPE(8), SIGILL(4)
    __u32 sig = ctx->sig;
    if (sig != 11 && sig != 6 && sig != 7 && sig != 8 && sig != 4)
        return 0;

    struct crash_context crash = {};
    crash.pid = bpf_get_current_pid_tgid() & 0xFFFFFFFF;
    crash.tgid = bpf_get_current_pid_tgid() >> 32;
    crash.signal = sig;
    crash.cause_sicode = ctx->si_code;
    crash.timestamp_ns = bpf_ktime_get_ns();

    // 采集内核堆栈
    crash.kstack_id = bpf_get_stackid(ctx, &crash_events, BPF_F_REUSE_STACKID);

    // 将 crash_context 发送到用户态
    bpf_perf_event_output(ctx, &crash_events, BPF_F_CURRENT_CPU, &crash, sizeof(crash));
    return 0;
}

char LICENSE[] SEC("license") = "GPL";

2.3.2 用户态加载与监听

使用 BCC(BPF Compiler Collection)框架加载并监听崩溃事件:

#!/usr/bin/env python3
# crash_listener.py
from bcc import BPF
from ctypes import c_int, c_uint32, c_uint64, Structure
import signal
import sys

# 加载 BPF 程序
with open("crash_intercept.bpf.c") as f:
    bpf_text = f.read()

b = BPF(text=bpf_text)

# 附加到 tracepoint
b.attach_tracepoint(tp="signal:signal_generate", fn_name="trace_signal_generate")

# 定义与内核端一致的结构体
class CrashContext(Structure):
    _fields_ = [
        ("pid", c_uint32),
        ("tgid", c_uint32),
        ("signal", c_uint32),
        ("cause_sicode", c_uint32),
        ("kstack_id", c_uint64),
        ("timestamp_ns", c_uint64),
    ]

SIGNAL_MAP = {6: "SIGABRT", 7: "SIGBUS", 8: "SIGFPE", 11: "SIGSEGV", 4: "SIGILL"}

def handle_crash_event(cpu, data, size):
    event = b["crash_events"].event(data)
    crash = CrashContext.from_buffer_copy(event)
    signal_name = SIGNAL_MAP.get(crash.signal, f"SIG{crash.signal}")

    print(f"[CRASH] PID={crash.pid} TGID={crash.tgid} "
          f"Signal={signal_name}({crash.signal}) "
          f"Timestamp={crash.timestamp_ns}")

    # 读取内核堆栈
    stack = b.get_stack(crash.kstack_id, True)
    if stack:
        print("Kernel stack:")
        for addr in stack:
            sym = b.ksym(addr)
            print(f"  {sym:#x}  {sym}")
    print("-" * 50)

# 打开 perf event 通道
b["crash_events"].open_perf_buffer(handle_crash_event, page_cnt=64)

print("eBPF crash interceptor started. Waiting for crashes... (Ctrl+C to stop)")

# 事件循环
try:
    while True:
        b.perf_buffer_poll(timeout=100)
except KeyboardInterrupt:
    print("Shutting down...")
    sys.exit(0)

2.3.3 编译与运行

# 安装依赖(Ubuntu/Debian)
sudo apt-get install bpfcc-tools python3-bpfcc

# 直接运行监听器(会自动编译 BPF 程序)
sudo python3 crash_listener.py

# 测试:模拟崩溃
kill -11 $$   # 当前 shell 发送 SIGSEGV
# 或者编译一个空指针解引用的 C 程序测试

2.4 崩溃上下文采集深度解析

采集到的上下文应包含以下关键信息,用于后续的崩溃分析:

字段采集方式用途
PID / TGIDbpf_get_current_pid_tgid()定位崩溃进程及线程组
信号编号ctx->sig区分崩溃类型(段错误、终止等)
信号原因码ctx->si_code细粒度原因(如 SI_USER、SEGV_MAPERR)
内核堆栈bpf_get_stackid()回溯内核态调用链
用户态堆栈需额外挂载 kprobe 或使用 bpf_get_stack() 指定 BPF_F_USER_STACK捕获用户态崩溃点
时间戳bpf_ktime_get_ns()崩溃时间序列分析
注意事项:
  • bpf_get_stack() 在 tracepoint 中有限制,无法同时获取用户态和内核态堆栈。若需用户态堆栈,建议使用 kprobe 方式,或通过 bpf_get_stack() 分两次调用。
  • 堆栈深度默认 127 层,可通过 BPF_STACK_BUILD_ID_EMBEDDED 启用 build-id 模式以提升符号解析准确性。

2.5 性能开销控制策略

在生产环境部署 eBPF 崩溃拦截程序时,必须严格控制性能开销:

优化项方法效果
信号过滤仅在 tracepoint 中匹配 5 种致命信号,其他信号直接 return 0减少 99% 的不必要调用
采样频率控制增加速率限制逻辑:每 PID 每秒最多采集 N 次防止异常高频崩溃造成环形缓冲区饱和
输出通道使用 BPF_MAP_TYPE_PERF_EVENT_ARRAY 替代 bpf_printk异步通信,避免阻塞上下文
堆栈采集使用 BPF_F_REUSE_STACKID 复用堆栈 ID 表降低内存占用,避免 OOM
perf 缓冲区适当增加 page_cnt(如 64 页),并设置 watermark平衡延迟与吞吐

2.5.1 速率限制代码示例

在 BPF 程序中增加速率限制映射:

// 速率限制映射:key=PID, value=上次时间戳(ns)
struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(key_size, sizeof(__u32));
    __uint(value_size, sizeof(__u64));
    __uint(max_entries, 1024);
} rate_limit SEC(".maps");

// 在 tracepoint 处理函数开头添加:
__u32 pid = bpf_get_current_pid_tgid() & 0xFFFFFFFF;
__u64 *last_ts = bpf_map_lookup_elem(&rate_limit, &pid);
__u64 now = bpf_ktime_get_ns();

if (last_ts && (now - *last_ts) < 1000000000ULL) // 1 秒内不重复上报
    return 0;

// 更新时间戳
bpf_map_update_elem(&rate_limit, &pid, &now, BPF_ANY);

2.6 调试与验证

使用以下方法验证 eBPF 程序是否正确拦截崩溃:

  1. bpftrace 快速验证:
    bpftrace -e 'tracepoint:signal:signal_generate { printf("sig=%d pid=%d\n", args->sig, pid); }'
  2. 使用 test-SIGSEGV 程序:
    // test_crash.c
    #include <stdlib.h>
    int main() {
        int *p = NULL;
        *p = 42;  // 触发 SIGSEGV
        return 0;
    }
  3. 检查 perf 事件输出:运行监听器后,执行 kill -11 [PID] 验证控制台输出。
  4. 查看 /sys/kernel/debug/tracing/trace_pipe:可额外添加 bpf_trace_printk() 用于调试。
常见错误排查:
  • "BPF program check failed":通常因内核配置未启用 CONFIG_BPF_EVENTS 或 CONFIG_TRACING。执行 zcat /proc/config.gz | grep -E 'CONFIG_BPF|CONFIG_TRACING' 检查。
  • perf 事件无输出:检查 page_cnt 是否太小导致丢包,或用户态未正确调用 open_perf_buffer。
  • 堆栈为空:确认 bpf_get_stackid 的 flags 设置正确,且内核支持 CONFIG_STACKTRACE。

2.7 系统部署与运维

生产环境部署建议:

  • 使用 systemd 服务:编写 service 文件,实现开机自启和自动重启:
    [Unit]
    Description=eBPF Crash Interceptor
    After=network.target
    
    [Service]
    ExecStart=/usr/local/bin/crash_listener.py
    Type=simple
    Restart=always
    RestartSec=5
    User=root
    
    [Install]
    WantedBy=multi-user.target
  • 日志持久化:在用户态监听器中增加文件输出或远程日志推送。
  • 监控与告警:监听 eBPF 程序自身状态,若 perf buffer 丢包率超过 5%,触发告警。
  • 内核版本兼容性:eBPF 功能随内核版本演进,建议统一内核版本(如 5.10+),并在 CI 中测试各内核版本的兼容性。
eBPF 崩溃实时拦截系统架构 eBPF 进程崩溃实时拦截系统架构 用户态应用程序 (崩溃发生主体) 信号投递 内核信号处理 signal_generate tracepoint eBPF 拦截程序 信号过滤 / 上下文采集 速率限制 / 堆栈回溯 触发 Perf Event Buffer crash_events 映射 用户态监听器 crash_listener.py 崩溃上下文处理 日志 / 推送 / 告警 崩溃拦截路径说明 1. 用户态程序崩溃 → 向自身发送致命信号(SIGSEGV/SIGABRT等) 2. 内核信号处理在 signal_generate tracepoint 处被 eBPF 程序截获 3. eBPF 程序过滤信号后采集崩溃上下文 → 通过 perf event buffer 发送至用户态监听器