首页 「Let's Go eBPF」认识数据源:kprobe
文章
取消

「Let's Go eBPF」认识数据源:kprobe

-16371540410297

上一篇文章中,我们介绍了Linux的静态插桩方式Tracepoint。本文我们将介绍Linux的动态插桩方式,包含两种实现机制:陷阱和蹦床。

陷阱机制

基于陷阱(trap)机制,我们可以在程序运行时动态打桩。Linux内核提供了kprobe,作为陷阱机制的实现。kprobe的好处是无须修改代码和重新编译内核,即可向内核中的大多数函数中插桩,且位置可由用户指定。kprobe很适合用于内核调试和性能调优。出于安全性考虑,并非所有函数都能插桩。kprobe维护了一个黑名单,记录了不允许插桩的函数,其中就包括kprobe自身,以此避免递归调用。

当用户在某条指令语句处启用kprobe后,该指令会被复制并被替换为断点(breakpoint),在x86上是int3指令。当CPU执行到断点后,会触发内核陷入中断处理。内核会和处理其他中断一样保存程序的状态(例如寄存器、栈等信息),当Linux发现这个中断是由kprobe安装的之后,引导至kprobe框架处理。当kprobe处理完成后,退出中断,执行下一条指令。

-16373141079173 kprobe的执行流程示意1

更具体地,kprobe提供了预处理探针(pre handler)和后处理探针(post handler)。当执行完pre handler后,kprobe框架会单步执行原来的指令,然后进入post handler的处理。

-16373141027691 pre handler和post handler2

kprobe可以在运行时插入任意位置,方便用户使用,但和Tracepoint相比,性能开销更大。此外,kprobe的接口与Tracepoint相比不够稳定。不过,如果kprobe被移除了,断点指令会恢复为原先的指令,不会有额外的性能负担,而Tracepoint的代码位置会有nop

此外,kprobe还提供了另一种插桩方式:kretprobe,在函数调用返回时触发。与kprobe不同,注册kretprobe后,被插桩的函数原有ret指令的返回地址(即调用方的IP值)会被kretprobe的探针函数替换,当该函数执行到ret时,跳转到探针继续执行。探针在执行完成后再次ret返回调用方。这种方式没有用到陷阱机制,而是使用了另一种动态插桩的方法:蹦床。

蹦床机制

蹦床(trampoline)机制是对陷阱机制的改进。与利用内核的中断处理机制不同,蹦床机制在原先代码上动态打上一段基于跳转指令jmp的代码,省去了陷入中断的开销。在较新版本的内核中,部分kprobe已经用基于跳转的蹦床机制优化了。这种方法的核心在于用detour buffer(称为optimized region)来模仿中断的行为。首先在栈上保存CPU的寄存器,然后跳转到蹦床上作为中间过渡,最后跳转到用户定义的探针函数上。当完成执行后,再反过来执行以上过程:先跳出optimized region,然后从栈上恢复CPU寄存器的值,最后执行原先的代码。

并非所有的kprobe都支持蹦床机制,该机制有一系列的条件限制,例如目标位置的指令长度等。如果条件不满足,内核会继续使用基于陷阱的方式。

kprobe的执行需要动态地修改Linux代码,内核提供了text_poke_smp函数支持这项功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
 * text_poke_smp - Update instructions on a live kernel on SMP
 * @addr: address to modify
 * @opcode: source of the copy
 * @len: length to copy
 *
 * Modify multi-byte instruction by using stop_machine() on SMP. This allows
 * user to poke/set multi-byte text on SMP. Only non-NMI/MCE code modifying
 * should be allowed, since stop_machine() does _not_ protect code against
 * NMI and MCE.
 *
 * Note: Must be called under get_online_cpus() and text_mutex.
 */
void *__kprobes text_poke_smp(void *addr, const void *opcode, size_t len)
{
    struct text_poke_params tpp;
    struct text_poke_param p;

    p.addr = addr;
    p.opcode = opcode;
    p.len = len;
    tpp.params = &p;
    tpp.nparams = 1;
    atomic_set(&stop_machine_first, 1);
    wrote_text = 0;
    /* Use __stop_machine() because the caller already got online_cpus. */
    __stop_machine(stop_machine_text_poke, (void *)&tpp, cpu_online_mask);
    return addr;
}

与kprobe交互

与Tracepoint类似,我们可以在tracefs中与kprobe交互3,例如开始事件和增加过滤规则。想要深入了解kprobe及其用法的读者可以参考文末的链接2456

References

本文由作者按照 CC BY 4.0 进行授权

「Let's Go eBPF」认识数据源:Tracepoint

「Let's Go eBPF」来,一起“偷窥”下进程在干什么?