本文转载自微信公众号「人人都是极客」,作者布道师Peter。转载本文请联系人人都是极客公众号。
Kprobe介绍
debug内核函数变量的时候最常用的是添加log,用printk看下相关的信息,但是这种方式往往需要重新编译内核,然后再启动设备。
而Kprobe可以在运行的内核中动态插入探测点,执行你预定义的操作。可以跟踪内核几乎所有的代码地址,并且当断点被击中后会响应处理函数。
使用kprobe最常用的就是查询函数调用的参数和返回值。
目前,使用kprobe可以通过两种方式:
- 第一种是开发人员自行编写内核模块,向内核注册探测点,探测函数可根据需要自行定制,使用灵活方便;
- 第二种方式是使用kprobes on trace,这种方式是kprobe和Ftrace结合使用,即可以通过kprobe来优化Ftrace来跟踪函数的调用。
编写kprobe探测模块
Kprobe结构体与API介绍
- struct hlist_node hlist:被用于kprobe全局hash,索引值为被探测点的地址;
- struct list_head list:用于链接同一被探测点的不同探测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:在执行pre_handler、post_handler或单步执行被探测指令时出现内存异常则会调用该回调函数;
- kprobe_break_handler_t break_handler:在执行某一kprobe过程中触发了断点指令后会调用该函数,用于实现jprobe;
- kprobe_opcode_t opcode:保存的被探测点原始指令;
- struct arch_specific_insn ainsn:被复制的被探测点的原始指令,用于单步执行,架构强相关(可能包含指令模拟函数);
- u32 flags:状态标记。
- 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) //恢复指定探测点的探测
用例kprobe_example.c分析与演示
linux内核源码中提供了kprobe的用例 samples/kprobes/kprobe_example.c
- /* For each probe you need to allocate a kprobe structure */
- static struct kprobe kp = {
- .symbol_name = "do_fork",
- };
- 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);
- if (ret < 0) {
- printk(KERN_INFO "register_kprobe failed, returned %d\n", ret);
- return ret;
- }
- printk(KERN_INFO "Planted kprobe at %p\n", kp.addr);
- return 0;
- }
- static void __exit kprobe_exit(void)
- {
- unregister_kprobe(&kp);
- printk(KERN_INFO "kprobe at %p unregistered\n", kp.addr);
- }
- module_init(kprobe_init)
- module_exit(kprobe_exit)
- MODULE_LICENSE("GPL");
程序中定义了一个struct kprobe结构实例kp并初始化其中的symbol_name字段为“do_fork”,表明它将要探测do_fork函数。在模块的初始化函数中,注册了 pre_handler、post_handler和fault_handler这3个回调函数分别为handler_pre、handler_post和handler_fault,最后调用register_kprobe注册。在模块的卸载函数中调用unregister_kprobe函数卸载kp探测点。
- static int handler_pre(struct kprobe *p, struct pt_regs *regs)
- {
- ......
- #ifdef CONFIG_ARM64
- pr_info("<%s> pre_handler: p->addr = 0x%p, pc = 0x%lx,"
- " pstate = 0x%lx\n",
- p->symbol_name, p->addr, (long)regs->pc, (long)regs->pstate);
- #endif
- /* A dump_stack() here will give a stack backtrace */
- return 0;
- }
handler_pre回调函数的第一个入参是注册的struct kprobe探测实例,第二个参数是保存的触发断点前的寄存器状态,它在do_fork函数被调用之前被调用,该函数仅仅是打印了被探测点的地址,保存的个别寄存器参数。
- static void handler_post(struct kprobe *p, struct pt_regs *regs,
- unsigned long flags)
- {
- ......
- #ifdef CONFIG_ARM64
- pr_info("<%s> post_handler: p->addr = 0x%p, pstate = 0x%lx\n",
- p->symbol_name, p->addr, (long)regs->pstate);
- #endif
- }
handler_post回调函数的前两个入参同handler_pre,第三个参数目前尚未使用,全部为0;该函数在do_fork函数调用之后被调用,这里打印的内容同handler_pre类似。
- static 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;
- }
handler_fault回调函数会在执行handler_pre、handler_post或单步执行do_fork时出现错误时调用,这里第三个参数时具体发生错误的trap number,与架构相关。
加载到内核中后,随便在终端上敲一个命令,可以看到dmesg中打印如下信息:
- <6>pre_handler: p->addr = 0xc0439cc0, ip = c0439cc1, flags = 0x246
- <6>post_handler: p->addr = 0xc0439cc0, flags = 0x246
- <6>pre_handler: p->addr = 0xc0439cc0, ip = c0439cc1, flags = 0x246
- <6>post_handler: p->addr = 0xc0439cc0, flags = 0x246
- <6>pre_handler: p->addr = 0xc0439cc0, ip = c0439cc1, flags = 0x246
- <6>post_handler: p->addr = 0xc0439cc0, flags = 0x246
可以看到被探测点的地址为0xc0439cc0,用以下命令确定这个地址就是do_fork的入口地址。
- echo 0 > /proc/sys/kernel/kptr_restrict
- cat /proc/kallsyms | grep do_fork
- c0439cc0 T do_fork
kprobes on trace
- /sys/kernel/debug/kprobes/list: 列出内核中已经设置kprobe断点的函数
- /sys/kernel/debug/kprobes/enabled: kprobe开启/关闭开关
- /sys/kernel/debug/kprobes/blacklist: kprobe黑名单(无法设置断点函数)
- /proc/sys/debug/kprobes-optimization: Turn kprobes optimization ON/OFF
Documentation/trace/kprobetrace.txt
使用前确定内核CONFIG打开:CONFIG_KPROBE_EVENT=y
/sys/kernel/debug/tracing/kprobe_events:添加断点接口
/sys/kernel/debug/tracing/events/kprobes/enabled:断点使能开关
/sys/kernel/debug/tracing/trace:查看trace日志接口
规则:
- Synopsis of kprobe_events-------------------------
- p[:[GRP/]EVENT] [MOD:]SYM[+offs]|MEMADDR [FETCHARGS] : Set a probe
- r[:[GRP/]EVENT] [MOD:]SYM[+0] [FETCHARGS] : Set a return probe
- -:[GRP/]EVENT : Clear a probe
- GRP : Group name. If omitted, use "kprobes" for it.
- EVENT : Event name. If omitted, the event name is generated
- based on SYM+offs or MEMADDR.
- MOD : Module name which has given SYM.
- SYM[+offs] : Symbol+offset where the probe is inserted.
- MEMADDR : Address where the probe is inserted.
- FETCHARGS : Arguments. Each probe can have up to 128 args.
- %REG : Fetch register REG
- @ADDR : Fetch memory at ADDR (ADDR should be in kernel)
- @SYM[+|-offs] : Fetch memory at SYM +|- offs (SYM should be a data symbol)
- $stackN : Fetch Nth entry of stack (N >= 0)
- $stack : Fetch stack address.
- $retval : Fetch return value.(*)
- $comm : Fetch current task comm.
- +|-offs(FETCHARG) : Fetch memory at FETCHARG +|- offs address.(**)
- NAME=FETCHARG : Set NAME as the argument name of FETCHARG.
- FETCHARG:TYPE : Set TYPE as the type of FETCHARG. Currently, basic types
- (u8/u16/u32/u64/s8/s16/s32/s64), hexadecimal types
- (x8/x16/x32/x64), "string" and bitfield are supported.
- (*) only for return probe.
- (**) this is useful for fetching a field of data structures.
查看对应的模块:
- 130|mek_8q:/sys/kernel/debug/tracing # cat /proc/devices
- Character devices:
- 1 mem
- 4 /dev/vc/0
- 4 tty
- 4 ttyS
- 5 /dev/tty
- 5 /dev/console
- 5 /dev/ptmx
- 7 vcs
- 10 misc
- 13 input
- 29 fb
- 81 video4linux
- 89 i2c
- 90 mtd
- 108 ppp
- 116 alsa
可以在System.map文件里找一下有没有你要观察的内核函数方法。这个文件其实相当于内核的符号表(symbol table)。如果拿不准内核方法名的时候可以在这里面grep一下看看。
- mek_8q:/ # cat /proc/kallsyms | grep do_sys_open
- 0000000000000000 T do_sys_open
以do_sys_open为例添加kprobe为例:
- 添加kprobe:
- echo 'p:myprobe do_sys_open' > /sys/kernel/debug/tracing/kprobe_events
- 添加kretprobe,返回值是数字:
- echo 'r:myretprobe do_sys_open $retval' > /sys/kernel/debug/tracing/kprobe_events
- 添加kretprobe,返回值是字符串:
- echo 'r:myprobe getname +0($retval):string' > /sys/kernel/debug/tracing/kprobe_events
- 删除添加的kprobe:
- echo '-:myprobe' > /sys/kernel/debug/tracing/events/kprobe_events
执行:
- cd /sys/kernel/debug/tracing
- echo 'p:myprobe do_sys_open' > kprobe_events
- echo 'r:myretprobe do_sys_open $retval' > kprobe_events
- echo 1 > tracing_on
- echo 1 > events/kprobes/myprobe/enable
结果为:
删除注册的kprobe:
- echo 0 > /sys/kernel/debug/tracing/events/kprobes/myprobe/enable
- echo 0 > /sys/kernel/debug/tracing/events/kprobes/myretprobe/enable
- echo '-:myprobe' > /sys/kernel/debug/tracing/events/kprobe_events
- echo '-:myretprobe' > /sys/kernel/debug/tracing/events/kprobe_events