在 Linux 系统的广袤世界里,内核调试就像是一位幕后英雄,默默支撑着整个系统的稳定运行与高效表现。无论是系统性能的优化,还是故障排查,内核调试都扮演着不可或缺的角色。想象一下,当你的服务器突然出现卡顿,或者某个关键服务莫名崩溃,这时候,精准的内核调试技术就如同黑暗中的明灯,帮助你快速定位问题,解决故障。而在众多的内核调试技术中,kprobe 无疑是一颗耀眼的明星。它是 Linux 内核提供的一种动态调试机制,允许开发者在不修改内核源码、不重启系统的情况下,对内核函数进行探测。
这就好比在不拆开精密仪器的前提下,能够直接观察其内部关键部件的运转情况,是不是很神奇?kprobe 的出现,极大地改变了内核调试的方式。在它诞生之前,开发者若要调试内核函数,往往需要在函数中添加大量的打印语句,然后重新编译内核,再重启系统才能生效。这个过程不仅繁琐耗时,还可能因为频繁重启影响到线上业务的正常运行。而 kprobe 则打破了这些限制,让内核调试变得更加灵活高效,随时随地都能对内核函数进行监测与分析 。
一、初相识 kprobe
1.1 kprobe 是什么
kprobe,即 Kernel Probes,是 Linux 内核精心打造的一种极为强大的动态跟踪技术 。它就像是一个灵活的 “观察者”,赋予开发者在不修改内核代码、不重启整个系统的前提下,于内核函数的特定位置巧妙插入探测点的神奇能力。这些探测点就如同一个个 “小哨兵”,实时监控着内核函数的一举一动。
当内核执行的流程运行到我们预先设置好的探测点时,kprobe 会迅速做出反应,执行我们提前定义好的处理函数。这个处理函数可以根据我们的需求,完成各种各样的任务,比如精准地记录下函数的参数值,实时监测函数的执行时间,甚至还能在必要时对函数的执行逻辑进行巧妙的干预。通过这种方式,开发者能够获取到丰富的内核执行信息,从而为深入理解内核的运行机制、高效排查系统故障以及精准优化系统性能提供了坚实有力的支持 。
1.2 kprobe 的类型
kprobe 主要包含三种类型,它们各自有着独特的功能和应用场景,就像一个团队里不同分工的成员,共同协作完成内核调试的任务。
- kprobes:作为 kprobe 家族中最基础、最通用的成员,kprobes 拥有令人惊叹的灵活性,它几乎可以被插入到内核的任意指令位置。想象一下,内核代码就像一条长长的生产线,kprobes 能够在这条生产线的任何一个环节设置 “观测点”。当内核执行到这个探测点时,就会如同触发了一个预设的机关,依次执行我们精心编写的 pre_handler 函数和 post_handler 函数。pre_handler 函数就像是在任务开始前的准备工作,它可以在指令执行前,对现场环境进行详细的记录,比如记录下当前寄存器的状态,获取函数的参数值等,为后续的分析提供第一手资料。而 post_handler 函数则像是任务结束后的总结汇报,它会在指令执行完毕后,对执行结果进行进一步的处理和分析,比如统计函数执行的时间,检查执行结果是否符合预期等。通过这两个函数的紧密配合,我们能够全方位地了解内核函数在执行过程中的各种细节信息 。
- jprobes:jprobes 相对来说比较 “专一”,它只能被插入到内核函数的入口处,就像是一个站在函数大门前的 “守卫”,专门负责迎接函数的启动。虽然它的插入位置相对固定,但它的作用却不可小觑。jprobes 的主要职责是帮助我们轻松获取函数的参数。当函数开始执行时,jprobes 会迅速行动,将函数的参数完整地 “收集” 起来,传递给我们的处理函数。这在很多场景下都非常有用,比如当我们需要分析某个函数在不同参数输入下的行为时,jprobes 就能为我们提供准确的参数信息,让我们的分析更加有的放矢 。需要注意的是,jprobes 处理函数的原型必须与被探测函数完全一致,就像是两个配合默契的搭档,只有这样,jprobes 才能准确无误地获取函数参数,并顺利完成后续的处理工作 。
- kretprobes:kretprobes 则专注于函数的返回阶段,就像是在函数执行结束后进行 “收尾工作” 的 “监督员”。它会在指定的内核函数返回时被触发执行,主要用于获取函数的返回值,以及精确计算函数的执行时间。当函数执行到返回指令时,kretprobes 会巧妙地捕获这个关键时刻,获取到函数的返回值,并根据之前记录的函数开始执行的时间点,精确计算出函数的执行耗时。这对于性能分析来说是非常关键的信息,通过分析不同函数的执行时间,我们可以快速定位到系统中的性能瓶颈,进而有针对性地进行优化 。在使用 kretprobes 时,我们需要注意设置合适的 maxactive 字段,它决定了被探测函数可以被同时探测的实例数,合理设置这个值可以避免因资源不足而导致的探测点执行丢失问题 。
二、kprobe 工作原理
2.1 基于 int3 的 kprobe 实现
在 x86 架构的系统中,CPU 提供了一条特殊的单字节指令 INT3,其机器码为 0xcc ,专门用于触发调试异常(#BP,向量号 3),这就像是一个预先设定好的 “警报器”。而 kprobe 正是巧妙地借用了这个 “警报器” 来实现对内核函数的探测。
当我们通过 register_kprobe()函数注册一个 kprobe 探测点时,一场精心策划的 “指令替换行动” 就开始了。kprobe 子系统会迅速行动,首先精准地查找我们指定符号对应的虚拟地址,这个地址就是我们想要探测的内核函数的关键位置。找到地址后,它会小心翼翼地将该地址处的第一个字节备份起来,这就好比是给原指令拍了一张 “照片”,以便后续恢复。然后,它会写入 0xcc,用这个断点指令替代原指令的第一个字节,就像是在原来的指令轨道上放置了一个特殊的 “路障” 。同时,kprobe 子系统还会建立异常处理关联,确保当 CPU 执行到这个被替换为 0xcc 的位置时,能够顺利进入 kprobe 的处理流程 。
一旦 CPU 执行到这个被设置了断点指令的位置,就如同触发了警报一样,会立即抛出 #BP 异常,然后转入 do_int3()异常处理函数。而此时,kprobe 早已通过 int3_handler 注册了自己的钩子。在这个钩子函数中,它会获取当前 CPU 寄存器的状态,这些寄存器就像是记录着内核执行过程的 “小账本”,里面包含了程序计数器(RIP)、栈指针(RSP)、标志寄存器(RFLAGS)等重要信息 。接着,kprobe 会根据这些寄存器的信息,准确地找到对应的 kprobe 结构体,这个结构体就像是一个 “任务清单”,记录了我们为这个探测点设置的各种信息,比如 pre_handler、post_handler 等处理函数的地址 。
在找到 kprobe 结构体后,kprobe 会先执行 pre_handler 函数,这个函数就像是一场演出前的 “预热表演”,它可以在指令执行前,对现场环境进行详细的记录和分析,比如记录下当前寄存器的状态,获取函数的参数值等 。执行完 pre_handler 函数后,kprobe 会恢复原始指令的一个副本,并设置 CPU 进入单步执行模式。在单步执行模式下,CPU 会一条一条地执行指令,这样 kprobe 就可以精确地监控指令的执行过程 。
当原始指令执行完毕后,kprobe 会执行 post_handler 函数,这个函数就像是演出结束后的 “总结发言”,它会对指令的执行结果进行进一步的处理和分析,比如统计函数执行的时间,检查执行结果是否符合预期等 。最后,kprobe 会恢复正常的指令执行流程,让内核继续按照原来的计划运行 。
2.2 kretprobe 的实现机制
kretprobe 的实现同样巧妙,它主要利用了 kprobes 在函数入口处设置探测点的能力 。当我们调用 register_kretprobe()函数来注册一个 kretprobe 时,实际上是在被探测函数的入口处建立了一个 kprobe 探测点 。
当内核执行到这个位于函数入口的探测点时,kprobe 会迅速行动,它会保存被探测函数的返回地址,这个返回地址就像是函数执行结束后要回到的 “家” 的地址。然后,kprobe 会将返回地址替换为一个预先定义好的 trampoline 地址 。这个 trampoline 就像是一个 “中转站”,当函数执行到返回指令时,控制流会被引导到这个 “中转站” 。
在 kprobe 初始化的时候,就已经为这个 trampoline 注册了一个 kprobe,并且关联了相应的处理函数 。当被探测函数执行到返回指令时,控制传递到 trampoline,此时 kprobe 注册的对应于 trampoline 的处理函数就会被执行 。这个处理函数会调用我们用户关联到该 kretprobe 上的处理函数,在这个处理函数中,我们可以获取函数的返回值,以及计算函数的执行时间等 。处理完毕后,kprobe 会设置指令寄存器指向已经备份的函数返回地址,就像是给函数指明了回家的路,让原来的函数返回能够正常执行 。
在这个过程中,被探测函数的返回地址会被保存在类型为 kretprobe_instance 的变量中,这个变量就像是一个 “包裹”,里面装着与被探测函数执行实例相关的重要信息 。kretprobe 结构体中的 maxactive 字段则指定了被探测函数可以被同时探测的实例数,这就像是一个 “容量限制器”,它决定了在同一时间内能够被 kretprobe 跟踪的函数执行实例的最大数量 。
当我们调用 register_kretprobe()函数时,会预先分配指定数量的 kretprobe_instance,以确保有足够的 “包裹” 来存放每个被探测函数执行实例的相关信息 。如果 maxactive 设置得太小,当被探测函数的调用非常频繁,同时有大量的执行实例需要被跟踪时,就可能会出现一些探测点的执行被丢失的情况 。不过,不用担心,kretprobe 结构体中的 nmissed 字段会忠实地记录下这些被丢失的探测点执行数,它就像是一个 “记录员”,在返回探测点被注册时,nmissed 会被设置为 0,每次当执行探测函数而没有可用的 kretprobe_instance 时,它就会加 1,通过查看这个字段的值,我们就可以知道是否存在探测点执行丢失的情况,以及丢失的数量 。
三、kprobe 使用指南:从基础到进阶
3.1 环境准备
在正式开启 kprobe 的探索之旅前,我们得先做好一系列的环境准备工作,就像是一场冒险前要准备好装备一样。首先,要确保你的内核已经开启了 CONFIG_KPROBE_EVENT 选项。这一步至关重要,它就像是打开 kprobe 大门的钥匙。你可以通过查看内核配置文件(通常位于/boot/config-版本号 )来确认这一点,如果文件中存在 CONFIG_KPROBE_EVENT=y,那就说明该选项已经开启。要是没有找到,你可能需要重新编译内核并开启此选项 。
另外,挂载 debugfs 也是必不可少的步骤。debugfs 是一个特殊的文件系统,专门用于调试,它就像是一个调试工具的 “仓库”。使用以下命令挂载 debugfs:
mount -t debugfs none /sys/kernel/debug
这行命令会将 debugfs 挂载到/sys/kernel/debug 目录下,为后续使用 kprobe 提供了必要的文件系统支持 。
3.2 使用 debugfs 创建 kprobe
(1)挂载与进入目录:在完成 debugfs 的挂载后,我们需要进入 kprobes 目录,这里是我们设置 kprobe 探测点的 “战场”。使用 cd 命令进入:
cd /sys/kernel/debug/kprobes
进入这个目录后,我们就可以开始进行各种与 kprobe 相关的操作了 。
(2)设置探测点:以追踪 do_fork 函数为例,这是一个在进程创建过程中非常关键的函数,追踪它可以让我们深入了解进程创建的细节。设置探测点的命令如下:
echo 『p:myprobe do_fork』 > events
在这个命令中,p 表示我们要创建的是一个普通的 kprobe 探测点;myprobe 是我们给这个探测点取的名字,就像是给一个士兵取个独特的代号,方便我们识别和管理;do_fork 则是我们要探测的目标函数 。通过执行这个命令,我们就在 do_fork 函数上成功设置了一个名为 myprobe 的探测点 。
(3)启用与查看结果:设置好探测点后,还需要启用它,让这个 “小哨兵” 开始工作。启用事件的命令很简单:
echo 1 > events/myprobe/enable
执行这个命令后,myprobe 探测点就正式开始生效,它会密切关注 do_fork 函数的一举一动 。
要查看追踪结果,我们可以查看 trace 文件,这个文件就像是一本 “记录册”,详细记录了 kprobe 探测到的信息:
cat /sys/kernel/debug/tracing/trace
执行这个命令后,你会看到类似下面这样的输出示例:
bash-3285[001] .... 1234.567890: myprobe: (do_fork+0x0/0x320)
在这个输出中,bash-3285 表示触发事件的进程名和进程 ID;[001]表示 CPU 编号;1234.567890 是时间戳,精确记录了事件发生的时间;myprobe 是我们设置的探测点名称;(do_fork+0x0/0x320)则表示是在 do_fork 函数处触发的事件,+0x0 表示在函数的起始位置,0x320 是函数的长度 。通过这些信息,我们可以清晰地了解到 do_fork 函数的调用情况 。
当我们不再需要这个探测点时,还可以禁用并删除它,让系统恢复到之前的状态。禁用探测点的命令是:
echo 0 > events/myprobe/enable
删除探测点的命令是:
echo『myprobe』 > events/unregister
这样,我们就完成了使用 debugfs 创建、启用、查看和删除 kprobe 探测点的整个过程 。
3.3 编写内核模块注册 kprobe
在编写内核模块注册 kprobe 时,struct kprobe 结构体是我们的核心工具,它就像是一个装满了各种调试信息的 “百宝箱”。这个结构体包含了多个重要字段 :
- symbol_name:用于指定要探测的内核函数名,这是我们定位目标函数的关键信息,就像是给 “百宝箱” 贴上了一个明确的标签 。
- pre_handler:是一个函数指针,指向在探测点指令执行前被调用的回调函数,这个回调函数就像是一个 “先锋”,在函数执行前进行一些预处理工作,比如记录寄存器状态、获取函数参数等 。
- post_handler:同样是一个函数指针,指向在探测点指令执行后被调用的回调函数,它就像是一个 “收尾员”,在函数执行后对结果进行处理和分析 。
- fault_handler:当执行 pre_handler、post_handler 或单步执行被探测指令时出现内存异常,就会调用这个回调函数,它是处理异常情况的 “救火队员” 。
除了 struct kprobe 结构体,register_kprobe 和 unregister_kprobe 函数也非常重要。register_kprobe 函数用于向内核注册一个 kprobe 探测点,就像是把一个新的 “士兵” 编入队伍;unregister_kprobe 函数则用于注销已经注册的 kprobe 探测点,让这个 “士兵” 离开队伍 。
下面是一个监视 do_fork 函数的内核模块代码示例,我们来逐行分析它的逻辑与功能 :
#include <Linux/module.h>#include <Linux/kprobes.h>// 定义 pre_handler 回调函数staticinthandler_pre(struct kprobe *p, struct pt_regs *regs){ printk(KERN_INFO 「Pre handler: %s called\n」, p->symbol_name); return 0;}// 定义 post_handler 回调函数staticvoidhandler_post(struct kprobe *p, struct pt_regs *regs, unsignedlong flags){ printk(KERN_INFO 「Post handler: %s finished\n」, p->symbol_name);}// 定义 kprobe 结构static struct kprobe kp = { .symbol_name = 「do_fork」, .pre_handler = handler_pre, .post_handler = handler_post,};// 模块初始化函数staticint __init kprobe_init(void){ int ret; // 注册 kprobe ret = register_kprobe(&kp); if (ret < 0) { printk(KERN_ERR 「Failed to register kprobe\n」); return ret; } printk(KERN_INFO 「Kprobe registered successfully\n」); return 0;}// 模块退出函数staticvoid __exit kprobe_exit(void){ // 注销 kprobe unregister_kprobe(&kp); printk(KERN_INFO 「Kprobe unregistered successfully\n」);}module_init(kprobe_init);module_exit(kprobe_exit);MODULE_LICENSE(「GPL」);
在这段代码中,首先包含了必要的头文件 Linux/module.h 和 Linux/kprobes.h,它们为我们提供了编写内核模块和使用 kprobe 的基本函数和数据结构 。
接着定义了 handler_pre 和 handler_post 两个回调函数。handler_pre 函数在 do_fork 函数执行前被调用,它使用 printk 函数打印一条信息,表明 do_fork 函数即将被调用;handler_post 函数在 do_fork 函数执行后被调用,同样使用 printk 函数打印一条信息,表明 do_fork 函数已经执行完毕 。
然后定义了 kp 这个 kprobe 结构体变量,并对其 symbol_name、pre_handler 和 post_handler 字段进行了初始化,指定要探测的函数是 do_fork,并关联了前面定义的两个回调函数 。
在 kprobe_init 函数中,通过调用 register_kprobe 函数注册 kp 这个 kprobe 探测点,如果注册成功,打印成功信息,否则打印错误信息并返回错误码 。
在 kprobe_exit 函数中,通过调用 unregister_kprobe 函数注销 kp 这个 kprobe 探测点,并打印注销成功的信息 。
最后,使用 module_init 和 module_exit 宏指定模块的初始化和退出函数,同时声明模块的许可证为 GPL 。
编写好内核模块代码后,我们需要编译它,将代码转换为可执行的模块文件。首先,创建一个 Makefile 文件,内容如下:
obj-m += kprobe_module.oall: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modulesclean: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
这个 Makefile 文件定义了两个目标:all 和 clean。all 目标用于编译模块,它会调用 make 命令,进入内核源代码目录(通过/lib/modules/$(shell uname -r)/build 指定),并在当前目录($(PWD))下编译模块;clean 目标用于清理编译生成的文件 。
在终端中执行 make 命令,就可以开始编译内核模块。编译完成后,会生成一个名为 kprobe_module.ko 的文件,这就是我们的内核模块文件 。
接下来,使用 insmod 命令加载模块:
sudo insmod kprobe_module.ko
加载模块后,可以通过 dmesg 命令查看内核日志,来验证模块是否加载成功以及查看回调函数的输出信息:
如果一切正常,你应该能看到类似下面这样的输出:
[ 1234.567890] Kprobe registered successfully[ 1235.678901] Pre handler: do_fork called[ 1235.678902] Post handler: do_fork finished
这些输出信息表明,我们的 kprobe 模块已经成功注册,并且在 do_fork 函数执行前后,分别调用了 pre_handler 和 post_handler 回调函数 。
当我们不再需要这个模块时,可以使用 rmmod 命令卸载它:
这样,我们就完成了编写内核模块注册 kprobe、编译模块、加载模块以及卸载模块的整个过程 。
3.4 结合 perf 和 ftrace 使用 kprobe
(1)perf 工具集成:perf 是 Linux 系统中一个强大的性能分析工具,它就像是一个专业的 “性能检测员”,可以帮助我们深入了解系统的性能状况。当 perf 与 kprobe 结合使用时,更是如虎添翼 。使用 perf probe 可以方便地添加探测点。例如,要在 do_fork 函数上添加探测点,可以执行以下命令:
perf probe -x /vmlinuz do_fork
在这个命令中,-x 参数指定了内核镜像文件/vmlinuz,do_fork 是我们要探测的函数 。执行这个命令后,perf 会在 do_fork 函数上添加一个探测点 。
添加探测点后,我们可以使用 perf record 命令记录事件。例如,记录 10 秒内的 do_fork 事件:
perf record -e 『probe:do_fork』 -a sleep 10
在这个命令中,-e 参数指定了要记录的事件为 probe:do_fork,表示我们要记录 do_fork 函数上的探测点事件;-a 参数表示记录所有 CPU 上的事件;sleep 10 表示记录时间为 10 秒 。
记录完成后,使用 perf script 命令查看结果:
执行这个命令后,会输出详细的事件记录信息,包括时间戳、CPU 编号、进程 ID、事件类型等,这些信息可以帮助我们分析 do_fork 函数的调用情况和性能表现 。
(2)ftrace 的联动:ftrace 是 Linux 内核中的一个功能强大的跟踪框架,它就像是一个全面的 “跟踪器”,可以跟踪内核的各种活动 。ftrace 可以通过/sys/kernel/debug/tracing/kprobe_events 动态添加事件,与 kprobe 配合使用 。例如,要在 do_fork 函数上添加一个 kprobe 事件,可以执行以下命令:
echo 『p:myprobe do_fork』 > /sys/kernel/debug/tracing/kprobe_events
这个命令的含义与前面使用 debugfs 设置探测点时类似,p 表示添加 kprobe 事件,myprobe 是事件名称,do_fork 是目标函数 。
添加事件后,需要启用 ftrace。首先,进入/sys/kernel/debug/tracing 目录:
cd /sys/kernel/debug/tracing
然后,启用事件:
echo 1 > events/kprobes/myprobe/enable
同时,启用 ftrace:
这样,ftrace 就会开始跟踪 do_fork 函数上的 kprobe 事件 。
要查看跟踪结果,可以查看 trace 文件:
通过查看 trace 文件,我们可以获取到 do_fork 函数的调用时间、调用者等详细信息,这些信息对于分析内核行为和性能优化非常有帮助 。
通过结合 perf 和 ftrace 使用 kprobe,我们可以从不同角度对内核函数进行探测和分析,获取更全面、更深入的系统信息,为内核调试和性能优化提供有力支持 。
四、Kprobe 使用实例
4.1 编写简单的 Kprobes 探测模块
接下来,让我们通过一个具体的例子,来深入了解如何编写一个简单的 Kprobes 探测模块。假设我们要探测 do_sys_open 函数,这个函数负责处理系统的文件打开操作,在实际的系统调试中,了解文件打开的具体情况,如文件名、打开标志等信息,对于排查文件相关的问题非常有帮助。以下是详细的代码实现:
#include <Linux/module.h>#include <Linux/kprobes.h>#include <Linux/sched.h>// 定义一个计数器,用于统计函数被调用的次数static int count = 0;// pre_handler 回调函数,在被探测指令执行前被调用staticinthandler_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,};// 模块初始化函数,用于注册 kprobestaticint __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;}// 模块退出函数,用于卸载 kprobestaticvoid __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);// 指定模块许可证为 GPLMODULE_LICENSE(「GPL」);
在上述代码中,首先定义了一个 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 函数被调用的次数。
4.2 基于 ftrace 使用 kprobe
kprobe 和内核的 ftrac 结合使用,需要对内核进行配置,然后添加探测点、进行探测、查看结果。
(1)kprobe 配置:打开「General setup」->「Kprobes」,以及「Kernel hacking」->「Tracers」->「Enable kprobes-based dynamic events」。
CONFIG_KPROBES=yCONFIG_OPTPROBES=yCONFIG_KPROBES_ON_FTRACE=yCONFIG_UPROBES=yCONFIG_KRETPROBES=yCONFIG_HAVE_KPROBES=yCONFIG_HAVE_KRETPROBES=yCONFIG_HAVE_OPTPROBES=yCONFIG_HAVE_KPROBES_ON_FTRACE=yCONFIG_KPROBE_EVENT=y
(3)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 事件显示格式
下面就结合实例,看一下如何使用 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----------------------------------------------------------删除一个探测点
细节解释如下:
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.
执行如下两条命令就会生成目录/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_eventsecho 『r:myretprobe do_sys_open ret=$retval』 >> /sys/kernel/debug/tracing/kprobe_events-----------------------------------------------------这里面一定要用「>>」,不然就会覆盖前面的设置。echo 『-:myprobe』 >> /sys/kernel/debug/tracing/kprobe_eventsecho 『-:myretprobe』 >> /sys/kernel/debug/tracing/kprobe_events
参数后面的寄存器是跟架构相关的,%ax、%dx、%cx 表示第 1/2/3 个参数,超出部分使用$stack 来存储参数。
函数返回值保存在$retval 中。
(4)kprobe 使能。对 kprobe 事件的是能通过往对应事件的 enable 写 1 开启探测;写 0 暂停探测。
echo > /sys/kernel/debug/tracing/traceecho 『p:myprobe do_sys_open dfd=%ax filename=%dx flags=%cx mode=+4($stack)』 > /sys/kernel/debug/tracing/kprobe_eventsecho 『r:myretprobe do_sys_open ret=$retval』 >> /sys/kernel/debug/tracing/kprobe_eventsecho 1 > /sys/kernel/debug/tracing/events/kprobes/myprobe/enableecho 1 > /sys/kernel/debug/tracing/events/kprobes/myretprobe/enablelsecho 0 > /sys/kernel/debug/tracing/events/kprobes/myprobe/enableecho 0 > /sys/kernel/debug/tracing/events/kprobes/myretprobe/enablecat /sys/kernel/debug/tracing/trace
然后在/sys/kernel/debug/tracing/trace 中可以看到结果。
sourceinsight4.-3356 [000] .... 3542865.754536: myprobe: (do_sys_open+0x0/0x290) dfd=0xffffffffbd6764a0 filename=0x8000 flags=0x1b6 mode=0xe3afff48ffffffffbash-26041 [001] .... 3542865.757014: myprobe: (do_sys_open+0x0/0x290) dfd=0xffffffffbd676460 filename=0x8241 flags=0x1b6 mode=0xe0c0ff48ffffffffls-18078 [005] .... 3542865.757950: myprobe: (do_sys_open+0x0/0x290) dfd=0xffffffffbd676460 filename=0x88000 flags=0x1 mode=0xc1b7bf48ffffffffls-18078 [005] d... 3542865.757953: myretprobe: (SyS_open+0x1e/0x20 <- do_sys_open) ret=0x3ls-18078 [005] .... 3542865.757966: myprobe: (do_sys_open+0x0/0x290) dfd=0xffffffffbd676460 filename=0x88000 flags=0x6168 mode=0xc1b7bf48ffffffffls-18078 [005] d... 3542865.757969: myretprobe: (SyS_open+0x1e/0x20 <- do_sys_open) ret=0x3ls-18078 [005] .... 3542865.758001: myprobe: (do_sys_open+0x0/0x290) dfd=0xffffffffbd676460 filename=0x88000 flags=0x6168 mode=0xc1b7bf48ffffffffls-18078 [005] d... 3542865.758004: myretprobe: (SyS_open+0x1e/0x20 <- do_sys_open) ret=0x3ls-18078 [005] .... 3542865.758030: myprobe: (do_sys_open+0x0/0x290) dfd=0xffffffffbd676460 filename=0x88000 flags=0x1000 mode=0xc1b7bf48ffffffffls-18078 [005] d... 3542865.758033: myretprobe: (SyS_open+0x1e/0x20 <- do_sys_open) ret=0x3ls-18078 [005] .... 3542865.758055: myprobe: (do_sys_open+0x0/0x290) dfd=0xffffffffbd676460 filename=0x88000 flags=0x1000 mode=0xc1b7bf48ffffffffls-18078 [005] d... 3542865.758057: myretprobe: (SyS_open+0x1e/0x20 <- do_sys_open) ret=0x3ls-18078 [005] .... 3542865.758080: myprobe: (do_sys_open+0x0/0x290) dfd=0xffffffffbd676460 filename=0x88000 flags=0x19d0 mode=0xc1b7bf48ffffffffls-18078 [005] d... 3542865.758082: myretprobe: (SyS_open+0x1e/0x20 <- do_sys_open) ret=0x3ls-18078 [005] .... 3542865.758289: myprobe: (do_sys_open+0x0/0x290) dfd=0xffffffffbd676460 filename=0x8000 flags=0x1b6 mode=0xc1b7bf48ffffffffls-18078 [005] d... 3542865.758297: myretprobe: (SyS_open+0x1e/0x20 <- do_sys_open) ret=0x3ls-18078 [005] .... 3542865.758339: myprobe: (do_sys_open+0x0/0x290) dfd=0xffffffffbd676460 filename=0x88000 flags=0x0 mode=0xc1b7bf48ffffffffls-18078 [005] d... 3542865.758343: myretprobe: (SyS_open+0x1e/0x20 <- do_sys_open) ret=0x3ls-18078 [005] .... 3542865.758444: myprobe: (do_sys_open+0x0/0x290) dfd=0xffffffffbd676460 filename=0x98800 flags=0x2 mode=0xc1b7bf48ffffffffls-18078 [005] d... 3542865.758446: myretprobe: (SyS_open+0x1e/0x20 <- do_sys_open) ret=0x3bash-26041 [001] .... 3542865.760416: myprobe: (do_sys_open+0x0/0x290) dfd=0xffffffffbd676460 filename=0x8241 flags=0x1b6 mode=0xe0c0ff48ffffffffbash-26041 [001] d... 3542865.760426: myretprobe: (SyS_open+0x1e/0x20 <- do_sys_open) ret=0x3bash-26041 [001] d... 3542865.793477: myretprobe: (SyS_open+0x1e/0x20 <- do_sys_open) ret=0x3
(5)kprobe 事件过滤。跟踪函数需要通过 filter 进行过滤,可以有效过滤掉冗余信息。filter 文件用于设置过滤条件,可以减少 trace 中输出的信息,它支持的格式和 C 语言的表达式类似,支持 ==,!=,>,<,>=,<=判断,并且支持与&&,或||,还有()。
echo 『filename==0x8241』 > /sys/kernel/debug/tracing/events/kprobes/myprobe/filter
(6)kprobe 和栈配合使用。如果要在显示函数的同时显示其栈信息,可以通过配置 trace_options 来达到。
echo stacktrace > /sys/kernel/debug/tracing/trace_options
(7)kprobe_profile 统计信息。获取一段 kprobe 时间之后,可以再 kprobe_profile 中查看统计信息。
后面两列分别表示命中和未命中的次数。
cat /sys/kernel/debug/tracing/kprobe_profile myprobe
4.3 调试工具搭配使用
在使用 Kprobes 进行调试时,搭配其他工具可以更高效地分析和解决问题,就像一场精彩的交响乐,不同的乐器相互配合,才能演奏出美妙的旋律。
查看内核日志是一个非常重要的辅助手段。在前面的代码中,我们使用了 printk 函数来输出调试信息,这些信息会被记录到内核日志中。通过查看内核日志,我们可以了解 Kprobes 探测模块的运行情况,如探测点是否成功注册、回调函数是否被正确调用、函数的参数和执行结果等。在 Linux 系统中,可以使用 dmesg 命令来查看内核日志,例如:dmesg | grep 「do_sys_open」,这个命令会过滤出内核日志中与 do_sys_open 相关的信息,方便我们快速定位问题。
gdb 调试器也能与 Kprobes 配合使用,为调试工作提供更多便利。虽然 Kprobes 主要用于动态调试运行中的内核,但在某些情况下,结合 gdb 可以更深入地分析问题。比如,当 Kprobes 探测到某个函数出现异常,但通过 printk 输出的信息不足以定位问题时,可以使用 gdb 来调试内核模块。首先,需要在内核编译时开启调试信息,然后使用 gdb 加载内核和内核模块,通过设置断点、单步执行等操作,详细分析函数的执行过程,找出问题的根源。
4.4 常见问题与解决方法
在使用 Kprobes 的过程中,可能会遇到一些常见问题,这些问题就像是前进道路上的绊脚石,但只要我们掌握了解决方法,就能轻松跨越。
探测点无法注册是一个常见的问题。这可能是由于目标函数不存在、符号未导出或内核保护等原因导致的。当遇到这种情况时,首先要确认目标函数是否存在,可以通过查看内核源码或使用 nm 命令查看内核符号表来确认。如果函数存在,再检查符号是否导出,可以查看/proc/kallsyms 文件,看目标函数的符号是否在其中。如果是内核保护导致的问题,例如内核处于写保护状态,可能需要临时关闭相关保护机制,但这需要谨慎操作,因为关闭保护机制可能会影响系统的稳定性和安全性。
回调函数未按预期执行也是一个需要关注的问题。这可能是由于回调函数中存在错误,如内存访问越界、空指针引用等,导致回调函数执行异常。在编写回调函数时,要确保代码的正确性和健壮性,避免出现这些常见的错误。同时,要注意回调函数的执行环境,因为回调函数运行在中断上下文中,所以不能执行可能会导致阻塞的操作,如睡眠、等待信号量等。如果需要进行一些复杂的操作,可以将这些操作放到工作队列或内核线程中执行,以避免影响回调函数的正常执行。