在Linux内核模块中获取未导出内核符号的正确方法

7

我正在编写一个Linux内核模块,用于拦截一些系统调用并对其进行系统范围的统计。

我已经尝试了不同的方法来获取sys_call_table符号的地址,但是在最近的内核(例如5.11)上还没有找到可行的方法。在旧的内核中,我们会使用kallsyms_lookup_name,看起来该符号不再被导出。

我可以直接查看/proc/kallsyms,但这似乎是一个不好的主意,也不具有普适性。有其他替代方案吗?


1
我可以直接查看/proc/kallsyms,但这似乎是一个不好的想法,也不具有普适性。另外,替换系统调用也是一种不好的模式。虽然一些初学者会将其作为第一个内核模块的“任务”,但Linux内核从来没有鼓励替换系统调用。 - Tsyvarev
2个回答

10

免责声明:通常情况下,使用非导出符号并不是一个好主意,因此您应该仅在测试/教育目的下使用它,而不是用于生产就绪的模块/驱动程序。

在Linux v5.7之前,您确实会使用kallsyms_lookup_name()从模块中查找非导出内核符号。如果您想了解如何操作,请参见如何在内核模块中访问任何内核符号?

然而,在v5.7中,该符号停止导出,因为除了核心内核代码外,没有人在外部使用它,它只是被滥用来查找和使用其他非导出符号。这里还有一篇相关的LWN文章。现在没有真正的“适当方式”来解决这个问题,但有许多不同的“黑科技”可以考虑。

以下方法涵盖了内核函数和全局对象(即全局变量):
  1. 如果您已经在编译内核,可以在您感兴趣的符号定义后添加EXPORT_SYMBOL()。这是最简单的选项,只要您愿意修改内核并构建自定义内核即可。如果您真的想要,也可以在kernel/kallsyms.c中导出kallsyms_lookup_name(),然后使用它。

  2. 您可以使用一个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)
    
  3. 如果您的内核支持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的自动符号地址解析。

  4. 仅对于函数,您还可以考虑在您的模块中重新实现功能。例如,在fs/proc/task_mmu.c中实现的task_statm()是一个相当小的函数,只使用其他导出的函数,因此“借用”它以供您的模块使用将非常简单。

    很有可能您想要调用一些非导出函数,以实现比其设计目的更具体的目的。在这种情况下,一个好主意是查看内核源代码以了解其工作原理,并仅重新实现您的模块所需的最少量。

  5. 最后,您可以技术上使用filp_open()+kernel_read()<linux/fs.h>中的内核空间打开和读取/proc/kallsyms,但这可能是整体上最糟糕的解决方案。


4
我们也可以使用 kprobes 找到 kallsyms_lookup_name 函数的地址。
引用自这里(kprobes):
Kprobes 可以让您动态地打断任何内核例程,并非破坏性地收集调试和性能信息。您可以在几乎任何内核代码地址处陷阱。
要注册一个 kprobe,首先需要使用需要被捕获的符号的名称初始化一个 kprobe 结构。我们可以通过在 kprobe 结构中设置 symbol_name 来实现。
#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;
    ...
}

随着在 kprobe 结构体中引入了“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");

kprobe结构的来源

kprobe技术的来源


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接