BPF KPROBE编程中的ctx是什么?

BPF KPROBE编程中的ctx是什么?

文章目录

简介

本文基于strace系统调用追踪与Linux内核源码,完整拆解eBPF kprobe的实现链路,定位kprobe中的ctx来源:用户态通过BPF_PROG_LOAD加载BPF字节码,依托perf子系统完成kprobe事件注册,通过BPF_LINK_CREATE完成事件与程序的绑定;内核触发时借助ftrace跳板激活kprobe处理逻辑,以struct pt_regs寄存器上下文作为入参执行BPF程序,最终实现内核函数的动态追踪。

配套BPF示例程序:gitee.com/kiraskyler/simple_bpf

用户态核心流程

BPF_PROG_LOAD:BPF程序加载与内核校验

加载编译后的kprobe类型BPF程序(如SEC("kprobe/handle_mm_fault") int kprobe_handle(void *ctx)),核心系统调用如下:

c 复制代码
bpf(BPF_PROG_LOAD, {
    prog_type=BPF_PROG_TYPE_KPROBE, 
    insn_cnt=48, 
    insns=0x1f0bb750, 
    license="GPL", 
    kern_version=KERNEL_VERSION(6, 6, 58), 
    prog_name="kprobe_handle", 
    prog_btf_fd=3, 
    func_info=0x1f0ba460, 
    func_info_cnt=1, 
    line_info=0x1f0ba480, 
    line_info_cnt=14
}, 144) = 5

核心技术细节:

  • 内核通过加载.bpf.c中的prog bpf 字节码。
  • 加载成功后构建struct bpf_prog核心结构体:
    • 编译后的BPF字节码存入struct bpf_prog->insns
    • 开启JIT时,struct bpf_prog->bpf_func指向编译后的本地机器码入口;未开启JIT时,指向内核通用BPF字节码解释器。
  • 系统调用返回值为BPF程序的文件描述符,用于后续绑定操作。

struct bpf_prog的核心定义如下:

c 复制代码
// linux-6.6.58/include/linux/bpf.h: 1485
struct bpf_prog {
    // BPF程序入口函数:JIT编译后为本地机器码,未开启则为解释器
    unsigned int		(*bpf_func)(const void *ctx,
					    const struct bpf_insn *insn);
    union {
        // 存储原始BPF字节码
		DECLARE_FLEX_ARRAY(struct sock_filter, insns);
        // ... 其他字段省略
    };
    // ... 程序类型、许可、校验信息、BTF信息等其他字段省略
};

perf_event_open:kprobe事件注册与插桩就绪

kprobe事件由perf子系统管理,首先获取kprobe PMU对应的类型ID:

c 复制代码
openat(AT_FDCWD, "/sys/bus/event_source/devices/kprobe/type", O_RDONLY|O_CLOEXEC) = 6
read(6, "8\n", 4096) = 2
close(6) = 0

随后完成kprobe perf事件的注册:

c 复制代码
perf_event_open({type=0x8, size=0x88, config=0, sample_period=0, sample_type=0, read_format=0, precise_ip=0}, -1, 0, -1, PERF_FLAG_FD_CLOEXEC) = 6

核心技术细节:

  • 内核匹配perf_kprobe PMU驱动,执行event_init完成kprobe初始化:解析目标函数符号、计算内核虚拟地址、初始化kprobe核心结构体。
  • 函数入口kprobe优先基于dynamic ftrace实现:复用内核编译时-pg选项插入的ftrace位点(内核启动时已替换为nop指令,无性能损耗),注册kprobe时修改为ftrace回调指令,相比传统int3断点插桩性能开销更低、稳定性更好。
  • 调用enable_kprobe完成内核代码段poke操作,探测点就绪,系统调用返回事件对应的文件描述符。

通过BPF Link完成kprobe事件与BPF程序的绑定,形成完整追踪闭环,核心系统调用:

c 复制代码
bpf(BPF_LINK_CREATE, {
    link_create={
        prog_fd=5, 
        target_fd=6, 
        attach_type=BPF_PERF_EVENT, 
        flags=0, 
        perf_event={bpf_cookie=0}
    }
}, 48) = 7

核心技术细节:

  • prog_fd为已加载的BPF程序fd,target_fd为已注册的kprobe perf事件fd;内核将BPF程序挂载到事件对应的tp_event->prog_array数组中,事件触发时会遍历数组依次执行绑定的BPF程序。
  • BPF Link实现了资源的强生命周期绑定:Link fd关闭时,内核自动完成BPF程序解绑、kprobe事件注销,彻底避免资源泄漏。
  • 系统调用返回值为BPF Link的文件描述符。

内核态执行链路

kprobe触发后的完整调用栈如下:

复制代码
#0  bpf_dispatcher_nop_func (bpf_func=0xffffffffa0008f1c, insnsi=0xffffc90000641048, ctx=0xffffc90000483df8) at ./include/linux/bpf.h:1205
#1  __bpf_prog_run (dfunc=<optimized out>, ctx=<optimized out>, prog=0xffffc90000641000) at ./include/linux/filter.h:612
#2  bpf_prog_run (ctx=<optimized out>, prog=0xffffc90000641000) at ./include/linux/filter.h:619
#3  bpf_prog_run_array (run_prog=<optimized out>, ctx=0xffffc90000483df8, array=<optimized out>) at ./include/linux/bpf.h:1964
#4  trace_call_bpf (call=call@entry=0xffff8880054e7150, ctx=ctx@entry=0xffffc90000483df8) at kernel/trace/bpf_trace.c:143
#5  0xffffffff811f4f3a in kprobe_perf_func (tk=tk@entry=0xffff8880054e7a00, regs=regs@entry=0xffffc90000483df8) at kernel/trace/trace_kprobe.c:1538
#6  0xffffffff811f51e5 in kprobe_dispatcher (kp=0xffff8880054e7a18, regs=0xffffc90000483df8) at kernel/trace/trace_kprobe.c:1676
#7  0xffffffff81075fa3 in kprobe_ftrace_handler (ip=18446744071581874308, parent_ip=<optimized out>, ops=<optimized out>, fregs=<optimized out>) at arch/x86/kernel/kprobes/ftrace.c:45
#8  0xffffffffa02010fb in ?? () // ftrace跳板
#9  0x0000000000000000 in ?? ()

核心链路拆解:

  1. 目标函数执行时触发ftrace跳板,进入kprobe_ftrace_handler,将当前CPU的全量寄存器上下文保存到struct pt_regs结构体中。
  2. 调用kprobe_dispatcher匹配注册的kprobe事件,执行kprobe_perf_func,将struct pt_regs *regs作为核心上下文入参向下传递。
  3. 通过trace_call_bpf遍历事件挂载的BPF程序数组,调用bpf_prog_run触发BPF程序执行。
  4. 最终进入bpf_dispatcher_nop_func,调用struct bpf_prog->bpf_func(即用户编写的BPF程序入口),入参ctx即为内核态保存的struct pt_regs *regs

x86_64架构struct pt_regs核心定义如下:

c 复制代码
struct pt_regs {
	/* callee-preserved regs */
	unsigned long r15;
	unsigned long r14;
	unsigned long r13;
	unsigned long r12;
	unsigned long rbp;
	unsigned long rbx;
	/* callee-clobbered regs */
	unsigned long r11;
	unsigned long r10;
	unsigned long r9;
	unsigned long r8;
	unsigned long rax;
	unsigned long rcx;
	unsigned long rdx;
	unsigned long rsi;
	unsigned long rdi;
	/* syscall/exception专用字段 */
	unsigned long orig_rax;
	/* iretq中断返回栈帧 */
	unsigned long rip;
	unsigned long cs;
	unsigned long eflags;
	unsigned long rsp;
	unsigned long ss;
};

BPF程序执行的核心调用链源码:

c 复制代码
// linux-6.6.58/include/linux/filter.h:593
static __always_inline u32 __bpf_prog_run(const struct bpf_prog *prog,
					  const void *ctx,
					  bpf_dispatcher_fn dfunc)
{
		ret = dfunc(ctx, prog->insnsi, prog->bpf_func); // prog->bpf_func(ctx, prog->insnsi)
c 复制代码
// linux-6.6.58/include/linux/bpf.h: 1203
static __always_inline __nocfi unsigned int bpf_dispatcher_nop_func(
	const void *ctx,
	const struct bpf_insn *insnsi,
	bpf_func_t bpf_func)
{
	return bpf_func(ctx, insnsi); // bpf程序运行,传递第一个参数ctx,第二个参数一般用不到,是bpf prog字节码

BPF程序参数提取实现

PT_REGS_PARMx 宏:基于System V ABI的参数提取

x86_64架构System V AMD64 ABI规定,内核态函数前6个入参依次通过rdi、rsi、rdx、rcx、r8、r9寄存器传递,对应bpf_tracing.h中的核心宏定义:

c 复制代码
// /usr/include/bpf/bpf_tracing.h:85
#if defined(bpf_target_x86) && (defined(__KERNEL__) || defined(__VMLINUX_H__))
#define __PT_PARM1_REG di
#define __PT_PARM2_REG si
#define __PT_PARM3_REG dx
#define __PT_PARM4_REG cx
#define __PT_PARM5_REG r8
#define __PT_PARM6_REG r9

#define PT_REGS_PARM1(x) ((x)->__PT_PARM1_REG)
#define PT_REGS_PARM2(x) ((x)->__PT_PARM2_REG)
#define PT_REGS_PARM3(x) ((x)->__PT_PARM3_REG)
#define PT_REGS_PARM4(x) ((x)->__PT_PARM4_REG)
#define PT_REGS_PARM5(x) ((x)->__PT_PARM5_REG)
#define PT_REGS_PARM6(x) ((x)->__PT_PARM6_REG)
#endif

BPF程序中可直接通过该宏,从入参ctx中提取目标函数的对应入参。

BPF_KPROBE 宏:参数提取的语法糖封装

BPF_KPROBE宏封装了底层寄存器解析逻辑,隐藏了struct pt_regs的细节,核心展开逻辑如下:

c 复制代码
#define ___bpf_kprobe_args0()           ctx
#define ___bpf_kprobe_args1(x)          ___bpf_kprobe_args0(), (void *)PT_REGS_PARM1(ctx)
#define ___bpf_kprobe_args2(x, args...) ___bpf_kprobe_args1(args), (void *)PT_REGS_PARM2(ctx)
#define ___bpf_kprobe_args3(x, args...) ___bpf_kprobe_args2(args), (void *)PT_REGS_PARM3(ctx)
#define ___bpf_kprobe_args4(x, args...) ___bpf_kprobe_args3(args), (void *)PT_REGS_PARM4(ctx)
#define ___bpf_kprobe_args5(x, args...) ___bpf_kprobe_args4(args), (void *)PT_REGS_PARM5(ctx)
#define ___bpf_kprobe_args6(x, args...) ___bpf_kprobe_args5(args), (void *)PT_REGS_PARM6(ctx)
#define ___bpf_kprobe_args(args...)     ___bpf_apply(___bpf_kprobe_args, ___bpf_narg(args))(args)

#define BPF_KPROBE(name, args...)					    \
name(struct pt_regs *ctx);						    \
static __always_inline typeof(name(0))					    \
____##name(struct pt_regs *ctx, ##args);				    \
typeof(name(0)) name(struct pt_regs *ctx)				    \
{									    \
	_Pragma("GCC diagnostic push")					    \
	_Pragma("GCC diagnostic ignored \"-Wint-conversion\"")		    \
	return ____##name(___bpf_kprobe_args(args));			    \
	_Pragma("GCC diagnostic pop")					    \
}									    \
static __always_inline typeof(name(0))					    \
____##name(struct pt_regs *ctx, ##args)

宏展开后分为两层函数:

  • 外层name(struct pt_regs *ctx):kprobe事件的内核回调入口,接收内核传入的寄存器上下文;
  • 内层____##name:用户编写的业务逻辑函数,宏自动完成寄存器到函数入参的解析,无需手动调用PT_REGS_PARMx宏。

参考

1\] [gitee.com/kiraskyler/simple_bpf](https://gitee.com/kiraskyler/simple_bpf) \[2\] [kprobe函数入口时的汇编跳板执行流程与栈帧机制.md](https://gitee.com/kiraskyler/Articles/blob/master/eBPF/kprobe%E5%87%BD%E6%95%B0%E5%85%A5%E5%8F%A3%E6%97%B6%E7%9A%84%E6%B1%87%E7%BC%96%E8%B7%B3%E6%9D%BF%E6%89%A7%E8%A1%8C%E6%B5%81%E7%A8%8B%E4%B8%8E%E6%A0%88%E5%B8%A7%E6%9C%BA%E5%88%B6.md) \[3\] [深入ftrace kprobe原理解析](https://blog.csdn.net/u012489236/article/details/127942216) \[4\] [深入ftrace function原理原理解析](https://blog.csdn.net/u012489236/article/details/127814059)

相关推荐
三万棵雪松4 小时前
【Linux 物联网网关主控系统-Linux主控部分(三)】
linux·物联网·嵌入式linux
萝卜白菜。4 小时前
TongWeb7.0 集中管理heimdall配置文件说明
linux·运维·服务器
IMPYLH4 小时前
Linux 的 install 命令
linux·运维·服务器·bash
浦信仿真大讲堂5 小时前
CST FAQ 006:Linux系统CST安装指导
linux·运维·服务器·仿真软件·达索软件
AI+程序员在路上5 小时前
Linux C 条件变量阻塞线程用法:等待时CPU占用率为0
linux·运维·c语言
HABuo6 小时前
【linux线程(三)】生产者消费者模型(条件变量阻塞队列版本、信号量环形队列版本)详细剖析
linux·运维·服务器·c语言·c++·ubuntu·centos
Milu_Jingyu6 小时前
Windows与Ubuntu文件共享详细指南
linux·windows·ubuntu
Java面试题总结6 小时前
Linux根分区爆满(占用81%)排查与解决实战
linux·运维·服务器
Bert.Cai6 小时前
Linux touch命令详解
linux·运维