kprobes是一个动态地收集调试和性能信息的工具,使用它几乎可以跟踪任何函数或被执行的指令。它的机制也很简单,就是将被探测的位置的指令替换为断点指令(不考虑jmp优化),断点指令被执行后会通过notifier_call_chain机制来通知kprobes,kprobes会首先调用用户指定的pre_handler接口。执行pre_handler接口后会单步执行原始的指令,如果用户也指定了post_handler接口,会在调用post_handler接口后结束处理。
基本的处理过程如下图所示:
如果你没接触过kprobes,上面提到的pre_handler接口和post_handler接口可能让你疑惑,所以首先我们来介绍下和这些接口相关的kprobe结构。
1.kprobe结构
当你要探测一个函数时,需要首先分配一个kprobe实例,然后设置要探测的函数名称或地址,以及自己的处理函数。kprobe结构描述了探测点的位置以及要做的处理操作,其结构定义如下:
struct kprobe {
struct hlist_node hlist;
/* list of kprobes for multi-handler support */
struct list_head list;
/*count the number of times this probe was temporarily disarmed */
unsigned
long nmissed;
/* location of the probe point */
kprobe_opcode_t
*addr;
/* Allow user to indicate symbol name of the probe point */
const
char
*symbol_name;
/* Offset into the symbol */
unsigned
int offset;
/* Called before addr is executed. */
kprobe_pre_handler_t pre_handler;
/* Called after addr is executed, unless... */
kprobe_post_handler_t post_handler;
/*
* ... called if executing addr causes a fault (eg. page fault).
* Return 1 if it handled fault, otherwise kernel will see it.
*/
kprobe_fault_handler_t fault_handler;
/*
* ... called if breakpoint trap occurs in probe handler.
* Return 1 if it handled break, otherwise kernel will see it.
*/
kprobe_break_handler_t break_handler;
/* Saved opcode (which has been replaced with breakpoint) */
kprobe_opcode_t opcode;
/* copy of the original instruction */
struct arch_specific_insn ainsn;
/*
* Indicates various status flags.
* Protected by kprobe_mutex after this kprobe is registered.
*/
u32 flags;
};
其成员说明如下:
hlist:所有注册的kprobe都会添加到kprobe_table哈希表中,hlist成员用来链接到某个槽位中。
list:如果在同一个位置注册了多个kprobe,这些kprobe会形成一个队列,队首是一个特殊的kprobe实例,list成员用来用来链接到这个队列中。当探测点被触发时,队首的kprobe实例中注册的handler会逐个遍历队列中注册的handler。
nmissed:记录当前的probe没有被处理的次数。
addr:这个成员有两个作用,一个是用户在注册前指定探测点的基地址(加上偏移得到真实的地址),另一个是在注册后保存探测点的实际地址。在注册前,
这个可以不指定,由kprobes来初始化。如果没有指定,必须指定探测的位置的符号信息,例如函数名。
symbol_name:探测点的符号名称。名称和地址不能同时指定,否则注册时会返回EINVAL错误。
offset:探测点相对于addr地址的偏移
pre_handler:这个接口在断点异常触发之后,开始单步执行原始的指令之前被调用
post_handler:在单步执行原始的指令后会被调用
fault_handler:如果执行过程中出错,则调用该接口。如果返回1,则表示错误由kprobes处理,否则由内核来处理。
break_handler:在调用probe的处理函数(比如pre_handler接口)时触发了断点异常会调用该接口,。断点异常是通过中断门来处理的(参见trap_init()),在调用相应的处理函数前会自动关闭中断。关中断的情况下虽然不会接收可屏蔽的中断,但是CPU引发的异常或者NMI还是会接收到,所以有可能会发生断点异常处理嵌套,jprobes的实现就用到了这点。
opcode:原始指令,在被替换为断点指令(X86下是int 3指令)前保存。
ainsn:保存了探测点原始指令的拷贝。这里拷贝的指令要比opcode中存储的指令多,拷贝的大小为MAX_INSN_SIZE * sizeof(kprobe_opcode_t)。
flags:探测点的标志,可取的值为KPROBE_FLAG_GONE和KPROBE_FLAG_DISABLED。如果设置了KPROBE_FLAG_GONE标志,表示断点指令被移除;如果设置了 KPROBE_FLAG_DISABLED,则表示只注册probe,但是并不启用它,也就是说在断点异常触发时并不会调用该probe的接口。
2.探测点的注册----register_kprobe()
2.1 探测点地址的计算
register_kprobe()用来在指定的位置注册探测点。它会首先调用kprobe_addr()计算需要插入探测点的地址,源码如下所示:
int __kprobes register_kprobe(
struct kprobe
*p)
{
int ret
=
0;
struct kprobe
*old_p;
struct module
*probed_mod;
kprobe_opcode_t
*addr;
addr
= kprobe_addr(p);
if (
!addr)
return
-EINVAL;
p
-
>addr
= addr;
......
}
kprobe_addr()会首先检查用户是否设置探测点的地址,如果指定了地址,并且指定了符号信息(symbol_name成员也设置了),它会直接返回NULL,不会去检查符号信息和指定的地址是否一致。通过这里的代码不难发现,如果同时指定了符号信息和地址,register_kprobe()会返回EINVAL错误。
如果没有指定探测点的地址,而是指定了符号信息,kprobe_addr()会调用kprobe_lookup_name()在内核符号表中查找符号对应的地址,在找到对应的符号地址后,加上偏移就得到探测点的实际位置。
如果只指定了探测点的地址,kprobe_addr()会将这个地址直接加上偏移返回。
kprobe_addr()返回的地址会被设置到kprobe的addr成员中,注册后通过这个成员就可以拿到被探测位置的地址。利用这个特性,你可以通过kprobe来获取内核中某一个函数的运行时地址。假设你在编写一个内核模块,但是你要使用函数内核没有导出,这个特性就很有用了。
2.2 检查探测点地址
计算探测点的地址后,接着就是要检查这个地址是否可以被探测,源码如下所示:
......
preempt_disable();
if (
!kernel_text_address((
unsigned
long) p
-
>addr)
||
in_kprobes_functions((
unsigned
long) p
-
>addr)) {
preempt_enable();
return
-EINVAL;
}
/* User can pass only KPROBE_FLAG_DISABLED to register_kprobe */
p
-
>flags
&= KPROBE_FLAG_DISABLED;
/*
* Check if are we probing a module.
*/
probed_mod
= __module_text_address((
unsigned
long) p
-
>addr);
if (probed_mod) {
/*
* We must hold a refcount of the probed module while updating
* its code to prohibit unexpected unloading.
*/
if (unlikely(
!try_module_get(probed_mod))) {
preempt_enable();
return
-EINVAL;
}
/*
* If the module freed .init.text, we couldn't insert
* kprobes in there.
*/
if (within_module_init((
unsigned
long)p
-
>addr, probed_mod)
&&
probed_mod
-
>state
!= MODULE_STATE_COMING) {
module_put(probed_mod);
preempt_enable();
return
-EINVAL;
}
}
preempt_enable();
......
kprobes只能用作内核函数的探测,所以在注册前必须检查探测点的地址是否是在内核地址空间中。探测点的地址要么是在内核映像中(
_stext和_etext之间,如果是在系统启动阶段,要在_sinittext和_einittext之间
),要么是是在某个内核模块的地址空间中。具体的判断是在
kernel_text_address()
函数中处理的。
实现kprobes的内核函数或者一些特殊的函数是不能探测的,所以还要进一步检测探测点的地址是否在这些限制空间内,这是由
in_kprobes_functions()
函数来处理的。kprobes把所有不能探测的函数都放在
.kprobes.text section(使用__kprobes宏)中,这个段的起始地址是
__kprobes_text_start和__kprobes_text_end。如果探测的地址在
__kprobes_text_start和__kprobes_text_end之间
,会返回EINVAL错误。某些函数也是不能探测的,但是已经放在其他section了,这些函数会放在
kprobe_blacklist数组中。如果用户探测的函数在这个黑名单中,也会返回EINVAL错误。2.6.32中只有preempt_schedule()函数在
kprobe_blacklist数组中,3.12中增加了
native_get_debugreg()、irq_entries_start()、common_interrupt()、mcount()。
如果探测点的地址是在一个内核模块中,需要增加对该模块的引用,以免模块提前卸载。但是如果模块已经开始卸载,此时也是不能注册探测点的,同样会返回EINVAL错误。模块中.init.text section占用的内存,在模块加载后,如果不再使用是可以被释放掉的。如果探测点在模块的.init.text section中,而该section已经被释放,此时是不能注册探测点的,同样会返回EINVAL错误。
前面说了一大堆会返回EINVAL错误的情况,是时候要总结一下了。在以下情况中,会注册失败,返回EINVAL错误:
1) 函数被__kprobes宏修饰的函数,这些函数会被放在.kprobes.text section中。这种类型大多数是实现kprobes的代码
2) kprobe_blacklist数组中列出的函数。2.6.32中只有preempt_schedule()函数,最新的3.12内核中又增加了native_get_debugreg()、irq_entries_start()、
common_interrupt()、mcount()。
3) 要探测的地址位于一个模块的.init.text section,但是这个section已经被释放。
2.3 注册kprobe
经过前面的处理后,可以开始执行真正的注册操作了,源码如下所示:
p
-
>nmissed
=
0;
INIT_LIST_HEAD(
&p
-
>list);
mutex_lock(
&kprobe_mutex);
old_p
= get_kprobe(p
-
>addr);
if (old_p) {
ret
= register_aggr_kprobe(old_p, p);
goto out;
}
mutex_lock(
&text_mutex);
ret
= arch_prepare_kprobe(p);
if (ret)
goto out_unlock_text;
INIT_HLIST_NODE(
&p
-
>hlist);
hlist_add_head_rcu(
&p
-
>hlist,
&kprobe_table[hash_ptr(p
-
>addr, KPROBE_HASH_BITS)]);
if (
!kprobes_all_disarmed
&&
!kprobe_disabled(p))
arch_arm_kprobe(p);
out_unlock_text
:
mutex_unlock(
&text_mutex);
out
:
mutex_unlock(
&kprobe_mutex);
在初始化kprobe的相关成员后,首先要获取kprobe_mutex互斥锁,这个互斥锁用来保护
kprobe_table哈希表,系统中所有已注册的kprobe实例都保存在这个哈希表中。
kprobes允许在同一个探测点注册多个kprobe,如果调用get_kprobe()能找到一个kprobe实例,说明已经在当前的探测点注册了一个kprobe,这种情况下会调用register_aggr_kprobe()来处理。如果已注册的kprobe只有一个,
register_aggr_kprobe()
会分配一个新的kprobe实例,将已注册的kprobe中的内容拷贝到新分配的kprobe中,然后把已注册的kprobe的hanlder设置为kprobes提供的特殊的hanlder,这些handler在异常触发时会逐个遍历用户注册的kprobe中的handler。接着会将新分配的kprobe(不是当前调用register_kprobe()注册的那个)链接到前面已经注册的kprobe,形成一个队列。之后如果还有新的kprobe注册到当前的探测点,会直接添加到队列的末尾。注意,前面描述的
register_aggr_kprobe()
过程中还没有操作当前要注册的kprobe实例,在前面的操作都做完之后,就直接把新注册的kprobe实例添加到队尾,然后返回。
这个过程中结构的变化如下图所示:
如果在当前探测点没有注册过kprobe,则调用
arch_prepare_kprobe()
将被探测位置的指令保存到kprobe结构的
ainsn
成员中,并且被探测位置的第一条指令保存到opcode成员中。
接着就可以将krobe实例添加到kprobe_table哈希表中,这个操作必须在将探测点的指令替换成断点指令前完成,否则有可能在注册完成前断点异常已经发生了。
经过前面的处理,现在就剩下最后一步了,将探测点的指令替换为断点指令。这是通过调用
text_poke()
来完成,它可以将替换指定位置的指令。不过在调用text_poke()替换指令前,必须要获取text_mutex互斥锁。
text_poke()
真是太好用了,可以做很多事情.......
至此,整个注册过程就完成了。