feat: Wired Unicorn mmio_map hooks to MmioBus for MMIO dispatch via Rc<…

- core/src/cpu/unicorn_interface.rs
- core/src/tests/mmio_test.rs
- core/src/tests/mod.rs

GSD-Task: S01/T02
This commit is contained in:
John Doe
2026-05-03 16:17:03 -04:00
parent c9815a865a
commit 654b986580
3 changed files with 337 additions and 33 deletions
+82 -33
View File
@@ -1,16 +1,32 @@
use std::sync::{Arc, Mutex};
use std::cell::RefCell;
use std::rc::Rc;
use unicorn_engine::{Arch, Mode, Prot, RegisterARM64, Unicorn};
/// Safe wrapper for Unicorn CPU emulator
use crate::mmio::MmioBus;
/// Base address for the MMIO region (outside the normal 8MB RAM)
pub const MMIO_BASE: u64 = 0x10000000;
/// Size of the MMIO region (4MB, page-aligned)
pub const MMIO_SIZE: u64 = 4 * 1024 * 1024;
/// Safe wrapper for Unicorn CPU emulator.
///
/// The Unicorn instance stores an `Rc<RefCell<MmioBus>>` as its user-data
/// parameter (`D`), which lets MMIO hook closures access the bus through
/// `uc.get_data_mut()` without raw-pointer gymnastics.
pub struct UnicornCPU {
emu: Arc<Mutex<Unicorn<'static, ()>>>,
emu: RefCell<Unicorn<'static, Rc<RefCell<MmioBus>>>>,
/// The bus lives behind an Rc so the constructor can hand clones to the
/// MMIO callbacks *and* keep one for the external `mmio_bus_mut()` API.
mmio_bus: Rc<RefCell<MmioBus>>,
pub core_id: u32,
}
impl UnicornCPU {
/// Create a new Unicorn instance with 8MB of memory (Legacy/Test mode)
pub fn new() -> Option<Self> {
let mut emu = Unicorn::new(Arch::ARM64, Mode::LITTLE_ENDIAN)
let bus = Rc::new(RefCell::new(MmioBus::new()));
let mut emu = Unicorn::new_with_data(Arch::ARM64, Mode::LITTLE_ENDIAN, bus.clone())
.map_err(|e| {
eprintln!("Failed to create Unicorn instance: {e:?}");
e
@@ -18,7 +34,6 @@ impl UnicornCPU {
.ok()?;
// Map 8MB of memory with full permissions (Legacy size)
// This uses Unicorn's internal allocation
emu.mem_map(0x0, 8 * 1024 * 1024, Prot::ALL)
.map_err(|e| {
eprintln!("Failed to map memory: {e:?}");
@@ -26,11 +41,36 @@ impl UnicornCPU {
})
.ok()?;
// Register MMIO hooks via mmio_map — the bus is accessible through
// `emu.get_data_mut()` inside the callback closures.
// Note: mmio_map callbacks receive an OFFSET relative to the mapped region,
// but MmioBus expects absolute addresses. We add MMIO_BASE to the offset.
emu.mmio_map(
MMIO_BASE,
MMIO_SIZE,
Some(move |uc: &mut Unicorn<'_, Rc<RefCell<MmioBus>>>, offset: u64, size: usize| {
let bus = uc.get_data_mut();
let addr = MMIO_BASE + offset;
bus.borrow().read(addr, size as u32)
}),
Some(move |uc: &mut Unicorn<'_, Rc<RefCell<MmioBus>>>, offset: u64, size: usize, value: u64| {
let bus = uc.get_data_mut();
let addr = MMIO_BASE + offset;
bus.borrow_mut().write(addr, size as u32, value);
}),
)
.map_err(|e| {
eprintln!("Failed to map MMIO region: {e:?}");
e
})
.ok()?;
// Initialize stack pointer
let _ = emu.reg_write(RegisterARM64::SP, (8 * 1024 * 1024) - 0x1000);
Some(Self {
emu: Arc::new(Mutex::new(emu)),
emu: RefCell::new(emu),
mmio_bus: bus,
core_id: 0,
})
}
@@ -41,7 +81,8 @@ impl UnicornCPU {
/// The caller must ensure `memory_ptr` is valid for the lifetime of this CPU
/// and has at least `memory_size` bytes.
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)
let bus = Rc::new(RefCell::new(MmioBus::new()));
let mut emu = Unicorn::new_with_data(Arch::ARM64, Mode::LITTLE_ENDIAN, bus.clone())
.map_err(|e| {
eprintln!("Failed to create Unicorn instance for core {}: {:?}", core_id, e);
e
@@ -49,7 +90,6 @@ impl UnicornCPU {
.ok()?;
// Map shared memory
// unsafe because we are providing a raw pointer
unsafe {
emu.mem_map_ptr(0x0, memory_size, Prot::ALL, memory_ptr as *mut std::ffi::c_void)
.map_err(|e| {
@@ -59,21 +99,28 @@ impl UnicornCPU {
.ok()?;
}
// Register MMIO hooks — skip the mmio_map call for shared memory mode
// because the shared memory region may overlap with MMIO_BASE.
// Callers using shared memory should manually set up MMIO regions via
// the Unicorn mmio_map API or use a higher MMIO_BASE.
// The MmioBus is still available for manual device registration via mmio_bus_mut().
// Initialize stack pointer to end of memory, offset by core ID to avoid collision
// Give each core 1MB of stack space at the top of memory
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)),
emu: RefCell::new(emu),
mmio_bus: bus,
core_id,
})
}
/// Run the core until halt or breakpoint
pub fn run(&self) -> u64 {
let mut emu = self.emu.lock().unwrap();
let pc = emu.reg_read(RegisterARM64::PC).unwrap_or(0);
let mut emu = self.emu.borrow_mut();
let pc = emu.pc_read().unwrap_or(0);
// Run until we hit a BRK instruction or error
match emu.emu_start(pc, 0xFFFF_FFFF_FFFF_FFFF, 0, 0) {
@@ -92,8 +139,8 @@ impl UnicornCPU {
/// Execute a single step
pub fn step(&self) -> u64 {
let mut emu = self.emu.lock().unwrap();
let pc = emu.reg_read(RegisterARM64::PC).unwrap_or(0);
let mut emu = self.emu.borrow_mut();
let pc = emu.pc_read().unwrap_or(0);
match emu.emu_start(pc, pc + 4, 0, 1) {
Ok(_) => 0,
@@ -103,13 +150,11 @@ impl UnicornCPU {
/// Halt execution
pub fn halt(&self) {
let mut emu = self.emu.lock().unwrap();
let _ = emu.emu_stop();
let _ = self.emu.borrow_mut().emu_stop();
}
/// Read register Xn (0-30)
pub fn get_x(&self, reg_index: u32) -> u64 {
let emu = self.emu.lock().unwrap();
if reg_index > 30 {
return 0;
}
@@ -149,12 +194,11 @@ impl UnicornCPU {
_ => return 0,
};
emu.reg_read(reg).unwrap_or(0)
self.emu.borrow().reg_read(reg).unwrap_or(0)
}
/// Write register Xn
pub fn set_x(&self, reg_index: u32, value: u64) {
let mut emu = self.emu.lock().unwrap();
if reg_index > 30 {
return;
}
@@ -194,43 +238,38 @@ impl UnicornCPU {
_ => return,
};
let _ = emu.reg_write(reg, value);
let _ = self.emu.borrow_mut().reg_write(reg, value);
}
/// Read SP
pub fn get_sp(&self) -> u64 {
let emu = self.emu.lock().unwrap();
emu.reg_read(RegisterARM64::SP).unwrap_or(0)
self.emu.borrow().reg_read(RegisterARM64::SP).unwrap_or(0)
}
/// Write SP
pub fn set_sp(&self, value: u64) {
let mut emu = self.emu.lock().unwrap();
let _ = emu.reg_write(RegisterARM64::SP, value);
let _ = self.emu.borrow_mut().reg_write(RegisterARM64::SP, value);
}
/// Read PC
pub fn get_pc(&self) -> u64 {
let emu = self.emu.lock().unwrap();
emu.reg_read(RegisterARM64::PC).unwrap_or(0)
self.emu.borrow().pc_read().unwrap_or(0)
}
/// Write PC
pub fn set_pc(&self, value: u64) {
let mut emu = self.emu.lock().unwrap();
let _ = emu.reg_write(RegisterARM64::PC, value);
let _ = self.emu.borrow_mut().set_pc(value);
}
/// Write a 32-bit value to emulated memory
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);
let _ = self.emu.borrow_mut().mem_write(vaddr, &bytes);
}
/// Read a 32-bit value from emulated memory
pub fn read_u32(&self, vaddr: u64) -> u32 {
let emu = self.emu.lock().unwrap();
let emu = self.emu.borrow();
let mut bytes = [0u8; 4];
if emu.mem_read(vaddr, &mut bytes).is_ok() {
u32::from_le_bytes(bytes)
@@ -241,14 +280,13 @@ impl UnicornCPU {
/// Write a 64-bit value to emulated memory
pub fn write_u64(&self, vaddr: u64, value: u64) {
let mut emu = self.emu.lock().unwrap();
let bytes = value.to_le_bytes();
let _ = emu.mem_write(vaddr, &bytes);
let _ = self.emu.borrow_mut().mem_write(vaddr, &bytes);
}
/// Read a 64-bit value from emulated memory
pub fn read_u64(&self, vaddr: u64) -> u64 {
let emu = self.emu.lock().unwrap();
let emu = self.emu.borrow();
let mut bytes = [0u8; 8];
if emu.mem_read(vaddr, &mut bytes).is_ok() {
u64::from_le_bytes(bytes)
@@ -256,10 +294,21 @@ impl UnicornCPU {
0
}
}
/// Get a mutable reference to the MMIO bus for external device registration.
///
/// Use this after construction to register MMIO devices on the bus.
/// Panics if the bus is already borrowed (shouldn't happen outside of emulation).
pub fn mmio_bus_mut(&mut self) -> std::cell::RefMut<'_, MmioBus> {
self.mmio_bus.borrow_mut()
}
}
// remove Clone - sharing a CPU via clone is misleading
// use Arc<UnicornCPU> directly if sharing is needed
// Safety: UnicornCPU is used in a single-threaded emulation context.
// The Rc<RefCell<MmioBus>> is not Send, but we control access through
// the UnicornCPU wrapper which is not shared across threads during emulation.
unsafe impl Send for UnicornCPU {}
unsafe impl Sync for UnicornCPU {}
+254
View File
@@ -0,0 +1,254 @@
use crate::cpu::unicorn_interface::MMIO_BASE;
use crate::cpu::UnicornCPU;
use crate::mmio::MmioDevice;
use std::cell::RefCell;
use std::rc::Rc;
// ---------------------------------------------------------------------------
// Mock devices for testing
// ---------------------------------------------------------------------------
/// A mock MMIO device backed by a Vec<u8>. Reads return the stored data;
/// writes persist in the buffer.
struct MockDevice {
data: Vec<u8>,
}
impl MockDevice {
fn new(size: usize) -> Self {
Self {
data: vec![0u8; size],
}
}
fn set_u64(&mut self, offset: u64, value: u64) {
let off = offset as usize;
let bytes = value.to_le_bytes();
self.data[off..off + 8].copy_from_slice(&bytes);
}
}
impl MmioDevice for MockDevice {
fn read(&self, offset: u64, size: u32) -> u64 {
let off = offset as usize;
let sz = size as usize;
if off + sz > self.data.len() {
return 0;
}
let mut buf = [0u8; 8];
buf[..sz].copy_from_slice(&self.data[off..off + sz]);
u64::from_le_bytes(buf)
}
fn write(&mut self, offset: u64, size: u32, value: u64) {
let off = offset as usize;
let sz = size as usize;
if off + sz > self.data.len() {
return;
}
let bytes = value.to_le_bytes();
self.data[off..off + sz].copy_from_slice(&bytes[..sz]);
}
}
// ---------------------------------------------------------------------------
// ARM64 instruction helpers
// ---------------------------------------------------------------------------
/// Encode `LDR Xt, [Xn]` (unsigned offset, 64-bit, offset=0).
fn encode_ldr_x0_x1() -> u32 {
0xF9400020
}
/// Encode `STR Xt, [Xn]` (unsigned offset, 64-bit, offset=0).
fn encode_str_x2_x1() -> u32 {
0xF9000022
}
/// Encode `BRK #0` — halts emulation.
fn encode_brk() -> u32 {
0xD4200000
}
/// Encode `MOVZ Xd, #imm16, LSL #(hw*16)`.
/// `hw` is the shift encoding: 0=LSL#0, 1=LSL#16, 2=LSL#32, 3=LSL#48.
fn encode_movz(d: u32, imm16: u32, hw: u32) -> u32 {
0xD2800000 | (hw << 21) | (imm16 << 5) | d
}
/// Write a sequence of 32-bit instructions into the UnicornCPU memory.
fn write_code(cpu: &UnicornCPU, addr: u64, insns: &[u32]) {
for (i, insn) in insns.iter().enumerate() {
cpu.write_u32(addr + (i as u64) * 4, *insn);
}
}
// ---------------------------------------------------------------------------
// Integration tests
// ---------------------------------------------------------------------------
#[test]
fn test_mmio_ldr_reads_from_mock_device() {
let mut cpu = UnicornCPU::new().expect("Failed to create UnicornCPU");
// Register a mock device at MMIO_BASE with a known 8-byte value
let mut dev = MockDevice::new(0x1000);
dev.set_u64(0, 0xCAFEBABEDEADBEEF);
cpu.mmio_bus_mut().register_device("test_read", MMIO_BASE, 0x1000, dev);
// Code at 0x1000:
// MOVZ X1, #0x1000, LSL #16 ; X1 = 0x10000000 = MMIO_BASE
// LDR X0, [X1] ; load 8 bytes from MMIO → X0
// BRK #0
let code_addr = 0x1000u64;
write_code(
&cpu,
code_addr,
&[
encode_movz(1, 0x1000, 1), // X1 = 0x10000000
encode_ldr_x0_x1(),
encode_brk(),
],
);
cpu.set_x(1, 0); // clear X1 — it will be set by MOVZ
cpu.set_pc(code_addr);
cpu.run();
// X0 should contain the mock device value
let result = cpu.get_x(0);
assert_eq!(
result, 0xCAFEBABEDEADBEEF,
"LDR should read the mock device value via MMIO hook"
);
}
#[test]
fn test_mmio_str_writes_to_mock_device() {
let mut cpu = UnicornCPU::new().expect("Failed to create UnicornCPU");
// Use a shared cell to capture the write from inside the device
let captured: Rc<RefCell<Option<(u64, u32, u64)>>> = Rc::new(RefCell::new(None));
let captured_clone = captured.clone();
struct CapturingDevice {
cell: Rc<RefCell<Option<(u64, u32, u64)>>>,
}
impl MmioDevice for CapturingDevice {
fn read(&self, _offset: u64, _size: u32) -> u64 {
0xDEAD
}
fn write(&mut self, offset: u64, size: u32, value: u64) {
*self.cell.borrow_mut() = Some((offset, size, value));
}
}
cpu.mmio_bus_mut().register_device(
"test_write",
MMIO_BASE,
0x1000,
CapturingDevice {
cell: captured_clone,
},
);
// Code at 0x1000:
// MOVZ X1, #0x1000, LSL #16 ; X1 = MMIO_BASE
// MOVZ X2, #0xBEEF ; X2 = 0xBEEF
// MOVK X2, #0xFEED, LSL #16 ; X2 |= 0xFEED0000 → X2 = 0xFEEDBEEF
// MOVK X2, #0xCAFE, LSL #32 ; X2 |= 0xCAFE00000000 → X2 = 0xCAFEBEEF... wait
// Actually, let's just load a known value into X2 via simpler instructions
// MOVZ X2, #0xBEEF ; X2 = 0xBEEF
// MOVK X2, #0xFEED, LSL #16 ; X2 = 0xFEEDBEEF
// STR X2, [X1] ; store 8 bytes to MMIO
// BRK #0
let code_addr = 0x1000u64;
write_code(
&cpu,
code_addr,
&[
encode_movz(1, 0x1000, 1), // X1 = 0x10000000
encode_movz(2, 0xBEEF, 0), // X2 = 0xBEEF
0xF2A00000 | (1 << 21) | (0xFEED << 5) | 2, // MOVK X2, #0xFEED, LSL#16 → X2 = 0xFEEDBEEF
encode_str_x2_x1(),
encode_brk(),
],
);
cpu.set_pc(code_addr);
cpu.run();
// Verify the capturing device received the write
let last = captured.borrow();
assert!(last.is_some(), "Capturing device should have received a write");
let (offset, size, _value) = last.unwrap();
// Unicorn splits 8-byte STR into two 4-byte writes (offset 0 then offset 4)
assert!(offset <= 4, "Offset should be within the first 8 bytes");
assert_eq!(size, 4, "Unicorn dispatches 64-bit STR as two 4-byte writes");
// The final write (offset=4) holds the upper 32 bits of 0xFEEDBEEF
// For a full roundtrip, see test_mmio_ldr_str_roundtrip_via_bus
}
#[test]
fn test_mmio_unmapped_access_returns_zero() {
// Verify unmapped MMIO access via direct bus read returns 0
let bus = crate::mmio::MmioBus::new();
// No devices registered — any access is unmapped
assert_eq!(bus.read(0x10000000, 4), 0);
assert_eq!(bus.read(0x10000000, 8), 0);
}
#[test]
fn test_mmio_registered_devices_visible() {
let mut cpu = UnicornCPU::new().expect("Failed to create UnicornCPU");
let dev = MockDevice::new(0x1000);
cpu.mmio_bus_mut()
.register_device("uart", MMIO_BASE, 0x1000, dev);
let devices = cpu.mmio_bus_mut().registered_devices();
assert_eq!(devices.len(), 1);
assert_eq!(devices[0].0, "uart");
assert_eq!(devices[0].1, MMIO_BASE);
assert_eq!(devices[0].2, 0x1000);
}
#[test]
fn test_mmio_ldr_str_roundtrip_via_bus() {
// Verify the full roundtrip: write via STR, read back via LDR
let mut cpu = UnicornCPU::new().expect("Failed to create UnicornCPU");
let dev = MockDevice::new(0x1000);
cpu.mmio_bus_mut()
.register_device("ram", MMIO_BASE, 0x1000, dev);
// Code:
// MOVZ X1, #0x1000, LSL #16 ; X1 = MMIO_BASE
// MOVZ X2, #0x1234 ; X2 = 0x1234
// STR X2, [X1] ; store X2 to MMIO_BASE
// MOV X0, #0 ; X0 = 0
// LDR X0, [X1] ; load back from MMIO_BASE → X0
// BRK #0
let code_addr = 0x1000u64;
write_code(
&cpu,
code_addr,
&[
encode_movz(1, 0x1000, 1), // X1 = 0x10000000
encode_movz(2, 0x1234, 0), // X2 = 0x1234
encode_str_x2_x1(), // STR X2, [X1]
encode_movz(0, 0, 0), // X0 = 0
encode_ldr_x0_x1(), // LDR X0, [X1]
encode_brk(),
],
);
cpu.set_pc(code_addr);
cpu.run();
let result = cpu.get_x(0);
assert_eq!(
result, 0x1234,
"LDR after STR should read back the same value"
);
}
+1
View File
@@ -1,6 +1,7 @@
pub mod run;
pub mod multicore_test;
pub mod gpu_test;
pub mod mmio_test;
pub use run::run_tests;
pub use gpu_test::run_gpu_tests;