Since you say you are just "playing around", I'm going to suggest a kinda dirty, but pretty straightforward solution.
The Linux kernel for ARM has its own way of handling undefined instructions to emulate them, this is done through simple "undefined instruction hooks", defined in 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);
};
These hooks are added through the (unfortunately not exported) function register_undef_hook()
, and removed through unregister_undef_hook()
.
To solve your problem, you have two options:
Export both functions by modifying arch/arm64/kernel/traps.c
adding the following two lines of code:
// after register_undef_hook
EXPORT_SYMBOL(register_undef_hook);
// after unregister_undef_hook
EXPORT_SYMBOL(unregister_undef_hook);
Now recompile the kernel and the functions will be exported and available to be used in modules. You now have a way of easily handling undefined instructions how you want.
Use kallsyms_lookup_name()
to lookup the symbols at runtime directly from your module, without the need to re-compile the kernel. A bit messier, but probably easier and surely overall a faster solution.
For option #1, here's an example module that does exactly what you want:
// 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");
For option #2, you can just modify the above code adding the following:
#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);
}
Here's the dmesg
output:
[ 1.508253] testmod: Jumping off a cliff...
[ 1.508781] testmod: *gotcha*
[ 1.509207] testmod: Woah, I survived!
Some notes
The above example sets the undef_hook
instruction/pstate masks/values to 0x0
, this means that the hook will be called for any undefined instruction that is executed. You probably want to limit this to msr XX,YY
, and you should be able to do it like this:
// didn't test these, you might want to double-check
.instr_mask = 0xfff00000,
.instr_val = 0xd5100000,
Where 0xfff00000
matches everything except the operands (according to the manual, page 779 of the PDF). You can look at the source code to see how these values are checked to decide whether to call the hook or not, it's pretty straightforward. You can also check the instr
value that is passed to the hook: pr_info("Instr: %x\n", instr)
.
From the comments it seems that the above isn't quite right, I don't really know much about ARM to give a correct answer for those values off the top of my head, but it should be easy to fix.
You can look at the struct pt_regs
to see how it's defined. You probably only want to skip the instruction and maybe print something, in that case what I did in the above example should be enough. You could potentially change any register value though if you wanted to.
Tested on Linux kernel v5.6, qemu-system-aarch64
.
dmesg
messages generated by the kernel oops that happens? That'd be helpful. – Theressa