在探测点注册kprobe后,会在执行到探测点的指令时触发断点异常(trap 3)。kprobes在断点异常的通知链die_chain上注册了自己的处理函数,这个函数就是kprobe_exception_notify()。kprobe_exception_notify()不仅会接收到断点异常的通知,还会接收到调试异常(trap 1)和保护异常的通知。这篇文章主要围绕这个函数的处理来展开。
1.断点异常处理
断点是int 3指令触发的,系统中这个异常是由int3()函数来处理,这个函数在entry_32.S中定义。断点异常的处理函数int3()是在trap_init()中注册,注册的时候使用的门类型是中断门,这意味着在处理断点异常时是要关闭中断的,这和普通的异常处理是不同的。在关中断的情况下,CPU仍然可以接收到NMI和CPU引发的异常,包括断点异常。
int3()中主要是调用do_int3()在处理。do_int3()中在由内核处理前,会通知注册在die_chian通知链上的模块,如果这个异常由内核其他模块处理,内核就不再处理了,源码如下所示:
dotraplinkage
void __kprobes do_int3(
struct pt_regs
*regs,
long error_code)
{
#
ifdef CONFIG_KPROBES
if (notify_die(DIE_INT3,
"int3", regs, error_code,
3, SIGTRAP)
== NOTIFY_STOP)
return;
#
else
if (notify_die(DIE_TRAP,
"int3", regs, error_code,
3, SIGTRAP)
== NOTIFY_STOP)
return;
#
endif
preempt_conditional_sti(regs);
do_trap(
3, SIGTRAP,
"int3", regs, error_code, NULL);
preempt_conditional_cli(regs);
}
如果断点异常是由kprobe引发的,
kprobe_exception_notify()会返回NOTIFY_STOP,不再由内核处理。通过前面的分析,我们可以看出
kprobe_exception_notify()是在关中断的情况下调用的,所以这个函数的处理要尽可能地短,不能发生调度,也不能出现会导致睡眠的操作(例如获取互斥锁或信号量)。
断点异常发生时,通知链上的通知类型为DIE_INT3,这种类型在kprobe_exception_notify()中会调用kprobe_handler()来处理,如下所示:
int __kprobes kprobe_exceptions_notify(
struct notifier_block
*self,
unsigned
long val,
void
*data)
{
......
switch (val) {
case DIE_INT3
:
if (kprobe_handler(args
-
>regs))
ret
= NOTIFY_STOP;
break;
case DIE_DEBUG
:
......
return ret;
}
kprobe_handler()中会处理普通的kprobe处理流程,即调用pre_handler接口,然后开始单步执行指令。由于在调用kprobe的handler的时候,handler中有可能会触发断点异常(虽然是关闭了中断,但是异常还是会处理的),所以kprobe_handler()也要处理kprobe重入的问题。重入的问题,这里我们只关注普通的kprobe处理流程,代码如下所示:
static
int __kprobes kprobe_handler(
struct pt_regs
*regs)
{
kprobe_opcode_t
*addr;
struct kprobe
*p;
struct kprobe_ctlblk
*kcb;
addr
= (kprobe_opcode_t
*)(regs
-
>ip
-
sizeof(kprobe_opcode_t));
if (
*addr
!= BREAKPOINT_INSTRUCTION) {
/*
* The breakpoint instruction was removed right
* after we hit it. Another cpu has removed
* either a probepoint or a debugger breakpoint
* at this address. In either case, no further
* handling of this interrupt is appropriate.
* Back up over the (now missing) int3 and run
* the original instruction.
*/
regs
-
>ip
= (
unsigned
long)addr;
return
1;
}
/*
* We don't want to be preempted for the entire
* duration of kprobe processing. We conditionally
* re-enable preemption at the end of this function,
* and also in reenter_kprobe() and setup_singlestep().
*/
preempt_disable();
kcb
= get_kprobe_ctlblk();
p
= get_kprobe(addr);
if (p) {
if (kprobe_running()) {
.......
}
else {
set_current_kprobe(p, regs, kcb);
kcb
-
>kprobe_status
= KPROBE_HIT_ACTIVE;
/*
* If we have no pre-handler or it returned 0, we
* continue with normal processing. If we have a
* pre-handler and it returned non-zero, it prepped
* for calling the break_handler below on re-entry
* for jprobe processing, so get out doing nothing
* more here.
*/
if (
!p
-
>pre_handler
||
!p
-
>pre_handler(p, regs))
setup_singlestep(p, regs, kcb);
return
1;
}
}
else
if (kprobe_running()) {
......
}
/* else: not a kprobe fault; let the kernel handle it */
preempt_enable_no_resched();
return
0;
}
执行完断点指令后,指令指针IP会指向下一个指令的位置,所以这里要使用regs->ip减去断点指令的长度,得到断点指令的地址,存在局部变量addr中。如果addr处的指令不是断点指令,kprobes就不会再处理了,将指令指针指向探测点的位置,然后返回。断点异常处理后,CPU会从addr处的指令开始执行。这种情况可能是断点指令在发生断点异常后被另一个CPU移除,或者是探测点被禁止。
如果断点指令没有被移除,则kprobes会继续处理。kprobes会首先调用get_kprobe_ctlblk()获取kprobe控制块,获取的变量是一个per-cpu变量,这个变量中会存储kprobe处理的状态,还有可能保存处理kprobe时的寄存器信息等。
接着会调用get_kprobe()获取addr的位置注册的kprobe。如果kprobe不存在,并且没有kprobe正在处理,则表示addr的位置没有注册kprobe,并且也不是jprobes的处理(kprobe不存在,但是有kprobe正在处理)。
如果addr处有一个对应的kprobe,则会调用set_current_kprobe()将addr处的kprobe设置到per-cpu变量current_kprobe中,并且把TF(单步执行)和IF(开关中断)标志保存到kprobe控制块中。将kprobe变量保存到current_kprobe中,这样就可以通过这个变量来判断是否当前CPU上正在处理kprobe。
在kprobe的处理过程中要记录当前kprobe处理的状态,在调用pre_handler之前,状态会设置为KPROBE_HIT_ACTIVE。
如果在注册kprobe时指定了pre_handler接口,则会调用用户指定的接口。如果pre_handler接口返回1,则不会进行单步执行的过程,post_handler接口也不会被调用(依赖于单步执行过程),直接结束kprobe的处理。
2.单步执行
单步执行由setup_singlestep()函数来启动,它做的主要工作就是调用prepare_singlestep()来为单步执行做准备,然后把kprobe的处理状态变为KPROBE_HIT_SS,如下所示:
static
void __kprobes setup_singlestep(
struct kprobe
*p,
struct pt_regs
*regs,
struct kprobe_ctlblk
*kcb)
{
.......
prepare_singlestep(p, regs);
kcb
-
>kprobe_status
= KPROBE_HIT_SS;
}
prepare_singlesetp()中的处理也很简单,主要是将标志寄存器的TF标志位置1,然后就是设置单步执行指令的地址。如果原始的指令就是断点指令,会将指令指针指向探测点的位置;如果不是,则从保存的指令开始。如果原始的指令真的是断点指令,则会发生kprobe重入,重入时的处理后面再讲,这里先跳过。
3.调试异常处理
TF标志置1时,CPU每执行完一条指令就产生调试异常(trap 1)。调试异常由debug()函数处理,该函数在entry_32.S中定义,主要是调用do_debug()函数来完成的。do_debug()中会通知注册在die_chian通知链上模块,通知的类型为DIE_DEBUG。Kprobes注册的处理函数仍然是kprobe_exception_notify(),对应的处理如下所示:
int __kprobes kprobe_exceptions_notify(
struct notifier_block
*self,
unsigned
long val,
void
*data)
{
......
switch (val) {
.......
case DIE_DEBUG
:
if (post_kprobe_handler(args
-
>regs))
ret
= NOTIFY_STOP;
break;
.......
return ret;
}
post_kprobe_handler()中主要是恢复原来的处理流程,并且会调用注册时指定的post_handler接口,源码如下所示:
static
int __kprobes post_kprobe_handler(
struct pt_regs
*regs)
{
.......
resume_execution(cur, regs, kcb);
regs
-
>flags
|= kcb
-
>kprobe_saved_flags;
if ((kcb
-
>kprobe_status
!= KPROBE_REENTER)
&& cur
-
>post_handler) {
kcb
-
>kprobe_status
= KPROBE_HIT_SSDONE;
cur
-
>post_handler(cur, regs,
0);
}
.......
reset_current_kprobe();
.......
return
1;
}
为了在异常处理后从探测点之后开始执行,在这里需要调用resume_execution()来将指令指针IP指向探测点之后的位置,在resume_execution()中还会清除TF标志。在kprobe处理完成后,要恢复之前保存的寄存器标志位,这里主要是TF和IF标志。
如果kprobe控制块的状态是KPROBE_REENTER,即发生了kprobe重入,则不会调用指定的post_handler接口。
在调用完指定的post_handler接口后,kprobe的处理就完成了。最后调用reset_current_kprobe()将current_kprobe(per-cpu变量)置为NULL,表示kprobe的处理完成了。
4.kprobe重入
kprobe重入是指在kprobe的处理过程中又触发了断点异常,这种情况有可能是用户指定的pre_handler或post_handler接口,或者探测点处的指令本身就是断点指令。
如果第二次发生断点异常的位置没有注册kprobe,kprobe_handler()中会调用正在处理的kprobe的break_handler接口,源码如下所示:
static
int __kprobes kprobe_handler(
struct pt_regs
*regs)
{
......
p
= get_kprobe(addr);
if (p) {
.......
}
else
if (kprobe_running()) {
p
= __get_cpu_var(current_kprobe);
if (p
-
>break_handler
&& p
-
>break_handler(p, regs)) {
setup_singlestep(p, regs, kcb);
return
1;
}
}
/* else: not a kprobe fault; let the kernel handle it */
.......
return
0;
}
如果指定了break_handler接口并且返回1,则会开始单步执行保存的指令,顺着普通的kprobe流程继续处理。如果在处理kprobe的过程中又发生了断点异常,这也算是一种“异常”(jprobes就利用了这点,或者是专门为jprobes准备的),这种情况需要由break_handler接口来处理。如果处理成功,即返回1,则表示可以继续处理。
如果第二次发生断点异常的位置也注册了kprobe,kprobe_handler()中会调用reenter_kprobe()来处理,源码如下所示:
static
int __kprobes reenter_kprobe(
struct kprobe
*p,
struct pt_regs
*regs,
struct kprobe_ctlblk
*kcb)
{
switch (kcb
-
>kprobe_status) {
case KPROBE_HIT_SSDONE
:
#
ifdef CONFIG_X86_64
/* TODO: Provide re-entrancy from post_kprobes_handler() and
* avoid exception stack corruption while single-stepping on
* the instruction of the new probe.
*/
arch_disarm_kprobe(p);
regs
-
>ip
= (
unsigned
long)p
-
>addr;
reset_current_kprobe();
preempt_enable_no_resched();
break;
#
endif
case KPROBE_HIT_ACTIVE
:
save_previous_kprobe(kcb);
set_current_kprobe(p, regs, kcb);
kprobes_inc_nmissed_count(p);
prepare_singlestep(p, regs);
kcb
-
>kprobe_status
= KPROBE_REENTER;
break;
case KPROBE_HIT_SS
:
if (p
== kprobe_running()) {
regs
-
>flags
&=
~X86_EFLAGS_TF;
regs
-
>flags
|= kcb
-
>kprobe_saved_flags;
return
0;
}
else {
/* A probe has been hit in the codepath leading up
* to, or just after, single-stepping of a probed
* instruction. This entire codepath should strictly
* reside in .kprobes.text section. Raise a warning
* to highlight this peculiar case.
*/
}
default
:
/* impossible cases */
WARN_ON(
1);
return
0;
}
return
1;
}
reenter_kprobe()中会根据当前kprobe处理的状态(kcb->kprobe_status)来做具体的处理。
如果是KPROBE_HIT_SSDONE状态,说明是在调试异常中调用用户指定的post_handler接口时,第二次触发了断点异常,相当于是断点异常把调试异常给中断了。如果不是X86-64环境,处理和KPROBE_HIT_ACTIVE状态的处理一样。根据代码中的TODO注释,x86-64环境中单步执行新的kprobe指令时会导致异常栈崩溃,具体的原因现在不清楚,如果有知道的,麻烦告知一下。如果是在x86-64下,会调用arch_disarm_kprobe()将触发二次断点异常的kprobe设置的断点指令恢复成原始的指令,相当于是把当前处理的kprobe给禁止掉。然后将指令指针指向触发二次异常的探测点位置(此时已经没有kprobe设置的断点指令),并且调用reset_current_kprobe()将current_kprobe设置为NULL,这样前一个kprobe的处理相当于也结束了。注意,这里的处理完成后,后面还会回到调试异常的处理中,也就是返回到post_kprobe_handler()函数中。
如果是KPROBE_HIT_ACTIVE状态,是在调用pre_handler接口时触发的断点异常。这种情况下会调用save_previous_kprobe()将前一个kprobe的信息保存到kprobe控制块kprobe_ctlblk(per-cpu变量)中,接着会调用set_current_kprobe()将当前处理的kprobe设置到current_kprobe中,最后开始单步执行新的kprobe处的指令。由于发生了kprobe重入,所以要将kprobe控制块的状态设置为KPROBE_REENTER。此时要开始新的kprobe的处理,把前一个kprobe的处理给挂起了,后面的处理流程和普通的kprobe处理流程相同。在当前的kprobe处理完成后,会继续前一个kprobe的处理,但是由于发生了重入,所以前一个kprobe的post_handler接口就不会调用了。
如果是KPROBE_HIT_SS,是在单步执行保存的探测点指令时触发的断点异常。如果触发断点异常的指令就是探测点处的指令,此时处理的kprobe和kprobe_running()返回的kprobe是同一个,这样情况下会取消单步执行,恢复保存的寄存器标志。此时会返回0,表示由内核来处理这种情况,因为这个指令不是krobe设置上去的。如果在探测点之后的指令中,这种函数是不应该被探测的,应该放在.kprobes.text section中。此时kprobes会产生一个警告,并且返回0,由内核来处理这种情况。