曾经,有一位开发者在维护一个基于 Linux 内核的服务器系统时,遇到了一个棘手的问题:系统时不时会出现短暂的卡顿,排查日志却找不到明显线索。经过一番分析,他怀疑是某个内核函数在特定条件下执行异常。但传统的调试方法,如添加打印语句,需要重新编译内核,不仅繁琐,还可能影响生产环境。
这时,Kprobes 技术进入了他的视野。通过 Kprobes,他在怀疑的内核函数入口处设置了探测点,收集函数的输入参数和执行时间等信息。经过一段时间的监测和分析,终于发现是一个资源竞争问题导致了卡顿。通过修改代码,成功解决了这个困扰已久的难题。这个故事充分展现了 Kprobes 在调试内核问题时的强大作用。
那么,Kprobes 究竟是什么呢?简单来说,Kprobes 是 Linux 内核提供的一种动态调试机制,它允许开发者在不修改内核源码、不重启系统的情况下,对内核函数进行探测。无论是内核开发者排查性能瓶颈,还是驱动开发者调试设备驱动,Kprobes 都能提供极大的便利,让你深入内核执行的 “幕后”,一探究竟。
一、Kprobes 概述
Kprobes 是 Linux 内核中一种强大的动态探测机制,犹如一把 “万能钥匙”,能打开内核函数的 “神秘大门” 。它允许开发者在不重新编译内核、不重启系统的情况下,对内核函数进行实时监测和分析,极大地提高了调试和性能优化的效率。这对于内核开发者来说,无疑是一项 “神器”,让他们能够在不中断系统运行的情况下,深入了解内核的运行状态,快速定位和解决问题。
Kprobes 的出现,解决了传统内核调试方法的诸多痛点。在 Kprobes 之前,开发者若要调试内核函数,往往需要在函数中添加打印语句,然后重新编译内核并重启系统。这个过程不仅繁琐耗时,还可能影响生产环境的稳定性。而 Kprobes 打破了这些限制,它就像一个 “隐形的观察者”,可以随时在运行中的内核函数中插入探测点,收集函数的执行信息,如函数参数、返回值、执行时间等,却不会对内核的正常运行造成干扰。
二、Kprobes 如何运作
2.1关键数据结构
在 Kprobes 的源码世界里,struct kprobe是一个核心结构体,它就像是一个 “探测点管理器”,承载着 Kprobes 运作的关键信息。下面是struct kprobe的简化定义:
复制
struct 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; // 此函数用于在出现内存访问错误时进行处理
kprobe_opcode_t opcode; // 保存被替换的原始指令
struct arch_specific_insn ainsn; // 架构相关的指令信息
u32 flags; // 各种状态标志
};1.2.3.4.5.6.7.8.9.10.11.
其中,addr成员指明了探测点的具体位置,它是 Kprobes 定位内核函数中特定指令的 “导航仪” 。symbol_name则以函数名的形式,为开发者提供了一种更直观的方式来指定探测目标,就像是给探测点贴上了一个清晰的 “标签” 。offset用于精确到函数内部的具体指令,让探测更加细致入微,如同在精密仪器中调整刻度,实现精准探测。
pre_handler、post_handler和fault_handler这三个回调函数,是 Kprobes 与内核交互的 “桥梁” 。pre_handler在被探测指令执行前被调用,就像是比赛前的热身,让开发者有机会提前获取信息、设置环境;post_handler在指令执行后登场,如同赛后的复盘,用于收集指令执行后的结果和状态;fault_handler则在内存访问出错时发挥作用,是处理异常情况的 “救火队员” 。
opcode保存了被断点指令替换的原始指令,确保在探测完成后,内核能够恢复到原本的执行状态,就像在书本中夹了一张书签,方便后续继续阅读。ainsn和flags则分别负责存储架构相关的指令信息和各种状态标志,为 Kprobes 在不同硬件架构上的稳定运行提供支持,以及记录探测点的各种状态,如是否启用、是否出错等。
2.2注册与卸载流程
当我们想要使用 Kprobes 对某个内核函数进行探测时,就需要将探测点注册到内核中。这个过程就像是在图书馆的书架上添加一本新书,需要遵循一定的流程。下面是注册 Kprobes 探测点的关键代码示例:
复制
#include <linux/module.h>
#include <linux/kprobes.h>
// 定义pre_handler回调函数
static int handler_pre(struct kprobe *p, struct pt_regs *regs) {
pr_info("< %s > pre_handler: p->addr = 0x%p, ip = %lx, flags = 0x%lx\n",
p->symbol_name, p->addr, regs->ip, regs->flags);
return 0;
}
// 定义post_handler回调函数
static void handler_post(struct kprobe *p, struct pt_regs *regs, unsigned long flags) {
pr_info("< %s > post_handler: p->addr = 0x%p, flags = 0x%lx\n",
p->symbol_name, p->addr, flags);
}
// 定义fault_handler回调函数
int handler_fault(struct kprobe *p, struct pt_regs *regs, int trapnr) {
pr_info("fault_handler: p->addr = 0x%p, trap #%d\n", p->addr, trapnr);
return 0;
}
// 定义kprobe结构
static struct kprobe kp = {
.symbol_name = "do_fork", // 要追踪的内核函数为do_fork
.pre_handler = handler_pre, // pre_handler回调函数
.post_handler = handler_post, // post_handler回调函数
.fault_handler = handler_fault // fault_handler回调函数
};
// 模块初始化函数,用于注册kprobe
static int __init kprobe_init(void) {
int ret;
ret = register_kprobe(&kp);
if (ret < 0) {
pr_err("register_kprobe failed, returned %d\n", ret);
return ret;
}
pr_info("Planted kprobe at %p\n", kp.addr);
return 0;
}
// 模块退出函数,用于卸载kprobe
static void __exit kprobe_exit(void) {
unregister_kprobe(&kp);
pr_info("kprobe at %p unregistered\n", kp.addr);
}
module_init(kprobe_init);
module_exit(kprobe_exit);
MODULE_LICENSE("GPL");1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.
在上述代码中,首先定义了三个回调函数handler_pre、handler_post和handler_fault,分别用于在被探测指令执行前、执行后和出现内存访问错误时执行。然后,创建了一个struct kprobe结构体实例kp,并指定要探测的内核函数为do_fork,同时将三个回调函数与kp关联起来。
在kprobe_init函数中,通过调用register_kprobe函数将kp注册到内核中。如果注册成功,会打印出探测点的地址;如果失败,则打印错误信息并返回错误码。
2.3回调函数机制
Kprobes 的回调函数机制是其实现动态探测的核心,它就像是一个精心编排的 “演出”,每个回调函数在不同的时刻登场,为开发者提供内核运行时的关键信息。
pre_handler回调函数在被探测指令执行前被触发,此时,内核的执行流程就像行驶到一个岔路口,暂时停下,先进入pre_handler函数。在这个函数中,开发者可以获取当前的寄存器状态、被探测函数的参数等信息。例如,在前面的代码中,handler_pre函数通过pr_info函数打印出了被探测函数的名称、探测点地址、指令指针ip和标志寄存器flags的值,这些信息就像是内核运行时的 “快照”,为开发者分析问题提供了重要线索。
post_handler回调函数则在被探测指令执行完毕后闪亮登场。它就像是在一场比赛结束后,对比赛结果进行总结和分析。在这个函数中,开发者可以获取指令执行后的返回值、内核状态的变化等信息。同样以之前的代码为例,handler_post函数打印出了被探测函数的名称、探测点地址和标志寄存器的值,帮助开发者了解指令执行后的情况。
fault_handler回调函数是在执行pre_handler、post_handler或单步执行被探测指令时出现内存访问异常时被调用的。它就像是一个紧急救援小组,在出现问题时迅速响应。当内核在执行探测相关操作时发生内存错误,fault_handler函数会被触发,开发者可以在这个函数中进行错误处理,如打印错误信息、记录故障现场等,以便后续排查问题。
通过这三个回调函数的协同工作,Kprobes 为开发者打造了一个全方位、多层次的内核探测体系,让开发者能够深入了解内核的运行细节,快速定位和解决问题。
三、Kprobe实现原理
当安装一个kprobes探測点时。kprobe首先备份被探測的指令,然后使用断点指令(即在i386和x86_64的int3指令)来代替被探測指令的头一个或几个字节。当CPU运行到探測点时,将因运行断点指令而运行trap操作,那将导致保存CPU的寄存器,调用对应的trap处理函数。而trap处理函数将调用对应的notifier_call_chain(内核中一种异步工作机制)中注冊的全部notifier函数。kprobe正是通过向trap对应的notifier_call_chain注冊关联到探測点的处理函数来实现探測处理的。
当kprobe注冊的notifier被运行时,它首先运行关联到探測点的pre_handler函数,并把对应的kprobe struct和保存的寄存器作为该函数的參数,接着,kprobe单步运行被探測指令的备份。最后,kprobe运行post_handler。等全部这些运行完成后。紧跟在被探測指令后的指令流将被正常运行。
kretprobe也使用了kprobes来实现,当用户调用register_kretprobe()时,kprobe在被探測函数的入口建立了一个探測点。当运行到探測点时,kprobe保存了被探測函数的返回地址并代替返回地址为一个trampoline的地址,kprobe在初始化时定义了该trampoline而且为该trampoline注冊了一个kprobe,当被探測函数运行它的返回指令时。控制传递到该trampoline,因此kprobe已经注冊的相应于trampoline的处理函数将被运行。而该处理函数会调用用户关联到该kretprobe上的处理函数。处理完成后,设置指令寄存器指向已经备份的函数返回地址。因而原来的函数返回被正常运行。
被探測函数的返回地址保存在类型为kretprobe_instance的变量中。结构kretprobe的maxactive字段指定了被探測函数能够被同一时候探測的实例数,函数register_kretprobe()将预分配指定数量的kretprobe_instance。假设被探測函数是非递归的而且调用时已经保持了自旋锁(spinlock),那么maxactive为1就足够了;假设被探測函数是非递归的且执行时是抢占失效的,那么maxactive为NR_CPUS就能够了;假设maxactive被设置为小于等于0, 它被设置到缺省值(假设抢占使能, 即配置了 CONFIG_PREEMPT,缺省值为10和2*NR_CPUS中的最大值,否则缺省值为NR_CPUS)。
假设maxactive被设置的太小了,一些探測点的运行可能被丢失,可是不影响系统的正常运行,在结构kretprobe中nmissed字段将记录被丢失的探測点运行数,它在返回探測点被注冊时设置为0,每次当运行探測函数而没有kretprobe_instance可用时,它就加1。
四 、Kprobe限制
kprobe同意在同一地址注册多个kprobes,可是不能同一时候在该地址上有多个jprobes。通常,用户能够在内核的不论什么位置注冊探測点,特别是能够对中断处理函数注冊探測点,可是也有一些例外。假设用户尝试在实现kprobe的代码(包含kernel/kprobes.c和arch/*/kernel/kprobes.c以及do_page_fault和notifier_call_chain)中注冊探測点。register_*probe将返回-EINVAL。
假设为一个内联(inline)函数注冊探測点,kprobe无法保证对该函数的全部实例都注冊探測点,由于gcc可能隐式地内联一个函数。因此,要记住,用户可能看不到预期的探測点的运行。一个探測点处理函数可以改动被探測函数的上下文,如改动内核数据结构,寄存器等。因此,kprobe可以用来安装bug解决代码或注入一些错误或測试代码。
假设一个探測处理函数调用了还有一个探測点,该探測点的处理函数不将执行,可是它的nmissed数将加1。多个探測点处理函数或同一处理函数的多个实例可以在不同的CPU上同一时候执行。除了注冊和卸载,kprobe不会使用mutexe或分配内存。探測点处理函数在执行时是失效抢占的。依赖于特定的架构,探測点处理函数执行时也可能是中断失效的。因此,对于不论什么探測点处理函数,不要使用导致睡眠或进程调度的不论什么内核函数(如尝试获得semaphore)。
kretprobe是通过代替返回地址为提前定义的trampoline的地址来实现的。因此栈回溯和gcc内嵌函数__builtin_return_address()调用将返回trampoline的地址而不是真正的被探測函数的返回地址。
假设一个函数的调用次数与它的返回次数不同样,那么在该函数上注冊的kretprobe探測点可能产生无法预料的结果(do_exit()就是一个典型的样例,但do_execve() 和 do_fork()没有问题)。
五、怎样在内核中引入Kprobe
probe已经被包括在2.6内核中。可是仅仅有最新的内核才提供了上面描写叙述的所有功能,因此假设读者想实验本文附带的内核模块,须要最新的内核,作者在2.6.18内核上測试的这些代码。内核缺省时并没有使能kprobe,因此用户需使能它。
为了使能kprobe。用户必须在编译内核时设置CONFIG_KPROBES,即选择在“Instrumentation Support“中的“Kprobes”项。假设用户希望动态载入和卸载使用kprobe的模块,还必须确保“Loadable module support” (CONFIG_MODULES)和“Module unloading” (CONFIG_MODULE_UNLOAD)设置为y。假设用户还想使用kallsyms_lookup_name()来得到被探測函数的地址,也要确保CONFIG_KALLSYMS设置为y,当然设置CONFIG_KALLSYMS_ALL为y将更好。
内核中引入Kprobe需要进行以下步骤:
首先需要确认内核版本是否支持Kprobe,可以通过查询文档或者源代码来确定。在内核配置文件中开启CONFIG_KPROBES选项。编译内核,并安装新的内核。写一个 Kprobe 监听函数,在该函数中可以添加相应的逻辑,例如日志输出、性能统计等等。Kprobe 监听函数需要使用 Kprobe API 来注册到系统中。使用 insmod 命令加载编写好的模块,即可开始监听指定的内核函数并执行相应操作。
六、Kprobe使用实例
6.1编写简单的 Kprobes 探测模块
接下来,让我们通过一个具体的例子,来深入了解如何编写一个简单的 Kprobes 探测模块。假设我们要探测do_sys_open函数,这个函数负责处理系统的文件打开操作,在实际的系统调试中,了解文件打开的具体情况,如文件名、打开标志等信息,对于排查文件相关的问题非常有帮助。以下是详细的代码实现:
复制
#include <linux/module.h>
#include <linux/kprobes.h>
#include <linux/sched.h>
// 定义一个计数器,用于统计函数被调用的次数
static int count = 0;
// pre_handler回调函数,在被探测指令执行前被调用
static int handler_pre(struct kprobe *p, struct pt_regs *regs) {
// 从寄存器中获取文件名和标志信息
char *filename = (char *)regs->di;
int flags = (int)regs->si;
// 打印函数调用信息,包括文件名和标志
printk(KERN_INFO "do_sys_open called with filename=%s, flags=%x\n", filename, flags);
// 计数器加一
count++;
return 0;
}
// 定义kprobe结构,指定要探测的函数为do_sys_open,并关联pre_handler回调函数
static struct kprobe kp = {
.symbol_name = "do_sys_open",
.pre_handler = handler_pre,
};
// 模块初始化函数,用于注册kprobe
static int __init mymodule_init(void) {
int ret;
// 调用register_kprobe函数注册kprobe
ret = register_kprobe(&kp);
if (ret < 0) {
// 如果注册失败,打印错误信息
printk(KERN_INFO "register_kprobe failed\n");
return ret;
}
// 如果注册成功,打印成功信息
printk(KERN_INFO "kprobe registered\n");
return 0;
}
// 模块退出函数,用于卸载kprobe
static void __exit mymodule_exit(void) {
// 调用unregister_kprobe函数卸载kprobe
unregister_kprobe(&kp);
// 打印卸载信息,包括函数被调用的次数
printk(KERN_INFO "kprobe unregistered\n");
printk(KERN_INFO "do_sys_open called %d times\n", count);
}
// 声明模块初始化和退出函数
module_init(mymodule_init);
module_exit(mymodule_exit);
// 指定模块许可证为GPL
MODULE_LICENSE("GPL");1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.52.53.54.
在上述代码中,首先定义了一个count变量,用于统计do_sys_open函数被调用的次数。handler_pre函数是pre_handler回调函数,它从寄存器中获取do_sys_open函数的参数filename和flags,并通过printk函数打印出来,同时将count加一。
然后,创建了一个struct kprobe结构体实例kp,指定要探测的函数为do_sys_open,并将handler_pre函数关联到kp的pre_handler成员。
在mymodule_init函数中,通过register_kprobe函数将kp注册到内核中,如果注册失败,打印错误信息并返回错误码;如果注册成功,打印成功信息。
在mymodule_exit函数中,通过unregister_kprobe函数将kp从内核中卸载,并打印卸载信息和do_sys_open函数被调用的次数。
6.2基于ftrace使用kprobe
kprobe和内核的ftrac结合使用,需要对内核进行配置,然后添加探测点、进行探测、查看结果。
(1)kprobe配置
打开"General setup"->"Kprobes",以及"Kernel hacking"->"Tracers"->"Enable kprobes-based dynamic events"。
复制
CONFIG_KPROBES=y
CONFIG_OPTPROBES=y
CONFIG_KPROBES_ON_FTRACE=y
CONFIG_UPROBES=y
CONFIG_KRETPROBES=y
CONFIG_HAVE_KPROBES=y
CONFIG_HAVE_KRETPROBES=y
CONFIG_HAVE_OPTPROBES=y
CONFIG_HAVE_KPROBES_ON_FTRACE=y
CONFIG_KPROBE_EVENT=y1.2.3.4.5.6.7.8.9.10.
(2)kprobe trace events使用
kprobe事件相关的节点有如下:
复制
/sys/kernel/debug/tracing/kprobe_events-----------------------配置kprobe事件属性,增加事件之后会在kprobes下面生成对应目录。
/sys/kernel/debug/tracing/kprobe_profile----------------------kprobe事件统计属性文件。
/sys/kernel/debug/tracing/kprobes/<GRP>/<EVENT>/enabled-------使能kprobe事件
/sys/kernel/debug/tracing/kprobes/<GRP>/<EVENT>/filter--------过滤kprobe事件
/sys/kernel/debug/tracing/kprobes/<GRP>/<EVENT>/format--------查询kprobe事件显示格式1.2.3.4.5.
下面就结合实例,看一下如何使用kprobe事件。
(3)kprobe事件配置
新增一个kprobe事件,通过写kprobe_events来设置。
复制
p[:[GRP/]EVENT] [MOD:]SYM[+offs]|MEMADDR [FETCHARGS]-------------------设置一个probe探测点
r[:[GRP/]EVENT] [MOD:]SYM[+0] [FETCHARGS]------------------------------设置一个return probe探测点
-:[GRP/]EVENT----------------------------------------------------------删除一个探测点1.2.3.
细节解释如下:
复制
GRP : Group name. If omitted, use "kprobes" for it.------------设置后会在events/kprobes下创建<GRP>目录。
EVENT : Event name. If omitted, the event name is generated based on SYM+offs or MEMADDR.---指定后在events/kprobes/<GRP>生成<EVENT>目录。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)----------------------------------获取指定栈空间值,即sp寄存器+N后的位置值
$stack : Fetch stack address.-----------------------------------------------获取sp寄存器值
$retval : Fetch return value.(*)--------------------------------------------获取返回值,用户return kprobe
$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.1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.
执行如下两条命令就会生成目录/sys/kernel/debug/tracing/events/kprobes/myprobe;第三条命令则可以删除指定kprobe事件,如果要全部删除则echo > /sys/kernel/debug/tracing/kprobe_events。
复制
echo p:myprobe do_sys_open dfd=%ax filename=%dx flags=%cx mode=+4($stack) > /sys/kernel/debug/tracing/kprobe_events
echo r:myretprobe do_sys_open ret=$retval >> /sys/kernel/debug/tracing/kprobe_events-----------------------------------------------------这里面一定要用">>",不然就会覆盖前面的设置。
echo -:myprobe >> /sys/kernel/debug/tracing/kprobe_events
echo -:myretprobe >> /sys/kernel/debug/tracing/kprobe_events1.2.3.4.5.
参数后面的寄存器是跟架构相关的,%ax、%dx、%cx表示第1/2/3个参数,超出部分使用$stack来存储参数。
函数返回值保存在$retval中。
(4)kprobe使能
对kprobe事件的是能通过往对应事件的enable写1开启探测;写0暂停探测。
复制
echo > /sys/kernel/debug/tracing/trace
echo p:myprobe do_sys_open dfd=%ax filename=%dx flags=%cx mode=+4($stack) > /sys/kernel/debug/tracing/kprobe_events
echo r:myretprobe do_sys_open ret=$retval >> /sys/kernel/debug/tracing/kprobe_events
echo 1 > /sys/kernel/debug/tracing/events/kprobes/myprobe/enable
echo 1 > /sys/kernel/debug/tracing/events/kprobes/myretprobe/enable
ls
echo 0 > /sys/kernel/debug/tracing/events/kprobes/myprobe/enable
echo 0 > /sys/kernel/debug/tracing/events/kprobes/myretprobe/enable
cat /sys/kernel/debug/tracing/trace1.2.3.4.5.6.7.8.9.10.11.
然后在/sys/kernel/debug/tracing/trace中可以看到结果。
复制
sourceinsight4.-3356 [000] .... 3542865.754536: myprobe: (do_sys_open+0x0/0x290) dfd=0xffffffffbd6764a0 filename=0x8000 flags=0x1b6 mode=0xe3afff48ffffffff
bash-26041 [001] .... 3542865.757014: myprobe: (do_sys_open+0x0/0x290) dfd=0xffffffffbd676460 filename=0x8241 flags=0x1b6 mode=0xe0c0ff48ffffffff
ls-18078 [005] .... 3542865.757950: myprobe: (do_sys_open+0x0/0x290) dfd=0xffffffffbd676460 filename=0x88000 flags=0x1 mode=0xc1b7bf48ffffffff
ls-18078 [005] d... 3542865.757953: myretprobe: (SyS_open+0x1e/0x20 <- do_sys_open) ret=0x3
ls-18078 [005] .... 3542865.757966: myprobe: (do_sys_open+0x0/0x290) dfd=0xffffffffbd676460 filename=0x88000 flags=0x6168 mode=0xc1b7bf48ffffffff
ls-18078 [005] d... 3542865.757969: myretprobe: (SyS_open+0x1e/0x20 <- do_sys_open) ret=0x3
ls-18078 [005] .... 3542865.758001: myprobe: (do_sys_open+0x0/0x290) dfd=0xffffffffbd676460 filename=0x88000 flags=0x6168 mode=0xc1b7bf48ffffffff
ls-18078 [005] d... 3542865.758004: myretprobe: (SyS_open+0x1e/0x20 <- do_sys_open) ret=0x3
ls-18078 [005] .... 3542865.758030: myprobe: (do_sys_open+0x0/0x290) dfd=0xffffffffbd676460 filename=0x88000 flags=0x1000 mode=0xc1b7bf48ffffffff
ls-18078 [005] d... 3542865.758033: myretprobe: (SyS_open+0x1e/0x20 <- do_sys_open) ret=0x3
ls-18078 [005] .... 3542865.758055: myprobe: (do_sys_open+0x0/0x290) dfd=0xffffffffbd676460 filename=0x88000 flags=0x1000 mode=0xc1b7bf48ffffffff
ls-18078 [005] d... 3542865.758057: myretprobe: (SyS_open+0x1e/0x20 <- do_sys_open) ret=0x3
ls-18078 [005] .... 3542865.758080: myprobe: (do_sys_open+0x0/0x290) dfd=0xffffffffbd676460 filename=0x88000 flags=0x19d0 mode=0xc1b7bf48ffffffff
ls-18078 [005] d... 3542865.758082: myretprobe: (SyS_open+0x1e/0x20 <- do_sys_open) ret=0x3
ls-18078 [005] .... 3542865.758289: myprobe: (do_sys_open+0x0/0x290) dfd=0xffffffffbd676460 filename=0x8000 flags=0x1b6 mode=0xc1b7bf48ffffffff
ls-18078 [005] d... 3542865.758297: myretprobe: (SyS_open+0x1e/0x20 <- do_sys_open) ret=0x3
ls-18078 [005] .... 3542865.758339: myprobe: (do_sys_open+0x0/0x290) dfd=0xffffffffbd676460 filename=0x88000 flags=0x0 mode=0xc1b7bf48ffffffff
ls-18078 [005] d... 3542865.758343: myretprobe: (SyS_open+0x1e/0x20 <- do_sys_open) ret=0x3
ls-18078 [005] .... 3542865.758444: myprobe: (do_sys_open+0x0/0x290) dfd=0xffffffffbd676460 filename=0x98800 flags=0x2 mode=0xc1b7bf48ffffffff
ls-18078 [005] d... 3542865.758446: myretprobe: (SyS_open+0x1e/0x20 <- do_sys_open) ret=0x3
bash-26041 [001] .... 3542865.760416: myprobe: (do_sys_open+0x0/0x290) dfd=0xffffffffbd676460 filename=0x8241 flags=0x1b6 mode=0xe0c0ff48ffffffff
bash-26041 [001] d... 3542865.760426: myretprobe: (SyS_open+0x1e/0x20 <- do_sys_open) ret=0x3
bash-26041 [001] d... 3542865.793477: myretprobe: (SyS_open+0x1e/0x20 <- do_sys_open) ret=0x31.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.
(5)kprobe事件过滤
跟踪函数需要通过filter进行过滤,可以有效过滤掉冗余信息。filter文件用于设置过滤条件,可以减少trace中输出的信息,它支持的格式和c语言的表达式类似,支持 ==,!=,>,<,>=,<=判断,并且支持与&&,或||,还有()。
复制
echo filename==0x8241 > /sys/kernel/debug/tracing/events/kprobes/myprobe/filter1.
(6)kprobe和栈配合使用
如果要在显示函数的同时显示其栈信息,可以通过配置trace_options来达到。
复制
echo stacktrace > /sys/kernel/debug/tracing/trace_options1.
(7)kprobe_profile统计信息
获取一段kprobe时间之后,可以再kprobe_profile中查看统计信息。
后面两列分别表示命中和未命中的次数。
复制
cat /sys/kernel/debug/tracing/kprobe_profile myprobe1.
6.3调试工具搭配使用
在使用 Kprobes 进行调试时,搭配其他工具可以更高效地分析和解决问题,就像一场精彩的交响乐,不同的乐器相互配合,才能演奏出美妙的旋律。
查看内核日志是一个非常重要的辅助手段。在前面的代码中,我们使用了printk函数来输出调试信息,这些信息会被记录到内核日志中。通过查看内核日志,我们可以了解 Kprobes 探测模块的运行情况,如探测点是否成功注册、回调函数是否被正确调用、函数的参数和执行结果等。在 Linux 系统中,可以使用dmesg命令来查看内核日志,例如:dmesg | grep "do_sys_open",这个命令会过滤出内核日志中与do_sys_open相关的信息,方便我们快速定位问题。
gdb调试器也能与 Kprobes 配合使用,为调试工作提供更多便利。虽然 Kprobes 主要用于动态调试运行中的内核,但在某些情况下,结合gdb可以更深入地分析问题。比如,当 Kprobes 探测到某个函数出现异常,但通过printk输出的信息不足以定位问题时,可以使用gdb来调试内核模块。首先,需要在内核编译时开启调试信息,然后使用gdb加载内核和内核模块,通过设置断点、单步执行等操作,详细分析函数的执行过程,找出问题的根源。
6.4常见问题与解决方法
在使用 Kprobes 的过程中,可能会遇到一些常见问题,这些问题就像是前进道路上的绊脚石,但只要我们掌握了解决方法,就能轻松跨越。
探测点无法注册是一个常见的问题。这可能是由于目标函数不存在、符号未导出或内核保护等原因导致的。当遇到这种情况时,首先要确认目标函数是否存在,可以通过查看内核源码或使用nm命令查看内核符号表来确认。如果函数存在,再检查符号是否导出,可以查看/proc/kallsyms文件,看目标函数的符号是否在其中。如果是内核保护导致的问题,例如内核处于写保护状态,可能需要临时关闭相关保护机制,但这需要谨慎操作,因为关闭保护机制可能会影响系统的稳定性和安全性。
回调函数未按预期执行也是一个需要关注的问题。这可能是由于回调函数中存在错误,如内存访问越界、空指针引用等,导致回调函数执行异常。在编写回调函数时,要确保代码的正确性和健壮性,避免出现这些常见的错误。同时,要注意回调函数的执行环境,因为回调函数运行在中断上下文中,所以不能执行可能会导致阻塞的操作,如睡眠、等待信号量等。如果需要进行一些复杂的操作,可以将这些操作放到工作队列或内核线程中执行,以避免影响回调函数的正常执行。