kprobe是linux内核提供的一种动态调试机制,通过这套机制,用户可以在执行指定代码前,先执行自己的代码。做一些统计,跟踪等工作。
想了解kprobe模块的工作机理,可以从内核源码的sample/kprobes/kprobes_example.c文件开始。
这个文件会被编译成内核的一个模块,我们从模块加载时执行的代码开始分析。
#define MAX_SYMBOL_LEN 64
static char symbol[MAX_SYMBOL_LEN] = "_do_fork"; //默认在执行_do_fork函数之前,执行用户插入的代码
module_param_string(symbol, symbol, sizeof(symbol), 0644);
/* For each probe you need to allocate a kprobe structure */
static struct kprobe kp = {
.symbol_name = symbol, //指定,在那个代码之前执行用户的代码
};
static int __init kprobe_init(void)
{
int ret;
kp.pre_handler = handler_pre; //在执行当前代码之前执行的函数在例子中函数只是打印一些地址和符号,实际用户可以做很多事
kp.post_handler = handler_post; //在执行完当前代码执行的函数在例子中函数只是打印一些地址和符号。
kp.fault_handler = handler_fault; //如果出现错误执行的函数
ret = register_kprobe(&kp); //注册kp结构体。
if (ret < 0) {
pr_err("register_kprobe failed, returned %d\n", ret);
return ret;
}
pr_info("Planted kprobe at %p\n", kp.addr);
return 0;
}
register_kprobe函数注册kprobe结构到内核中,并替换内核指定位置的指令为int 3 指令,保存被替换的指令。当内核执行这个位置时,将被int 3 捕获,然后执行用户代码。
register_kprobe代码如下
int 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); //检查地址是否可以被probe,如果不能也返回
if (ret)
return ret;
mutex_lock(&kprobe_mutex);
old_p = get_kprobe(p->addr); //检查地址是否已经被其他kprobe替换过了。如果有,走register_aggr_kprobe逻辑
if (old_p) {
/* Since this may unoptimize old_p, locking text_mutex. */
ret = register_aggr_kprobe(old_p, p); //如果地址已经被kprobe,则只需要将新的kprobe挂在老的kprobe后面,顺序执行即可
goto out;
}
cpus_read_lock();
/* Prevent text modification */
mutex_lock(&text_mutex);
ret = prepare_kprobe(p); //将当前指令拷贝到kprobe结构中,
mutex_unlock(&text_mutex);
cpus_read_unlock();
if (ret)
goto out;
INIT_HLIST_NODE(&p->hlist);
hlist_add_head_rcu(&p->hlist,
&kprobe_table[hash_ptr(p->addr, KPROBE_HASH_BITS)]); //将kprobe结构添加到全局变量中
if (!kprobes_all_disarmed && !kprobe_disabled(p)) { //判断是否注册即生效,如果生效调用arm_kprobe函数,替换内核指令
ret = arm_kprobe(p); //将内核代码空间指定的地址,替换成int 3 指令 在x86架构中指令为0xcc。
if (ret) {
hlist_del_rcu(&p->hlist);
synchronize_sched();
goto out;
}
}
/* Try to optimize kprobe */
try_to_optimize_kprobe(p); //这个函数是为了后续有probe同样地址的kprobe准备的。
out:
mutex_unlock(&kprobe_mutex);
if (probed_mod)
module_put(probed_mod);
return ret;
}
当注册完成,且内核地址被替换完成,当内核运行时,运行到被替换的代码时将被int3 捕获,执行int3的代码。
int3的代码是arch/x86/kernel/traps.c文件中的do_int3函数。中间有一句话 kprobe_int3_handle(regs)。
函数中关键代码如下
p = get_kprobe(addr); //根据地址查找对应的kprobe结构体
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)) //执行结构体中pre_handler函数,即例子中的handler_pre 函数
setup_singlestep(p, regs, kcb, 0);
return 1;
}
由此可以看出kprobe的工作流程如下,注册时,将kprobe结构挂全局链表中,然后将想要kprobe的指令替换成int 3 指令,当执行到这个指令时,会执行do_int3函数,最终调用kprobe_int3_handle执行用户代码。
kprobe本身功能简单,但是在trace 框架下,它能发挥巨大的作用。