BPF KPROBE编程中的ctx是什么?
文章目录
- [BPF KPROBE编程中的ctx是什么?](#BPF KPROBE编程中的ctx是什么?)
-
- 简介
- 用户态核心流程
- 内核态执行链路
- BPF程序参数提取实现
-
- [PT_REGS_PARMx 宏:基于System V ABI的参数提取](#PT_REGS_PARMx 宏:基于System V ABI的参数提取)
- [BPF_KPROBE 宏:参数提取的语法糖封装](#BPF_KPROBE 宏:参数提取的语法糖封装)
- 参考
简介
本文基于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字节码存入
- 系统调用返回值为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_kprobePMU驱动,执行event_init完成kprobe初始化:解析目标函数符号、计算内核虚拟地址、初始化kprobe核心结构体。 - 函数入口kprobe优先基于dynamic ftrace实现:复用内核编译时
-pg选项插入的ftrace位点(内核启动时已替换为nop指令,无性能损耗),注册kprobe时修改为ftrace回调指令,相比传统int3断点插桩性能开销更低、稳定性更好。 - 调用
enable_kprobe完成内核代码段poke操作,探测点就绪,系统调用返回事件对应的文件描述符。
BPF_LINK_CREATE:事件与程序的生命周期绑定
通过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 ?? ()
核心链路拆解:
- 目标函数执行时触发ftrace跳板,进入
kprobe_ftrace_handler,将当前CPU的全量寄存器上下文保存到struct pt_regs结构体中。 - 调用
kprobe_dispatcher匹配注册的kprobe事件,执行kprobe_perf_func,将struct pt_regs *regs作为核心上下文入参向下传递。 - 通过
trace_call_bpf遍历事件挂载的BPF程序数组,调用bpf_prog_run触发BPF程序执行。 - 最终进入
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)