内核调测工具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」,即可免费获取!!
记得点击分享、赞和在看,给我充点儿电吧