Skip to main content

Overview

Oboromi’s CPU emulation layer provides a safe wrapper around the Unicorn Engine to emulate the ARM64 CPU cores of the Nintendo Switch 2. The implementation focuses on both single-core legacy mode and multi-core shared memory execution.

Architecture

The CPU emulation is implemented in core/src/cpu/unicorn_interface.rs and provides a thread-safe wrapper around Unicorn’s ARM64 emulator.

Core Structure

use std::sync::{Arc, Mutex};
use unicorn_engine::{Arch, Mode, Prot, RegisterARM64, Unicorn};

pub struct UnicornCPU {
    emu: Arc<Mutex<Unicorn<'static, ()>>>,
    pub core_id: u32,
}
The UnicornCPU struct wraps the Unicorn engine instance in an Arc<Mutex<>> for thread-safe access across cores. Each instance has a unique core_id to distinguish between multiple cores.

Memory Management

Single-Core Allocation

Legacy mode creates an 8MB memory region with full permissions, suitable for testing:
pub fn new() -> Option<Self> {
    let mut emu = Unicorn::new(Arch::ARM64, Mode::LITTLE_ENDIAN)
        .map_err(|e| {
            eprintln!("Failed to create Unicorn instance: {e:?}");
            e
        })
        .ok()?;

    // Map 8MB of memory with full permissions
    emu.mem_map(0x0, 8 * 1024 * 1024, Prot::ALL)
        .map_err(|e| {
            eprintln!("Failed to map memory: {e:?}");
            e
        })
        .ok()?;

    // Initialize stack pointer
    let _ = emu.reg_write(RegisterARM64::SP, (8 * 1024 * 1024) - 0x1000);

    Some(Self {
        emu: Arc::new(Mutex::new(emu)),
        core_id: 0,
    })
}
Key Points:
  • 8MB contiguous address space starting at 0x0
  • Stack pointer initialized near the top of memory (0x7FF000)
  • Unicorn manages memory allocation internally

Execution Control

Running Code

The emulator provides two execution modes:
pub fn run(&self) -> u64 {
    let mut emu = self.emu.lock().unwrap();
    let pc = emu.reg_read(RegisterARM64::PC).unwrap_or(0);

    // Run until BRK instruction or error
    match emu.emu_start(pc, 0xFFFF_FFFF_FFFF_FFFF, 0, 0) {
        Ok(_) => 1, // Success - normal completion
        Err(e) => {
            // BRK instruction causes EXCEPTION - expected
            if format!("{e:?}").contains("EXCEPTION") {
                1 // Success - terminated by BRK
            } else {
                eprintln!("Emulation error: {e:?}");
                0 // Failure - actual emulation error
            }
        }
    }
}
The run() method treats EXCEPTION errors as successful termination because the BRK instruction (used as a breakpoint/halt) triggers an exception in Unicorn.

Register Access

General Purpose Registers (X0-X30)

The emulator provides symmetrical read/write access to all 31 general-purpose registers:
pub fn get_x(&self, reg_index: u32) -> u64 {
    let emu = self.emu.lock().unwrap();
    if reg_index > 30 {
        return 0;
    }

    let reg = match reg_index {
        0 => RegisterARM64::X0,
        1 => RegisterARM64::X1,
        // ... X2 through X29 ...
        30 => RegisterARM64::X30,
        _ => return 0,
    };

    emu.reg_read(reg).unwrap_or(0)
}

pub fn set_x(&self, reg_index: u32, value: u64) {
    let mut emu = self.emu.lock().unwrap();
    if reg_index > 30 {
        return;
    }

    let reg = match reg_index {
        0 => RegisterARM64::X0,
        // ... mapping for all registers ...
        30 => RegisterARM64::X30,
        _ => return,
    };

    let _ = emu.reg_write(reg, value);
}

Special Registers

pub fn get_sp(&self) -> u64 {
    let emu = self.emu.lock().unwrap();
    emu.reg_read(RegisterARM64::SP).unwrap_or(0)
}

pub fn set_sp(&self, value: u64) {
    let mut emu = self.emu.lock().unwrap();
    let _ = emu.reg_write(RegisterARM64::SP, value);
}
pub fn get_pc(&self) -> u64 {
    let emu = self.emu.lock().unwrap();
    emu.reg_read(RegisterARM64::PC).unwrap_or(0)
}

pub fn set_pc(&self, value: u64) {
    let mut emu = self.emu.lock().unwrap();
    let _ = emu.reg_write(RegisterARM64::PC, value);
}

Memory Access API

The CPU interface provides type-safe memory access methods:
pub fn write_u32(&self, vaddr: u64, value: u32) {
    let mut emu = self.emu.lock().unwrap();
    let bytes = value.to_le_bytes();
    let _ = emu.mem_write(vaddr, &bytes);
}

pub fn read_u32(&self, vaddr: u64) -> u32 {
    let emu = self.emu.lock().unwrap();
    let mut bytes = [0u8; 4];
    if emu.mem_read(vaddr, &mut bytes).is_ok() {
        u32::from_le_bytes(bytes)
    } else {
        0
    }
}
Memory Layout:
  • All values are stored in little-endian format (matching ARM64)
  • Failed reads return 0 instead of propagating errors
  • Virtual addresses map directly to physical addresses (flat address space)

Thread Safety

The implementation is explicitly marked as thread-safe:
unsafe impl Send for UnicornCPU {}
unsafe impl Sync for UnicornCPU {}
While UnicornCPU is Send + Sync, actual Unicorn instances are protected by a Mutex. This means:
  • Only one thread can execute or modify CPU state at a time per core
  • For true multi-core parallelism, create separate UnicornCPU instances
  • Shared memory mode allows multiple cores to see each other’s memory writes

Design Decisions

Why Unicorn Engine?

  1. Mature ARM64 support - Battle-tested implementation of ARM64 instruction set
  2. Bindings availability - Well-maintained Rust bindings via unicorn-engine crate
  3. Dynamic translation - Uses QEMU’s TCG backend for reasonable performance
  4. Debugging features - Built-in support for breakpoints and single-stepping

Memory Model Tradeoffs

Advantages:
  • Simple allocation model
  • No unsafe code in creation
  • Good for unit tests
Disadvantages:
  • Fixed 8MB limit
  • Cannot share memory between cores
  • Unicorn controls memory lifetime

Usage Example

use oboromi_core::cpu::UnicornCPU;

// Create a CPU instance
let cpu = UnicornCPU::new().expect("Failed to create CPU");

// Load some ARM64 code at address 0x1000
let code = vec![
    0x20, 0x00, 0x80, 0xd2,  // mov x0, #1
    0x00, 0x00, 0x00, 0xd4,  // brk #0
];
for (i, byte) in code.iter().enumerate() {
    cpu.write_u32(0x1000 + i as u64, *byte as u32);
}

// Set PC to code start
cpu.set_pc(0x1000);

// Execute
let result = cpu.run();

// Read result from X0
let x0_value = cpu.get_x(0);
println!("X0 = {}", x0_value); // X0 = 1

Source Files

  • Implementation: core/src/cpu/unicorn_interface.rs:1-266
  • Module: core/src/cpu/mod.rs:1-5

Build docs developers (and LLMs) love