当前位置: 首页 > 科技观察

内核调试工具Kprobe的实践

时间:2023-03-13 04:10:54 科技观察

本文转载自微信公众号《人人都是极客》,作者为布道者Peter。转载本文请联系大家是极客公众号。kprobe引入调试内核函数变量时,最常用的方法是添加日志,使用printk查看相关信息,但这种方法往往需要重新编译内核,然后再启动设备。并且Kprobe可以在运行的内核中动态插入探测点并执行您预定义的操作。它可以跟踪内核几乎所有的代码地址,当命中断点时,它会响应处理函数。kprobe最常见的用途是查询函数调用的参数和返回值。目前kprobe的使用方式有两种:第一种是开发者编写内核模块,向内核注册探测点。可根据需要定制探头功能,使用灵活方便;第一种方式是结合使用kprobe和Ftrace,即可以使用kprobe优化Ftrace来跟踪函数调用。编写kprobe检测模块Kprobe结构体及API介绍structhlist_nodehlist:用于kprobe全局hash,索引值为被探测点的地址;structlist_headlist:用于链接同一探测点的不同探测kprobe;kprobe_opcode_t*addr:被探测点的地址;constchar*symbol_name:被探测函数的名称;unsignedintoffset:探测点在函数内部的偏移量,用于探测函数内部指令,值为0表示函数入口;kprobe_pre_handler_tpre_handler:命令在被探测点执行前调用的回调函数;kprobe_post_handler_tpost_handler:被探测指令执行后调用的回调函数;kprobe_fault_handler_tfault_handler:在执行pre_handler、post_handler或单步执行被探测指令过程中发生内存异常时调用的回调函数;kprobe_break_handler_tbreak_handler:当执行某个kprobe进程中触发断点指令后会调用该函数来实现jp??robe;kprobe_opcode_topcode:待保存的检测点的原始指令;structach_specific_insnainsn:待复制检测点的原始指令,用于单步执行,架构强相关(可能包含指令仿真功能);u32flags:状态标志。intregister_kprobe(structkprobe*kp)//注册kprobe检测点到内核voidunregister_kprobe(structkprobe*kp)//卸载kprobe检测点intregister_kprobes(structkprobe**kps,intnum)//注册检测函数向量,包括多个检测点**kps,intnum)//卸载探测函数向量,包括多个探测点intdisable_kprobe(structkprobe*kp)//暂停指定探测点的探测intenable_kprobe(structkprobe*kp)//恢复指定探测点的探测情况探测点kprobe_example。c分析演示linux内核源码提供kprobe用例samples/kprobes/kprobe_example.c/*Foreachprobeyouneedtoallocateakprobestructure*/staticstructkprobekp={.symbol_name="do_fork",};staticint__initkprobe_init(void){intret;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_kprobefailed,returned%d\n",ret);returnret;}printk(KERN_INFO"Plantedkprobeat%p\n",kp.addr);return0;}staticvoid__exitkprobe_exit(void){unregister_kprobe(&kp);printk(KERN_INFO"kprobeat%punregistered\n",kp.addr);}module_init(kprobe_init)module_exit(kprobe_exit)MODULE_LICENSE("GPL");程序中定义了一个structkprobe结构体实例kp,其中的symbol_name字段初始化为“do_fork”,表示将检测do_fork函数。在模块的初始化函数中,注册了pre_handler三个回调函数,post_handler和fault_handler分别是handler_pre、handler_post和handler_fault,最后调用register_kprobe进行注册。在模块的卸载函数中调用unregister_kprobe函数卸载kp探测点。staticinthandler_pre(structkprobe*p,structpt_regs*regs){...#ifdefCONFIG_ARM64pr_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/*Adump_stack()herewillgiveastackbacktrace*/return0;}handler_pre第一个回调函数入参为注册的structkprobe检测实例。第二个参数是断点触发前保存的寄存器状态。它在调用do_fork函数之前被调用。此函数仅打印探测点的地址,以及保存的单个Register参数。staticvoidhandler_post(structkprobe*p,structpt_regs*regs,unsignedlongflags){...#ifdefCONFIG_ARM64pr_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类似。staticinthandler_fault(structkprobe*p,structpt_regs*regs,inttrapnr){pr_info("fault_handler:p->addr=0x%p,trap#%dn",p->addr,trapnr);/*返回0因为我们不处理故障。*/return0;}handler_fault回调函数,在执行handler_pre、handler_post或单步执行do_fork过程中出现错误时,会调用handler_fault回调函数。这里的第三个参数是具体发生错误的trap编号,跟架构有关。加载到内核后,在终端输入命令,可以看到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的入口地址。echo0>/proc/sys/kernel/kptr_restrictcat/proc/kallsyms|grepdo_forkc0439cc0Tdo_forkkprobesontrace/sys/kernel/debug/kprobes/list:列出在内核中设置了kprobe断点的函数/sys/kernel/debug/kprobes/enabled:kprobeon/off开关/sys/kernel/debug/kprobes/blacklist:kprobe黑名单(不能设置断点功能)/proc/sys/debug/kprobes-optimization:TurnkprobeoptimizationON/OFFDocumentation/trace/kprobetrace.txtMake使用前确保内核CONFIG已开启:CONFIG_KPROBE_EVENT=y/sys/kernel/debug/tracing/kprobe_events:addbreakpointinterface/sys/kernel/debug/tracing/events/kprobes/enabled:breakpointenableswitch/sys/kernel/debug/tracing/trace:查看trace日志接口规则:Synopsisofkprobe_events------------------------p[:[GRP/]EVENT][MOD:]SYM[+offs]|MEMADDR[FETCHARGS]:Setaprober[:[GRP/]EVENT][MOD:]SYM[+0][FETCHARGS]:Setareturnprobe-:[GRP/]EVENT:ClearaprobeGRP:Groupname.Ifomitted,use"kprobes"forit.EVENT:Eventname.Ifomitted,theeventnameisgeneratedbasedonSYM+offsorMEMADDR.MOD:ModulenamewhichhasgivenSYM.SYM[+offs]:Symbol+offsetwheretheprobeisinserted.MEMADDR:插入探测器的地址。FETCHARGS:参数。每个探测器最多可以有128个参数。%REG:FetchregisterREG@ADDR:在ADDR处获取内存(ADDR应该是内核)@SYM[+|-offs]:在SYM+|-offs处获取内存(SYM应该是数据符号)N$stackN:ofstackN:Fetch)。$retackvaladdress。获取返回值。(*)$comm:获取当前任务通讯。+|-offs(FETCHARG):获取位于FETCHARG+|-offs地址的内存。(**)NAME=FETCHARG:将NAME设置为FETCHARG类型的参数名称。FETCHARG:TYPE:将TYPE设置为FETCHARG类型(bau16/u32/u64/s8/s16/s32/s64),十六进制类型(x8/x16/x32/x64),支持“字符串”和位域。(*)仅用于返回探测器。(**)这对于获取数据结构字段很有用。Viewthecorrespondingmodule:130|mek_8q:/sys/kernel/debug/tracing#cat/proc/devicesCharacterdevices:1mem4/dev/vc/04tty4ttyS5/dev/tty5/dev/console5/dev/ptmx7vcs10misc13input29fb81video4linux89i2c90mtd108ppp116alsacanbefoundintheSystem.mapfileKernelfunctionmethod这个文件其实相当于内核的符号表(symboltable)。如果不确定内核方法的名称,可以在这里grep。mek_8q:/#cat/proc/kallsyms|grepdo_sys_open0000000000000000Tdo_sys_open以do_sys_open为例添加kprobe为例:addkprobe:echo'p:myprobedo_sys_open'>/sys/kernel/debug/tracing/kprobe_events添加kretprobe,返回值为一个数字:echo'r:myretprobedo_sys_open$retval'>/sys/kernel/debug/tracing/kprobe_events添加kretprobe,返回值为字符串:echo'r:myprobegetname+0($retval):string'>/sys/kernel/debug/tracing/kprobe_events删除添加的kprobe:echo'-:myprobe'>/sys/kernel/debug/tracing/events/kprobe_eventsexecute:cd/sys/kernel/debug/tracingecho'p:myprobedo_sys_open'>kprobe_eventsecho'r:myretprobedo_sys_open$retval'>kprobe_eventsecho1>tracing_onecho1>events/kprobes/myprobe/enable结果是:删除注册的kprobe:echo0>/sys/kernel/debug/tracing/events/kprobes/myprobe/enableecho0>/sys/kernel/调试/跟踪/事件/kprobes/myretprobe/enableecho'-:myprobe'>/sys/kernel/debug/tracing/events/kprobe_eventsecho'-:myretprobe'>/sys/kernel/debug/tracing/事件/kprobe_events