Linux内核调试技术——kprobe使⽤与实现
Linux kprobes调试技术是内核开发者们专门为了便于跟踪内核函数执⾏状态所设计的⼀种轻量级内核调试技术。利⽤kprobes技术,内核开发⼈员可以在内核的绝⼤多数指定函数中动态的插⼊探测点来收集所需的调试状态信息⽽基本不影响内核原有的执⾏流程。kprobes技术⽬前提供了3种探测⼿段:kprobe、jprobe和kretprobe,其中jprobe和kretprobe是基于kprobe实现的,他们分别应⽤于不同的探测场景中。本⽂⾸先简单描述这3种探测技术的原理与区别,然后主要围绕其中的kprobe技术进⾏分析并给出⼀个简单的实例介绍如何利⽤kprobe进⾏内核函数探测,最后分析kprobe的实现过程(jprobe和kretprobe会在后续的博⽂中进⾏分析)。
内核源码:Linux-4.1.15
实验环境:CentOS(x86_64)、树莓派1b
⼀、kprobes技术背景
开发⼈员在内核或者模块的调试过程中,往往会需要要知道其中的⼀些函数有⽆被调⽤、何时被调⽤、执⾏是否正确以及函数的⼊参和返回值是什么等等。⽐较简单的做法是在内核代码对应的函数中添加⽇志打印信息,但这种⽅式往往需要重新编译内核或模块,重新启动设备之类的,操作较为复杂甚⾄可能会破坏原有的代码执⾏过程。
⽽利⽤kprobes技术,⽤户可以定义⾃⼰的回调函数,然后在内核或者模块中⼏乎所有的函数中(有些函数是不可探测的,例如kprobes⾃⾝的相关实现函数,后⽂会有详细说明)动态的插⼊探测点,当内核执⾏流程执⾏到指定的探测函数时,会调⽤该回调函数,⽤户即可收集所需的信息了,同时内核最后还会回到原本的正常执⾏流程。如果⽤户已经收集⾜够的信息,不再需要继续探测,则同样可以动态的移除探测点。因此kprobes技术具有对内核执⾏流程影响⼩和操作⽅便的优点。
kprobes技术包括的3种探测⼿段分别时kprobe、jprobe和kretprobe。⾸先kprobe是最基本的探测⽅式,是实现后两种的基础,它可以在任意的位置放置探测点(就连函数内部的某条指令处也可以),它提供了探测点的调⽤前、调⽤后和内存访问出错3种回调⽅式,分别是pre_handler、post_handler和fault_handler,其中pre_handler函数将在被探测指令被执⾏前回调,post_handler会在被探测指令执⾏完毕后回调(注意不是被探测函数),fault_handler会在内存访问出错时被调⽤;jprobe基于kprobe实现,它⽤于获取被探测函数的⼊参值;最后kretprobe从名字种就可以看出其⽤途了,它同样基于kprobe实现,⽤于获取被探测函数的返回值。
kprobes的技术原理并不仅仅包含存软件的实现⽅案,它也需要硬件架构提供⽀持。其中涉及硬件架构相关的是CPU的异常处理和单步调试技术,前者⽤于让程序的执⾏流程陷⼊到⽤户注册的回调函数中去,⽽后者则⽤于单步执⾏被探测点指令,因此并不是所有的架构均⽀持,⽬前kprobes技术已经⽀持多种架构,包括i386、x86_64、ppc64、ia64、sparc64、arm、ppc和mips(有些架构实现可能并不
完全,具体可参考内核的)。
kprobes的特点与使⽤限制:
1、kprobes允许在同⼀个被被探测位置注册多个kprobe,但是⽬前jprobe却不可以;同时也不允许以其他的jprobe回掉函数和kprobe的post_handler回调函数作为被探测点。
2、⼀般情况下,可以探测内核中的任何函数,包括中断处理函数。不过在kernel/kprobes.c和arch/*/kernel/kprobes.c程序中⽤于实现kprobes⾃⾝的函数是不允许被探测的,另外还有do_page_fault和notifier_call_chain;
3、如果以⼀个内联函数为探测点,则kprobes可能⽆法保证对该函数的所有实例都注册探测点。由于gcc可能会⾃动将某些函数优化为内联函数,因此可能⽆法达到⽤户预期的探测效果;
4、⼀个探测点的回调函数可能会修改被探测函数运⾏的上下⽂,例如通过修改内核的数据结构或者保存与struct pt_regs结构体中的触发探测之前寄存器信息。因此kprobes可以被⽤来安ug修复代码或者注⼊故障测试代码;
5、kprobes会避免在处理探测点函数时再次调⽤另⼀个探测点的回调函数,例如在printk()函数上注册了探测点,则在它的回调函数中可能再次调⽤printk函数,此时将不再触发printk探测点的回调,仅仅
时增加了kprobe结构体中nmissed字段的数值;
6、在kprobes的注册和注销过程中不会使⽤mutex锁和动态的申请内存;
7、kprobes回调函数的运⾏期间是关闭内核抢占的,同时也可能在关闭中断的情况下执⾏,具体要视CPU架构⽽定。因此不论在何种情况下,在回调函数中不要调⽤会放弃CPU的函数(如信号量、mutex锁等);
8、kretprobe通过替换返回地址为预定义的trampoline的地址来实现,因此栈回溯和gcc内嵌函数__builtin_return_address()调⽤将返回trampoline的地址⽽不是真正的被探测函数的返回地址;
9、如果⼀个函数的调⽤此处和返回次数不相等,则在类似这样的函数上注册kretprobe将可能不会达到预期的效果,例如do_exit()函数会存在问题,⽽do_execve()函数和do_fork()函数不会;
10、如果当在进⼊和退出⼀个函数时,CPU运⾏在⾮当前任务所有的栈上,那么往该函数上注册kretprobe可能会导致不可预料的后果,因此,kprobes不⽀持在X86_64的结构下为__switch_to()函数注册kretprobe,将直接返回-EINVAL。
⼆、kprobe原理
下⾯来介绍⼀下kprobe是如何⼯作的。具体流程见下图:
图1 kprobe的⼯作流程
1、当⽤户注册⼀个探测点后,kprobe⾸先备份被探测点的对应指令,然后将原始指令的⼊⼝点替换为断点指令,该指令是CPU架构相关的,如i386和x86_64是int3,arm是设置⼀个未定义指令(⽬前的x86_64架构⽀持⼀种跳转优化⽅案Jump Optimization,内核需开启CONFIG_OPTPROBES选项,该种⽅案使⽤跳转指令来代替断点指令);
2、当CPU流程执⾏到探测点的断点指令时,就触发了⼀个trap,在trap处理流程中会保存当前CPU的寄存器信息并调⽤对应的trap处理函数,该处理函数会设置kprobe的调⽤状态并调⽤⽤户注册的pre_handler回调函数,kprobe会向该函数传递注册的struct kprobe结构地址以及保存的CPU寄存器信息;
3、随后kprobe单步执⾏前⾯所拷贝的被探测指令,具体执⾏⽅式各个架构不尽相同,arm会在异常处理流程中使⽤模拟函数执⾏,⽽
x86_64架构则会设置单步调试flag并回到异常触发前的流程中执⾏;
4、在单步执⾏完成后,kprobe执⾏⽤户注册的post_handler回调函数;
5、最后,执⾏流程回到被探测指令之后的正常流程继续执⾏。
三、kprobe使⽤实例
在分析kprobe的实现之前先来看⼀下如何利⽤kprobe对函数进⾏探测,以便于让我们对kprobre所完成功能有⼀个⽐较清晰的认识。⽬前,使⽤kprobe可以通过两种⽅式,第⼀种是开发⼈员⾃⾏编写内核模块,向内核注册探测点,探测函数可根据需要⾃⾏定制,使⽤灵活⽅便;第⼆种⽅式是使⽤kprobes on ftrace,这种⽅式是kprobe和ftrace结合使⽤,即可以通过kprobe来优化ftrace来跟踪函数的调⽤。下⾯来分别介绍:
1、编写kprobe探测模块
内核提供了⼀个struct kprobe结构体以及⼀系列的内核API函数接⼝,⽤户可以通过这些接⼝⾃⾏实现探测回调函数并实现struct kprobe 结构,然后将它注册到内核的kprobes⼦系统中来达到探测的⽬的。同时在内核的samples/kprobes⽬录下有⼀个例程
kprobe_example.c描述了kprobe模块最简单的编写⽅式,开发者可以以此为模板编写⾃⼰的探测模块。
1.1、kprobe结构体与API介绍
struct kprobe结构体定义如下:
struct kprobe {
struct hlist_node hlist;
/* list of kprobes for multi-handler support */
struct list_head list;
/*count the number of times this probe was temporarily disarmed */ unsigned long nmissed;
/* location of the probe point */
kprobe_opcode_t *addr;
/* Allow user to indicate symbol name of the probe point */
const char *symbol_name;
/* Offset into the symbol */
unsigned int offset;
/
* Called before addr is executed. */
kprobe_pre_handler_t pre_handler;
/* Called after addr is executed, */
kprobe_post_handler_t post_handler;
/*
* ... called if executing addr causes a fault (eg. page fault).
* Return 1 if it handled fault, otherwise kernel will see it.
*/
kprobe_fault_handler_t fault_handler;
/*
* ... called if breakpoint trap occurs in probe handler.
* Return 1 if it handled break, otherwise kernel will see it.
*/
kprobe_break_handler_t break_handler;
/* Saved opcode (which has been replaced with breakpoint) */
kprobe_opcode_t opcode;
/* copy of the original instruction */
struct arch_specific_insn ainsn;
/*
* Indicates various status flags.
* Protected by kprobe_mutex after this kprobe is registered.
*/
u32 flags;
};
其中各个字段的含义如下:
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:状态标记。
涉及的API函数接⼝如下:
int register_kprobe(struct kprobe *kp) //向内核注册kprobe探测点
void unregister_kprobe(struct kprobe *kp) //卸载kprobe探测点
int register_kprobes(struct kprobe **kps, int num) //注册探测函数向量,包含多个探测点x86架构和arm架构区别
void unregister_kprobes(struct kprobe **kps, int num) //卸载探测函数向量,包含多个探测点
int disable_kprobe(struct kprobe *kp) //临时暂停指定探测点的探测
int enable_kprobe(struct kprobe *kp) //恢复指定探测点的探测
1.2、⽤例kprobe_example.c分析与演⽰
该⽤例函数⾮常简单,它实现了内核函数do_fork的探测,该函数会在fork系统调⽤或者内核kernel_thread函数创建进程时被调⽤,触发也⼗分的频繁。下⾯来分析⼀下⽤例代码:
/* 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函数。在模块的
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论