概述
Linux kprobes技术是一种可以跟踪内核函数执行状态的轻量级内核调试技术,利用kprobes技术可以在运行的内核中动态的插入探测点,当内核运行到该探测点后可以执行用户预定义的回调函数,以收集所需的调试状态信息而基本不影响内核原有的执行流程。
kprobes探测手段
kprobes技术包括3种探测手段分别为kprobe、jprobe和kretprobe,其中:
- kprobe是最基本的探测方式,是实现后两种的基础,它可以在内核的任何指令位置插入探测点;
- jprobe基于kprobe实现,只能插入到一个内核函数的入口,它用于获取被探测函数的入参值;
- kretprobe也是基于kprobe实现,可以在指定的内核函数返回时才被执行。利用该方式可以获取被探测函数的返回值,还可以用于计算函数执行时间等方面。
kprobes的工作机制
kprobes技术依赖硬件架构相关的支持,主要包括CPU的异常处理和单步调试机制,前者用于让程序的执行流程陷入到用户注册的回调函数中去,而后者则用于单步执行被探测点指令,目前支持kprobes技术的架构包括i386、x86_64、ppc64、ia64、sparc64、arm、ppc和mips。kprobe的工作原理和流程如图所示:
- 注册kprobe时,kprobe会复制被探测位置的指令,并用断点指令(例如i386和x86_64上的int3,ARM上的brk)替换被探测指令的第一个字节;
- 当CPU执行到断点指令时,会触发陷阱,CPU自动保存寄存器上下文,并通过notifier_call_chain机制将控制传递给kprobe;
- kprobe将kprobe结构的地址和保存的寄存器传递给注册时指定的pre_handler处理程序,并执行;
- 接下来,kprobe单步执行其被探测指令的副本;
- 指令单步执行后,kprobe执行注册的post_handler处理程序;
- 继续执行探测点后面的指令。
kprobe使用方式
kprobe的使用方式有两种:
- 方式一:通过编写内核模块,向内核注册探测点。探测函数可根据需要自行定制,使用灵活方便;
- 方式二:是使用 kprobes on ftrace,这种方式是 kprobe 和 ftrace 结合使用,即可以通过 kprobe 来优化 ftrace 来跟踪函数的调用。
本文关注于编写内核模块的方式使用kprobe。
编写kprobe探测模块
内核提供了一个struct kprobe结构体以及一系列的内核API函数接口,用户可以通过这些接口自行实现探测回调函数并实现struct kprobe结构,然后将它注册到内核的kprobes子系统中来达到探测的目的。struct kprobe结构体定义如下:
struct kprobe {
...
kprobe_opcode_t *addr; /* 被探测点的地址 */
const char *symbol_name; /* 被探测函数的名称 */
unsigned int offset; /* 被探测点在函数内部的偏移,可用于探测函数内部的指令,若为0则表示函数入口 */
kprobe_pre_handler_t pre_handler; /* 该回调函数用于在执行被探测指令前执行 */
kprobe_post_handler_t post_handler; /* 该回调函数用于在执行完被探测指令后执行 */
kprobe_fault_handler_t fault_handler; /* 此函数用于在出现内存访问错误时进行处理 */
kprobe_opcode_t opcode;
...
};
kprobe另外提供了注册与卸载kprobe探测点的API函数接口,其函数原型定义如下:
int register_kprobe(struct kprobe *kp); /* 向内核注册kprobe探测点 */
void unregister_kprobe(struct kprobe *kp); /* 从内核卸载kprobe探测点 */
在内核的samples/kprobes目录下有一个实例kprobe_example.c描述了kprobe模块最简单的编写方式,后续分析实际问题时,我们可以以此为模板编写自己的探测模块。
定义回调函数
static int handler_pre(struct kprobe *p, struct pt_regs *regs)
{
pr_info("<%s> pre_handler: p->addr = 0x%p, ip = %lx, flags = 0x%lx\n",
p->symbol_name, p->addr, regs->ip, regs->flags);
return 0;
}
/* kprobe post_handler: called after the probed instruction is executed */
static void handler_post(struct kprobe *p, struct pt_regs *regs,
unsigned long flags)
{
pr_info("<%s> post_handler: p->addr = 0x%p, flags = 0x%lx\n",
p->symbol_name, p->addr, regs->flags);
}
int handler_fault(struct kprobe *p, struct pt_regs *regs, int trapnr)
{
pr_info("fault_handler: p->addr = 0x%p, trap #%dn", p->addr, trapnr);
/* Return 0 because we don't handle the fault. */
return 0;
}
定义kprobe结构
static struct kprobe kp = {
.symbol_name = "do_fork", // 要追踪的内核函数为 do_fork
.pre_handler = handler_pre // pre_handler 回调函数
.post_handler = handler_post; // post_handler 回调函数
.fault_handler = handler_fault; // fault_handler 回调函数
};
注册探测点
static int __init kprobe_init(void)
{
int ret;
ret = register_kprobe(&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;
}
static void __exit kprobe_exit(void)
{
unregister_kprobe(&kp);
pr_info("kprobe at %p unregistered\n", kp.addr);
}
module_init(kprobe_init)
module_exit(kprobe_exit)
MODULE_LICENSE("GPL");
最后,使用如下Makefile编译kprobe_example模块:
obj-m := kprobe_example.o
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
default:
$(MAKE) -C $(KDIR) SUBDIRS=$(PWD) modules
clean:
rm -f *.mod.c *.ko *.o
安装内核模块,任意执行一条shell命令,都会在后台看到kprobe_example模块的输出。
参考链接
- 官方文档:https://www.kernel.org/doc/Documentation/kprobes.txt
- kprobe kretprobe example:https://kernelgo.org/kprobe.html
- Kernel调试追踪技术之 Kprobe on ARM64:https://www.cnblogs.com/hpyu/articles/14257305.html
- Linux内核调试工具Kprobe机制的研究:https://wenku.baidu.com/view/98d7864acf84b9d528ea7ad5.html