Linux使用kprobes跟踪内核函数
一、kprobes
模块代码
创建一个名为kprobes_debug.c
的文件,拷贝下面的内容,注意粘贴时使用粘贴模式,即:set paste
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/kprobes.h>/* 定义kprobe结构体 */
static struct kprobe kp;/* 前置处理函数:在探测点指令执行前调用 */
static int handler_pre(struct kprobe *p, struct pt_regs *regs)
{printk(KERN_INFO "Register dump for sys_ioctl:\n");printk(KERN_INFO " ebx: 0x%lx\n", regs->ebx);printk(KERN_INFO " ecx: 0x%lx\n", regs->ecx); printk(KERN_INFO " edx: 0x%lx\n", regs->edx);printk(KERN_INFO " esi: 0x%lx\n", regs->esi);printk(KERN_INFO " edi: 0x%lx\n", regs->edi);printk(KERN_INFO " ebp: 0x%lx\n", regs->ebp);printk(KERN_INFO " esp: 0x%lx\n", regs->esp);printk(KERN_INFO " eip: 0x%lx\n", regs->eip);dump_stack();return 0;
}/* 后置处理函数:在探测点指令执行后调用 */
static void handler_post(struct kprobe *p, struct pt_regs *regs,unsigned long flags)
{//printk(KERN_INFO "<%s> post_handler: p->addr = 0x%p\n",// p->symbol_name, p->addr);
}/* 错误处理函数:当处理函数或单步执行出现异常时调用 */
static int handler_fault(struct kprobe *p, struct pt_regs *regs, int trapnr)
{//printk(KERN_INFO "fault_handler: p->addr = 0x%p, trap #%dn\n",// p->addr, trapnr);return 0;
}static int __init kprobe_init(void)
{int ret;kp.addr = (kprobe_opcode_t *)0xc0266d1f;/* 设置处理函数 */kp.pre_handler = handler_pre;kp.post_handler = handler_post;kp.fault_handler = handler_fault;/* 注册kprobe */ret = register_kprobe(&kp);if (ret < 0) {printk(KERN_ERR "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");
使用 kprobes
机制在内核指定地址(0xc0266d1f
)动态插入探测点,用于监控该地址指令的执行前后的寄存器状态
1. 核心功能
kprobes
是 Linux 内核提供的一种动态追踪机制,允许在几乎任何内核地址插入探测点,捕获执行时的上下文(如寄存器、栈信息)- 本模块在地址
0xc0266d1f
处注册了一个kprobe
,并在指令执行前打印寄存器值,执行后调用后置处理函数
2. 代码逐段解析
2.1.头文件引入
#include <linux/kernel.h> // 内核基础功能(如 printk)
#include <linux/module.h> // 内核模块宏(如 module_init/exit)
#include <linux/kprobes.h> // kprobes 相关API
2.2.定义 kprobe
结构体
static struct kprobe kp;
struct kprobe
是kprobes
的核心数据结构,用于配置探测点地址和处理函数
2.3.前置处理函数 (handler_pre
)
static int handler_pre(struct kprobe *p, struct pt_regs *regs) {printk(KERN_INFO "Register dump for sys_ioctl:\n");printk(KERN_INFO " ebx: 0x%lx\n", regs->ebx);// ... 打印其他寄存器(ecx, edx, esi, edi, ebp, esp, eip)dump_stack(); // 打印内核调用栈return 0;
}
- 触发时机:在探测点指令执行前调用
- 参数
p
:指向当前kprobe
结构体的指针regs
:保存 CPU 寄存器状态的pt_regs
结构体(包含ebx
,ecx
,edx
等寄存器值)
- 功能
- 打印所有通用寄存器的值(x86 架构)
dump_stack()
输出内核调用栈,帮助定位代码路径
- 返回值
0
表示继续执行;非零会触发handler_fault
2.4.后置处理函数 (handler_post
)
static void handler_post(struct kprobe *p, struct pt_regs *regs, unsigned long flags) {// 当前为空,可用于分析指令执行后的状态
}
- 触发时机:在探测点指令执行后调用。
- 参数
flags
:执行过程中的标志位(如中断状态)
2.5.错误处理函数 (handler_fault
)
static int handler_fault(struct kprobe *p, struct pt_regs *regs, int trapnr) {// 当前为空,可用于处理探测点执行时的异常(如缺页)return 0;
}
- 触发时机:当
handler_pre
或指令单步执行触发异常(如访问非法地址)。 - 参数
trapnr
:异常编号(如#PF
表示缺页)。
2.6.模块初始化 (kprobe_init
)
static int __init kprobe_init(void) {int ret;kp.addr = (kprobe_opcode_t *)0xc0266d1f; // 硬编码探测地址kp.pre_handler = handler_pre;kp.post_handler = handler_post;kp.fault_handler = handler_fault;ret = register_kprobe(&kp); // 注册kprobeif (ret < 0) {printk(KERN_ERR "register_kprobe failed, returned %d\n", ret);return ret;}printk(KERN_INFO "Planted kprobe at %p\n", kp.addr);return 0;
}
- 关键操作
- 设置探测地址
0xc0266d1f
(需根据实际内核版本调整) - 绑定处理函数(
pre_handler
,post_handler
,fault_handler
) - 调用
register_kprobe()
注册探测点 - 失败时打印错误并返回
- 设置探测地址
2.7.模块退出 (kprobe_exit
)
static void __exit kprobe_exit(void) {unregister_kprobe(&kp); // 注销kprobeprintk(KERN_INFO "kprobe at %p unregistered\n", kp.addr);
}
- 调用
unregister_kprobe()
清理探测点,避免内核污染。
2.8.模块声明
module_init(kprobe_init);
module_exit(kprobe_exit);
MODULE_LICENSE("GPL"); // 声明模块许可证(GPL兼容)
3.关键注意事项
0xc0266d1f
是内核虚拟地址,可能因内核版本/配置不同而变化。- 获取方式,比如想要跟踪
sys_ioctl
内核函数,可以这样获取它的地址:
cat /proc/kallsyms | grep sys_ioctl
预期输出:
c017bbc4 T sys_ioctl
c017bbc4 就是我们要跟踪的地址
调试信息:
dump_stack()
的输出可通过dmesg
查看,帮助分析调用链
性能影响
kprobes
会轻微降低内核性能,避免在高频路径上使用
二、Makefile
文件
ifneq ($(KERNELRELEASE),)# 在内核构建系统中(由 kbuild 调用时)obj-m := kprobes_debug.o
elseKERNELDIR ?= /lib/modules/$(shell uname -r)/buildPWD := $(shell pwd)default:@echo "[DEBUG] 正在执行内核模块编译..."@echo "[DEBUG] MAKE = $(MAKE)" # 打印 MAKE 变量@echo "[DEBUG] uname -r = $(shell uname -r)" # 打印当前内核版本@echo "[DEBUG] KERNELRELEASE = $(KERNELRELEASE)"$(MAKE) -C $(KERNELDIR) M=$(PWD) modulesendifclean:@echo "[DEBUG] 正在清理编译文件..."$(MAKE) -C $(KERNELDIR) M=$(PWD) cleanrm -f *.ko *.mod.c *.mod.o *.o
-
obj-m := kprobes_debug.o
:这是最重要的部分,它告诉内核的kbuild
系统将kprobes_debug.o
构建为可加载内核模块(kprobes_debug.o
)。-
如果你的模块由多个源文件组成(比如
main.c
和helper.c
),则需要这样写:obj-m := mymodule.o mymodule-objs := main.o helper.o
-
-
KERNELDIR:指定内核源码树的路径。示例中
?=
表示如果该变量未设置,则使用/lib/modules/$(shell uname -r)/build
,这通常是当前运行内核的源码或头文件链接。- 注意:必须确保
KERNELDIR
指向的内核源码版本与你当前运行的内核版本一致,否则编译出的模块可能无法加载。
- 注意:必须确保
-
编译命令
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
:-C $(KERNELDIR)
:让make
先切换到内核源码目录,读取顶层的Makefile
。M=$(PWD)
:然后告诉kbuild
系统回到当前模块源码所在的目录执行编译。modules
:指定编译目标为模块。
-
ifneq ($(KERNELRELEASE),)
:这个条件判断用于处理Makefile
的两次执行。 第一次在当前目录,KERNELRELEASE
未定义,执行else
部分,调用内核源码的Makefile
。内核的Makefile
会设置KERNELRELEASE
变量,并再次回到当前Makefile
时,KERNELRELEASE
已被设置,于是执行obj-m := kprobes_debug.o
这一行。
1.模块加载和卸载
加载模块:
sudo insmod kprobes_debug.ko
加载后,模块的初始化函数会被调用,其中printk
信息会输出到内核日志。使用 dmesg
命令查看:
dmesg | tail -n 20
使用 lsmod
命令 grep 查找你的模块
lsmod | grep kprobes_debug
卸载模块:
sudo rmmod kprobes_debug
同样,模块的退出函数 会被调用,信息也会输出到内核日志。使用 dmesg | tail -n 20
再次查看。
三、结果验证
在终端执行命令
sudo ethtool eth0
dmesg
命令会有如下输出
Register dump for sys_ioctl:ebx: 0x8946ecx: 0xd7870e08edx: 0xee546d88esi: 0xbffff9b0edi: 0x8946ebp: 0xe61b3f68esp: 0xc02a5c97eip: 0xc0265c21[<c01047d2>] dump_stack+0x16/0x18[<f89f8083>] handler_pre+0x83/0x8a [kprobes_debug][<c011443f>] kprobe_exceptions_notify+0x11c/0x219[<c012cd71>] notifier_call_chain+0x1c/0x35[<c01055b2>] do_int3+0x37/0x6c[<c01044e2>] int3+0x1e/0x2c[<c025b802>] sock_ioctl+0x249/0x25d[<c017bd7e>] sys_ioctl+0x1ba/0x1d8[<c0103919>] sysenter_past_esp+0x52/0x75
我们可以根据寄存器获取函数参数,例如:ebx
寄存器是函数第一个参数,值是0x8946
,代表ethtool
相关命令,其次,调用栈可以显示函数的具体代码位置,比如,我们想获取sock_ioctl
函数具体是执行的哪一行代码,可以这样做
sudo gdb /usr/src/linux-2.6.10/vmlinux /proc/kcore
然后输入
list *0xc025b802
预期输出
(gdb) list *0xc025b802
0xc025b802 is in sock_ioctl (net/socket.c:902).
897 err = dlci_ioctl_hook(cmd, argp);
898 up(&dlci_ioctl_mutex);
899 }
900 break;
901 default:
902 err = sock->ops->ioctl(sock, cmd, arg);
903 break;
904 }
905 lock_kernel();
906
(gdb)