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为例,其他架构类似,相关寄存器有些差异

  1. 定义 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_handlerpost_handlerfault_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 各个寄存器的值。

  1. 注册和注销 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); 
}
  1. 编写 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*
  1. 编译并安装内核模块
#编译
$ 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]标记会随其状态的改变而改变。

参考