解决Linux kprobe报错: Hook标记't'函数无效(-22 EINVAL)
2024-12-04 18:41:59
深入解析 “can’t kprobe “t” functions” 问题及解决方案
在 Linux 内核模块开发中,kprobe 是一种强大的调试工具,允许开发者动态地 Hook 到内核函数,从而监控或修改内核行为。但有时尝试 Hook 一些在 /proc/kallsyms
中标记为 t
(text) 的函数时会遇到 register_kprobe()
返回 -22 (EINVAL) 的错误,即“参数无效”。本文将深入探讨该问题的原因,并提供多种解决方案。
问题分析:t
标记函数与 kprobe
/proc/kallsyms
中函数标记为 t
表示该函数属于内核代码段(text section)。通常情况下,kprobe 可以 Hook 这些函数。但当 register_kprobe()
返回 -22 时,意味着尝试 Hook 的函数存在一些特殊性,使得 kprobe 无法正常工作。常见原因包括:
- 函数被内联: 编译器优化可能将函数内联到调用它的函数中,导致 kprobe 无法找到独立的函数实体。
- 函数被优化: 编译器可能对函数进行了各种优化,例如尾调用优化、栈帧省略等,导致 kprobe 无法正确处理。
- 函数地址不可探测: 在某些内核版本或配置下,一些函数地址可能无法被 kprobe 安全访问。
- 内核版本限制: 老旧内核版本(例如 2.6.32 和 3.x 系列)可能对 kprobe 的支持存在一些限制。
- 受限函数: 一些内核安全模块(如 SELinux)或内核加固机制可能限制了对某些函数的 kprobe 操作。
- 地址空间布局随机化 (KASLR): 虽然 KASLR 本身不影响 kprobe 的使用,但在 kprobe 使用函数地址而不是符号名称时,需要正确处理 KASLR 偏移。
解决方案:多种方法应对 kprobe 限制
针对上述问题,可以尝试以下解决方案:
-
禁用函数内联:
原理: 通过编译器选项避免目标函数被内联,确保 kprobe 可以找到独立的函数实体。
操作步骤: 修改内核模块 Makefile,添加
-fno-inline
编译选项,重新编译内核模块。代码示例 (Makefile):
obj-m += gap.o KBUILD_CFLAGS += -Wno-unused-function CCFLAG-y := -g -O3 -fno-inline -flto -march=native -mtune=native -fomit-frame-pointer -funroll-loops all: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules clean: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
安全建议: 禁用内联可能会略微增加内核大小,并可能影响性能。仅在必要时使用此选项,并在测试完成后恢复原始编译选项。
-
降低优化级别:
原理: 减少编译器对目标函数的优化,避免因优化导致的 kprobe 失败。
操作步骤: 修改内核模块 Makefile,降低优化级别,例如将
-O3
改为-O1
或-O0
。代码示例 (Makefile):
obj-m += gap.o KBUILD_CFLAGS += -Wno-unused-function CCFLAG-y := -g -O1 -flto -march=native -mtune=native all: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules clean: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
安全建议: 降低优化级别可能会降低内核性能,建议仅在调试阶段使用较低优化级别,并在完成后恢复原始优化选项。
-
使用 kprobe 地址:
原理: 直接通过函数地址注册 kprobe,绕过符号查找过程。但需要注意 KASLR 影响。
操作步骤:
- 使用
cat /proc/kallsyms | grep <function_name>
获取函数地址。 - 在内核模块中,定义一个指向函数地址的指针。
- 将 kprobe 的
kp.addr
设置为该指针的值。
代码示例 (C):
#include <linux/kprobes.h> #include <linux/kernel.h> #include <linux/module.h> #include <linux/sched.h> #include <linux/version.h> static int kprobe_seq_ret_handler(struct kretprobe_instance *ri, struct pt_regs *regs); #ifdef KPROBE_ON_ADDRESS // Assume the address you got from cat /proc/kallsyms is ffffffff81e29760 static unsigned long kprobe_seq_next_addr = (unsigned long)0xffffffff81e29760; #endif static struct kretprobe kretseqnext = { .handler = kprobe_seq_ret_handler, #ifdef KPROBE_ON_ADDRESS .kp.addr = (kprobe_opcode_t *)kprobe_seq_next_addr, #else .kp.symbol_name = "kprobe_seq_next", #endif }; static int kprobe_seq_ret_handler(struct kretprobe_instance *ri, struct pt_regs *regs) { set_return_value(regs, 0); return 0; } static int __init kprobe_init(void) { int ret; ret = register_kretprobe(&kretseqnext); if (ret < 0) { printk("Failed registering kretprobe kprobe_seq_next %d\n", ret); return ret; } printk("Registering kprobe_seq_next\n"); #ifdef KPROBE_ON_ADDRESS printk("kprobe_seq_next address: %px\n", (void *)kprobe_seq_next_addr); #endif return 0; } static void __exit kprobe_exit(void) { unregister_kretprobe(&kretseqnext); printk("Unregistering kretprobe kprobe_seq_next\n"); } module_init(kprobe_init); module_exit(kprobe_exit); MODULE_LICENSE("GPL");
修改
Makefile
obj-m += gap.o KBUILD_CFLAGS += -Wno-unused-function -DKPROBE_ON_ADDRESS CCFLAG-y := -g -O3 -flto -march=native -mtune=native -fomit-frame-pointer -funroll-loops -finline-functions all: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules clean: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
操作步骤:
- 获取目标函数的地址
sudo cat /proc/kallsyms | grep kprobe_seq_next
- 根据输出,修改上面C代码段中
kprobe_seq_next_addr
变量值 - 重新编译加载模块。
安全建议: 使用硬编码地址可能导致模块在不同内核版本或配置下无法加载。建议结合内核版本判断或 KASLR 偏移计算来动态获取地址。
- 使用
-
检查内核配置和安全模块:
原理: 确认内核配置是否禁用了 kprobe 或启用了限制 kprobe 的安全模块。
操作步骤:
* 检查内核配置文件(如.config
)中是否包含CONFIG_KPROBE_EVENT=y
, 确认内核编译开启了 kprobe 功能
* 确认没有其他安全模块(如 SELinux)限制对目标函数的 kprobe 操作,或者有的话,配置允许当前模块kprobe.
* 查看是否有其他安全限制如kernel.kptr_restrict
, 若值为2或者3 , 请暂时将其修改为1, 或者0,sudo sysctl -w kernel.kptr_restrict=1
, 并在完成kprobe调试后恢复原来的值。 -
升级内核版本:
原理: 新版本内核通常会修复一些 kprobe 相关的 bug,并提供更好的兼容性。
操作步骤: 考虑升级到较新的内核版本,尤其是对于