系统遇到debug异常时,执行do_debug函数,然后执行通知链上的回调函数kprobe_exceptions_notify,对于debug 指令就是post_kprobe_handler函数,然后执行用户态的回调函数post_handler 。
小结:
kprobe原理类似与GDB中断点调试和单步调试:
对于x86平台涉及到cpu的两个异常机制指令就是:断点异常 int3 指令,debug 中的单步异常 int1 指令。
由英特尔手册可以看到debug异常(单步异常是debug异常中的一种)是中断向量表的vector1,断点异常是中断向量表中的vector3。
(1)Kprobe把探测点的指令替换成断点指令BREAKPOINT,执行到探测点以后,系统会陷入断点异常int3,触发了一个trap,执行kprobe_handler函数,在kprobe_handler中执行pre_handler函数,trap处理流程中会保存当前CPU的寄存器信息并调用对应的trap处理函数,该处理函数会设置kprobe的调用状态并调用用户注册的pre_handler回调函数,kprobe会向该函数传递注册的struct kprobe结构地址以及保存的CPU寄存器信息。
(2)然后把cpu设置为单步模式继续执行探测点原有的指令,原有指令执行完成以后又会陷入单步异常int1(EFLAGS 寄存器TF位被设置后,每执行一条指令就会触发debug异常)。
(3)在int1的函数do_debug中继续调用post_handler,并恢复单步模式到正常模式,然后返回继续执行探测点后续的指令。
kprobe pre_handler:在执行 the probed instruction 之前调用,在 do_int3 中执行。
kprobe post_handler:在执行 the probed instruction 后调用,在 do_debug 中执行。
备注:单步指令的目的就是为了执行完探测的函数后,执行用户自定义的post_handler函数,收集探测函数执行完之后的信息:修改所有寄存器和全局变量。如下图所示:
三、do_int3/bug函数
3.1 init_kprobes
kprobes作为一个内核中的一个模块,init_kprobes函数用来初始化kprobes模块:
// linux-3.10/kernel/kprobes.c
#define KPROBE\_HASH\_BITS 6
#define KPROBE\_TABLE\_SIZE (1 << KPROBE\_HASH\_BITS)
static struct hlist\_head kprobe_table[KPROBE_TABLE_SIZE];
static struct hlist\_head kretprobe_inst_table[KPROBE_TABLE_SIZE];
static struct {
raw\_spinlock\_t lock ____cacheline_aligned_in_smp;
} kretprobe_table_locks[KPROBE_TABLE_SIZE];
static int __init init\_kprobes(void)
{
int i, err = 0;
unsigned long offset = 0, size = 0;
char \*modname, namebuf[128];
const char \*symbol_name;
void \*addr;
struct kprobe\_blackpoint \*kb;
/\* FIXME allocate the probe table, currently defined statically \*/
/\* initialize all list heads \*/
for (i = 0; i < KPROBE_TABLE_SIZE; i++) {
INIT\_HLIST\_HEAD(&kprobe_table[i]);
INIT\_HLIST\_HEAD(&kretprobe_inst_table[i]);
raw\_spin\_lock\_init(&(kretprobe_table_locks[i].lock));
}
/\*
\* Lookup and populate the kprobe\_blacklist.
\*
\* Unlike the kretprobe blacklist, we'll need to determine
\* the range of addresses that belong to the said functions,
\* since a kprobe need not necessarily be at the beginning
\* of a function.
\*/
for (kb = kprobe_blacklist; kb->name != NULL; kb++) {
kprobe\_lookup\_name(kb->name, addr);
if (!addr)
continue;
kb->start_addr = (unsigned long)addr;
symbol_name = kallsyms\_lookup(kb->start_addr,
&size, &offset, &modname, namebuf);
if (!symbol_name)
kb->range = 0;
else
kb->range = size;
}
if (kretprobe_blacklist_size) {
/\* lookup the function address from its name \*/
for (i = 0; kretprobe_blacklist[i].name != NULL; i++) {
kprobe\_lookup\_name(kretprobe_blacklist[i].name,
kretprobe_blacklist[i].addr);
if (!kretprobe_blacklist[i].addr)
printk("kretprobe: lookup failed: %s\n",
kretprobe_blacklist[i].name);
}
}
#if defined(CONFIG\_OPTPROBES)
#if defined(\_\_ARCH\_WANT\_KPROBES\_INSN\_SLOT)
/\* Init kprobe\_optinsn\_slots \*/
kprobe_optinsn_slots.insn_size = MAX_OPTINSN_SIZE;
#endif
/\* By default, kprobes can be optimized \*/
kprobes_allow_optimization = true;
#endif
/\* By default, kprobes are armed \*/
kprobes_all_disarmed = false;
err = arch\_init\_kprobes();
if (!err)
err = register\_die\_notifier(&kprobe_exceptions_nb);
if (!err)
err = register\_module\_notifier(&kprobe_module_nb);
kprobes_initialized = (err == 0);
if (!err)
init\_test\_probes();
return err;
}
module\_init(init_kprobes);
(1)分配当前静态定义的探测表,初始化所有的哈希链表头。并初始化kretprobe用到的自旋锁。
(2)查找并填充kprobe_blacklist,与kretprobe blacklist不同的是,我们需要确定属于所述函数的地址范围,因为kprobe不一定位于函数的开头(kprobe可以插入到内核中的任何指令上,不一定是函数开头)。
函数前面加__kprobes修饰的不能被探测,比如:
/\*
\* This routine is called either:
\* - under the kprobe\_mutex - during kprobe\_[un]register()
\* OR
\* - with preemption disabled - from arch/xxx/kernel/kprobes.c
\*/
struct kprobe __kprobes \*get\_kprobe(void \*addr)
{
struct hlist\_head \*head;
struct kprobe \*p;
head = &kprobe_table[hash\_ptr(addr, KPROBE_HASH_BITS)];
hlist\_for\_each\_entry\_rcu(p, head, hlist) {
if (p->addr == addr)
return p;
}
return NULL;
}
下面这一些函数也不能被探测:
/\*
\* Normally, functions that we'd want to prohibit kprobes in, are marked
\* \_\_kprobes. But, there are cases where such functions already belong to
\* a different section (\_\_sched for preempt\_schedule)
\*
\* For such cases, we now have a blacklist
\*/
static struct kprobe\_blackpoint kprobe_blacklist[] = {
{"preempt\_schedule",},
{"native\_get\_debugreg",},
{"irq\_entries\_start",},
{"common\_interrupt",},
{"mcount",}, /\* mcount can be called from everywhere \*/
{NULL} /\* Terminator \*/
};
(3)查找并填充retkprobe_blacklist。
struct kretprobe\_blackpoint kretprobe_blacklist[] = {
{"\_\_switch\_to", }, /\* This function switches only current task, but
doesn't switch kernel stack.\*/
{NULL, NULL} /\* Terminator \*/
};
const int kretprobe_blacklist_size = ARRAY\_SIZE(kretprobe_blacklist);
(4)注册 die 通知链
注册内核通知链:kprobe_exceptions_nb,注释标明了该通知链最高,最先被调用,执行被探测指令期间若发生了内存异常,比如执行了int3指令, 将最优先调用kprobe_exceptions_notify函数。
static struct notifier\_block kprobe_exceptions_nb = {
.notifier_call = kprobe_exceptions_notify,
.priority = 0x7fffffff /\* we need to be notified first \*/
};
register\_die\_notifier(&kprobe_exceptions_nb);
/\*
\* Wrapper routine for handling exceptions.
\*/
int __kprobes
kprobe\_exceptions\_notify(struct notifier\_block \*self, unsigned long val, void \*data)
{
struct die\_args \*args = data;
int ret = NOTIFY_DONE;
if (args->regs && user\_mode\_vm(args->regs))
return ret;
switch (val) {
case DIE_INT3:
if (kprobe\_handler(args->regs))
ret = NOTIFY_STOP;
break;
case DIE_DEBUG:
if (post\_kprobe\_handler(args->regs)) {
/\*
\* Reset the BS bit in dr6 (pointed by args->err) to
\* denote completion of processing
\*/
(\*(unsigned long \*)ERR\_PTR(args->err)) &= ~DR_STEP;
ret = NOTIFY_STOP;
}
break;
case DIE_GPF:
/\*
\* To be potentially processing a kprobe fault and to
\* trust the result from kprobe\_running(), we have
\* be non-preemptible.
\*/
if (!preemptible() && kprobe\_running() &&
kprobe\_fault\_handler(args->regs, args->trapnr))
ret = NOTIFY_STOP;
break;
default:
break;
}
return ret;
}
(5)注册模块通知链
除了内核中的代码段函数外,还有模块中的代码段,我们可以给模块中的函数添加 kprobe点,当模块被卸载时,模块的.text 和.init.text sections都被释放,移除模块中的 kprobe点,当模块加载时,可以给模块的.text添加kprobe点,但是模块的.init.text sections再加载后就被释放,因此要禁止.init.text sections的kprobe点。
模块正常运行(已经完成了模块的初始化)的状态是MODULE_STATE_LIVE。
模块卸载是状态是MODULE_STATE_GOING。
/\* Module notifier call back, checking kprobes on the module \*/
static int __kprobes kprobes\_module\_callback(struct notifier\_block \*nb,
unsigned long val, void \*data)
{
struct module \*mod = data;
struct hlist\_head \*head;
struct kprobe \*p;
unsigned int i;
int checkcore = (val == MODULE_STATE_GOING);
if (val != MODULE_STATE_GOING && val != MODULE_STATE_LIVE)
return NOTIFY_DONE;
/\*
\* When MODULE\_STATE\_GOING was notified, both of module .text and
\* .init.text sections would be freed. When MODULE\_STATE\_LIVE was
\* notified, only .init.text section would be freed. We need to
\* disable kprobes which have been inserted in the sections.
\*/
mutex\_lock(&kprobe_mutex);
for (i = 0; i < KPROBE_TABLE_SIZE; i++) {
head = &kprobe_table[i];
hlist\_for\_each\_entry\_rcu(p, head, hlist)
if (within\_module\_init((unsigned long)p->addr, mod) ||
(checkcore &&
within\_module\_core((unsigned long)p->addr, mod))) {
/\*
\* The vaddr this probe is installed will soon
\* be vfreed buy not synced to disk. Hence,
\* disarming the breakpoint isn't needed.
\*/
kill\_kprobe(p);
}
}
mutex\_unlock(&kprobe_mutex);
return NOTIFY_DONE;
}
static struct notifier\_block kprobe_module_nb = {
.notifier_call = kprobes_module_callback,
.priority = 0
};
register\_module\_notifier(&kprobe_module_nb)
注册module notify回调kprobes_module_callback函数的作用是若当某个内核模块发生卸载操作时有必要检测并移除注册到该模块函数的探测点。
当模块处于加载状态时,由于模块的.init.text节在加载后就被释放,不会存留在内存中,因此不能再.init.text节添加 kprobe点。
当模块的状态等于MODULE_STATE_GOING时,模块的.text 和.init.text sections都要禁用kprobe点。
val = MODULE_STATE_GOING
if (within\_module\_init((unsigned long)p->addr, mod) ||
within\_module\_core((unsigned long)p->addr, mod)) {
/\*
\* The vaddr this probe is installed will soon
\* be vfreed buy not synced to disk. Hence,
\* disarming the breakpoint isn't needed.
\*/
kill\_kprobe(p);
}
当模块的状态等于MODULE_STATE_LIVE时,模块的.init.text sections要禁用kprobe点。
val = MODULE_STATE_LIVE
if (within\_module\_init((unsigned long)p->addr, mod)) {
/\*
\* The vaddr this probe is installed will soon
\* be vfreed buy not synced to disk. Hence,
\* disarming the breakpoint isn't needed.
\*/
kill\_kprobe(p);
}
3.2 do_int3
前面说到系统执行到探测点以后,系统会陷入断点异常int3,触发了一个trap,也就是执行 do_int3 函数:
// linux-3.10/arch/x86/include/asm/kdebug.h
/\* Grossly misnamed. \*/
enum die\_val {
DIE_OOPS = 1,
DIE_INT3,
DIE_DEBUG,
DIE_PANIC,
DIE_NMI,
DIE_DIE,
DIE_KERNELDEBUG,
DIE_TRAP,
DIE_GPF,
DIE_CALL,
DIE_PAGE_FAULT,
DIE_NMIUNKNOWN,
};
// linux-3.10/arch/x86/include/asm/traps.h
/\* Interrupts/Exceptions \*/
enum {
X86_TRAP_DE = 0, /\* 0, Divide-by-zero \*/
X86_TRAP_DB, /\* 1, Debug \*/
X86_TRAP_NMI, /\* 2, Non-maskable Interrupt \*/
X86_TRAP_BP, /\* 3, Breakpoint \*/
X86_TRAP_OF, /\* 4, Overflow \*/
X86_TRAP_BR, /\* 5, Bound Range Exceeded \*/
X86_TRAP_UD, /\* 6, Invalid Opcode \*/
X86_TRAP_NM, /\* 7, Device Not Available \*/
X86_TRAP_DF, /\* 8, Double Fault \*/
X86_TRAP_OLD_MF, /\* 9, Coprocessor Segment Overrun \*/
X86_TRAP_TS, /\* 10, Invalid TSS \*/
X86_TRAP_NP, /\* 11, Segment Not Present \*/
X86_TRAP_SS, /\* 12, Stack Segment Fault \*/
X86_TRAP_GP, /\* 13, General Protection Fault \*/
X86_TRAP_PF, /\* 14, Page Fault \*/
X86_TRAP_SPURIOUS, /\* 15, Spurious Interrupt \*/
X86_TRAP_MF, /\* 16, x87 Floating-Point Exception \*/
X86_TRAP_AC, /\* 17, Alignment Check \*/
X86_TRAP_MC, /\* 18, Machine Check \*/
X86_TRAP_XF, /\* 19, SIMD Floating-Point Exception \*/
X86_TRAP_IRET = 32, /\* 32, IRET Exception \*/
};
// linux-3.10/arch/x86/kernel/traps.c
/\* May run on IST stack. \*/
dotraplinkage void __kprobes notrace do\_int3(struct pt\_regs \*regs, long error_code)
{
......
//当 CPU 遇到断点指令时,会发生陷阱,保存 CPU 的寄存器,并通过 notifier\_call\_chain 机制将控制权传递给 Kprobes。
if (notify\_die(DIE_INT3, "int3", regs, error_code, X86_TRAP_BP,
SIGTRAP) == NOTIFY_STOP)
goto exit;
......
}
之后会执行通知链机制上注册的回调函数:kprobe_exceptions_notify,对于int 3 指令就是kprobe_handler函数:
int3
-->do_int3
-->notify\_die(DIE_INT3, "int3", regs, error_code, X86_TRAP_BP,
SIGTRAP) == NOTIFY_STOP)
-->kprobe\_exceptions\_notify(){
case DIE_INT3:
if (kprobe\_handler(args->regs))
ret = NOTIFY_STOP;
break;
}
/\*
\* Interrupts are disabled on entry as trap3 is an interrupt gate and they
\* remain disabled throughout this function.
\*/
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));
/\*
\* 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()) {
if (reenter\_kprobe(p, regs, kcb))
return 1;
} 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, 0);
return 1;
}
} else 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;
preempt\_enable\_no\_resched();
return 1;
} else if (kprobe\_running()) {
p = \_\_this\_cpu\_read(current_kprobe);
if (p->break_handler && p->break\_handler(p, regs)) {
if (!skip\_singlestep(p, regs, kcb))
setup\_singlestep(p, regs, kcb, 0);
return 1;
}
} /\* else: not a kprobe fault; let the kernel handle it \*/
preempt\_enable\_no\_resched();
return 0;
}
对于kprobe我们主要分析这一部分:
与x86_64有关的EFLAGS 寄存器的flag位:
(1)TF Trap (bit 8):设置启用单步模式进行调试; 清除以禁用单步模式。 在单步模式下,处理器在每条指令后生成一个调试异常。 这允许在每条指令之后检查程序的执行状态。如果应用程序使用 POPF、POPFD 或 IRET 指令设置 TF 标志,则会在 POPF、POPFD 或 IRET 之后的指令之后生成调试异常。
(2)IF Interrupt enable (bit 9):控制处理器对可屏蔽硬件中断请求的响应,该标志设置为响应可屏蔽的硬件中断; 清除以禁止可屏蔽的硬件中断。 IF 标志不影响异常或不可屏蔽中断(NMI 中断)的生成。控制寄存器 CR4 中的 CPL、IOPL 和 VME 标志的状态决定了 IF 标志是否可以被 CLI、STI、POPF、POPFD 和 IRET 修改
set_current_kprobe设置struct kprobe *p为当前正在处理的 probe点。
// linux-3.10/arch/x86/kernel/kprobes/core.c
static void __kprobes set\_current\_kprobe(struct kprobe \*p, struct pt\_regs \*regs,
struct kprobe\_ctlblk \*kcb)
{
\_\_this\_cpu\_write(current_kprobe, p);
kcb->kprobe_saved_flags = kcb->kprobe_old_flags
= (regs->flags & (X86_EFLAGS_TF | X86_EFLAGS_IF));
if (p->ainsn.if_modifier)
kcb->kprobe_saved_flags &= ~X86_EFLAGS_IF;
}
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, 0);
return 1;
这里在设置current_kprobe全局变量的同时,还会同时设置kprobe_saved_flags和kprobe_old_flags的flag值,它们用于具体的架构指令相关处理。接下来处理pre_handler回调函数,有注册的话就调用执行,然后调用setup_singlestep启动单步执行。在调试完成后直接返回1。
static void __kprobes
setup\_singlestep(struct kprobe \*p, struct pt\_regs \*regs, struct kprobe\_ctlblk \*kcb, int reenter)
{
if (setup\_detour\_execution(p, regs, reenter))
return;
#if !defined(CONFIG\_PREEMPT)
if (p->ainsn.boostable == 1 && !p->post_handler) {
/\* Boost up -- we can execute copied instructions directly \*/
if (!reenter)
reset\_current\_kprobe();
/\*
\* Reentering boosted probe doesn't reset current\_kprobe,
\* nor set current\_kprobe, because it doesn't use single
\* stepping.
\*/
regs->ip = (unsigned long)p->ainsn.insn;
preempt\_enable\_no\_resched();
return;
}
#endif
if (reenter) {
save\_previous\_kprobe(kcb);
set\_current\_kprobe(p, regs, kcb);
kcb->kprobe_status = KPROBE_REENTER;
} else
kcb->kprobe_status = KPROBE_HIT_SS;
/\* Prepare real single stepping \*/
clear\_btf();
//设置regs->flags中的TF位,开启单步调试
regs->flags |= X86_EFLAGS_TF;
//屏蔽regs->flags中的IF位,屏蔽中断
regs->flags &= ~X86_EFLAGS_IF;
/\* single step inline if the instruction is an int3 \*/
//指令寄存器地址改为前面保存的被探测指令(备份的原始指令)
if (p->opcode == BREAKPOINT_INSTRUCTION)
regs->ip = (unsigned long)p->addr;
else
regs->ip = (unsigned long)p->ainsn.insn;
}
单步执行,首先设置EFLAGS 寄存器flags中的TF位,并屏蔽IF位,同时把int3异常返回的指令寄存器地址改为前面保存的被探测指令,当int3异常返回时这些设置就会生效,即立即执行保存的原始指令(注意这里是在触发int3之前原来的上下文中执行,因此直接执行原始指令即可,无需特别的模拟操作)。该函数返回后do_int3函数立即返回,由于EFLAGS 寄存器TF位被设置,在单步执行完被探测指令后立即触发debug异常,进入debug异常处理函数do_debug,执行post_kprobe_handler函数,即post_handler()。
3.3 do_bug
dotraplinkage void __kprobes do\_debug(struct pt\_regs \*regs, long error_code)
{
......
if (notify\_die(DIE_DEBUG, "debug", regs, PTR\_ERR(&dr6), error_code,
SIGTRAP) == NOTIFY_STOP)
......
}
由于初始化时注册了内核通知链:kprobe_exceptions_nb,执行被探测指令期间若发生了内存异常,比如执行了debug指令, 将最优先调用kprobe_exceptions_notify函数。
/\*
\* Wrapper routine for handling exceptions.
\*/
int __kprobes
kprobe\_exceptions\_notify(struct notifier\_block \*self, unsigned long val, void \*data)
{
......
case DIE_DEBUG:
if (post\_kprobe\_handler(args->regs)) {
/\*
\* Reset the BS bit in dr6 (pointed by args->err) to
\* denote completion of processing
\*/
(\*(unsigned long \*)ERR\_PTR(args->err)) &= ~DR_STEP;
ret = NOTIFY_STOP;
}
break;
......
}
/\*
\* Interrupts are disabled on entry as trap1 is an interrupt gate and they
\* remain disabled throughout this function.
\*/
static int __kprobes post\_kprobe\_handler(struct pt\_regs \*regs)
{
struct kprobe \*cur = kprobe\_running();
struct kprobe\_ctlblk \*kcb = get\_kprobe\_ctlblk();
if (!cur)
return 0;
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);
}
/\* Restore back the original saved kprobes variables and continue. \*/
if (kcb->kprobe_status == KPROBE_REENTER) {
restore\_previous\_kprobe(kcb);
goto out;
}
reset\_current\_kprobe();
out:
preempt\_enable\_no\_resched();
/\*
\* if somebody else is singlestepping across a probe point, flags
\* will have TF set, in which case, continue the remaining processing
\* of do\_debug, as if this is not a probe hit.
\*/
if (regs->flags & X86_EFLAGS_TF)
return 0;
return 1;
}
首先调用resume_execution函数将debug异常返回的下一条指令设置为被探测之后的指令,这样异常返回后程序的流程就会按正常的流程继续执行;然后恢复kprobe执行前保存的flags标识;接下来如果kprobe不是重入的并且设置了post_handler回调函数,就设置kprobe_status状态为KPROBE_HIT_SSDONE并调用post_handler函数,即调用用户态设置的post_handler回调函数。
四、Changing Execution Path
由于 kprobes 可以探测正在运行的内核代码,它可以更改寄存器集,包括指令指针。 此操作需要非常小心,例如保留堆栈帧,恢复执行路径等。因为它在运行的内核上运行并且需要深入了解计算机体系结构。
如果您更改 pre_handler 中的指令指针(并设置其他相关寄存器),则必须返回 !0 以便 kprobes 停止单步执行并返回到给定地址。 这也意味着不应再调用 post_handler。
请注意,在某些使用 TOC(Table of Contents)进行函数调用的架构上,此操作可能会更难,因为您必须在模块中为您的函数设置一个新的 TOC,并在从它返回后恢复旧的 TOC。
五、Return Probes
5.1 How Does a Return Probe Work
当您调用 register_kretprobe() 时,Kprobes 在函数的入口处建立一个 kprobe。 当被探测的函数被调用并且这个探测被命中时,Kprobes 会保存一份返回地址的副本,并将返回地址替换为“trampoline”的地址。trampoline是一段任意代码——通常只是一条 nop 指令。 在启动时,Kprobes 在 trampoline 上注册一个 kprobe。
当被探测的函数执行它的 return instruction时,控制权传递给trampoline并且该探测被命中。 Kprobes 的 trampoline 处理程序调用与 kretprobe 关联的用户指定的返回处理程序,然后将保存的指令指针设置为保存的返回地址,这就是从陷阱返回后恢复执行的地方。
当被探测函数正在执行时,它的返回地址存储在一个 kretprobe_instance 类型的对象中。 在调用 register_kretprobe() 之前,用户设置 kretprobe 结构的 maxactive 字段来指定可以同时探测多少个指定函数的实例。 register_kretprobe() 预分配指定数量的 kretprobe_instance 对象。
例如,如果函数是非递归的并且在调用时持有自旋锁,那么 maxactive = 1 就足够了。 如果函数是非递归的并且永远不会放弃 CPU(例如,通过信号量或抢占),则 NR_CPUS 应该足够了。 如果 maxactive <= 0,则设置为默认值。 如果启用了 CONFIG_PREEMPT,则默认值为 max(10, 2*NR_CPUS)。 否则,默认值为 NR_CPUS。
如果你将 maxactive 设置得太低,这不是一场灾难; 你只会错过一些探测。 在 kretprobe 结构中,nmissed 字段在注册返回探针时设置为零,并且每次进入被探测函数但没有可用于建立返回探针的 kretprobe_instance 对象时递增。
5.2 Kretprobe entry-handler
Kretprobes 还提供了一个可选的用户指定的处理程序,它在函数入口上运行。 该处理程序是通过设置 kretprobe 结构的 entry_handler 字段来指定的。 每当 kretprobe 放置在函数入口处的 kprobe 被命中时,都会调用用户定义的 entry_handler,如果有的话。 如果 entry_handler 返回 0(成功),则保证在函数返回时调用相应的返回处理程序。 如果 entry_handler 返回非零错误,则 Kprobes 将返回地址保持原样,并且 kretprobe 对该特定函数实例没有进一步的影响。
使用与它们关联的唯一 kretprobe_instance 对象来匹配多个入口和返回处理程序调用。 此外,用户还可以将每个返回实例的私有数据指定为每个 kretprobe_instance 对象的一部分。 这在相应的用户条目和返回处理程序之间共享私有数据时特别有用。 每个私有数据对象的大小可以在 kretprobe 注册时通过设置 kretprobe 结构的 data_size 字段来指定。 可以通过每个 kretprobe_instance 对象的数据字段访问此数据。
如果输入了探测函数但没有可用的 kretprobe_instance 对象,则除了增加 nmissed 计数外,还会跳过用户 entry_handler 调用。
六、How Does Jump Optimization Work
关于kprobe的优化可以参考这篇文章:linux kprobe实现原理
如果Linux 内核是使用 CONFIG_OPTPROBES=y 构建的(目前此标志在 x86/x86-64 非抢占式内核上自动设置为 ‘y’)并且“debug.kprobes_optimization”内核参数设置为 1 ,Kprobes 会尝试减少探测 - 通过在每个探测点使用跳转指令而不是断点指令来降低开销。
int 3 指令会产生一个 a trap ,比较耗时,可以用跳转指令替换断点指令,优化成jmp指令跳转到kprobe探测点。
当前的机器默认配置了 CONFIG_OPTPROBES 选项:
[root@localhost ~]# cat /etc/centos-release
CentOS Linux release 7.6.1810 (Core)
[root@localhost ~]# uname -r
3.10.0-957.el7.x86_64
# Kernel Performance Events And Counters
#
CONFIG\_SLUB=y
CONFIG_PROFILING=y
CONFIG_TRACEPOINTS=y
CONFIG_CRASH_CORE=y
CONFIG_KEXEC_CORE=y
CONFIG_HOTPLUG_SMT=y
CONFIG_OPROFILE=m
CONFIG_OPROFILE_EVENT_MULTIPLEX=y
CONFIG_HAVE_OPROFILE=y
CONFIG_OPROFILE_NMI_TIMER=y
CONFIG_KPROBES=y
CONFIG_JUMP_LABEL=y
CONFIG_OPTPROBES=y //当前的机器配置了 CONFIG\_OPTPROBES 选项
debug.kprobes_optimization内核参数同样也设置为 1:
[root@localhost ~]# cat /proc/sys/debug/kprobes-optimization
1
[root@localhost ~]#
6.1 Init a Kprobe
注册一个 probe 后,在尝试此优化之前,Kprobes会在指定地址插入一个基于断点的普通kprobe。因此,即使无法优化这个特定的probepoint,也会有一个探针。
6.2 Safety Check
在优化探针之前,Kprobes会执行以下安全检查,不符合条件不可以进行优化:
(1)Kprobes 验证将被跳转指令替换的区域(“优化区域”)是否完全位于一个函数中。 (跳转指令是5个字节:near relative jump,因此可能会覆盖多个指令。)
(2)Kprobes 分析整个函数并验证没有跳转到优化区域,不能有跳转到这块要被优化区域的指令,这块区域将会被jmp覆盖,具体如下:
a:函数中不包含间接跳转(indirect jump);
b:该函数不包含导致异常的指令(因为由异常触发的修复代码可以跳回优化区域 - Kprobes 检查异常表以验证这一点);
c:没有到优化区域的近跳转(near jump)(除了第一个字节)。
(3)对于优化区域中的每条指令,Kprobes将验证该指令是否可以单独执行。
使用如下跳转指令(near jump)形式:
JMP 跳转指令:
0xE9(E9 cd) :Jump near 后面的4个字节是偏移:一个保存jmp本身的机器码,另4个保存偏移 -->总共5个字节
6.3 Preparing Detour Buffer
接下来,Kprobes准备了一个 Detour 缓冲区,其中包含以下指令序列:
(1)能够将cpu寄存器压栈(模拟int3的trap过程)。
(2)调用用户的探测处理程序的蹦床代码(trampoline code)。
(3)恢复寄存器的代码。
(4)来自优化区域的指令。
(5)跳转回原来的执行路径。
6.4 Pre-optimization
准备 Detour 缓冲区后,Kprobes验证以下情况是否存在:
(1)探针有一个 post_handler。
(2)探测优化区域中的其他指令。
(3)探针被禁用。
在上述任何一种情况下,Kprobes 都不会开始优化探针。 由于这些是临时情况,如果情况发生变化,Kprobes 会尝试再次开始优化。
如果可以优化 kprobe,则 Kprobes 将 kprobe 排入优化列表,并启动 kprobe-optimizer 工作队列以对其进行优化。如果要优化的probepoint在优化之前被命中,则Kprobes通过将CPU的指令指针设置为 the detour buffer 中复制的代码,将控制权返回到原始指令路径,从而至少避免了单步执行。
6.5 Optimization
Kprobe-optimizer 不会立即插入跳转指令; 相反,它首先出于安全考虑调用 synchronize_rcu(),因为 CPU 在执行优化区域的过程中可能会被中断。 synchronize_rcu() 可以确保在调用 synchronize_rcu() 时处于活动状态的所有中断都已完成,但前提是 CONFIG_PREEMPT=n。 因此,此版本的 kprobe 优化仅支持具有 CONFIG_PREEMPT=n 的内核。
centos 7.6 :3.10.0默认没有开启 CONFIG_PREEMPT选项:
# CONFIG\_PREEMPT is not set
**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**
**[需要这份系统化的资料的朋友,可以点击这里获取!](https://bbs.csdn.net/topics/618542503)**
**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**