AIOT人人都是极客

文章数:1087 被阅读:1556316

推荐内容
账号入驻

内核调测工具kprobe之实践篇

最新更新时间:2021-02-08
    阅读数:

【推荐阅读】

深入探究Linux Kprobe机制

eBPF在android上的使用

这才是kprobe工作的本质

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



5T技术资源大放送!包括但不限于:C/C++,Arm, Linux,Android,人工智能,单片机,树莓派,等等。在公众号内回复「peter」,即可免费获取!!


 记得点击分享在看,给我充点儿电吧

 
EEWorld订阅号

 
EEWorld服务号

 
汽车开发圈

About Us 关于我们 客户服务 联系方式 器件索引 网站地图 最新更新 手机版

站点相关: TI培训

北京市海淀区中关村大街18号B座15层1530室 电话:(010)82350740 邮编:100190

电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号 Copyright © 2005-2024 EEWORLD.com.cn, Inc. All rights reserved