c-处理内核中未定义的指令



所以我一直在读内核中的系统寄存器,最近遇到了一些障碍。

在ARM64中,并不总是实现某些系统寄存器(例如OSECCR_EL1(。如果它们被实现了,那么尝试mrs指令就可以了——不会发生什么不好的事情。但是,如果它们没有实现,那么内核会由于未定义的指令而抛出一个Oops。

然而,这并不是没有道理的,因为在运行mrs指令时,我在内核模块中,我看不到从这种错误中恢复的简单方法,甚至没有意识到特定的系统寄存器读取将首先失败。

有没有什么简单的方法可以预先确定系统寄存器是否有效,或者至少可以以一种不会立即停止内核模块函数执行的方式处理内核oops?

既然你说你只是在"玩",我将提出一个有点脏但非常简单的解决方案。

ARM的Linux内核有自己的处理未定义指令的方法来模拟它们,这是通过arch/arm64/include/asm/traps.h:中定义的简单的"未定义指令挂钩"来完成的

struct undef_hook {
struct list_head node;
u32 instr_mask;
u32 instr_val;
u64 pstate_mask;
u64 pstate_val;
int (*fn)(struct pt_regs *regs, u32 instr);
};

这些钩子是通过函数register_undef_hook()添加的(不幸的是没有导出(,并通过unregister_undef_hook()删除。

要解决您的问题,您有两种选择:

  1. 通过修改arch/arm64/kernel/traps.c并添加以下两行代码来导出这两个函数:

    // after register_undef_hook
    EXPORT_SYMBOL(register_undef_hook);
    // after unregister_undef_hook
    EXPORT_SYMBOL(unregister_undef_hook);
    

    现在重新编译内核,函数将被导出并可在模块中使用。现在,您可以根据需要轻松处理未定义的指令。

  2. 使用kallsyms_lookup_name()在运行时直接从模块中查找符号,而无需重新编译内核。有点混乱,但可能更容易,总体上肯定是一个更快的解决方案。

对于选项#1,这里有一个示例模块,它可以完全满足您的要求:

// SPDX-License-Identifier: GPL-3.0
#include <linux/init.h>   // module_{init,exit}()
#include <linux/module.h> // THIS_MODULE, MODULE_VERSION, ...
#include <asm/traps.h>    // struct undef_hook, register_undef_hook()
#include <asm/ptrace.h>   // struct pt_regs
#ifdef pr_fmt
#undef pr_fmt
#endif
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
static void whoops(void)
{
// Execute a known invalid instruction.
asm volatile (".word 0xf7f0a000");
}
static int undef_instr_handler(struct pt_regs *regs, u32 instr)
{
pr_info("*gotcha*n");
// Just skip over to the next instruction.
regs->pc += 4;
return 0; // All fine!
}
static struct undef_hook uh = {
.instr_mask  = 0x0, // any instruction
.instr_val   = 0x0, // any instruction
.pstate_mask = 0x0, // any pstate
.pstate_val  = 0x0, // any pstate
.fn          = undef_instr_handler
};
static int __init modinit(void)
{
register_undef_hook(&uh);
pr_info("Jumping off a cliff...n");
whoops();
pr_info("Woah, I survived!n");
return 0;
}
static void __exit modexit(void)
{
unregister_undef_hook(&uf);
}
module_init(modinit);
module_exit(modexit);
MODULE_VERSION("0.1");
MODULE_DESCRIPTION("Test undefined instruction handling on arm64.");
MODULE_AUTHOR("Marco Bonelli");
MODULE_LICENSE("GPL");

对于选项#2,您可以修改上面的代码,添加以下内容:

#include <linux/kallsyms.h> // kallsyms_lookup_name()
// Define two global pointers.
static void (*register_undef_hook_ptr)(struct undef_hook *);
static void (*unregister_undef_hook_ptr)(struct undef_hook *);
static int __init modinit(void)
{
// Lookup wanted symbols.
register_undef_hook_ptr   = (void *)kallsyms_lookup_name("register_undef_hook");
unregister_undef_hook_ptr = (void *)kallsyms_lookup_name("unregister_undef_hook");
if (!register_undef_hook_ptr)
return -EFAULT;
// ...
return 0;
}
static void __exit modexit(void)
{
if (unregister_undef_hook_ptr)
unregister_undef_hook_ptr(&uh);
}

这是dmesg的输出:

[    1.508253] testmod: Jumping off a cliff...
[    1.508781] testmod: *gotcha*
[    1.509207] testmod: Woah, I survived!

一些注意事项

  • 上述示例将undef_hook指令/pstate掩码/值设置为0x0,这意味着将为执行的任何未定义指令调用钩子。您可能想将其限制为msr XX,YY,并且应该可以这样做:

    // didn't test these, you might want to double-check
    .instr_mask  = 0xfff00000,
    .instr_val   = 0xd5100000,
    

    其中0xfff00000匹配除操作数之外的所有内容(根据PDF手册第779页(。您可以查看源代码,了解如何检查这些值以决定是否调用钩子,这非常简单。您还可以检查传递给钩子的instr值:pr_info("Instr: %xn", instr)

    从评论来看,上面的内容似乎不太正确,我对ARM真的不太了解,无法对这些值给出正确的答案,但它应该很容易修复。

  • 您可以查看struct pt_regs来了解它是如何定义的。你可能只想跳过说明,也许打印一些东西,在这种情况下,我在上面的例子中所做的就足够了。如果你想的话,你可能会更改任何寄存器值。

  • 在Linux内核v5.6、qemu-system-aarch64上测试。

最新更新