内核调测工具Kprobe之实践篇

系统
debug内核函数变量的时候最常用的是添加log,用printk看下相关的信息,但是这种方式往往需要重新编译内核,然后再启动设备。

[[381473]]

本文转载自微信公众号「人人都是极客」,作者布道师Peter。转载本文请联系人人都是极客公众号。   

 Kprobe介绍

debug内核函数变量的时候最常用的是添加log,用printk看下相关的信息,但是这种方式往往需要重新编译内核,然后再启动设备。

而Kprobe可以在运行的内核中动态插入探测点,执行你预定义的操作。可以跟踪内核几乎所有的代码地址,并且当断点被击中后会响应处理函数。

使用kprobe最常用的就是查询函数调用的参数和返回值。

目前,使用kprobe可以通过两种方式:

  • 第一种是开发人员自行编写内核模块,向内核注册探测点,探测函数可根据需要自行定制,使用灵活方便;
  • 第二种方式是使用kprobes on trace,这种方式是kprobe和Ftrace结合使用,即可以通过kprobe来优化Ftrace来跟踪函数的调用。

编写kprobe探测模块

Kprobe结构体与API介绍

  1. struct hlist_node hlist:被用于kprobe全局hash,索引值为被探测点的地址; 
  2. struct list_head list:用于链接同一被探测点的不同探测kprobe; 
  3. kprobe_opcode_t *addr:被探测点的地址; 
  4. const char *symbol_name:被探测函数的名字; 
  5. unsigned int offset:被探测点在函数内部的偏移,用于探测函数内部的指令,如果该值为0表示函数的入口; 
  6. kprobe_pre_handler_t pre_handler:在被探测点指令执行之前调用的回调函数; 
  7. kprobe_post_handler_t post_handler:在被探测指令执行之后调用的回调函数; 
  8. kprobe_fault_handler_t fault_handler:在执行pre_handler、post_handler或单步执行被探测指令时出现内存异常则会调用该回调函数; 
  9. kprobe_break_handler_t break_handler:在执行某一kprobe过程中触发了断点指令后会调用该函数,用于实现jprobe; 
  10. kprobe_opcode_t opcode:保存的被探测点原始指令; 
  11. struct arch_specific_insn ainsn:被复制的被探测点的原始指令,用于单步执行,架构强相关(可能包含指令模拟函数); 
  12. u32 flags:状态标记。 
  1. int register_kprobe(struct kprobe *kp)      //向内核注册kprobe探测点 
  2. void unregister_kprobe(struct kprobe *kp)   //卸载kprobe探测点 
  3. int register_kprobes(struct kprobe **kps, int num)     //注册探测函数向量,包含多个探测点 
  4. void unregister_kprobes(struct kprobe **kps, int num)  //卸载探测函数向量,包含多个探测点 
  5. int disable_kprobe(struct kprobe *kp)       //临时暂停指定探测点的探测 
  6. int enable_kprobe(struct kprobe *kp)        //恢复指定探测点的探测 

用例kprobe_example.c分析与演示

linux内核源码中提供了kprobe的用例 samples/kprobes/kprobe_example.c

  1. /* For each probe you need to allocate a kprobe structure */ 
  2. static struct kprobe kp = { 
  3.  .symbol_name = "do_fork"
  4. }; 
  5.   
  6. static int __init kprobe_init(void) 
  7.  int ret; 
  8.  kp.pre_handler = handler_pre; 
  9.  kp.post_handler = handler_post; 
  10.  kp.fault_handler = handler_fault; 
  11.   
  12.  ret = register_kprobe(&kp); 
  13.  if (ret < 0) { 
  14.   printk(KERN_INFO "register_kprobe failed, returned %d\n", ret); 
  15.   return ret; 
  16.  } 
  17.  printk(KERN_INFO "Planted kprobe at %p\n", kp.addr); 
  18.  return 0; 
  19.   
  20. static void __exit kprobe_exit(void) 
  21.  unregister_kprobe(&kp); 
  22.  printk(KERN_INFO "kprobe at %p unregistered\n", kp.addr); 
  23.   
  24. module_init(kprobe_init) 
  25. module_exit(kprobe_exit) 
  26. 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探测点。

  1. static int handler_pre(struct kprobe *p, struct pt_regs *regs) 
  2. ...... 
  3. #ifdef CONFIG_ARM64 
  4.  pr_info("<%s> pre_handler: p->addr = 0x%p, pc = 0x%lx," 
  5.    " pstate = 0x%lx\n"
  6.   p->symbol_name, p->addr, (long)regs->pc, (long)regs->pstate); 
  7. #endif 
  8.  
  9.  /* A dump_stack() here will give a stack backtrace */ 
  10.  return 0; 

handler_pre回调函数的第一个入参是注册的struct kprobe探测实例,第二个参数是保存的触发断点前的寄存器状态,它在do_fork函数被调用之前被调用,该函数仅仅是打印了被探测点的地址,保存的个别寄存器参数。

  1. static void handler_post(struct kprobe *p, struct pt_regs *regs, 
  2.     unsigned long flags) 
  3. ...... 
  4. #ifdef CONFIG_ARM64 
  5.  pr_info("<%s> post_handler: p->addr = 0x%p, pstate = 0x%lx\n"
  6.   p->symbol_name, p->addr, (long)regs->pstate); 
  7. #endif 

handler_post回调函数的前两个入参同handler_pre,第三个参数目前尚未使用,全部为0;该函数在do_fork函数调用之后被调用,这里打印的内容同handler_pre类似。

  1. static int handler_fault(struct kprobe *p, struct pt_regs *regs, int trapnr) 
  2.  pr_info("fault_handler: p->addr = 0x%p, trap #%dn", p->addr, trapnr); 
  3.  /* Return 0 because we don't handle the fault. */ 
  4.  return 0; 

handler_fault回调函数会在执行handler_pre、handler_post或单步执行do_fork时出现错误时调用,这里第三个参数时具体发生错误的trap number,与架构相关。

加载到内核中后,随便在终端上敲一个命令,可以看到dmesg中打印如下信息:

  1. <6>pre_handler: p->addr = 0xc0439cc0, ip = c0439cc1, flags = 0x246 
  2. <6>post_handler: p->addr = 0xc0439cc0, flags = 0x246 
  3. <6>pre_handler: p->addr = 0xc0439cc0, ip = c0439cc1, flags = 0x246 
  4. <6>post_handler: p->addr = 0xc0439cc0, flags = 0x246 
  5. <6>pre_handler: p->addr = 0xc0439cc0, ip = c0439cc1, flags = 0x246 
  6. <6>post_handler: p->addr = 0xc0439cc0, flags = 0x246 

可以看到被探测点的地址为0xc0439cc0,用以下命令确定这个地址就是do_fork的入口地址。

  1. echo 0 > /proc/sys/kernel/kptr_restrict 
  2. cat /proc/kallsyms | grep do_fork 
  3. 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日志接口

规则:

  1. Synopsis of kprobe_events------------------------- 
  2.   p[:[GRP/]EVENT] [MOD:]SYM[+offs]|MEMADDR [FETCHARGS]  : Set a probe 
  3.   r[:[GRP/]EVENT] [MOD:]SYM[+0] [FETCHARGS]             : Set a return probe 
  4.   -:[GRP/]EVENT                                         : Clear a probe 
  5.  
  6.  GRP            : Group name. If omitted, use "kprobes" for it. 
  7.  EVENT          : Event name. If omitted, the event name is generated 
  8.                   based on SYM+offs or MEMADDR. 
  9.  MOD            : Module name which has given SYM. 
  10.  SYM[+offs]     : Symbol+offset where the probe is inserted. 
  11.  MEMADDR        : Address where the probe is inserted. 
  12.  
  13.  FETCHARGS      : Arguments. Each probe can have up to 128 args. 
  14.   %REG          : Fetch register REG 
  15.   @ADDR         : Fetch memory at ADDR (ADDR should be in kernel) 
  16.   @SYM[+|-offs] : Fetch memory at SYM +|- offs (SYM should be a data symbol) 
  17.   $stackN       : Fetch Nth entry of stack (N >= 0) 
  18.   $stack        : Fetch stack address. 
  19.   $retval       : Fetch return value.(*) 
  20.   $comm         : Fetch current task comm. 
  21.   +|-offs(FETCHARG) : Fetch memory at FETCHARG +|- offs address.(**) 
  22.   NAME=FETCHARG : Set NAME as the argument name of FETCHARG. 
  23.   FETCHARG:TYPE : Set TYPE as the type of FETCHARG. Currently, basic types 
  24.                   (u8/u16/u32/u64/s8/s16/s32/s64), hexadecimal types 
  25.                   (x8/x16/x32/x64), "string" and bitfield are supported. 
  26.  
  27.   (*) only for return probe. 
  28.   (**) this is useful for fetching a field of data structures. 

查看对应的模块:

  1. 130|mek_8q:/sys/kernel/debug/tracing # cat /proc/devices 
  2. Character devices: 
  3.   1 mem 
  4.   4 /dev/vc/0 
  5.   4 tty 
  6.   4 ttyS 
  7.   5 /dev/tty 
  8.   5 /dev/console 
  9.   5 /dev/ptmx 
  10.   7 vcs 
  11.  10 misc 
  12.  13 input 
  13.  29 fb 
  14.  81 video4linux 
  15.  89 i2c 
  16.  90 mtd 
  17. 108 ppp 
  18. 116 alsa 

可以在System.map文件里找一下有没有你要观察的内核函数方法。这个文件其实相当于内核的符号表(symbol table)。如果拿不准内核方法名的时候可以在这里面grep一下看看。

  1. mek_8q:/ # cat /proc/kallsyms | grep do_sys_open 
  2. 0000000000000000 T do_sys_open 

以do_sys_open为例添加kprobe为例:

  1. 添加kprobe: 
  2. echo 'p:myprobe do_sys_open' > /sys/kernel/debug/tracing/kprobe_events 
  3. 添加kretprobe,返回值是数字: 
  4. echo 'r:myretprobe do_sys_open $retval' > /sys/kernel/debug/tracing/kprobe_events 
  5. 添加kretprobe,返回值是字符串: 
  6. echo 'r:myprobe getname +0($retval):string' > /sys/kernel/debug/tracing/kprobe_events 
  7. 删除添加的kprobe: 
  8. echo '-:myprobe' > /sys/kernel/debug/tracing/events/kprobe_events 

执行:

  1. cd /sys/kernel/debug/tracing 
  2. echo 'p:myprobe do_sys_open' > kprobe_events 
  3. echo 'r:myretprobe do_sys_open $retval' > kprobe_events 
  4. echo 1 > tracing_on 
  5. echo 1 > events/kprobes/myprobe/enable 

结果为:

删除注册的kprobe:

  1. echo 0 > /sys/kernel/debug/tracing/events/kprobes/myprobe/enable 
  2. echo 0 > /sys/kernel/debug/tracing/events/kprobes/myretprobe/enable 
  3. echo '-:myprobe' > /sys/kernel/debug/tracing/events/kprobe_events 
  4. echo '-:myretprobe' > /sys/kernel/debug/tracing/events/kprobe_events 

 

责任编辑:武晓燕 来源: 人人都是极客
相关推荐

2021-02-20 20:51:24

工具内核kprobe

2022-03-21 09:40:48

TektonJenkinsPipeline

2022-04-08 09:53:56

TektonJenkinsKubesphere

2010-07-29 09:57:34

SCVMM虚拟机

2009-12-07 14:53:07

RHEL网络安装

2022-03-10 13:57:23

TektonJenkinsPipeline

2022-04-14 07:51:39

TektonTaskRun

2010-07-20 08:49:00

Objective C

2017-10-17 14:02:30

jvm调优工具

2019-06-12 10:15:19

运维开发Linux

2023-06-27 15:02:47

2011-03-18 11:21:48

2021-03-04 08:39:21

SparkRDD调优

2011-07-27 14:10:43

javascript

2021-11-07 23:49:19

SQL数据库工具

2021-11-15 04:00:07

Linux 内核动态

2023-11-29 20:19:35

实践云计算

2009-08-07 10:28:03

2010-11-30 11:26:49

2021-04-08 09:32:17

鸿蒙HarmonyOS应用
点赞
收藏

51CTO技术栈公众号