kprobe调试工具
前言
debug内核函数变量的时候最常用的是添加log,用printk看下相关的信息,但是这种方式往往需要重新编译内核,然后再启动设备。
而Kprobe可以在运行的内核中动态插入探测点,执行你预定义的操作。可以跟踪内核几乎所有的代码地址,并且当断点被击中后会响应处理函数。
使用kprobe最常用的就是查询函数调用的参数和返回值。
目前,使用kprobe可以通过自行编写内核模块,向内核注册探测点,探测函数可根据需要自行定制,使用灵活方便;
1 Kprobe结构体与API介绍
60 struct kprobe {
61 struct hlist_node hlist;
62
63 /* list of kprobes for multi-handler support */
64 struct list_head list;
65
66 /*count the number of times this probe was temporarily disarmed */
67 unsigned long nmissed;
68
69 /* location of the probe point */
70 kprobe_opcode_t *addr;
71
72 /* Allow user to indicate symbol name of the probe point */
73 const char *symbol_name;
74
75 /* Offset into the symbol */
76 unsigned int offset;
77
78 /* Called before addr is executed. */
79 kprobe_pre_handler_t pre_handler;
80
81 /* Called after addr is executed, unless... */
82 kprobe_post_handler_t post_handler;
83
84 /* Saved opcode (which has been replaced with breakpoint) */
85 kprobe_opcode_t opcode;
86
87 /* copy of the original instruction */
88 struct arch_specific_insn ainsn;
89
90 /*
91 * Indicates various status flags.
92 * Protected by kprobe_mutex after this kprobe is registered.
93 */
94 u32 flags;
95 };
int register_kprobe(struct kprobe *kp) //向内核注册kprobe探测点
void unregister_kprobe(struct kprobe *kp) //卸载kprobe探测点
int register_kprobes(struct kprobe **kps, int num) //注册探测函数向量,包含多个探测点
void unregister_kprobes(struct kprobe **kps, int num) //卸载探测函数向量,包含多个探测点
int disable_kprobe(struct kprobe *kp) //临时暂停指定探测点的探测
int enable_kprobe(struct kprobe *kp) //恢复指定探测点的探测
2 用例kprobe_example.c分析与演示
2.1 开启kprobe的config
CONFIG_KPROBES=y
CONFIG_HAVE_KPROBES=y
2.2 linux内核源码中提供了kprobe的用例
samples/kprobes/kprobe_example.c
程序中定义了一个struct kprobe结构实例kp并初始化其中的symbol_name字段为“kernel_clone”,表明它将要探测kernel_clone函数。在模块的初始化函数中,注册了 pre_handler、post_handler这2个回调函数分别为handler_pre、handler_post,最后调用register_kprobe注册。在模块的卸载函数中调用unregister_kprobe函数卸载kp探测点。
另外如果想获取传入函数的参数和调用进程等信息,因为pre_handler和post_handler回调函数的输入参数里面包含了struct pt_regs *regs ,所以可以类似这样:
struct file *file = (struct file *)regs->regs[0]; //具体参数对应哪个寄存器需看反汇编代码
pr_info("<%s>,pid = %ld\n",current->comm, current->pid);
dump_stack();
2.3 编写Makefile
1 export ARCH=arm64
2 export CROSS_COMPILE=aarch64-linux-gnu-
3
4 KERNEL_DIR ?=~/linux_rt/linux-rt-5.15/
5 obj-m := kprobe_example.o
6
7 modules:
8 $(MAKE) -C $(KERNEL_DIR) M=$(PWD) modules
9
10 clean:
11 $(MAKE) -C $(KERNEL_DIR) M=$(PWD) clean
12
13 install:
14 cp *.ko $(KERNEL_DIR)/kmodules
2.4 测试效果
[root@liebao kprobe]# insmod kprobe_example.ko
[root@liebao kprobe]# [ 1552.862075] kprobe_init: Planted kprobe at (____ptrval____)
[ 1552.864013] handler_pre: <kernel_clone> p->addr = 0x(____ptrval____), pc = 0xffff8000080830f0, pstate = 0x60000005
[ 1552.864299] handler_post: <kernel_clone> p->addr = 0x(____ptrval____), pstate = 0x60000005
[ 1552.866225] handler_pre: <kernel_clone> p->addr = 0x(____ptrval____), pc = 0xffff8000080830f0, pstate = 0x80000005
[ 1552.866284] handler_post: <kernel_clone> p->addr = 0x(____ptrval____), pstate = 0x80000005
[root@liebao kprobe]# ls
[ 1556.661789] handler_pre: <kernel_clone> p->addr = 0x(____ptrval____), pc = 0xffff8000080830f0, pstate = 0x80000005
[ 1556.661913] handler_post: <kernel_clone> p->addr = 0x(____ptrval____), pstate = 0x80000005
Makefile kprobe_example.ko kprobe_example.mod.o
Module.symvers kprobe_example.mod kprobe_example.o
kprobe_example.c kprobe_example.mod.c modules.order
2.5 查看函数符号的地址方法
[root@liebao kprobes]# cat /proc/kallsyms | grep kernel_clone
ffff8000080830f0 T kernel_clone
2.6 动态开关kprobe功能
- /sys/kernel/debug/kprobes/list: 列出内核中已经设置kprobe断点的函数
- /sys/kernel/debug/kprobes/enabled: kprobe开启/关闭开关
- /sys/kernel/debug/kprobes/blacklist: kprobe黑名单(无法设置断点函数)
3 Kprobe的原理
Kprobe实现的本质是breakpoint和single-step的结合,这一点和大多数调试工具一样,比如kgdb/gdb。整个过程的来龙去脉:
1.注册kprobe。注册的每个kprobe对应一个kprobe结构体,该结构体记录着插入点(位置),以及该插入点本来对应的指令original_opcode;
2.替换原有指令。使能kprobe的时候,将插入点位置的指令替换为一条异常(BRK)指令,这样当CPU执行到插入点位置时会陷入到异常态;
3.执行pre_handler。进入异常态后,首先执行pre_handler,然后利用CPU提供的单步调试(single-step)功能,设置好相应的寄存器,将下一条指令设置为插入点处本来的指令,从异常态返回;
4.再次陷入异常态。上一步骤中设置了single-step相关的寄存器,所以original_opcode刚一执行,便会再次陷入异常态,此时将signle-step清除,并且执行post_handler,然后从异常态安全返回。
步骤2,3,4便是一次kprobe工作的过程,它的一个基本思路就是将本来执行一条指令扩展成执行kprobe->pre_handler—>原指令—>kprobe–>post_handler这样三个过程。