kprobe是一种重要的内核调试技术, 本文主要从内核代码实现的角度, 分析kprobe程序是如何运行的。
由于不同架构不同配置对应的执行流程都有所不同, 不文介绍的环境是x86架构下,且没有开启ftrace。
还是以下面的demo为例介绍。
第一步就是调用register_kprobe函数。
我们传入的一个struct kprobe类型的变量。该变量包含了回调函数pre_set_state,内核函数名tcp_set_state。
register_kprobe函数会根据内核函数名, 在符号表中找到对应的函数地址, 并保存原始指令。并根据内核函
数地址计算hash key,将kprobe实例存放到全局hash表kprobe_table中。最后就是将原始指令替换成一个int3指令。
第二部就是int3指令的执行。
由于原始内核函数的指令被替换成了int3指令, 这样在执行到该处时,就会执行int3指令,进入了内核的do_int3函数逻辑。
这里会根据地址再次找到之前保存的kprobe实例和原始内核指令。依次执行回调函数和内核原始指令。
我们这里忽略掉了int3指令相关的初始化,这和内核启动时初始化操作相关,我们后续再看。
#include <linux/kprobes.h>
#include <net/tcp.h>
#define INET_ADDRSTRLEN 16
#define INET6_ADDRSTRLEN 46
static int set_state_pre(struct kprobe *p, struct pt_regs *regs)
{
/* 从寄存器获取函数参数 */
struct sock *sk = (struct sock *)regs->di;
int state = (int)regs->si;
struct inet_sock *inet = inet_sk(sk);
u16 dport = ntohs(sk->__sk_common.skc_dport);
char saddr[INET6_ADDRSTRLEN], daddr[INET6_ADDRSTRLEN];
if (sk->sk_family == AF_INET) {
snprintf(saddr, INET_ADDRSTRLEN, "%pI4", &inet->inet_saddr);
snprintf(daddr, INET_ADDRSTRLEN, "%pI4", &inet->inet_daddr);
} else {
snprintf(saddr, INET6_ADDRSTRLEN, "%pI6", &inet->inet_saddr);
snprintf(daddr, INET6_ADDRSTRLEN, "%pI6", &inet->inet_daddr);
}
pr_info("%s %u %s %u sk=%p, pre_state=%u, state=%u\n",
daddr, dport, saddr, ntohs(inet->inet_sport), sk, sk->sk_state, state);
return 0;
}
static struct kprobe set_state_kp = {
.symbol_name = "tcp_set_state",
.pre_handler = set_state_pre,
};
static int __init kprobe_init(void)
{
return register_kprobe(&set_state_kp);
}
static void __exit kprobe_exit(void)
{
unregister_kprobe(&set_state_kp);
}
module_init(kprobe_init)
module_exit(kprobe_exit)
MODULE_LICENSE("GPL");