在《Linux 内核调试利器 | kprobe 的使用》一文中,我们介绍过怎么使用 kprobe
来追踪内核函数,而本文将会介绍 kprobe 的原理和实现。
kprobe 原理
kprobe 可以用来跟踪内核函数中某一条指令在运行前和运行后的情况。
我们只需在 kprobe 模块中定义好指令执行前的回调函数 pre_handler()
和执行后的回调函数 post_handler()
,那么内核将会在被跟踪的指令执行前调用 pre_handler()
函数,并且在指令执行后调用 post_handler()
函数。如下图所示:
那么,内核是怎样做到在被跟踪指令执行前调用 pre_handler()
函数和指令执行后调用 post_handler()
函数的呢?
如果你读过我们之前写的一篇文章《断点的原理》,那么就比较容易理解 kprobe 的原理了,因为 kprobe 使用了类似于断点的机制来实现的。
如果不了解断点的原理,那么请先看看这篇文章《断点的原理》。
当使用 kprobe 来跟踪内核函数的某条指令时,kprobe 首先会把要追踪的指令保存起来,然后把要追踪的指令替换成 int3
指令。如下图所示:
被追踪的指令替换成 int3
指令后,当内核执行到这条指令时,将会触发 do_int3()
异常处理例程。
do_int3()
异常处理例程的执行过程如下:
-
首先调用 kprobe 模块的
pre_handler()
回调函数。 -
然后将 CPU 设置为单步调试模式。
-
接着从异常处理例程中返回,并且执行原来的指令。
我们通过下图来展示 do_int3()
函数的执行过程:
由于设置了单步调试模式,当执行完原来的指令后,将会触发 debug异常
(这是 Intel x86 CPU 的一个特性)。
当 CPU 触发 debug异常
后,内核将会执行 debug 异常处理例程 do_debug()
,而 do_debug()
异常处理例程将会调用 kprobe 模块的 post_handler()
回调函数。
下图展示了 kprobe 的执行流程:
kprobe 实现
了解了 kprobe 的原理后,现在我们开始分析 kprobe 的代码实现。
由于 kprobe 的细节很多,本文只会对 kprobe 整个大体实现方式进行分析,有些细节需要读者自行阅读源码了解。
1. kprobe 初始化
一个功能的实现,一般都需要先初始化其所使用的资源和环境,kprobe 功能也不例外。
下面我们来看看 kprobe 的初始化过程,kprobe 的初始化由 init_kprobes()
函数实现:
static int __init init_kprobes(void)
{
int i, err = 0;
unsigned long offset = 0, size = 0;
char *modname, namebuf[128];
const char *symbol_name;
void *addr;
struct kprobe_blackpoint *kb;
// 1) 初始化用于存储 kprobe 模块的哈希表
for (i = 0; i < KPROBE_