use std::io::Write;
use std::sync::{Arc, Mutex};
use nixvm::abi::Arch;
use nixvm::fs::MountTable;
use nixvm::kernel::Kernel;
use nixvm::loader::{ProcessSpec, load_static};
use nixvm::vcpu::Backend;
use nixvm::vcpu::GuestMemory;
use nixvm::vcpu::interp::InterpBackend;
use nixvm::vcpu::mem::PAGE_SIZE;
const EHDR_LEN: usize = 64;
const PHDR_LEN: usize = 56;
const EM_AARCH64: u16 = 183;
const BODY_OFF: u64 = (EHDR_LEN + PHDR_LEN) as u64;
const NR_CLONE: u32 = 220;
const NR_EXIT: u32 = 93;
const NR_WAIT4: u32 = 260;
fn movz(rd: u32, imm16: u32) -> u32 {
0xD280_0000 | (imm16 << 5) | rd
}
fn movk16(rd: u32, imm16: u32) -> u32 {
0xF2A0_0000 | (imm16 << 5) | rd
}
fn svc0() -> u32 {
0xD400_0001
}
fn mov_imm32(rd: u32, val: u32) -> Vec<u32> {
let lo = val & 0xffff;
let hi = (val >> 16) & 0xffff;
let mut words = vec![movz(rd, lo)];
if hi != 0 {
words.push(movk16(rd, hi));
}
words
}
fn mov_addr(rd: u32, val: u32) -> [u32; 2] {
[movz(rd, val & 0xffff), movk16(rd, (val >> 16) & 0xffff)]
}
fn ldrb(rt: u32, rn: u32, imm12: u32) -> u32 {
0x3940_0000 | (imm12 << 10) | (rn << 5) | rt
}
fn mul(rd: u32, rn: u32, rm: u32) -> u32 {
0x9B00_7C00 | (rm << 16) | (rn << 5) | rd
}
fn add_reg(rd: u32, rn: u32, rm: u32) -> u32 {
0x8B00_0000 | (rm << 16) | (rn << 5) | rd
}
fn mov_reg(rd: u32, rm: u32) -> u32 {
0xAA00_03E0 | (rm << 16) | rd
}
fn cbz(rt: u32, byte_offset: i32) -> u32 {
let imm19 = ((byte_offset / 4) as u32) & 0x7_ffff;
0xB400_0000 | (imm19 << 5) | rt
}
#[derive(Clone)]
struct SharedBuf(Arc<Mutex<Vec<u8>>>);
impl Write for SharedBuf {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.0.lock().unwrap().extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
fn build_elf(vaddr: u64, body: &[u8]) -> Vec<u8> {
let mut f = vec![0u8; EHDR_LEN + PHDR_LEN];
f[0..4].copy_from_slice(&[0x7f, b'E', b'L', b'F']);
f[4] = 2; f[5] = 1; f[6] = 1;
f[16..18].copy_from_slice(&2u16.to_le_bytes()); f[18..20].copy_from_slice(&EM_AARCH64.to_le_bytes());
f[20..24].copy_from_slice(&1u32.to_le_bytes());
f[24..32].copy_from_slice(&(vaddr + BODY_OFF).to_le_bytes()); f[32..40].copy_from_slice(&(EHDR_LEN as u64).to_le_bytes()); f[52..54].copy_from_slice(&(EHDR_LEN as u16).to_le_bytes());
f[54..56].copy_from_slice(&(PHDR_LEN as u16).to_le_bytes());
f[56..58].copy_from_slice(&1u16.to_le_bytes());
let total = BODY_OFF + body.len() as u64;
let p = EHDR_LEN;
f[p..p + 4].copy_from_slice(&1u32.to_le_bytes()); f[p + 4..p + 8].copy_from_slice(&7u32.to_le_bytes()); f[p + 16..p + 24].copy_from_slice(&vaddr.to_le_bytes()); f[p + 24..p + 32].copy_from_slice(&vaddr.to_le_bytes());
f[p + 32..p + 40].copy_from_slice(&total.to_le_bytes()); f[p + 40..p + 48].copy_from_slice(&total.to_le_bytes()); f[p + 48..p + 56].copy_from_slice(&PAGE_SIZE.to_le_bytes());
f.extend_from_slice(body);
f
}
fn words_to_bytes(words: &[u32]) -> Vec<u8> {
let mut bytes = Vec::with_capacity(words.len() * 4);
for w in words {
bytes.extend_from_slice(&w.to_le_bytes());
}
bytes
}
fn run_program(vaddr: u64, body: &[u8], ncpus: usize) -> (i32, Vec<u8>, Kernel) {
let elf = build_elf(vaddr, body);
let mut mem = GuestMemory::new(vaddr, 256 * PAGE_SIZE);
let spec = ProcessSpec {
argv: vec!["prog".into()],
envp: vec![],
};
let img = load_static(&mut mem, &elf, &spec).unwrap();
let backend = InterpBackend::new(Arch::Aarch64).unwrap();
let vcpu = backend.new_vcpu(img.entry, img.stack_pointer).unwrap();
let captured = Arc::new(Mutex::new(Vec::new()));
let mut kernel = Kernel::new(Arch::Aarch64, MountTable::new());
kernel.set_stdout(Box::new(SharedBuf(captured.clone())));
kernel.set_ncpus(ncpus);
let exit_code = kernel.run(vcpu, mem).unwrap();
let out = captured.lock().unwrap().clone();
(exit_code, out, kernel)
}
fn build_code(wstatus_addr: u32) -> Vec<u32> {
let mut code = Vec::new();
let mut cbz_positions = Vec::new();
for _ in 0..4u32 {
code.extend(mov_imm32(0, 0)); code.extend(mov_imm32(1, 0)); code.extend(mov_imm32(8, NR_CLONE));
code.push(svc0()); cbz_positions.push(code.len());
code.push(0); }
code.extend(mov_imm32(21, 0));
for _ in 0..4u32 {
code.extend(mov_imm32(0, 0)); code.extend(mov_addr(1, wstatus_addr)); code.extend(mov_imm32(2, 0)); code.extend(mov_imm32(8, NR_WAIT4));
code.push(svc0());
code.push(ldrb(2, 1, 1)); code.push(add_reg(21, 21, 2)); }
code.push(mov_reg(0, 21));
code.extend(mov_imm32(8, NR_EXIT));
code.push(svc0());
let mut child_starts = Vec::new();
for i in 1..=4u32 {
child_starts.push(code.len());
code.extend(mov_imm32(0, i)); code.extend(mov_imm32(1, 7)); code.push(mul(0, 0, 1)); code.extend(mov_imm32(8, NR_EXIT));
code.push(svc0());
}
for (k, pos) in cbz_positions.into_iter().enumerate() {
let off_bytes = ((child_starts[k] as i64 - pos as i64) * 4) as i32;
code[pos] = cbz(0, off_bytes);
}
code
}
const EXPECTED_EXIT: i32 = 7 + 14 + 21 + 28;
#[test]
fn smp_scheduling_matches_serial_fork_sum() {
let vaddr = 0x1_0000u64;
let code_words = build_code(0).len() as u64;
let wstatus_addr = u32::try_from(vaddr + BODY_OFF + code_words * 4).unwrap();
let instrs = build_code(wstatus_addr);
assert_eq!(
instrs.len() as u64,
code_words,
"two-pass build must be length-stable"
);
let mut body = words_to_bytes(&instrs);
body.extend_from_slice(&[0u8; 4]);
for iter in 0..15 {
let (serial_code, serial_out, serial_kernel) = run_program(vaddr, &body, 1);
assert_eq!(serial_code, EXPECTED_EXIT, "serial (ncpus=1) run {iter}");
assert!(serial_out.is_empty());
assert!(
serial_kernel.unsupported().is_empty(),
"{:?}",
serial_kernel.unsupported()
);
let (smp_code, smp_out, smp_kernel) = run_program(vaddr, &body, 4);
assert_eq!(
smp_code, EXPECTED_EXIT,
"SMP (ncpus=4) run {iter} must agree with the serial scheduler"
);
assert!(smp_out.is_empty());
assert!(
smp_kernel.unsupported().is_empty(),
"{:?}",
smp_kernel.unsupported()
);
}
}