TL;DR
From the supervisor mode:
(*(volatile uint32 *) 0x100000) = 0x5555;
You also need to change the kernel page table to map the address 0x100000. You can see this commit for full implementation.
Long answer
Unfortunately, the documentation for QEMU RISC-V emulator is poor. So we need to dive into the code. We know from the xv6's Makefile that it is using QEMU's Generic Virtual Platform (virt) in order to virtualize the xv6.
The virt.c file from QEMU's source code contains the implementation of the QEMU's Generic Virtual Platform. If we search poweroff in that file, we will end up with this code snippet:
name = g_strdup_printf("/poweroff"); qemu_fdt_add_subnode(ms->fdt, name); qemu_fdt_setprop_string(ms->fdt, name, "compatible", "syscon-poweroff"); qemu_fdt_setprop_cell(ms->fdt, name, "regmap", test_phandle); qemu_fdt_setprop_cell(ms->fdt, name, "offset", 0x0); qemu_fdt_setprop_cell(ms->fdt, name, "value", FINISHER_PASS); g_free(name);
In a nutshell, this code snippet is telling the QEMU that if FINISHER_PASS is written to the 0th index of test_phandle register map, then poweroff the QEMU. If we look a few lines above it, we will realize that upon writing FINISHER_RESET we can reset the QEMU. A few lines before we can observe this line indicates the memory mapped location in the guest that we should write the FINISHER_PASS in:
qemu_fdt_setprop_cells(ms->fdt, name, "reg", 0x0, memmap[VIRT_TEST].base, 0x0, memmap[VIRT_TEST].size);
If we search VIRT_TEST in the same file, we come up with this line:
[VIRT_TEST] = { 0x100000, 0x1000 },
So the location in which we should write FINISHER_PASS in is 0x100000. Also, if we search FINISHER_PASS and FINISHER_RESET in the whole project, we can see their values in this file:
enum { FINISHER_FAIL = 0x3333, FINISHER_PASS = 0x5555, FINISHER_RESET = 0x7777 };
Long story short, we currently know that to poweroff an operating system, we have to write 0x5555 into address 0x100000. You might think that you can reboot the operating system by writing 0x7777 in the same address but I couldn't do it for some reason... Let me know if you have found a reason!
Now we shall implement a syscall to poweroff xv6. To get started, we first must change the page table of the kernel in order to map the virtual address 0x100000 to physical address 0x100000 (you can map other virtual addresses if you have implemented ASLR). I did it by adding the following line to vm.c and function kvmmake:
kvmmap(kpgtbl, 0x100000, 0x100000, PGSIZE, PTE_R | PTE_W);
We use the page size because the size of the area is 0x1000 bytes according to QEMU's source.
After that, we simply add a syscall to the operating system. I did it like this:
// Power off QEMU static uint64 sys_poweroff(void) { printf("Powering off...\n"); (*(volatile uint32 *) 0x100000) = 0x5555; panic("sys_poweroff"); }
Add this syscall to the syscall table and now we are good to go!
You can then write a simple user program in order to use the syscall:
#include "kernel/types.h" #include "kernel/stat.h" #include "user/user.h" int main(int argc, char *argv[]) { poweroff(); exit(1); // never reached }
Add this user program to Makefile and then you can use this application to poweroff the operating systems.
As I've said, you can see this commit for full implementation.