一、简介
kprobe是内核的动态探测工具,几乎可以探测任何一条内核指令。kprobe根据探测点类型可分为三种: kprobes, jprobes和kretprobes (也叫返回探测点)。 kprobes是可以被插入到内核的任何指令位置的探测点,jprobes则只能被插入到一个内核函数的入口,而kretprobes则是在指定的内核函数返回时才被执行。
kprobe根据实现原理也可以分为三种:基于动态ftrace的kprobe,基于int3的kprobe和基于jump相对跳转指令实现的kprobe。
这里主要介绍基于int3的kprobe实现以及kprobe和kretprobe的实现和使用。
内核版本基于3.10.33,x86_64平台。
二、注册流程
一个kprobe探测实例是struct kprobe。
struct kprobe { //哈希链表, 被静态全局变量kprobe_table管理, 每个被监测地址作为索引
struct hlist_node hlist; //如果一个地址存在多个kprobe则该哈希节点会用aggregate节点替代
struct list_head list; //对于一个地址存在的多个kprobe的链表
unsigned long nmissed; //因断点指令不能重入处理, 当多个kprobe一起触发时会放弃执行后面的probe, 同时该计数增加
kprobe_opcode_t *addr; //观察点对应的地址, 用户在调用注册接口时可以指定地址, 也可以传入函数名让内核自己查找
const char *symbol_name;//观察点对应的函数名, 在注册kprobe时会将其翻译为十六进制地址并修改addr
unsigned int offset; //相对于入口点地址的偏移, 会在计算addr以后再加上offset得到最终的addr
kprobe_pre_handler_t pre_handler; //在执行kprobe地址addr指令之前执行的handler
kprobe_post_handler_t post_handler; //在执行kprobe地址addr指令之后执行的handler
kprobe_fault_handler_t fault_handler; //异常处理句柄, 在执行pre_handler返回值非0时会调用
/* * ... called if breakpoint trap occurs in probe handler. * Return 1 if it handled break, otherwise kernel will see it. 24 */
kprobe_break_handler_t break_handler;
kprobe_opcode_t opcode; //保存的操作码, 当注册kprobe后对应地址会用中断指令替代
struct arch_specific_insn ainsn; //平台相关结构, 具体见下
u32 flags; 32 //状态标记, 被kprobe_mutex保护
};
struct arch_specific_insn用来备份原来的探测指令的。
struct arch_specific_insn {
/* copy of the original instruction */
kprobe_opcode_t *insn; //原指令opcode的拷贝
/*
* boostable = -1: This instruction type is not boostable.
* boostable = 0: This instruction type is boostable.
* boostable = 1: This instruction has been boosted: we have
* added a relative jump after the instruction copy in insn,
* so no single-step and fixup are needed (unless there's
* a post_handler or break_handler).
*/
int boostable;
bool if_modifier;
};
typedef u8 kprobe_opcode_t
register_kprobe函数完成kprobe的注册。
int __kprobes register_kprobe(struct kprobe *p)
{
int ret;
struct kprobe *old_p;
struct module *probed_mod;
kprobe_opcode_t *addr;
/* Adjust probe address from symbol */
addr = kprobe_addr(p);
if (IS_ERR(addr))
return PTR_ERR(addr);
p->addr = addr;
ret = check_kprobe_rereg(p);
if (ret)
return ret;
/* User can pass only KPROBE_FLAG_DISABLED to register_kprobe */
p->flags &= KPROBE_FLAG_DISABLED;
p->nmissed = 0;
INIT_LIST_HEAD(&p->list);
ret = check_kprobe_address_safe(p, &probed_mod);
if (ret)
return ret;
mutex_lock(&kprobe_mutex);
old_p = get_kprobe(p->addr);
if (old_p) {
/* Since this may unoptimize old_p, locking text_mutex. */
ret = register_aggr_kprobe(old_p, p);
goto out;
}
mutex_lock(&text_mutex); /* Avoiding text modification */
ret = prepare_kprobe(p);
mutex_unlock(&text_mutex);
if (ret)
goto out;
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))
arm_kprobe(p);
/* Try to optimize kprobe */
try_to_optimize_kprobe(p);
out:
mutex_unlock(&kprobe_mutex);
if (probed_mod)
module_put(probed_mod);
return ret;
}
register_kprobe主要完成如下工作:
(1)通过kprobe_addr函数获取要探测的具体位置。根据用户提供的symbol_name调用kallsyoms_lookup_name获取函数的首地址,再加上要探测的函数offset。这里的offset必须是一条汇编指令的开始地址,不能是指令的中间。
(2)通过check_kprobe_rereg检测本kprobe时候已经注册,如果已经注册,直接返回完成注册。
(3)调用check_kprobe_address_safe函数检查探测的地址。主要检查这几项:
调用ftrace_location函数检测探测地址是否属于动态ftrace的探测点mcount段。一般是函数开始5个nop指令。如果是这个地址,那么置位p->flags的 KPROBE_FLAG_FTRACE,本次kprobe将走动态ftrace路径,不走int3异常。
检查探测地址是否属于内核代码段,否则不能探测。
是否在kprobe本身实现的关键函数地址内,是的话不允许探测。
探测地址不能是内核jump label的保留地址。
如果探测的是内核模块的地址,需要增加模块计数。没有这个计数的话,模块卸载之后,等到unregister_kprobe的时候会恢复原指令,或导致内核内存被被修改。
如果该探测地址已经注册了kprobe,会调用register_aggr_kprobe注册一个aggregate kprobe,用来管理所有同一探测地址的的kprobe。
(4)prepare_kprobe函数负责kprobe注册准备工作。
static int __kprobes prepare_kprobe(struct kprobe *p)
{
if (!kprobe_ftrace(p))
return arch_prepare_kprobe(p);
return arch_prepare_kprobe_ftrace(p);
}
如果探测的是动态ftrace的mcount段地址,那么调用arch_prepare_kprobe_ftrace走ftrace kprobe的路径。
负责调用arch_prepare_kprobe位int3 kprobe的准备工作。
arch_prepare_kprobe首先调用can_probe检查探测地址是否地址所在代码的指令的边界地址,不是的话不能探测。
调用get_insn_slot函数为kprobe的insn在x86的可执行page里申请一段内存用来备份完整原指令,kprobe执行流程中的单步环节需要在这里执行,所以需要执行权限。
最后arch_copy_kprobe备份原指令到kprobe->ainsn.insn里面,并把原指令的opcode保存到kprobe->opcode。
(5)将kprobe通过hlist字段添加到系统kprobe_table的hash table里面。
(6)调用arm_kprobe函数替换原指令的opcode为int3指令(0xcc)。
至此,kprobe注册完成。
三、kprobe的执行
(1) int3指令属于x86上的断点指令,对应的异常属于trap类型,即异常恢复的时候执行int3后面的下一条指令。
内核执行到被kprobe探测的指令,触发int3异常。
paranoidzeroentry_ist int3 do_int3 DEBUG_STACK
......
.macro paranoidzeroentry_ist sym do_sym ist
ENTRY(\sym)
INTR_FRAME
ASM_CLAC
PARAVIRT_ADJUST_EXCEPTION_FRAME
pushq_cfi $-1 /* ORIG_RAX: no syscall to restart */
subq $ORIG_RAX-R15, %rsp
CFI_ADJUST_CFA_OFFSET ORIG_RAX-R15
call save_paranoid
TRACE_IRQS_OFF_DEBUG
movq %rsp,%rdi /* pt_regs pointer */
xorl %esi,%esi /* no error code */
subq $EXCEPTION_STKSZ, INIT_TSS_IST(\ist)
call \do_sym
addq $EXCEPTION_STKSZ, INIT_TSS_IST(\ist)
jmp paranoid_exit /* %ebx: no swapgs flag */
CFI_ENDPROC
END(\sym)
.endm
int3异常后进入ENTRY(int3),call_paranoid保存寄存到栈里,并将栈顶rsp和错误码0作为第一第二个参数调用do_int3函数。
(2)do_int3()
dotraplinkage void __kprobes notrace do_int3(struct pt_regs *regs, long error_code)
{
......
if (notify_die(DIE_INT3, "int3", regs, error_code, X86_TRAP_BP,
SIGTRAP) == NOTIFY_STOP)
goto exit;
......
}
调用notifier_die向通知链die_chain通知DIE_INT3事件。在int_kprobe的时候会向die_chain注册了回调函数kprobe_exceptions_notify函数,且为最高优先级。现在执行kprobe_exceptions_notify。
(3)kprobe_exceptions_notify根据DIE_INT3事件码调用kprobe_handler函数,kprobe_handler里kprobe的pre_handler执行。
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));
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 (!p->pre_handler || !p->pre_handler(p, regs))
setup_singlestep(p, regs, kcb, 0);
return 1;
}
} else if (*addr != BREAKPOINT_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;
}
13行如果当前cpu上有其他kprobe正在运行,本次kprobe的handler不会执行,仅增加nmissed计数。
16是正常流程,设置当前kprobe为正在运行的kprobe。如果有pre_handler,执行pre_handler。存在pre_handler并且pre_hanler返回值为0,调用setup_singlestep设置单步模式。函数开始是关抢占的,所以kprobe的pre_handler的执行上下文是关抢占的。
(4)被探测原指令单步执行
static void __kprobe 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();
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 |= X86_EFLAGS_TF;
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;
}
设置reg->flags的x86_EFLAGS_IF,如果原来指令本身就是int3指令,那么直接在原探测地址上执行单步。正常情况下,设置单步地址为kprobe备份指令所在的slot地址。
设置完单步模式,kprobe_handler函数返回后,int3异常就正常返回了。int3异常会恢复到异常前的函数上下文,但此时EFLAGS寄存器被设置位单步模式,pc指向备份指令的所在的地址。会单步执行备份指令。
单步执行完之后,会触发单步异常。
(5)单步异常里执行kprobe的post_handler
备份指令单步执行完毕之后,会触发单步调试异常,对应int 1异常,因为和int3异常很类似,这里简要介绍下他的流程。int1异常触发后,进入debug函数,debug函数保存寄存器线程栈里,调用do_debug。do_debug调用notify_die向die_chain报告DIE_DEBUG事件,然后执行kprobe的通知回调函数kprobe_exceptions_notify。
kprobe_exceptions_notify函数根据DIE_DEBUG事件码执行post_kprobe_handler函数。
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 (regs->flags & X86_EFLAGS_TF)
return 0;
return 1;
}
首先调用resume_execution为int1调试异常恢复到正常执行流程做准备。具体来说:
清除regs->flags的X86_EFLAGS_TF标志,否则int1异常返回后,每执行一条指令都是单步模式。
如果单步的备份指令是绝对跳转指令,那么regs->ip就是指向正确的下一条指令,无需修正。
如果是单步的备份指令是相对跳转指令,那么需要修正regs->ip。执行完单步之后regs->ip的值是基于当前位置,也就是备份指令的位置+offset算出来的,所以要修正为基于原指令位置的偏移,加上kprobe->addr 和copy instruction的地址的差值即可。
如果单步的备份指令是call指令,单步之后,call自动将返回地址push,也就是已经把备份指令的之后的位置入栈到regs->sp的位置,此时需要把regs->sp指向的内容替换位kprobe->addr所在指令的下一条指令的地址。
如果kprobe->ainsn.boostable == 0,则在备份指令的下一条指令的位置构造一个jump指令,跳转到探测地址所在的指令的下一条指令的地址,前提是分配的slot空间要大于等于备份指令长度+5,5是相对跳转的指令的长度。设置成功后,kprobe->ainsn.boostable = 1。如果系统没有使能抢占,并且kprobe的post_handler位空,那么下次在int3异常中执行完pre_handler不需要在设置备份指令为单步模式,直接设置regs->ip位备份指令的位置,boost执行效率。
继续kprobe_post_handler函数,执行kprobe->post_handler函数,函数最后开抢占。所以的kprobe的post_handler的执行上下文也是关抢占下执行的。
int1异常返回后,恢复到正常执行流程。整个kprobe触发执行流程结束。