016.优化:uprobe与kprobe结合千大事
前面咱们不是提过ebpf函数取参数的问题
int uprobe_malloc(struct pt_regs *ctx)
malloc的函数原型
void *malloc(size_t size);
那能不能写成
int uprobe_malloc(size_t size)
这样取参数多方便
我就想起了之前写kprobe程序,是可以这样的
然后我就试了一下,确实可以
int BPF_KPROBE(uprobe_malloc, size_t size)
需要用到宏BPF_KPROBE、BPF_KRETPROBE
BPF_KPROBE / BPF_KRETPROBE 是 libbpf 提供的 kprobe/retprobe 函数声明宏
用来自动从 pt_regs 中解析函数参数
但是ChatGPT不推荐这样写,原因见副笔记
uprobe不推荐使用BPF_KPROBE
结论先给:❌ 不推荐,也不算“正确写法”。
这段代码语义上是错位的,只是“可能能编译”,但概念和工程实践都是不对的。
SEC("uprobe")
int BPF_KPROBE(uprobe_malloc, size_t size)
{
bpf_printk("malloc(%lu)\n", size);
return 0;
}
下面我用判定 → 原因 → 正确写法 → 为什么有人会这么写还能跑,把这事讲清楚。
一、为什么说这是「不对的写法」
核心问题只有一句话:
BPF_KPROBE** 是为 kprobe 设计的参数解包宏,
**你却把它用在了SEC("uprobe")上。
这在概念层面是错位的。
二、把角色彻底分清(这是关键)
1️⃣ SEC("uprobe") 决定了什么?
SEC("uprobe")
它决定的是:
这是一个 uprobe 程序(用户态探针)
也就是说:
- hook 的是 用户态函数
- 参数来自 用户态 ABI
- 触发点是 ELF + uprobes
2️⃣ BPF_KPROBE 是为谁设计的?
BPF_KPROBE(...)
它的设计前提是:
kprobe / kretprobe(内核态)
- 参数从
struct pt_regs里按 内核调用约定 取- 解决的是 不同架构寄存器布局的问题
📌 它的语义是:
“这是一个 kprobe 风格的函数声明”
三、那 uprobe 应该用什么?
✅ 正确答案:BPF_UPROBE / BPF_URETPROBE
libbpf 提供了对称的一组宏:
| 场景 | 正确宏 |
|---|---|
| kprobe | BPF_KPROBE |
| kretprobe | BPF_KRETPROBE |
| uprobe | BPF_UPROBE |
| uretprobe | BPF_URETPROBE |
四、你的代码的「正确写法」
✅ 正确、规范、工程级写法
SEC("uprobe")
int BPF_UPROBE(uprobe_malloc, size_t size)
{
bpf_printk("malloc(%lu)\n", size);
return 0;
}
这才是:
- 语义正确
- 架构无关
- libbpf 官方推荐
- 教学上 绝对不会被挑错 的写法
五、那为什么「用 BPF_KPROBE 也可能能跑」?
这是一个非常容易误导人的点,但你作为讲课的人,一定要讲清楚。
原因只有一个:
在 x86_64 上,kprobe 和 uprobe 在“寄存器取参”这件事上恰好长得很像
也就是说:
- 第一个参数 → rdi
- 第二个参数 → rsi
- …
所以:
BPF_KPROBE(...)
在 uprobe 场景下:
- 可能刚好取到了对的寄存器
- 但这是 ABI 巧合
- 不是语义保证
📌 一旦:
- 换架构(arm64)
- 换编译器
- 换调用约定
- 换函数签名
👉 就会悄悄出 bug
六、为什么这在“课程 / 工程”里必须禁止?
因为这是典型的:
“碰巧能跑,但概念是错的代码”
在教学里如果不纠正,会导致学生:
- 以为
BPF_KPROBE是“万能宏” - 分不清 kprobe / uprobe
- 把 ABI 差异当成 CO-RE 能解决的问题(这是大坑)
七、你可以在课上这样一句话“定性”(非常好用)
**能不能跑,不代表写得对;
**eBPF 里最忌讳“ABI 巧合正确”。
或者更狠一点:
**BPF_KPROBE 写 uprobe,
****就像用内核 API 读用户内存——
**有时不炸,但一定不对。
八、最终结论(可直接当板书)
❌ 不推荐:
SEC("uprobe")
int BPF_KPROBE(...)
✅ 正确写法:
SEC("uprobe")
int BPF_UPROBE(...)
一句话终极总结
SEC("uprobe")** 决定“挂在哪”,
BPF_*PROBE决定“按谁的 ABI 解包参数”,
**这两者必须语义一致。
BPF_UPROBE展开
#define BPF_UPROBE(name, args...) BPF_KPROBE(name, ##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)
#define ___bpf_kprobe_args(args...) ___bpf_apply(___bpf_kprobe_args, ___bpf_narg(args))(args)
我们就只干一件事:把宏完整、逐步地展开,让你清楚:
BPF_KPROBE / BPF_UPROBE** 到底是怎么把pt_regs里的寄存器,变成你函数参数的**
一、先给结论(先立住整体认知)
BPF_KPROBE / BPF_UPROBE** 本质是一个“三段式函数包装宏”**:
- 对外暴露的 BPF 程序入口函数(签名是
struct pt_regs *ctx)- 一个内联的“真正业务函数”(你写的参数形式)
- 一个“参数解包器”(把寄存器 → C 参数)
二、你给出的宏(我们逐段拆)
你给的是(略微整理过):
#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)
⚠️
BPF_UPROBE的结构几乎一模一样,
只是把___bpf_kprobe_args换成了___bpf_uprobe_args
三、你的代码长这样
int BPF_UPROBE(uprobe_malloc, size_t size)
我们一步一步展开。
四、第一步展开:替换 name 和 args
int uprobe_malloc(struct pt_regs *ctx);
static __always_inline int
____uprobe_malloc(struct pt_regs *ctx, size_t size);
int uprobe_malloc(struct pt_regs *ctx)
{
_Pragma("GCC diagnostic push")
_Pragma("GCC diagnostic ignored \"-Wint-conversion\"")
return ____uprobe_malloc(___bpf_uprobe_args(size));
_Pragma("GCC diagnostic pop")
}
static __always_inline int
____uprobe_malloc(struct pt_regs *ctx, size_t size)
⚠️ 注意:
你真正写代码的地方,其实是下面这个函数:
____uprobe_malloc(struct pt_regs *ctx, size_t size)
五、第二步:___bpf_uprobe_args(size) 是什么?
这是关键中的关键。
在 bpf_tracing.h 里(简化):
#define ___bpf_uprobe_args(args...) \
___bpf_uprobe_arg(args)
而 ___bpf_uprobe_arg(size) 会被展开成类似:
x86_64 下(示意)
(size_t)PT_REGS_PARM1(ctx)
也就是说:
return ____uprobe_malloc(
(size_t)PT_REGS_PARM1(ctx)
);
六、最终“完全展开后的等价代码”(教学级)
你这行:
int BPF_UPROBE(uprobe_malloc, size_t size)
{
bpf_printk("malloc(%lu)\n", size);
return 0;
}
等价于你手写下面这坨代码:
SEC("uprobe")
int uprobe_malloc(struct pt_regs *ctx)
{
size_t size = (size_t)PT_REGS_PARM1(ctx);
bpf_printk("malloc(%lu)\n", size);
return 0;
}
👉 这就是全部真相。
七、那为什么要搞这么复杂?
1️⃣ 架构无关
PT_REGS_PARM1在 x86 / arm64 下不同- 宏统一处理
2️⃣ 类型友好(CO-RE 友好,但不是 CO-RE)
(size_t)PT_REGS_PARM1(ctx)
- 有类型
- 有编译期检查
- 但 ❗ 这不是 BTF / CO-RE
3️⃣ verifier 友好
- 内联函数
- 无函数指针
- verifier 能完整展开 CFG
八、为什么要有两个函数(name / ____name)?
这是一个非常经典的内核/编译器技巧:
| 函数 | 角色 |
|---|---|
name(struct pt_regs *ctx) |
BPF 程序入口(SEC 绑定的函数) |
____name(ctx, args…) |
真正写逻辑的地方 |
这样做的好处:
- 入口函数签名固定(BPF 要求)
- 逻辑函数签名友好(给人写)
九、BPF_KPROBE 和 BPF_UPROBE 的唯一区别
| 宏 | 解包器 |
|---|---|
BPF_KPROBE |
___bpf_kprobe_args() |
BPF_UPROBE |
___bpf_uprobe_args() |
👉 区别只在“寄存器 ABI 解释规则”
十、一句话终极总结(非常重要)
BPF_KPROBE / BPF_UPROBE**
****不是魔法,也不是 CO-RE,
****它只是把pt_regs→ 参数 的脏活,
**用宏和内联函数帮你写完了。

浙公网安备 33010602011771号