个人总结:具体一点就是:
- 埋点:就是设置观察点
- 观察点就是函数名
- 将 ebpf 程序挂载到内核函数:其实就是将函数的埋点对应到 ebpf 程序
- Linux内核钩子(hook)机制能够实现内核函数被调用的时候,把 ebpf 程序先跑起来,等内核函数调用结束之后再结束
要理解 eBPF 程序挂载到内核函数后为何会被自动调用,需要从内核机制、触发原理和设计模式三个层面来解析:
一、内核层面的触发原理
eBPF 程序能够被自动调用的核心是内核钩子(Hook)机制,具体过程如下:
-
挂载点注册
当通过bpf_prog_attach()等系统调用将 eBPF 程序挂载到目标内核函数(如sys_write、tcp_sendmsg)时,内核会在该函数的执行路径中插入一个触发点。- 对于静态内核函数(编译时确定的函数),通常通过 kprobe(动态调试钩子)实现:内核会在目标函数的入口/出口处插入一条特殊指令(如 x86 的
int3断点)。 - 对于动态事件(如网络包、进程创建),则通过内核预设的静态钩子(如
tracepoint、kretprobe)触发,这些钩子是内核开发者预先在关键路径中埋入的调用点。
- 对于静态内核函数(编译时确定的函数),通常通过 kprobe(动态调试钩子)实现:内核会在目标函数的入口/出口处插入一条特殊指令(如 x86 的
-
执行流程拦截
当目标内核函数被调用时,CPU 执行到预设的触发点指令时会暂停当前流程,转而执行以下操作:- 保存当前寄存器状态和栈信息(确保原函数能正常恢复)。
- 调用内核中的 eBPF 虚拟机(
bpf_prog_run()),加载并执行已挂载的 eBPF 程序。 - eBPF 程序执行完毕后,恢复现场,继续执行原内核函数的剩余逻辑。
-
权限与安全控制
内核会通过BPF 验证器(verifier) 确保 eBPF 程序不会破坏内核稳定性(如不访问非法内存、不执行无限循环),只有通过验证的程序才能被挂载和执行。
二、内核实现的关键机制
-
动态代码修改(kprobe机制)
kprobe 通过动态修改内核代码段(在目标函数入口写入断点指令)实现拦截。当 CPU 执行到断点时,会触发内核异常处理流程(int3中断),进而调用 kprobe 注册的回调函数,而 eBPF 程序就通过这个回调被间接执行。 -
事件通知框架(tracepoint)
tracepoint 是内核编译时静态插入的事件点(本质是一个函数指针),当内核运行到对应位置时会检查是否有注册的 eBPF 程序,若有则调用。相比 kprobe,tracepoint 更稳定(不会修改内核代码),但仅能用于内核预设的事件点。 -
eBPF虚拟机
内核中的 eBPF 虚拟机负责解释执行 eBPF 字节码,它限制了程序的执行时间(通过rlimit)和内存访问范围,确保 eBPF 程序无法干扰内核正常运行。
三、设计模式:观察者模式(Observer Pattern)
eBPF的挂载与触发机制本质上是观察者模式的内核级实现:
- 被观察者(Subject):内核中的目标函数或事件(如
sys_write、进程调度)。 - 观察者(Observer):eBPF 程序,通过挂载操作注册到被观察者中。
- 触发逻辑:当被观察者(内核函数)发生状态变化(被调用)时,自动通知所有注册的观察者(eBPF 程序)执行。
个人总结:具体一点就是:
- 埋点:就是设置观察点
- 观察点就是函数名
- 将 ebpf 程序挂载到内核函数:其实就是将函数的埋点对应到 ebpf 程序
- Linux内核钩子(hook)机制能够实现内核函数被调用的时候,把 ebpf 程序先跑起来,等内核函数调用结束之后再结束
这种模式的优势在于:
- 解耦:内核功能与 eBPF 程序的逻辑完全分离,无需修改内核源码即可扩展功能。
- 动态性:eBPF 程序可在运行时动态挂载/卸载,不影响内核整体稳定性。
- 高效性:通过内核级钩子直接拦截,避免了用户态与内核态的频繁切换开销。
总结
eBPF 程序被自动调用的核心是内核钩子机制:通过 kprobe/tracepoint 等技术在目标函数中插入触发点,当函数执行时触发 eBPF 虚拟机执行已挂载的程序。
其设计模式符合观察者模式,实现了内核事件与用户扩展逻辑的解耦,兼顾了灵活性与安全性。