处理内核中未定义的指令
Handling an undefined instruction in the kernel
所以我正在研究在内核中读取系统寄存器,我最近 运行 遇到了一些障碍。
在 ARM64 中,某些系统寄存器(例如 OSECCR_EL1
)并不总是实现。如果它们被实施,那么尝试 mrs 指令就可以了——没有什么不好的事情发生。但是,如果它们未实现,则内核会由于未定义的指令而抛出 Oops。
然而,这并非不合理,因为我在内核模块中 运行 执行此 mrs
指令时,我没有看到从该错误中恢复的简单方法,甚至认识到特定的系统寄存器读取首先会失败。
是否有任何简单的方法可以预先确定系统寄存器是否有效,或者至少以不会立即停止我的内核模块函数执行的方式处理内核 oops?
既然你说你只是 "playing around",我将建议一个有点脏但非常简单的解决方案。
ARM 的 Linux 内核有自己的方式来处理未定义的指令以模拟它们,这是通过简单的 "undefined instruction hooks" 完成的,定义在 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()
, and removed through unregister_undef_hook()
。
要解决您的问题,您有两种选择:
通过修改arch/arm64/kernel/traps.c
添加以下两行代码来导出两个函数:
// after register_undef_hook
EXPORT_SYMBOL(register_undef_hook);
// after unregister_undef_hook
EXPORT_SYMBOL(unregister_undef_hook);
现在重新编译内核,函数将被导出并可以在模块中使用。您现在可以轻松地按照自己的意愿处理未定义的指令。
使用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
instruction/pstatemasks/values设置为0x0
,这意味着钩子将被调用any 执行的未定义指令。您可能希望将其限制为 msr XX,YY
,您应该可以这样做:
// didn't test these, you might want to double-check
.instr_mask = 0xfff00000,
.instr_val = 0xd5100000,
其中 0xfff00000
匹配除操作数以外的所有内容(根据 the manual, page 779 of the PDF). You can look at the source code 查看这些值如何检查以决定是否调用钩子,非常简单。您也可以检查instr
传递给挂钩的值:pr_info("Instr: %x\n", instr)
.
从评论来看,上面的内容似乎不太正确,我对 ARM 的了解并不多,无法为我脑海中的这些值给出正确的答案,但应该很容易修复.
您可以查看struct pt_regs
以了解其定义方式。您可能只想跳过指令并打印一些东西,在这种情况下,我在上面的示例中所做的就足够了。如果您愿意,您可以更改任何寄存器值。
在 Linux 内核 v5.6 上测试,qemu-system-aarch64
。
所以我正在研究在内核中读取系统寄存器,我最近 运行 遇到了一些障碍。
在 ARM64 中,某些系统寄存器(例如 OSECCR_EL1
)并不总是实现。如果它们被实施,那么尝试 mrs 指令就可以了——没有什么不好的事情发生。但是,如果它们未实现,则内核会由于未定义的指令而抛出 Oops。
然而,这并非不合理,因为我在内核模块中 运行 执行此 mrs
指令时,我没有看到从该错误中恢复的简单方法,甚至认识到特定的系统寄存器读取首先会失败。
是否有任何简单的方法可以预先确定系统寄存器是否有效,或者至少以不会立即停止我的内核模块函数执行的方式处理内核 oops?
既然你说你只是 "playing around",我将建议一个有点脏但非常简单的解决方案。
ARM 的 Linux 内核有自己的方式来处理未定义的指令以模拟它们,这是通过简单的 "undefined instruction hooks" 完成的,定义在 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()
, and removed through unregister_undef_hook()
。
要解决您的问题,您有两种选择:
通过修改
arch/arm64/kernel/traps.c
添加以下两行代码来导出两个函数:// after register_undef_hook EXPORT_SYMBOL(register_undef_hook); // after unregister_undef_hook EXPORT_SYMBOL(unregister_undef_hook);
现在重新编译内核,函数将被导出并可以在模块中使用。您现在可以轻松地按照自己的意愿处理未定义的指令。
使用
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
instruction/pstatemasks/values设置为0x0
,这意味着钩子将被调用any 执行的未定义指令。您可能希望将其限制为msr XX,YY
,您应该可以这样做:// didn't test these, you might want to double-check .instr_mask = 0xfff00000, .instr_val = 0xd5100000,
其中
0xfff00000
匹配除操作数以外的所有内容(根据 the manual, page 779 of the PDF). You can look at the source code 查看这些值如何检查以决定是否调用钩子,非常简单。您也可以检查instr
传递给挂钩的值:pr_info("Instr: %x\n", instr)
.从评论来看,上面的内容似乎不太正确,我对 ARM 的了解并不多,无法为我脑海中的这些值给出正确的答案,但应该很容易修复.
您可以查看
struct pt_regs
以了解其定义方式。您可能只想跳过指令并打印一些东西,在这种情况下,我在上面的示例中所做的就足够了。如果您愿意,您可以更改任何寄存器值。在 Linux 内核 v5.6 上测试,
qemu-system-aarch64
。