5 min read
Exiting QEMU AArch64 from BIOS

In OpenVADL, we validate our generated simulators through co-simulation with the upstream QEMU implementation of the target architecture. For AArch64, I needed a method to terminate bare-metal programs cleanly during testing. While QEMU full system emulation generally lacks explicit termination support, many architectures provide mechanisms to communicate with the host system. For example, RISC-V uses the HTIF protocol via the Spike machine, whereas AArch64 supports semihosting for this purpose.

Semihosting is a mechanism triggered by specific instruction sequences that raise an exception on the emulator host, allowing the host to handle predefined requests such as system calls. This enables bare-metal programs to perform operations like file I/O or termination through the host environment. To exit a running bare-metal program, the semihosting interface can be used to request the emulator to terminate execution. The complete specification for AArch64 and AArch32 semihosting is available here.

To trigger a semihosting request in AArch64, the hlt #0xF000 instruction is used. The semihosting handler expects two registers to be set:

  • W0: the operation number, which is #0x20 (TARGET_SYS_EXIT_EXTENDED) for an exit request.
  • X1: a pointer to a struct containing the operation’s arguments, such as the exit code and reason.

The TARGET_SYS_EXIT_EXTENDED operation expects two arguments:

  1. Exit reason code — in this case, ADP_Stopped_ApplicationExit.
  2. Exit status code — passed to the C exit() function by the semihosting handler.

Below is a minimal AArch64 assembly program that exits with status code 5:

prog.s
.equ TARGET_SYS_EXIT_EXTENDED, 0x20
.equ ADP_Stopped_ApplicationExit, 0x20026
.global _start
_start:
// my bios code
// exit simulation
b exit
exit:
mov w0, TARGET_SYS_EXIT_EXTENDED // SYS_EXIT operation
ldr x1, =args // Load address of args struct
hlt #0xF000 // Trigger semihosting call
.align 3
args:
.xword ADP_Stopped_ApplicationExit // ADP_Stopped_ApplicationExit command
.xword 0x5 // Exit Code (5 in this case)

The program loads TARGET_SYS_EXIT_EXTENDED into W0, the address of the args struct into X1, and then executes hlt #0xF000 to trigger the semihosting request.

To run this with qemu-system-aarch64, we must compile the program to a raw binary. The virt machine loads firmware using the -bios option as a binary image at address 0x0 and does not support ELF files.

Compiling prog.s to binary
gcc -nostdlib -static -Wl,-Ttext=0x0 -o prog.elf prog.s
objcopy -O binary prog.elf prog.bin

Note: You can use objdump -D -b binary -m aarch64 prog.bin to disassemble the raw binary.

Now we can run QEMU and check the exit code, which should be 5:

Run QEMU
qemu-system-aarch64 -nographic -M virt -cpu cortex-a57 -semihosting -bios prog.bin
echo $?

The -semihosting flag is required; without it, hlt #0xF000 will not trigger a semihosting request.

Dynamic Exit Code

The above version works if we always want to exit with a fixed exit code.
To set the exit code dynamically, we need to overwrite the second field of the args struct at runtime.

However, since the firmware is loaded at address 0x0 (flash memory), it is not writable during execution.
This means we can’t just insert a str x0, [x1, 8] to update the exit code.

Virt memory map in hw/arm/virt.c
static const MemMapEntry base_memmap[] = {
/* Space up to 0x8000000 is reserved for a boot ROM */
[VIRT_FLASH] = { 0, 0x08000000 },
// ...
/* Actual RAM size depends on initial RAM and device memory settings */
[VIRT_MEM] = { GiB, LEGACY_RAMLIMIT_BYTES },
};

Looking at the memory map of the virt machine, the main RAM starts at 0x40000000 (1 GiB). By placing the args struct in this RAM region, we ensure it is writable at runtime, allowing us to dynamically set the exit code.

prog.s with dynamic exit code
.equ TARGET_SYS_EXIT_EXTENDED, 0x20
.equ ADP_Stopped_ApplicationExit, 0x20026
.equ Args_Ptr_Addr, 0x40000000
.global _start
_start:
// my bios code
// exit simulation
mov x0, #2 // exit with code 2
b exit
exit:
ldr x1, =Args_Ptr_Addr // Load address of args struct
ldr x2, =ADP_Stopped_ApplicationExit
str x2, [x1] // Set first argument (exit reason)
str x0, [x1, 8] // Set second argument (exit status code)
mov w0, TARGET_SYS_EXIT_EXTENDED // SYS_EXIT operation
hlt #0xF000 // Trigger semihosting call

In this version, we initialize the argument struct at 0x40000000 during execution.
Before jumping to the exit label, we store the desired exit code in X0, which is then placed in the argument struct.

Make sure Args_Ptr_Addr is set to an address in RAM that does not overlap with any kernel or program you load (with -kernel).
The virt machine places RAM at 0x40000000 by default, and the kernel is typically loaded at the start of that region.