【问题标题】:Handling an undefined instruction in the kernel处理内核中未定义的指令
【发布时间】:2020-07-29 00:38:19
【问题描述】:

所以我正在尝试在内核中读取系统寄存器,但最近遇到了一些障碍。

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

然而,这并非不合理,因为我在运行此 mrs 指令时位于内核模块中,我看不到从该 oops 中恢复的简单方法,甚至无法识别特定系统寄存器已读取一开始就会失败。

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

【问题讨论】:

  • 您能添加由内核生成的dmesg 消息吗?那会很有帮助。
  • 你检查过this吗?
  • 看起来内核异常处理表可能是做到这一点的最佳方式,但这似乎......很难。
  • 我自己从未尝试过,但是是否可以在您的模块中捕获 SIGILL 信号并在异常触发时执行某些操作?

标签: c linux-kernel kernel kernel-module arm64


【解决方案1】:

既然你说你只是在“玩弄”,我将建议一个有点肮脏但非常简单的解决方案。

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 匹配除操作数之外的所有内容(根据the manual,PDF 的第 779 页)。你可以look at the source code 看看这些值是如何被检查来决定是否调用钩子的,这很简单。您还可以检查传递给挂钩的instr 值:pr_info("Instr: %x\n", instr)

    从 cmets 看来,上面的内容似乎不太正确,我对 ARM 的了解并不多,无法对这些值给出正确答案,但应该很容易解决。

  • 您可以查看struct pt_regs 以了解它是如何定义的。您可能只想跳过指令并打印一些东西,在这种情况下,我在上面的示例中所做的就足够了。如果您愿意,您可以更改任何寄存器值。

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

【讨论】:

  • 是的,我不久前就发现了这种技术——我试图避免修改内核,但也许我只需要咬紧牙关。
  • 啊。好吧。当我们处于“玩耍”的过程中时,我想我可能有另一种技巧。虽然它们不是由内核导出的,但这些符号在 /proc/kallsyms 中是可见的。我知道这不是处理此问题的“官方”方式,但我可以从那里获取地址,完全手动调用它们,从而避免导出,同时仍然获得所需的功能。
  • @Roguebantha 我想过,但这似乎更奇怪。不过,我会将该方法添加到我的答案中,您完全正确,为什么要排除这种可能性?我也在寻找是否有办法使用 kprobes(应该有,虽然我从未使用过)或类似的东西,在这种情况下,我会创建另一个答案。
  • 是的,尽管 instr_mask 和 instr_val 以某种方式无法正常工作。将它们更改为 0x0 “修复”了它(并且可能破坏了其他东西)。
  • @Roguebantha 你能发布你的指令的反汇编吗?只需objdump -d mymodule.ko 并寻找它。我想看看编译成哪些字节。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-02-19
  • 2014-01-30
  • 1970-01-01
  • 2013-01-25
  • 2020-04-07
相关资源
最近更新 更多