Linux内核调试工具之Kprobes简单使用
Linux内核调试工具之Kprobes简单使用
上次看了下Kprobes的相关概念:Linux内核调试工具之Kprobes相关概念,这里看看它的简单使用
配置Kprobes
内核需要打开以下配置:
CONFIG_KPROBES = y
#保证能加载和卸载基于Kprobes的模块
CONFIG_MODULES = y
CONFIG_MODULE_UNLOAD = y
#kprobe地址解析使用了kallsyms_lookup_name()
CONFIG_KALLSYMS_ALL = y
CONFIG_DEBUG_INFO = y
API
请参考官方文档,这里不赘述了。
使用
使用 kprobes
主要有以下两种方式:
- 通过编写内核模块
- kprobes on ftrace
通过编写内核模块
通过编写内核模块,向内核注册探测点。探测函数可根据需要自行定制,使用灵活方便,官方例子就是这种方式。
官方例子
内核源码中有2个关于kprobes的2个例子:
- kprobes例子:
内核源码/samples/kprobes/kprobe_example.c
- kretprobes例子:
内核源码/samples/kprobes/kretprobe_example.c
这里以 kprobes 例子为例,看看整个过程
主要步骤
第一步:定义 kprobe
结构体,并实现里面的相关回调
第二步:调用 register_kprobe
注册一个探测点
第三步:编写 Makefile
文件
第四步:编译并安装内核模块
实操
参考官方的例子,按照上面的步骤手动造一个内核模块,这里以X86为例,其他架构类似,相关寄存器有些差异
- 定义
kprobe
结构体,并实现里面的相关回调kprobe
最重要的就是这个struct kprobe
结构体
static struct kprobe kp = {
.symbol_name = "_do_fork", // 要追踪的内核函数
.pre_handler = handler_pre, // pre_handler
.post_handler = handler_post, // post_handler
.fault_handler = handler_fault, // fault_handler
};
根据需要编写相应的回调,主要是 pre_handler
、post_handler
和 fault_handler
这三个。
pre_handler函数:要追踪的内核函数被调用前的回调函数
static int __kprobes handler_pre(struct kprobe *p, struct pt_regs *regs)
{
#ifdef CONFIG_X86
pr_info("<%s> p->addr = 0x%p, ip = %lx, flags = 0x%lx\n",
p->symbol_name, p->addr, regs->ip, regs->flags);
#endif
/* A dump_stack() here will give a stack backtrace */
return 0;
}
post_handler函数:要追踪的内核函数被调用后的回调函数
static void __kprobes handler_post(struct kprobe *p, struct pt_regs *regs,
unsigned long flags)
{
#ifdef CONFIG_X86
pr_info("<%s> p->addr = 0x%p, flags = 0x%lx\n",
p->symbol_name, p->addr, regs->flags);
#endif
}
fault_handler函数:当发生内存异常时的回调函数
这里有个很重要的结构 – struct pt_regs
,其主要保存了 CPU 各个寄存器的值,不同 CPU 架构的定义是不一样的,我们可以通过这个结构来获取 CPU 各个寄存器的值。
- 注册和注销 kprobe
在模块初始化函数中注册kprobe,在退出函数中注销kprobe
static int __init kprobe_init(void)
{
int ret;
ret = register_kprobe(&kp);
if (ret < 0) {
printk(KERN_INFO "register_kprobe failed, returned %d\n", ret);
return ret;
}
return 0;
}
static void __exit kprobe_exit(void)
{
unregister_kprobe(&kp);
}
- 编写 Makefile
obj-m := kprobe-test.o
CROSS_COMPILE=''
KDIR := /lib/modules/$(shell uname -r)/build
all:
make -C $(KDIR) M=$(PWD) modules
clean:
rm -f *.ko *.o *.mod.o *.mod.c .*.cmd *.symvers modul*
- 编译并安装内核模块
#编译
$ make
#加载模块
$ sudo insmod kprobe-test.ko
#查看内核模块打印
dmesg
#卸载模块
$ sudo rmmod kprobe-test.ko
kprobes on ftrace
这种方式是 kprobe 和 ftrace 结合使用,即可以通过 kprobe 来优化 ftrace 来跟踪函数的调用。
这个到时候留到 ftrace
的部分,这里就不展开了。
debugfs interface
kprobes的debugfs
接口路径:/sys/kernel/debug/kprobes/
(假设 debugfs 是挂载在 /sys/kernel/debug
)。
/sys/kernel/debug/kprobes/list
列出系统上所有已注册的探测点,例如注册前面的例子后,可以看到:
$ sudo cat /sys/kernel/debug/kprobes/list
ffffffff9aaa00b0 k _do_fork+0x0 [FTRACE]
说明:
ffffffff9aaa00b0
: 第一列表示探测点的内核地址k
: 第二列表示 probe 的类型(k - kprobe, r - kretprobe)_do_fork+0x0
: 第三列表示探测的符号+偏移,如果被探测的函数属于某个模块,则还会显示模块名称[FTRACE]
: 最后一列表示probe的状态([GONE] - 不在有效的虚拟地址;[DISABLED] - 暂时被禁用;[OPTIMIZED] - 已优化;[FTRACE] - 基于ftrace)
/sys/kernel/debug/kprobes/enabled
强制打开/关闭kprobes的开关,默认情况下,所有的kprobes都是打开的。这个开关只是解除所有kprobe,并不会改变每个探测点的禁用状态。这意味着,如果通过它打开所有kprobe,则禁用的kprobe(标记为[DISABLED])将不会启用。
sysctl interface
/proc/sys/debug/kprobes-optimization
: kprobes优化的开关
当CONFIG_OPTPROBES=y
时,才会有此sysctl接口,它用于全局强制打开或关闭跳转优化。默认情况下,是打开的。如果echo 0
,或通过sysctl
将“debug.kprobes_optimization
”设置为0,则所有优化的探测都将不优化,此后注册的任何新探测都不会优化。[OPTIMIZED]标记会随其状态的改变而改变。