我正在编写一个Linux内核模块,用于拦截一些系统调用并对其进行系统范围的统计。
我已经尝试了不同的方法来获取sys_call_table
符号的地址,但是在最近的内核(例如5.11)上还没有找到可行的方法。在旧的内核中,我们会使用kallsyms_lookup_name
,看起来该符号不再被导出。
我可以直接查看/proc/kallsyms
,但这似乎是一个不好的主意,也不具有普适性。有其他替代方案吗?
我正在编写一个Linux内核模块,用于拦截一些系统调用并对其进行系统范围的统计。
我已经尝试了不同的方法来获取sys_call_table
符号的地址,但是在最近的内核(例如5.11)上还没有找到可行的方法。在旧的内核中,我们会使用kallsyms_lookup_name
,看起来该符号不再被导出。
我可以直接查看/proc/kallsyms
,但这似乎是一个不好的主意,也不具有普适性。有其他替代方案吗?
免责声明:通常情况下,使用非导出符号并不是一个好主意,因此您应该仅在测试/教育目的下使用它,而不是用于生产就绪的模块/驱动程序。
在Linux v5.7之前,您确实会使用kallsyms_lookup_name()
从模块中查找非导出内核符号。如果您想了解如何操作,请参见如何在内核模块中访问任何内核符号?。
然而,在v5.7中,该符号停止导出,因为除了核心内核代码外,没有人在外部使用它,它只是被滥用来查找和使用其他非导出符号。这里还有一篇相关的LWN文章。现在没有真正的“适当方式”来解决这个问题,但有许多不同的“黑科技”可以考虑。
以下方法涵盖了内核函数和全局对象(即全局变量):如果您已经在编译内核,可以在您感兴趣的符号定义后添加EXPORT_SYMBOL()
。这是最简单的选项,只要您愿意修改内核并构建自定义内核即可。如果您真的想要,也可以在kernel/kallsyms.c
中导出kallsyms_lookup_name()
,然后使用它。
您可以使用一个unsigned long
模块参数,在加载模块时传递所需的符号地址(从/proc/kallsyms
中获取),然后将其转换为适当的类型:
static unsigned long addr;
module_param_named(addr, addr, ulong, 0);
MODULE_PARM_DESC(addr, "Address of the `foo` symbol");
static <type_of_foo_here> *foo_ptr;
// Examples:
// int foo(char *) -> int (*foo_ptr)(char *)
// unsigned long foo -> unsigned long *foo_ptr
static int __init mymodule_init(void)
{
foo_ptr = (typeof(foo_ptr))addr;
// ...
return 0;
}
然后您就可以像这样做:
sudo insmod mymodule.ko addr=0x$(sudo grep ' some_symbol_name' /proc/kallsyms | cut -d' ' -f1)
如果您的内核支持kprobes,您可以通过kprobe_register()
滥用kprobe来使内核为您查找符号。这种方法在另一个答案中有详细说明。由于kprobe的预期用途,这仅适用于函数,但是您可以先查找kallsyms_lookup_name()
,然后使用它来查找任何其他符号。
为了使此方法起作用,您的内核需要配置为CONFIG_KPROBES=y
以及CONFIG_KALLSYMS=y
(并可能还需要根据您想要的符号配置CONFIG_KALLSYMS_ALL=y
),因为register_kprobe()
在幕后使用了kallsyms_lookup_name()
。自Linux v2.6.16以来已支持kprobe的自动符号地址解析。
仅对于函数,您还可以考虑在您的模块中重新实现功能。例如,在fs/proc/task_mmu.c
中实现的task_statm()
是一个相当小的函数,只使用其他导出的函数,因此“借用”它以供您的模块使用将非常简单。
很有可能您想要调用一些非导出函数,以实现比其设计目的更具体的目的。在这种情况下,一个好主意是查看内核源代码以了解其工作原理,并仅重新实现您的模块所需的最少量。
最后,您可以技术上使用filp_open()
+kernel_read()
从<linux/fs.h>
中的内核空间打开和读取/proc/kallsyms
,但这可能是整体上最糟糕的解决方案。
#include <linux/kprobes.h>
static struct kprobe kp = {
.symbol_name = "kallsyms_lookup_name"
};
kprobe
结构体中包含以下元素(为简洁起见已缩短):"struct kprobe {
...
/* location of the probe point */
kprobe_opcode_t *addr;
/* Allow user to indicate symbol name of the probe point */
const char *symbol_name;
...
}
symbol_name
,探测点的地址将由内核确定。
因此,现在只需要注册探测点,提取探测点地址,然后注销它即可。typedef unsigned long (*kallsyms_lookup_name_t)(const char *name);
kallsyms_lookup_name_t kallsyms_lookup_name;
register_kprobe(&kp);
kallsyms_lookup_name = (kallsyms_lookup_name_t) kp.addr;
unregister_kprobe(&kp);
kallsyms_lookup_name
地址。使用它,我们可以通过老式方法找到 sys_call_table
的地址:kallsyms_lookup_name("sys_call_table");
/proc/kallsyms
,但这似乎是一个不好的想法,也不具有普适性。另外,替换系统调用也是一种不好的模式。虽然一些初学者会将其作为第一个内核模块的“任务”,但Linux内核从来没有鼓励替换系统调用。 - Tsyvarev