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
Legacy Mode
Shared Memory Mode
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
Multi-Core Shared Memory For multi-core emulation, cores share a common memory region: pub unsafe fn new_with_shared_mem (
core_id : u32 ,
memory_ptr : * mut u8 ,
memory_size : u64
) -> Option < Self > {
let mut emu = Unicorn :: new ( Arch :: ARM64 , Mode :: LITTLE_ENDIAN )
. map_err ( | e | {
eprintln! ( "Failed to create Unicorn instance for core {}: {:?}" , core_id , e );
e
})
. ok () ? ;
// Map shared memory
unsafe {
emu . mem_map_ptr ( 0x0 , memory_size , Prot :: ALL , memory_ptr as * mut std :: ffi :: c_void )
. map_err ( | e | {
eprintln! ( "Failed to map shared memory for core {}: {:?}" , core_id , e );
e
})
. ok () ? ;
}
// Each core gets 1MB stack space, offset by core ID
let stack_top = memory_size - ( core_id as u64 * 0x100000 );
let _ = emu . reg_write ( RegisterARM64 :: SP , stack_top );
Some ( Self {
emu : Arc :: new ( Mutex :: new ( emu )),
core_id ,
})
}
Key Points:
All cores share the same physical memory region
Each core has a dedicated 1MB stack space
Stack offsets prevent collision: Core 0 at top, Core N at top - (N * 1MB)
Caller must ensure memory pointer validity
Execution Control
Running Code
The emulator provides two execution modes:
Run Until Halt
Single Step
Manual Halt
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:
32-bit Access
64-bit Access
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 = [ 0 u8 ; 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?
Mature ARM64 support - Battle-tested implementation of ARM64 instruction set
Bindings availability - Well-maintained Rust bindings via unicorn-engine crate
Dynamic translation - Uses QEMU’s TCG backend for reasonable performance
Debugging features - Built-in support for breakpoints and single-stepping
Memory Model Tradeoffs
Legacy Mode
Shared Memory
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
Advantages:
True multi-core memory sharing
Flexible memory size
External memory management
Disadvantages:
Requires unsafe code
Caller must manage memory lifetime
More complex stack allocation
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