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:
- Exit reason code — in this case,
ADP_Stopped_ApplicationExit
. - 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:
.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 3args:.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.
gcc -nostdlib -static -Wl,-Ttext=0x0 -o prog.elf prog.sobjcopy -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:
qemu-system-aarch64 -nographic -M virt -cpu cortex-a57 -semihosting -bios prog.binecho $?
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.
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.
.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.