上一篇文章中,我们介绍了Linux的静态插桩方式Tracepoint。本文我们将介绍Linux的动态插桩方式,包含两种实现机制:陷阱和蹦床。
陷阱机制
基于陷阱(trap)机制,我们可以在程序运行时动态打桩。Linux内核提供了kprobe,作为陷阱机制的实现。kprobe的好处是无须修改代码和重新编译内核,即可向内核中的大多数函数中插桩,且位置可由用户指定。kprobe很适合用于内核调试和性能调优。出于安全性考虑,并非所有函数都能插桩。kprobe维护了一个黑名单,记录了不允许插桩的函数,其中就包括kprobe自身,以此避免递归调用。
当用户在某条指令语句处启用kprobe后,该指令会被复制并被替换为断点(breakpoint),在x86上是int3
指令。当CPU执行到断点后,会触发内核陷入中断处理。内核会和处理其他中断一样保存程序的状态(例如寄存器、栈等信息),当Linux发现这个中断是由kprobe安装的之后,引导至kprobe框架处理。当kprobe处理完成后,退出中断,执行下一条指令。
kprobe的执行流程示意1
更具体地,kprobe提供了预处理探针(pre handler)和后处理探针(post handler)。当执行完pre handler后,kprobe框架会单步执行原来的指令,然后进入post handler的处理。
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。