Recap: Register Handling for ARM64

In the preceding article, we successfully ported the register handling logic of a Rust KVM hypervisor from x86 architecture to ARM64. This involved meticulous reverse-engineering, tracing QEMU with strace, and carefully examining Linux headers. Key findings revealed that ARM64 interacts with CPU state primarily through the KVM_GET_ONE_REG and KVM_SET_ONE_REG ioctls. We then reconstructed the register id encoding within Rust, enabling the hypervisor to compile successfully. However, initial attempts to access even fundamental registers like the program counter (PC) resulted in an ENOEXEC error, signaling a significant roadblock.

The ENOEXEC Roadblock: Register Access Failures

The optimism surrounding the initial compilation quickly evaporated when attempting basic register operations. The ENOEXEC error, typically associated with executable file format issues, was an unexpected outcome when trying to read the PC register. This suggested a deeper problem than just incorrect register IDs or encoding. The hypervisor was failing at a fundamental level to interact with the KVM module for register manipulation on ARM64. This error code was particularly baffling because the hypervisor itself was not being executed as a guest; it was the host attempting to manage guest CPU state through KVM. The issue pointed towards a misconfiguration or an uninitialized state within the KVM vCPU context specific to ARM64, rather than a problem with the guest binary itself.

Investigating ARM64 KVM vCPU Initialization

To overcome the ENOEXEC error, a more in-depth investigation into the ARM64 KVM vCPU initialization process was necessary. Unlike x86, where certain states might be implicitly handled, ARM64 requires explicit setup for various CPU features and registers. The Linux KVM documentation and source code became critical resources. We needed to understand precisely which registers are mandatory for initial vCPU setup and what values they should hold before any guest code can be executed or even before host-level operations like KVM_GET_ONE_REG can succeed without error. This involved understanding the ARM64 architecture's specific register groups, such as the General Purpose Registers (GPRs), Floating-Point/Advanced SIMD registers, and system registers (like the Program Status Register - PSR).

Uncovering Essential ARM64 KVM Registers

The reverse-engineering process continued, focusing on identifying the critical registers that KVM expects to be correctly set or at least queryable upon vCPU creation for ARM64. Tracing QEMU’s ARM64 KVM initialization path provided invaluable insights. We observed that QEMU explicitly sets several registers before attempting any guest operations. These often include:

  • General Purpose Registers (GPRs): While not all need initial values, certain ones might be expected.
  • Program Counter (PC): Crucial for execution flow, its initial value dictates where the guest will start.
  • Stack Pointer (SP): Essential for function calls and local variable management.
  • Processor Status Register (PSR) / Program Status Register (PSTATE): Controls CPU operating state, exception levels, and flags.
  • Control Registers: Such as the Virtual Process ID Register (VPID) and Translation Control Register (TCR).

The ENOEXEC error specifically when accessing the PC register suggested that the KVM module, on ARM64, might perform validation checks on register access based on the vCPU's current state. If the vCPU is not in a state where the PC is considered valid or accessible, KVM might return this error. This implies a need to not only set the PC but potentially other related system registers that define the execution context.

ARM64 CPU architecture diagram highlighting key register groups

Correcting vCPU State for First Execution

With a better understanding of the essential registers, the next step was to implement the correct initialization logic in the Rust hypervisor. This involved:

  1. Setting the initial PC: The program counter must point to the entry point of the guest code. This requires careful management of memory mapping to ensure the guest code is loaded at an address the PC can reference.
  2. Initializing the SP: A valid stack pointer is necessary for guest operations.
  3. Configuring the PSTATE/PSR: The processor state register needs to be set to an appropriate exception level (e.g., EL1 for typical guest OS execution) and mode. This is a complex area on ARM64, involving bits for mode, interrupt masks, and flags.
  4. Setting up memory management registers: Crucially, registers like the Translation Control Register (TCR) and the various Address Space Control Registers (ASCRs) must be configured to enable virtual memory translation for the guest. Without proper MMU setup, even basic memory accesses will fail.

The key insight was realizing that KVM_GET_ONE_REG and KVM_SET_ONE_REG are not just simple key-value pairs for registers. They operate within the context of the vCPU's current state, which itself must be correctly initialized and maintained. The ENOEXEC error was a symptom of the vCPU being in an invalid or uninitialized state from KVM's perspective on ARM64, preventing even read operations on registers like the PC.

Achieving First Guest Execution

After implementing the necessary vCPU state initialization, including setting the PC, SP, PSTATE, and essential MMU-related control registers, the moment of truth arrived. The hypervisor was instructed to load a minimal ARM64 guest binary. This time, instead of the immediate ENOEXEC error, the hypervisor successfully executed the KVM ioctls to set the vCPU's state and then initiated execution via KVM_RUN. The guest code began to execute. While this first execution was likely to a simple halt or a basic exit, it represented a monumental step forward. It confirmed that the core KVM interaction for ARM64, particularly around vCPU state management and register access, was now correctly implemented. The path was cleared to begin writing more complex guest code and handling KVM exit events.

What’s Next? Handling KVM Exits

With the first successful guest execution under our belt, the immediate challenge shifts to handling KVM exits. The KVM_RUN ioctl returns when the guest triggers an event that requires host intervention. This could be an I/O request, a page fault, an HLT instruction, or a system register access that the hypervisor needs to emulate. Each exit has a specific type, indicated by the exit_reason field in the kvm_run structure. The next phase of development will involve inspecting this exit_reason, reading the relevant guest state (registers, memory) using KVM_GET_ONE_REG and other KVM ioctls, emulating the required behavior, updating the guest state, and then re-arming the vCPU for execution using KVM_SET_ONE_REG and KVM_RUN. This cycle of guest execution, exit, emulation, and re-execution forms the core loop of any KVM-based hypervisor.