c-获取Linux内核模块中未导出内核符号地址的正确方法



我目前正在开发一个Linux内核模块,以拦截一些系统调用,从而在系统范围内打印有关它们的统计信息。

我遇到过不同的方法来获取sys_call_table符号的地址,但还没有找到一种适用于最近的内核(例如5.11(的方法。在旧的内核上,我们不是使用了kallsyms_lookup_name吗?看起来该符号不再导出。

我可以看看/proc/kallsyms,但这似乎是个坏主意,不能推广。还有其他选择吗?

免责声明:使用非导出符号通常不是一个好主意,因此您只应将其用于测试/教育目的,而不应用于生产模块/驱动程序。

在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. 如果您的内核支持kprobe,那么您可以[ab]使用kprobe使内核查找成为kprobe_register()中的一个符号。这种方法在另一个答案中有详细说明。由于kprobes的预期用途,这只适用于函数,但您可以先找到kallsyms_lookup_name(),然后使用它查找任何其他符号。

    为了实现这一点,您的内核需要配置CONFIG_KPROBES=yCONFIG_KALLSYMS=y(根据您想要的符号,可能还有CONFIG_KALLSYMS_ALL=y(,因为register_kprobe()在后台使用的正是kallsyms_lookup_name()。kprobes的自动符号地址解析从Linux v2.6.16开始就得到支持。

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

    很可能您想要调用一些非导出函数的目的比它的设计目的更具体。在这种情况下,一个好主意是查看内核源代码,了解它是如何工作的,并且只重新实现模块所需的最低限度。

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

我们还可以使用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;
...
}

随着在structkprobe中引入"symbol_name"字段,probepoint地址解析现在将由内核负责。

一旦设置了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技术的来源

最新更新