use alloc::sync::Arc;
use alloc::vec::Vec;
use crate::core::device::{DebugTranslation, Deferred, Device, RealizeCtx, ResetKind};
use crate::core::space::{AddressSpace, RamStore, Region, RequesterId};
use crate::core::state::{ChunkReader, MachineShape, StateWriter};
use super::isa::{Arg, Class, Grp, Op, decode, resolve};
use super::{Config, Features, Interrupt, Reg, Regs, Variant, X86, flags, linear};
struct Machine {
cpu: Arc<X86>,
ram: Arc<RamStore>,
ports: Arc<RamStore>,
}
impl Machine {
fn new(cfg: Config) -> Machine {
let ram = Arc::new(RamStore::new(0x10_0000));
let mem = AddressSpace::new("mem", 20);
mem.topology()
.map(Region::ram("ram", ram.clone()), 0)
.expect("1 MiB fits in 20 bits");
let ports = Arc::new(RamStore::new(0x1_0000));
let io = AddressSpace::new("io", 16);
io.topology()
.map(Region::ram("ports", ports.clone()), 0)
.expect("64 KiB fits in 16 bits");
let cpu = Arc::new(X86::new(cfg));
cpu.attach_space(Arc::new(mem));
cpu.attach_io_space(Arc::new(io));
Machine { cpu, ram, ports }
}
fn load(&self, cs: u16, ip: u16, code: &[u8]) {
for (i, byte) in code.iter().enumerate() {
let addr = linear(cs, ip.wrapping_add(i as u16));
self.ram.write_u8(addr, *byte).unwrap();
}
let mut regs = self.cpu.regs();
regs.cs = cs;
regs.rip = u64::from(ip);
self.cpu.set_regs(regs);
self.cpu.session.lock().state.reset_pending = false;
}
fn poke(&self, addr: u64, byte: u8) {
self.ram.write_u8(addr, byte).unwrap();
}
fn peek(&self, addr: u64) -> u8 {
self.ram.read_u8(addr).unwrap()
}
fn regs(&self) -> Regs {
self.cpu.regs()
}
fn set_regs(&self, f: impl FnOnce(&mut Regs)) {
let mut regs = self.cpu.regs();
f(&mut regs);
self.cpu.set_regs(regs);
}
}
fn machine() -> Machine {
Machine::new(Config::I8088)
}
#[test]
fn a_segmented_address_is_twenty_bits_and_wraps_at_one_megabyte() {
assert_eq!(linear(0x0000, 0x0000), 0x0_0000);
assert_eq!(linear(0x1000, 0x0010), 0x1_0010);
assert_eq!(linear(0xf000, 0xfff0), 0xf_fff0);
assert_eq!(linear(0xffff, 0x0010), 0x0_0000);
assert_eq!(linear(0xffff, 0x0020), 0x0_0010);
assert_eq!(linear(0xffff, 0xffff), 0x0_ffef);
}
#[test]
fn a_read_above_the_first_megabyte_wraps_to_the_bottom() {
let m = machine();
m.poke(0x0_0000, 0x5a);
m.load(0x0000, 0x0100, &[0xa0, 0x10, 0x00]);
m.set_regs(|r| r.ds = 0xffff);
m.cpu.step();
assert_eq!(m.regs().rax & 0xff, 0x5a);
}
#[test]
fn bp_based_addressing_defaults_to_the_stack_segment() {
let m = machine();
m.set_regs(|r| {
r.ds = 0x1000;
r.ss = 0x2000;
r.rbp = 0x0004;
r.rbx = 0x0004;
});
m.poke(linear(0x2000, 0x0004), 0x11);
m.poke(linear(0x1000, 0x0004), 0x22);
m.load(0x0000, 0x0100, &[0x8a, 0x46, 0x00]);
m.cpu.step();
assert_eq!(m.regs().rax & 0xff, 0x11);
m.load(0x0000, 0x0200, &[0x8a, 0x07]);
m.cpu.step();
assert_eq!(m.regs().rax & 0xff, 0x22);
m.load(0x0000, 0x0300, &[0x3e, 0x8a, 0x46, 0x00]);
m.cpu.step();
assert_eq!(m.regs().rax & 0xff, 0x22);
}
#[test]
fn the_direct_address_encoding_is_not_bp_relative() {
let m = machine();
m.set_regs(|r| {
r.ds = 0x1000;
r.ss = 0x2000;
r.rbp = 0xbeef;
});
m.poke(linear(0x1000, 0x0034), 0x77);
m.load(0x0000, 0x0100, &[0x8a, 0x06, 0x34, 0x00]);
m.cpu.step();
assert_eq!(m.regs().rax & 0xff, 0x77);
}
#[test]
fn reset_starts_sixteen_bytes_below_the_top_of_memory() {
let m = machine();
m.cpu.step();
let regs = m.regs();
assert_eq!((regs.cs, regs.rip), (0xffff, 0x0000));
assert_eq!(linear(regs.cs, regs.rip as u16), 0xf_fff0);
assert_eq!((regs.ds, regs.es, regs.ss), (0, 0, 0));
assert_eq!(regs.eflags, flags::RESERVED_SET);
assert!(!m.cpu.reset_pending());
}
#[test]
fn a_reset_vector_jump_lands_where_it_says() {
let m = machine();
for (i, byte) in [0xea, 0x5b, 0xe0, 0x00, 0xf0].into_iter().enumerate() {
m.poke(0xf_fff0 + i as u64, byte);
}
m.cpu.step(); m.cpu.step(); let regs = m.regs();
assert_eq!((regs.cs, regs.rip), (0xf000, 0xe05b));
}
#[test]
fn halt_stops_the_core_until_an_interrupt_arrives() {
let m = machine();
m.load(0x0000, 0x0100, &[0xf4]);
m.cpu.step();
assert!(m.cpu.is_halted());
assert_eq!(m.cpu.step(), 0);
m.poke(0x0008, 0x00);
m.poke(0x0009, 0x40);
m.poke(0x000a, 0x00);
m.poke(0x000b, 0x00);
m.cpu.pulse_nmi();
assert!(m.cpu.step() > 0);
assert!(!m.cpu.is_halted());
let regs = m.regs();
assert_eq!((regs.cs, regs.rip), (0x0000, 0x4000));
}
#[test]
fn an_interrupt_pushes_flags_then_cs_then_the_return_address() {
let m = machine();
m.load(0x1000, 0x0100, &[0x90]);
m.set_regs(|r| {
r.ss = 0x2000;
r.rsp = 0x0100;
r.eflags |= flags::IF | flags::CF;
});
m.poke(0x80, 0x34);
m.poke(0x81, 0x12);
m.poke(0x82, 0x00);
m.poke(0x83, 0x30);
m.cpu.set_intr_vector(0x20);
m.cpu.set_intr(true);
m.cpu.step();
let regs = m.regs();
assert_eq!((regs.cs, regs.rip), (0x3000, 0x1234));
assert_eq!(regs.rsp, 0x00fa);
let word = |off: u16| {
u16::from(m.peek(linear(0x2000, off))) | (u16::from(m.peek(linear(0x2000, off + 1))) << 8)
};
assert_eq!(u32::from(word(0x00fe)) & flags::IF, flags::IF);
assert_eq!(word(0x00fc), 0x1000); assert_eq!(word(0x00fa), 0x0100); assert_eq!(regs.eflags & (flags::IF | flags::TF), 0);
}
#[test]
fn an_interrupt_is_masked_by_the_interrupt_flag_but_an_nmi_is_not() {
let m = machine();
m.load(0x0000, 0x0100, &[0x90, 0x90]);
m.set_regs(|r| {
r.ss = 0x2000;
r.rsp = 0x0100;
r.eflags &= !flags::IF;
});
m.cpu.set_intr_vector(0x20);
m.cpu.set_intr(true);
m.cpu.step();
assert_eq!(
m.regs().rip,
0x0101,
"INTR must be ignored while IF is clear"
);
m.poke(0x0008, 0x00);
m.poke(0x0009, 0x40);
m.cpu.pulse_nmi();
m.cpu.step();
assert_eq!(m.regs().rip, 0x4000, "NMI is not maskable");
}
#[test]
fn writing_the_stack_segment_shadows_the_next_instruction() {
let m = machine();
m.load(0x0000, 0x0100, &[0x8e, 0xd0, 0x89, 0xdc]);
m.set_regs(|r| {
r.rax = 0x3000;
r.rbx = 0x0200;
r.eflags |= flags::IF;
});
m.cpu.set_intr_vector(0x20);
m.poke(0x80, 0x00);
m.poke(0x81, 0x50);
m.cpu.step(); assert!(m.cpu.interrupt_shadow());
m.cpu.set_intr(true); m.cpu.step(); assert_eq!(m.regs().ss, 0x3000);
assert_eq!(m.regs().rsp, 0x0200);
assert!(!m.cpu.interrupt_shadow());
m.cpu.step(); assert_eq!(m.regs().rip, 0x5000);
}
#[test]
fn the_trap_flag_takes_a_type_one_interrupt_after_each_instruction() {
let m = machine();
m.load(0x0000, 0x0100, &[0x90]);
m.set_regs(|r| {
r.ss = 0x2000;
r.rsp = 0x0100;
r.eflags |= flags::TF;
});
m.poke(0x04, 0x00);
m.poke(0x05, 0x60);
m.cpu.step();
let regs = m.regs();
assert_eq!(regs.rip, 0x6000);
assert_eq!(regs.eflags & flags::TF, 0);
}
#[test]
fn the_queue_depth_follows_the_part() {
let m = Machine::new(Config::I8088);
m.load(0x0000, 0x0100, &[0x90; 8]);
m.cpu.step();
assert_eq!(m.cpu.prefetch_queue().len(), 3);
assert!(m.cpu.set_prefetch_queue(&[0; 4]).is_ok());
assert!(m.cpu.set_prefetch_queue(&[0; 5]).is_err());
let m = Machine::new(Config::I8086);
m.load(0x0000, 0x0100, &[0x90; 8]);
m.cpu.step();
assert_eq!(m.cpu.prefetch_queue().len(), 5);
assert!(m.cpu.set_prefetch_queue(&[0; 6]).is_ok());
assert!(m.cpu.set_prefetch_queue(&[0; 7]).is_err());
}
#[test]
fn a_control_transfer_flushes_the_queue() {
let m = machine();
m.load(0x0000, 0x0100, &[0xeb, 0x00, 0xf4]);
m.cpu.step();
assert_eq!(m.regs().rip, 0x0102);
assert!(
m.cpu.prefetch_queue().is_empty(),
"the queue held bytes fetched before the jump"
);
}
#[test]
fn an_installed_queue_is_executed_before_memory_is_read() {
let m = machine();
m.load(0x0000, 0x0100, &[0x90, 0x90]);
m.cpu.set_prefetch_queue(&[0x40]).unwrap();
m.cpu.step();
assert_eq!(m.regs().rax, 1);
assert_eq!(m.regs().rip, 0x0101);
}
#[test]
fn wait_and_lock_do_nothing_observable() {
let m = machine();
m.load(0x0000, 0x0100, &[0x9b, 0xf0, 0x40]);
m.cpu.step(); assert_eq!(m.regs().rip, 0x0101);
m.cpu.step(); assert_eq!(m.regs().rip, 0x0103);
assert_eq!(m.regs().rax, 1);
}
#[test]
fn separate_address_spaces_mean_a_port_is_not_a_memory_address() {
let m = machine();
m.ports.write_u8(0x0060, 0xa5).unwrap();
m.poke(0x0060, 0x5a);
m.load(0x0000, 0x0100, &[0xe4, 0x60]);
m.cpu.step();
assert_eq!(m.regs().rax & 0xff, 0xa5, "IN must not read memory");
m.set_regs(|r| r.rax = 0x0012);
m.load(0x0000, 0x0200, &[0xe6, 0x61]);
m.cpu.step();
assert_eq!(m.ports.read_u8(0x0061).unwrap(), 0x12);
assert_eq!(m.peek(0x0061), 0x00, "OUT must not write memory");
}
#[test]
fn a_word_port_access_is_two_consecutive_ports() {
let m = machine();
m.ports.write_u8(0x0300, 0x34).unwrap();
m.ports.write_u8(0x0301, 0x12).unwrap();
m.set_regs(|r| r.rdx = 0x0300);
m.load(0x0000, 0x0100, &[0xed]); m.cpu.step();
assert_eq!(m.regs().rax, 0x1234);
}
#[test]
fn a_core_with_no_io_space_reads_ones() {
let ram = Arc::new(RamStore::new(0x10_0000));
let mem = AddressSpace::new("mem", 20);
mem.topology()
.map(Region::ram("ram", ram.clone()), 0)
.unwrap();
let cpu = X86::new(Config::I8088);
cpu.attach_space(Arc::new(mem));
for (i, byte) in [0xe4u8, 0x60].into_iter().enumerate() {
ram.write_u8(0x100 + i as u64, byte).unwrap();
}
cpu.set_regs(Regs {
cs: 0,
rip: 0x100,
..Regs::new()
});
cpu.session.lock().state.reset_pending = false;
cpu.step();
assert_eq!(cpu.regs().rax & 0xff, 0xff);
}
#[test]
fn string_moves_follow_the_direction_flag() {
let m = machine();
for i in 0..4u32 {
m.poke(0x1_0000 + u64::from(i), 0xa0 + i as u8);
}
m.set_regs(|r| {
r.ds = 0x1000;
r.es = 0x2000;
r.rsi = 0;
r.rdi = 0;
r.rcx = 4;
});
m.load(0x0000, 0x0100, &[0xf3, 0xa4]); m.cpu.step();
assert_eq!(m.regs().rcx, 0);
assert_eq!(m.regs().rsi, 4);
assert_eq!(m.regs().rdi, 4);
for i in 0..4u32 {
assert_eq!(m.peek(0x2_0000 + u64::from(i)), 0xa0 + i as u8);
}
m.set_regs(|r| {
r.es = 0x2000;
r.rdi = 3;
r.rcx = 4;
r.rax = 0xa2;
r.eflags |= flags::DF;
});
m.load(0x0000, 0x0200, &[0xf2, 0xae]); m.cpu.step();
assert_eq!(m.regs().rcx, 2, "scan stops the moment it matches");
assert_eq!(m.regs().rdi, 1);
}
#[test]
fn a_repeat_with_a_zero_count_does_nothing_at_all() {
let m = machine();
m.set_regs(|r| {
r.rcx = 0;
r.rsi = 0x10;
r.rdi = 0x20;
});
m.load(0x0000, 0x0100, &[0xf3, 0xa4]);
m.cpu.step();
let regs = m.regs();
assert_eq!((regs.rsi, regs.rdi, regs.rcx), (0x10, 0x20, 0));
}
#[test]
fn a_repeat_is_interruptible_between_iterations() {
let m = machine();
m.set_regs(|r| {
r.rax = 0x3000;
r.ds = 0x1000;
r.es = 0x2000;
r.rcx = 100;
});
m.poke(0x08, 0x00);
m.poke(0x09, 0x70);
m.load(0x0000, 0x0100, &[0x8e, 0xd0, 0xf3, 0xa4]);
m.cpu.step();
m.cpu.pulse_nmi();
m.cpu.step();
assert_eq!(m.regs().rip, 0x0102);
assert!(m.regs().rcx < 100 && m.regs().rcx > 0);
m.cpu.step();
assert_eq!(m.regs().rip, 0x7000);
}
#[test]
fn push_sp_stores_the_decremented_pointer() {
let m = machine();
m.set_regs(|r| {
r.ss = 0x2000;
r.rsp = 0x0100;
});
m.load(0x0000, 0x0100, &[0x54]);
m.cpu.step();
assert_eq!(m.regs().rsp, 0x00fe);
let pushed = u16::from(m.peek(linear(0x2000, 0x00fe)))
| (u16::from(m.peek(linear(0x2000, 0x00ff))) << 8);
assert_eq!(pushed, 0x00fe);
}
#[test]
fn the_decimal_adjust_threshold_moves_with_the_auxiliary_carry() {
let m = machine();
m.load(0x0000, 0x0100, &[0x27]);
m.set_regs(|r| {
r.rax = 0x009a;
r.eflags = (r.eflags | flags::AF) & !flags::CF;
});
m.cpu.step();
assert_eq!(m.regs().rax & 0xff, 0xa0);
assert_eq!(m.regs().eflags & flags::CF, 0);
m.load(0x0000, 0x0200, &[0x27]);
m.set_regs(|r| {
r.rax = 0x009a;
r.eflags &= !(flags::AF | flags::CF);
});
m.cpu.step();
assert_eq!(m.regs().rax & 0xff, 0x00);
assert_eq!(m.regs().eflags & flags::CF, flags::CF);
}
#[test]
fn an_unadjusted_ascii_add_still_sets_sign_zero_and_parity() {
let m = machine();
m.load(0x0000, 0x0100, &[0x37]);
m.set_regs(|r| {
r.rax = 0x0081; r.eflags &= !(flags::AF | flags::SF | flags::PF | flags::ZF);
});
m.cpu.step();
let regs = m.regs();
assert_eq!(regs.rax, 0x0001, "only the low digit survives");
assert_eq!(
regs.eflags & flags::SF,
flags::SF,
"sign of 0x81, not of 0x01"
);
assert_eq!(regs.eflags & (flags::CF | flags::AF), 0);
}
#[test]
fn a_shift_by_zero_still_writes_its_operand_back() {
let m = machine();
let log = Arc::new(BusLog::default());
let mem = AddressSpace::new("mem", 20);
mem.topology()
.map(Region::ram("ram", m.ram.clone()), 0)
.unwrap();
mem.topology()
.map_with_priority(Region::io("watch", 0x10, log.clone()), 0x2_0000, 1)
.unwrap();
m.cpu.attach_space(Arc::new(mem));
m.ram.write_u8(0x0100, 0xd2).unwrap(); m.ram.write_u8(0x0101, 0x07).unwrap();
m.set_regs(|r| {
r.cs = 0;
r.rip = 0x100;
r.ds = 0x2000;
r.rbx = 0;
r.rcx = 0; r.eflags |= flags::CF;
});
m.cpu.session.lock().state.reset_pending = false;
log.clear();
m.cpu.step();
assert_eq!(
log.entries(),
alloc::vec![(0u64, false), (0u64, true)],
"the operand is read and written back even with a zero count"
);
assert_eq!(
m.regs().eflags & flags::CF,
flags::CF,
"flags are untouched"
);
}
#[derive(Debug, Default)]
struct BusLog {
cells: crate::core::sync::Mutex<BusLogState>,
}
#[derive(Debug, Default)]
struct BusLogState(alloc::vec::Vec<u8>, alloc::vec::Vec<(u64, bool)>);
impl BusLog {
fn clear(&self) {
let mut m = self.cells.lock();
m.0.resize(0x10, 0);
m.1.clear();
}
fn entries(&self) -> alloc::vec::Vec<(u64, bool)> {
self.cells.lock().1.clone()
}
}
impl crate::core::space::MemOps for BusLog {
fn read(
&self,
offset: u64,
dst: &mut [u8],
attrs: crate::core::space::MemAttrs,
) -> crate::core::space::MemResult {
let mut m = self.cells.lock();
m.0.resize(0x10, 0);
for (i, slot) in dst.iter_mut().enumerate() {
let at = (offset as usize + i) & 0xf;
*slot = m.0[at];
if !attrs.debug {
m.1.push((at as u64, false));
}
}
Ok(())
}
fn write(
&self,
offset: u64,
src: &[u8],
attrs: crate::core::space::MemAttrs,
) -> crate::core::space::MemResult {
let mut m = self.cells.lock();
m.0.resize(0x10, 0);
for (i, byte) in src.iter().enumerate() {
let at = (offset as usize + i) & 0xf;
m.0[at] = *byte;
if !attrs.debug {
m.1.push((at as u64, true));
}
}
Ok(())
}
fn constraints(&self) -> crate::core::space::AccessConstraints {
crate::core::space::AccessConstraints::ANY
}
}
#[test]
fn a_multiply_takes_its_undefined_flags_from_the_high_half() {
let m = machine();
m.load(0x0000, 0x0100, &[0xf6, 0xe3]); m.set_regs(|r| {
r.rax = 0x0010;
r.rbx = 0x0010;
});
m.cpu.step();
let regs = m.regs();
assert_eq!(regs.rax, 0x0100);
assert_eq!(regs.eflags & flags::ZF, 0);
assert_eq!(regs.eflags & flags::SF, 0);
assert_eq!(regs.eflags & flags::PF, 0);
assert_eq!(regs.eflags & flags::AF, 0);
assert_eq!(regs.eflags & (flags::CF | flags::OF), flags::CF | flags::OF);
}
#[test]
fn a_divide_error_pushes_the_following_instruction() {
let m = machine();
m.load(0x0000, 0x0100, &[0xf6, 0xf3, 0x90]); m.set_regs(|r| {
r.rax = 0xffff;
r.rbx = 0x0001; r.ss = 0x2000;
r.rsp = 0x0100;
});
m.poke(0x00, 0x00);
m.poke(0x01, 0x04); m.cpu.step();
let regs = m.regs();
assert_eq!((regs.cs, regs.rip), (0x0000, 0x0400));
let pushed_ip = u16::from(m.peek(linear(0x2000, 0x00fa)))
| (u16::from(m.peek(linear(0x2000, 0x00fb))) << 8);
assert_eq!(pushed_ip, 0x0102, "the address after `div`, not of it");
}
#[test]
fn a_repeat_prefix_inverts_an_idiv_quotient() {
let m = machine();
m.load(0x0000, 0x0100, &[0xf3, 0xf6, 0xfb]); m.set_regs(|r| {
r.rax = 0x0064; r.rbx = 0x000a; });
m.cpu.step();
assert_eq!(m.regs().rax & 0xff, 0xf6, "100 / 10 = 10, negated to -10");
m.load(0x0000, 0x0200, &[0xf6, 0xfb]); m.set_regs(|r| {
r.rax = 0x0064;
r.rbx = 0x000a;
});
m.cpu.step();
assert_eq!(m.regs().rax & 0xff, 0x0a);
}
#[test]
fn logical_operations_clear_the_auxiliary_carry() {
let m = machine();
m.load(0x0000, 0x0100, &[0x24, 0xff]); m.set_regs(|r| {
r.rax = 0x0001;
r.eflags |= flags::AF | flags::CF | flags::OF;
});
m.cpu.step();
let regs = m.regs();
assert_eq!(regs.eflags & (flags::AF | flags::CF | flags::OF), 0);
}
#[test]
fn a_left_shift_leaves_bit_four_of_its_result_in_the_auxiliary_carry() {
let m = machine();
for (value, want) in [(0x08u32, true), (0x04u32, false)] {
m.load(0x0000, 0x0100, &[0xd0, 0xe0]); m.set_regs(|r| {
r.rax = u64::from(value);
r.eflags &= !flags::AF;
});
m.cpu.step();
assert_eq!(
m.regs().eflags & flags::AF != 0,
want,
"shl of {value:#x} should leave AF = {want}"
);
}
}
#[test]
fn byte_registers_are_the_halves_of_the_word_registers() {
let mut regs = Regs::new();
regs.rax = 0x1234;
assert_eq!(regs.byte(0), 0x34); assert_eq!(regs.byte(4), 0x12); regs.set_byte(4, 0xab);
assert_eq!(regs.rax, 0xab34);
regs.set_byte(0, 0xcd);
assert_eq!(regs.rax, 0xabcd);
regs.rcx = 0x0000;
regs.set_byte(5, 0xff);
assert_eq!(regs.rcx, 0xff00);
}
#[test]
fn the_hard_wired_flag_bits_cannot_be_written() {
let m = machine();
m.load(0x0000, 0x0100, &[0xb8, 0x00, 0x00, 0x50, 0x9d]);
m.set_regs(|r| {
r.ss = 0x2000;
r.rsp = 0x0100;
});
m.cpu.step();
m.cpu.step();
m.cpu.step();
assert_eq!(m.regs().eflags, flags::RESERVED_SET);
assert_eq!(Regs::normalise_flags(Variant::I8088, 0x0000), 0xf002);
assert_eq!(Regs::normalise_flags(Variant::I8088, 0xffff), 0xffd7);
}
#[test]
fn registers_are_reachable_by_name() {
assert_eq!(Reg::from_name("ax"), Some(Reg::Ax));
assert_eq!(Reg::from_name("flags"), Some(Reg::Flags));
assert_eq!(Reg::from_name("eax"), Some(Reg::Eax));
assert_eq!(Reg::from_name("cr0"), None);
for reg in Reg::ALL.iter().chain(Reg::NARROW) {
assert_eq!(Reg::from_name(reg.name()), Some(*reg));
}
assert_eq!(Reg::from_dword_index(4), Reg::Esp);
assert_eq!(Reg::from_word_index(3), Reg::Bx);
assert_eq!(Reg::from_word_index(4), Reg::Sp);
}
#[test]
fn realize_does_nothing_outward_because_the_space_has_not_arrived_yet() {
let cpu = X86::new(Config::default());
let mut deferred = Deferred::new();
let ctx_hosts = crate::core::HostObjects::new();
let mut ctx = RealizeCtx::new("/cpu0", RequesterId::ANONYMOUS, &mut deferred, &ctx_hosts);
assert!(cpu.realize(&mut ctx).is_ok());
}
fn options_with_the_core() -> (crate::core::Registry, crate::machine::BuildOptions) {
let mut options = crate::machine::BuildOptions::new();
for schema in super::schemas() {
options.classes.insert(schema);
}
for schema in crate::machine::builtin::schemas() {
options.classes.insert(schema);
}
super::bind(&mut options.bindings).expect("nothing else claims these names");
crate::machine::builtin::bind(&mut options.bindings).expect("ram and rom");
let mut registry = crate::core::Registry::new();
crate::machine::builtin::register(&mut registry).expect("ram and rom");
super::register(&mut registry).expect("nothing else claims these names");
(registry, options)
}
#[test]
fn binding_a_core_with_no_address_space_is_a_machine_error() {
let (registry, options) = options_with_the_core();
let text = "machine \"m\" {\n osc x = 1000000 Hz\n space mem { width = 32 }\n \
object dram \"ram\" { size = 4K }\n object cpu \"cpu.x86\" { clock = x }\n \
map mem 0 size 4K = dram\n}\n";
let err = crate::machine::build("t.machine", text, ®istry, &options)
.expect_err("a core with no `space =` cannot fetch");
let text = alloc::format!("{err}");
assert!(text.contains("address space"), "{text}");
}
#[test]
fn an_iospace_that_names_nothing_is_a_machine_error() {
let (registry, options) = options_with_the_core();
let text = "machine \"m\" {\n osc x = 1000000 Hz\n space mem { width = 32 }\n \
object dram \"ram\" { size = 4K }\n \
object cpu \"cpu.x86\" { clock = x, space = mem, iospace = \"ports\" }\n \
map mem 0 size 4K = dram\n}\n";
let err = crate::machine::build("t.machine", text, ®istry, &options)
.expect_err("there is no space called `ports`");
let text = alloc::format!("{err}");
assert!(text.contains("ports"), "{text}");
}
#[test]
fn a_machine_file_names_the_core_gives_it_two_spaces_and_it_runs() {
let (registry, mut options) = options_with_the_core();
options
.realize
.media
.insert("firmware", alloc::vec![0xb0u8, 0x5a, 0xe6, 0x42, 0xf4]);
let text = "machine \"m\" {\n osc x = 4772726 Hz\n \
space mem { width = 20 }\n space port { width = 16 }\n \
object cpu \"cpu.x86\" \
{ clock = x, space = mem, iospace = \"port\", variant = \"8088\" }\n \
object ram \"ram\" { size = 64K }\n \
object boot \"rom\" { size = 16, image = \"firmware\" }\n \
object io \"ram\" { size = 64K }\n \
map mem 0x00000 size 64K = ram\n \
map mem 0xffff0 size 16 = boot\n \
map port 0 size 64K = io\n}\n";
let mut machine = match crate::machine::build("t.machine", text, ®istry, &options) {
Ok(m) => m,
Err(e) => panic!("the board does not realize: {e}"),
};
machine
.run_for(crate::core::clock::GlobalTime::from_nanos(2_000_000))
.expect("it runs");
let port = machine.space("port").expect("the I/O space");
assert_eq!(
port.read(
0x42,
crate::core::value::Width::U8,
crate::core::space::MemAttrs::DEFAULT,
)
.expect("a port"),
0x5a,
"the `OUT` did not reach the space `iospace` names"
);
}
#[test]
fn state_round_trips_through_a_snapshot() {
let m = machine();
m.load(0x1234, 0x5678, &[0x40, 0x41, 0x42]);
m.set_regs(|r| {
r.rax = 0x1111;
r.rbx = 0x2222;
r.rcx = 0x3333;
r.rdx = 0x4444;
r.rsp = 0x5555;
r.rbp = 0x6666;
r.rsi = 0x7777;
r.rdi = 0x8888;
r.es = 0x9999;
r.ss = 0xaaaa;
r.ds = 0xbbbb;
r.eflags |= flags::CF | flags::DF;
});
m.cpu.step();
m.cpu.set_intr_vector(0x42);
m.cpu.set_intr(true);
let before = m.regs();
let queue_before = m.cpu.prefetch_queue();
let cycles_before = m.cpu.cycles();
let mut shape = MachineShape::new();
shape.add_device("/cpu0", "cpu.i8086").unwrap();
let mut writer = StateWriter::new(shape);
{
let mut chunk = writer.chunk("/cpu0", "cpu.i8086", 1).unwrap();
m.cpu.save(&mut chunk).unwrap();
}
let bytes = writer.to_vec().unwrap();
m.cpu.reset(ResetKind::Cold);
assert_ne!(m.cpu.regs(), before);
let reader = crate::core::state::StateReader::new(&bytes).unwrap();
let (_, _, data) = reader.load_raw("/cpu0").unwrap();
let mut chunk = ChunkReader::new(data);
m.cpu.load(&mut chunk).unwrap();
chunk.end().unwrap();
assert_eq!(m.cpu.regs(), before);
assert_eq!(m.cpu.prefetch_queue(), queue_before);
assert_eq!(m.cpu.cycles(), cycles_before);
assert_eq!(m.cpu.intr_vector(), 0x42);
assert!(m.cpu.intr_asserted());
}
#[test]
fn a_warm_reset_keeps_the_general_registers_and_a_cold_one_does_not() {
let m = machine();
m.set_regs(|r| r.rax = 0xbeef);
m.cpu.reset(ResetKind::Warm);
assert!(m.cpu.reset_pending());
m.cpu.step();
assert_eq!(m.regs().rax, 0xbeef);
assert_eq!(m.regs().cs, 0xffff);
m.cpu.reset(ResetKind::Cold);
assert_eq!(m.cpu.regs().rax, 0);
assert_eq!(m.cpu.cycles(), 0);
}
#[test]
fn the_variant_property_picks_the_part() {
use crate::core::props::Props;
let cpu = X86::from_props(&Props::new().with("variant", "8086")).unwrap();
assert_eq!(cpu.config().variant, Variant::I8086);
let cpu = X86::from_props(&Props::new().with("model", "80386")).unwrap();
assert_eq!(cpu.config().variant, Variant::I80386);
let err = X86::from_props(&Props::new().with("variant", "6502")).unwrap_err();
let text = alloc::format!("{err}");
assert!(text.contains("8086") && text.contains("80486"), "{text}");
assert!(
X86::from_props(&Props::new().with("varaint", "8088")).is_err(),
"a typo'd property must not be ignored"
);
assert_eq!(
X86::from_props(&Props::new()).unwrap().config().variant,
Variant::I8088
);
}
#[test]
fn the_interrupt_pin_drives_the_core_through_a_wire() {
use crate::core::wire::{Level, WireId, WireSink};
let m = machine();
let a = WireId(1);
let b = WireId(2);
let pin = super::InterruptPin::new(m.cpu.clone(), Interrupt::Intr, &[a, b]);
assert_eq!(pin.which(), Interrupt::Intr);
pin.set_level(a, 0, Level::High);
assert!(m.cpu.intr_asserted());
pin.set_level(b, 0, Level::High);
pin.set_level(a, 0, Level::Low);
assert!(m.cpu.intr_asserted());
pin.set_level(b, 0, Level::Low);
assert!(!m.cpu.intr_asserted());
}
#[test]
fn the_opcode_map_describes_every_byte() {
let described = super::describe_isa();
assert!(described.lines().count() > 256);
assert!(described.contains("d6 *salc"));
assert!(described.contains("ff/3 callf"));
}
#[test]
fn the_undocumented_encodings_execute_rather_than_fault() {
let m = machine();
m.load(0x0000, 0x0100, &[0xf9, 0xd6]);
m.cpu.step();
m.cpu.step();
assert_eq!(m.regs().rax & 0xff, 0xff);
m.set_regs(|r| r.rax &= 0xff00);
m.load(0x0000, 0x0200, &[0xd0, 0xf0]);
m.cpu.step();
assert_eq!(m.regs().rax & 0xff, 0xff);
assert_eq!(resolve(decode(0xd0), 6).op, Op::SETMO);
assert_eq!(decode(0x64).op, decode(0x74).op);
assert_eq!(decode(0x64).class, Class::Alias);
}
#[test]
fn the_disassembler_and_the_interpreter_read_the_same_bytes() {
let m = machine();
let code = [0xb8u8, 0x34, 0x12, 0x03, 0x46, 0xfe, 0xeb, 0xfa];
m.load(0x0000, 0x0100, &code);
let listing = m.cpu.disassemble(0x0000, 0x0100, 3);
let text: Vec<_> = listing
.iter()
.map(alloc::string::ToString::to_string)
.collect();
assert_eq!(text[0], "mov ax, 0x1234");
assert_eq!(text[1], "add ax, [ss:bp-0x2]");
assert_eq!(text[2], "jmp 0x102");
let mut ip = 0x0100u16;
for entry in &listing[..2] {
m.cpu.step();
ip = ip.wrapping_add(u16::from(entry.len));
assert_eq!(m.regs().rip, u64::from(ip));
}
}
#[test]
fn group_rows_and_primary_rows_share_one_description() {
for opcode in [0x80u8, 0x81, 0x82, 0x83] {
let primary = decode(opcode);
assert_eq!(primary.group, Grp::Alu);
for reg in 0..8 {
let row = resolve(primary, reg);
assert_eq!(row.dst, primary.dst);
assert_eq!(row.src, primary.src);
}
}
assert_eq!(resolve(decode(0xf6), 0).src, Arg::Ib);
assert_eq!(resolve(decode(0xf6), 2).src, Arg::None);
}
use super::isa;
use super::prot::{SegReg, Sys, ar, cr0, sys_type, tss32};
mod at {
pub(super) const GDT: u64 = 0x1000;
pub(super) const IDT: u64 = 0x1800;
pub(super) const TSS: u64 = 0x2000;
pub(super) const CODE0: u64 = 0x3000;
pub(super) const CODE3: u64 = 0x4000;
pub(super) const PDIR: u64 = 0x5000;
pub(super) const PTAB: u64 = 0x6000;
pub(super) const MARK: u64 = 0x7000;
pub(super) const STACK0: u64 = 0x9000;
pub(super) const STACK3: u64 = 0xa000;
}
mod rights {
use super::ar;
pub(super) const CODE32: u32 = ar::PRESENT | ar::S | ar::CODE | ar::RW | ar::DB;
pub(super) const DATA32: u32 = ar::PRESENT | ar::S | ar::RW | ar::DB;
pub(super) const CODE16: u32 = ar::PRESENT | ar::S | ar::CODE | ar::RW;
pub(super) const DATA16: u32 = ar::PRESENT | ar::S | ar::RW;
pub(super) const DPL3: u32 = ar::DPL;
}
fn descriptor(base: u64, limit: u32, ar_bits: u32) -> (u32, u32) {
let (limit, ar_bits) = if limit > 0xf_ffff {
(limit >> 12, ar_bits | ar::GRANULAR)
} else {
(limit, ar_bits)
};
let base = base as u32;
let low = (limit & 0xffff) | (base << 16);
let high = ((base >> 16) & 0xff) | ar_bits | (limit & 0x000f_0000) | (base & 0xff00_0000);
(low, high)
}
fn gate(selector: u16, offset: u32, kind: u8, dpl: u8) -> (u32, u32) {
let low = (offset & 0xffff) | (u32::from(selector) << 16);
let high = (offset & 0xffff_0000)
| ar::PRESENT
| (u32::from(dpl) << ar::DPL_SHIFT)
| (u32::from(kind) << 8);
(low, high)
}
struct Pc {
cpu: Arc<X86>,
ram: Arc<RamStore>,
rom: Arc<RamStore>,
ports: Arc<RamStore>,
}
impl Pc {
fn new(variant: Variant) -> Pc {
Pc::with_features(variant, variant.features())
}
fn with_features(variant: Variant, features: Features) -> Pc {
let ram = Arc::new(RamStore::new(0x40_0000));
let rom = Arc::new(RamStore::new(0x1_0000));
let mem = AddressSpace::new("mem", 32);
mem.topology()
.map(Region::ram("ram", ram.clone()), 0)
.expect("4 MiB at zero");
mem.topology()
.map(Region::ram("rom", rom.clone()), 0xffff_0000)
.expect("64 KiB at the top of the space");
let ports = Arc::new(RamStore::new(0x1_0000));
let io = AddressSpace::new("io", 16);
io.topology()
.map(Region::ram("ports", ports.clone()), 0)
.expect("64 KiB fits in 16 bits");
let cpu = Arc::new(X86::new(
Config::default()
.with_variant(variant)
.with_features(features),
));
cpu.attach_space(Arc::new(mem));
cpu.attach_io_space(Arc::new(io));
Pc {
cpu,
ram,
rom,
ports,
}
}
fn write(&self, addr: u64, bytes: &[u8]) {
for (i, byte) in bytes.iter().enumerate() {
self.ram.write_u8(addr + i as u64, *byte).unwrap();
}
}
fn write32(&self, addr: u64, value: u64) {
for i in 0..4u64 {
self.ram
.write_u8(addr + i, (value >> (8 * i)) as u8)
.unwrap();
}
}
fn read32(&self, addr: u64) -> u64 {
let mut value = 0u64;
for i in 0..4u64 {
value |= u64::from(self.ram.read_u8(addr + i).unwrap()) << (8 * i);
}
value
}
fn rom(&self, offset: u32, bytes: &[u8]) {
for (i, byte) in bytes.iter().enumerate() {
self.rom
.write_u8(u64::from(offset) + i as u64, *byte)
.unwrap();
}
}
fn gdt(&self, index: u64, pair: (u32, u32)) {
self.write32(at::GDT + index * 8, u64::from(pair.0));
self.write32(at::GDT + index * 8 + 4, u64::from(pair.1));
}
fn idt(&self, vector: u64, pair: (u32, u32)) {
self.write32(at::IDT + vector * 8, u64::from(pair.0));
self.write32(at::IDT + vector * 8 + 4, u64::from(pair.1));
}
fn start_real(&self, cs: u16, eip: u32) {
let mut regs = self.cpu.regs();
regs.cs = cs;
regs.rip = u64::from(eip);
self.cpu.set_regs(regs);
self.cpu.session.lock().state.reset_pending = false;
}
fn start_protected(&self) {
self.gdt(0, (0, 0));
self.gdt(1, descriptor(0, 0xffff_ffff, rights::CODE32));
self.gdt(2, descriptor(0, 0xffff_ffff, rights::DATA32));
let mut sys = Sys::reset();
sys.cr0 |= cr0::PE;
sys.gdtr.base = at::GDT;
sys.gdtr.limit = 0xff;
sys.idtr.base = at::IDT;
sys.idtr.limit = 0x7ff;
sys.segs[usize::from(isa::seg::CS)] = SegReg {
selector: 0x08,
base: 0,
limit: 0xffff_ffff,
ar: rights::CODE32,
};
for index in [
isa::seg::DS,
isa::seg::ES,
isa::seg::SS,
isa::seg::FS,
isa::seg::GS,
] {
sys.segs[usize::from(index)] = SegReg {
selector: 0x10,
base: 0,
limit: 0xffff_ffff,
ar: rights::DATA32,
};
}
self.cpu.set_sys(sys);
let mut regs = Regs::new();
regs.cs = 0x08;
regs.ss = 0x10;
regs.ds = 0x10;
regs.es = 0x10;
regs.fs = 0x10;
regs.gs = 0x10;
regs.rsp = at::STACK0;
regs.rip = at::CODE0;
regs.eflags = flags::ALWAYS_SET;
self.cpu.set_regs(regs);
self.cpu.session.lock().state.reset_pending = false;
}
fn run(&self, limit: usize) -> usize {
for n in 0..limit {
if self.cpu.step() == 0 {
return n;
}
}
limit
}
fn regs(&self) -> Regs {
self.cpu.regs()
}
}
fn pc386() -> Pc {
Pc::new(Variant::I80486)
}
#[test]
fn the_reset_vector_is_sixteen_bytes_below_the_top_of_the_address_space() {
let pc = pc386();
pc.rom(0xfff0, &[0xea, 0x00, 0x10, 0x00, 0x00]);
pc.cpu.step();
let regs = pc.regs();
assert_eq!((regs.cs, regs.rip), (0xf000, 0xfff0));
assert_eq!(pc.cpu.sys().seg(isa::seg::CS).base, 0xffff_0000);
assert_eq!(
pc.cpu.regs().rdx,
u64::from(Variant::I80486.reset_signature())
);
pc.cpu.step();
let regs = pc.regs();
assert_eq!((regs.cs, regs.rip), (0x0000, 0x1000));
assert_eq!(pc.cpu.sys().seg(isa::seg::CS).base, 0);
}
#[test]
fn an_operand_size_prefix_selects_the_wide_form_in_real_mode() {
let pc = pc386();
pc.start_real(0, 0x1000);
pc.write(
0x1000,
&[
0x66, 0xb8, 0x78, 0x56, 0x34, 0x12, 0x66, 0x05, 0x11, 0x11, 0x11, 0x11, 0xb8, 0x34, 0x12, 0xf4, ],
);
pc.run(8);
assert_eq!(pc.regs().rax, 0x2345_1234);
}
#[test]
fn an_address_size_prefix_brings_the_scaled_index_forms_into_real_mode() {
let pc = pc386();
pc.start_real(0, 0x1000);
pc.write32(0x2010, 0xdead_beef);
pc.write(
0x1000,
&[
0x66, 0xb8, 0x00, 0x20, 0x00, 0x00, 0x66, 0xb9, 0x04, 0x00, 0x00, 0x00, 0x67, 0x66, 0x8b, 0x14, 0x88, 0xf4, ],
);
pc.run(8);
assert_eq!(pc.regs().rdx, 0xdead_beef);
}
#[test]
fn entering_protected_mode_reloads_the_cached_descriptor() {
let pc = pc386();
pc.gdt(0, (0, 0));
pc.gdt(1, descriptor(0, 0xffff_ffff, rights::CODE32));
pc.gdt(2, descriptor(0, 0xffff_ffff, rights::DATA32));
pc.write(0x7000, &[0xff, 0x00]);
pc.write32(0x7002, at::GDT);
pc.start_real(0, 0x7c00);
pc.write(
0x7c00,
&[
0xfa, 0x0f, 0x01, 0x16, 0x00, 0x70, 0x0f, 0x20, 0xc0, 0x66, 0x83, 0xc8, 0x01, 0x0f, 0x22, 0xc0, 0xea, 0x00, 0x7d, 0x08, 0x00, ],
);
pc.write(
0x7d00,
&[
0xb8, 0x10, 0x00, 0x00, 0x00, 0x8e, 0xd8, 0x8e, 0xd0, 0xbc, 0x00, 0x90, 0x00, 0x00, 0xb8, 0xbe, 0xba, 0xfe, 0xca, 0xa3, 0x00, 0x70, 0x00, 0x00, 0xbb, 0x22, 0x22, 0x11, 0x11, 0x53, 0x59, 0xf4, ],
);
let steps = pc.run(20);
assert!(steps < 20, "the program should have reached its hlt");
let sys = pc.cpu.sys();
assert!(sys.protected());
assert_eq!(sys.gdtr.base, at::GDT);
assert_eq!(sys.gdtr.limit, 0xff);
let cs = sys.seg(isa::seg::CS);
assert_eq!(cs.selector, 0x08);
assert_eq!(cs.limit, 0xffff_ffff, "granularity expands the limit");
assert!(cs.big(), "the D bit makes this a 32-bit segment");
assert_eq!(pc.regs().rcx, 0x1111_2222);
assert_eq!(pc.read32(0x7000), 0xcafe_babe);
assert_eq!(pc.regs().rsp, at::STACK0);
}
#[test]
fn a_segment_limit_violation_raises_general_protection() {
let pc = pc386();
pc.start_protected();
let mut sys = pc.cpu.sys();
sys.segs[usize::from(isa::seg::DS)].limit = 0x0fff;
pc.cpu.set_sys(sys);
pc.idt(13, gate(0x08, 0x3100, sys_type::INT_GATE32, 0));
pc.write(0x3100, &[0xf4]);
pc.write(
at::CODE0,
&[
0xa1, 0x00, 0x08, 0x00, 0x00, 0xa1, 0x00, 0x20, 0x00, 0x00, 0xf4,
],
);
pc.cpu.step();
pc.cpu.step();
let regs = pc.regs();
assert_eq!(regs.rip, 0x3100, "the fault took the #GP gate");
assert_eq!(regs.cs & 0xfffc, 0x08);
assert_eq!(pc.read32(at::STACK0 - 16), 0);
assert_eq!(pc.read32(at::STACK0 - 12), at::CODE0 + 5);
}
#[test]
fn an_unassigned_encoding_raises_invalid_opcode() {
let pc = pc386();
pc.start_protected();
pc.idt(6, gate(0x08, 0x3100, sys_type::INT_GATE32, 0));
pc.write(0x3100, &[0xf4]);
pc.write(at::CODE0, &[0x0f, 0x0a]);
pc.cpu.step();
assert_eq!(pc.regs().rip, 0x3100);
assert_eq!(pc.read32(at::STACK0 - 12), at::CODE0);
}
#[test]
fn paging_translates_through_the_directory_and_the_table() {
let pc = pc386();
pc.start_protected();
pc.write32(at::PDIR, at::PTAB | 0b111);
for page in 0..1024u64 {
pc.write32(at::PTAB + page * 4, (page << 12) | 0b111);
}
pc.write32(at::PTAB + 0x200 * 4, at::MARK | 0b111);
let mut sys = pc.cpu.sys();
sys.cr3 = at::PDIR;
sys.cr0 |= cr0::PG;
pc.cpu.set_sys(sys);
pc.write(
at::CODE0,
&[
0xb8, 0x0d, 0xf0, 0xad, 0x0b, 0xa3, 0x00, 0x00, 0x20, 0x00, 0x8b, 0x1d, 0x00, 0x00, 0x20, 0x00, 0xf4,
],
);
let steps = pc.run(10);
assert!(steps < 10);
assert_eq!(pc.regs().rbx, 0x0bad_f00d);
assert_eq!(pc.read32(at::MARK), 0x0bad_f00d);
let pte = pc.read32(at::PTAB + 0x200 * 4);
assert_eq!(pte & 0b110_0000, 0b110_0000, "accessed and dirty");
assert_eq!(pc.read32(at::PDIR) & 0b10_0000, 0b10_0000, "accessed");
}
#[test]
fn a_debug_translation_walks_the_tables_and_touches_nothing() {
let pc = pc386();
pc.start_protected();
pc.write32(at::PDIR, at::PTAB | 0b111);
for page in 0..1024u64 {
pc.write32(at::PTAB + page * 4, (page << 12) | 0b111);
}
pc.write32(at::PTAB + 0x200 * 4, at::MARK | 0b111);
assert_eq!(
pc.cpu.translate_debug(0x0020_0034),
DebugTranslation::Identity
);
let mut sys = pc.cpu.sys();
sys.cr3 = at::PDIR;
sys.cr0 |= cr0::PG;
pc.cpu.set_sys(sys);
assert_eq!(
pc.cpu.translate_debug(0x0020_0034),
DebugTranslation::Mapped(at::MARK + 0x34),
"through the directory and the table, offset kept"
);
assert_eq!(
pc.cpu.translate_debug(0x0040_1234),
DebugTranslation::Unmapped
);
assert_eq!(
pc.read32(at::PDIR) & 0b110_0000,
0,
"the directory is untouched"
);
assert_eq!(
pc.read32(at::PTAB + 0x200 * 4) & 0b110_0000,
0,
"the table entry is untouched"
);
assert_eq!(pc.cpu.sys().cr2, 0, "a miss latched no fault address");
}
#[test]
fn a_missing_page_faults_with_the_address_in_cr2_and_the_reason_in_the_code() {
let pc = pc386();
pc.start_protected();
pc.write32(at::PDIR, at::PTAB | 0b111);
for page in 0..1024u64 {
pc.write32(at::PTAB + page * 4, (page << 12) | 0b111);
}
let mut sys = pc.cpu.sys();
sys.cr3 = at::PDIR;
sys.cr0 |= cr0::PG;
pc.cpu.set_sys(sys);
pc.idt(14, gate(0x08, 0x3100, sys_type::INT_GATE32, 0));
pc.write(0x3100, &[0xf4]);
pc.write(
at::CODE0,
&[0xa3, 0x34, 0x12, 0x40, 0x00, 0xf4], );
pc.cpu.step();
assert_eq!(pc.regs().rip, 0x3100);
assert_eq!(pc.cpu.sys().cr2, 0x0040_1234);
assert_eq!(pc.read32(at::STACK0 - 16), 0b010);
}
#[test]
fn write_protect_decides_whether_ring_zero_obeys_a_read_only_page() {
for (variant, wp, expect_fault) in [
(Variant::I80386, false, false),
(Variant::I80486, false, false),
(Variant::I80486, true, true),
] {
let pc = Pc::new(variant);
pc.start_protected();
pc.write32(at::PDIR, at::PTAB | 0b111);
for page in 0..1024u64 {
pc.write32(at::PTAB + page * 4, (page << 12) | 0b111);
}
pc.write32(at::PTAB + (at::MARK >> 12) * 4, at::MARK | 0b101);
let mut sys = pc.cpu.sys();
sys.cr3 = at::PDIR;
sys.cr0 |= cr0::PG;
if wp {
sys.cr0 |= cr0::WP;
}
pc.cpu.set_sys(sys);
pc.idt(14, gate(0x08, 0x3100, sys_type::INT_GATE32, 0));
pc.write(0x3100, &[0xf4]);
pc.write(at::CODE0, &[0xa3, 0x00, 0x70, 0x00, 0x00, 0xf4]);
pc.cpu.step();
let faulted = pc.regs().rip == 0x3100;
assert_eq!(faulted, expect_fault, "{variant} with WP={wp}");
}
}
#[test]
fn an_interrupt_gate_switches_to_the_stack_the_task_state_segment_names() {
let pc = pc386();
pc.start_protected();
pc.gdt(3, descriptor(0, 0xffff_ffff, rights::CODE32 | rights::DPL3));
pc.gdt(4, descriptor(0, 0xffff_ffff, rights::DATA32 | rights::DPL3));
pc.gdt(
5,
descriptor(
at::TSS,
0x67,
ar::PRESENT | (u32::from(sys_type::TSS32_AVAIL) << 8),
),
);
pc.write32(at::TSS + tss32::ESP0, at::STACK0);
pc.write32(at::TSS + tss32::SS0, 0x10);
pc.write32(at::TSS + tss32::IOMAP_BASE - 2, 0x0068_0000);
pc.idt(0x80, gate(0x08, 0x3100, sys_type::INT_GATE32, 3));
pc.write(
0x3100,
&[
0xb8, 0x10, 0x00, 0x00, 0x00, 0x8e, 0xd8, 0x8c, 0xc8, 0xa3, 0x00, 0x70, 0x00, 0x00, 0xcf, ],
);
pc.write(
at::CODE0,
&[
0xb8, 0x28, 0x00, 0x00, 0x00, 0x0f, 0x00, 0xd8, 0x6a, 0x23, 0x68, 0x00, 0xa0, 0x00, 0x00, 0x6a, 0x02, 0x6a, 0x1b, 0x68, 0x00, 0x40, 0x00, 0x00, 0xcf, ],
);
pc.write(
at::CODE3,
&[
0xcd, 0x80, 0xf4, ],
);
for _ in 0..8 {
pc.cpu.step();
}
let regs = pc.regs();
assert_eq!(regs.cs, 0x1b, "the iret entered ring 3");
assert_eq!(regs.rsp, at::STACK3);
assert_eq!(pc.cpu.sys().task.selector, 0x28);
pc.cpu.step(); let regs = pc.regs();
assert_eq!(regs.cs & 3, 0, "the gate raised the privilege level");
assert_eq!(regs.rip, 0x3100);
assert_eq!(regs.ss, 0x10, "the stack came out of the TSS");
assert_eq!(regs.rsp, at::STACK0 - 20);
assert_eq!(pc.read32(at::STACK0 - 4), 0x23, "the caller's SS");
assert_eq!(pc.read32(at::STACK0 - 8), at::STACK3, "the caller's ESP");
assert_eq!(pc.read32(at::STACK0 - 16), 0x1b, "the caller's CS");
assert_eq!(pc.read32(at::STACK0 - 20), at::CODE3 + 2, "after the INT");
for _ in 0..4 {
pc.cpu.step(); }
assert_eq!(pc.read32(at::MARK) & 3, 0);
pc.cpu.step(); let regs = pc.regs();
assert_eq!(regs.cs, 0x1b, "and back out to ring 3");
assert_eq!(regs.rsp, at::STACK3);
}
#[test]
fn a_ring_three_program_may_not_touch_the_privileged_instructions() {
let pc = pc386();
pc.start_protected();
pc.gdt(3, descriptor(0, 0xffff_ffff, rights::CODE32 | rights::DPL3));
pc.gdt(4, descriptor(0, 0xffff_ffff, rights::DATA32 | rights::DPL3));
pc.gdt(
5,
descriptor(
at::TSS,
0x67,
ar::PRESENT | (u32::from(sys_type::TSS32_AVAIL) << 8),
),
);
pc.write32(at::TSS + tss32::ESP0, at::STACK0);
pc.write32(at::TSS + tss32::SS0, 0x10);
pc.idt(13, gate(0x08, 0x3100, sys_type::INT_GATE32, 0));
pc.write(0x3100, &[0xf4]);
pc.write(
at::CODE0,
&[
0xb8, 0x28, 0x00, 0x00, 0x00, 0x0f, 0x00, 0xd8, 0x6a, 0x23, 0x68, 0x00, 0xa0, 0x00, 0x00, 0x6a, 0x02, 0x6a, 0x1b, 0x68, 0x00, 0x40,
0x00, 0x00, 0xcf,
],
);
pc.write(at::CODE3, &[0xf4]);
for _ in 0..8 {
pc.cpu.step();
}
assert_eq!(pc.regs().cs, 0x1b);
pc.cpu.step();
assert_eq!(pc.regs().rip, 0x3100, "hlt in ring 3 is #GP");
assert!(!pc.cpu.is_halted());
}
#[test]
fn unreal_mode_keeps_the_limit_a_protected_mode_load_cached() {
let pc = pc386();
pc.start_protected();
pc.gdt(5, descriptor(0, 0xffff, rights::CODE16));
pc.write(
at::CODE0,
&[0xea, 0x00, 0x41, 0x00, 0x00, 0x28, 0x00], );
pc.write(
0x4100,
&[
0x0f, 0x20, 0xc0, 0x66, 0x83, 0xe0, 0xfe, 0x0f, 0x22, 0xc0, 0xea, 0x00, 0x40, 0x00, 0x00, ],
);
pc.write(
at::CODE3,
&[
0x66, 0xb8, 0x0d, 0xf0, 0xad, 0x0b, 0x67, 0x66, 0xa3, 0x00, 0x00, 0x20, 0x00, 0xf4,
],
);
for _ in 0..8 {
pc.cpu.step();
}
assert!(!pc.cpu.sys().protected());
assert_eq!(
pc.cpu.sys().seg(isa::seg::DS).limit,
0xffff_ffff,
"the cached limit survived the return to real mode"
);
assert_eq!(pc.read32(0x20_0000), 0x0bad_f00d);
}
#[test]
fn cpuid_reports_the_vendor_and_a_feature_set_this_core_implements() {
let pc = pc386();
pc.start_protected();
pc.write(at::CODE0, &[0x0f, 0xa2, 0xf4]);
pc.cpu.step();
let regs = pc.regs();
assert_eq!(regs.rax, 1, "the highest leaf");
assert_eq!(
(regs.rbx, regs.rdx, regs.rcx),
(
u64::from(u32::from_le_bytes(*b"Genu")),
u64::from(u32::from_le_bytes(*b"ineI")),
u64::from(u32::from_le_bytes(*b"ntel"))
)
);
let pc = pc386();
pc.start_protected();
pc.write(at::CODE0, &[0xb8, 0x01, 0x00, 0x00, 0x00, 0x0f, 0xa2, 0xf4]);
pc.cpu.step();
pc.cpu.step();
let regs = pc.regs();
assert_eq!(regs.rax, 0x0000_0480);
assert_eq!(regs.rdx, 1, "FPU, and nothing else, on a 486DX");
let pc = Pc::with_features(Variant::I80486, Features::I80486SX);
pc.start_protected();
pc.write(at::CODE0, &[0xb8, 0x01, 0x00, 0x00, 0x00, 0x0f, 0xa2, 0xf4]);
pc.cpu.step();
pc.cpu.step();
assert_eq!(pc.regs().rdx, 0, "no FPU bit on a 486SX");
let pc = Pc::new(Variant::I80386);
pc.start_protected();
pc.idt(6, gate(0x08, 0x3100, sys_type::INT_GATE32, 0));
pc.write(0x3100, &[0xf4]);
pc.write(at::CODE0, &[0x0f, 0xa2]);
pc.cpu.step();
assert_eq!(pc.regs().rip, 0x3100, "#UD on a 386");
}
#[test]
fn the_386_instruction_additions_compute_what_the_manual_says() {
let pc = pc386();
pc.start_protected();
pc.write32(0x7100, 0x0000_8001);
pc.write(
at::CODE0,
&[
0xb9, 0x81, 0x00, 0x00, 0x00, 0x0f, 0xb6, 0xc1, 0x0f, 0xbe, 0xd9, 0xb9, 0x00, 0x01, 0x00, 0x00, 0x0f, 0xbc, 0xd1, 0x0f, 0xbd, 0xf1, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xba, 0xe8, 0x05, 0x0f, 0xba, 0xf0, 0x05, 0x0f, 0xba, 0xf8, 0x07, 0xf4,
],
);
let steps = pc.run(20);
assert!(steps < 20);
let regs = pc.regs();
assert_eq!(regs.rax, 0x80, "bts then btr then btc");
assert_eq!(regs.rbx, 0xffff_ff81, "movsx sign-extended");
assert_eq!(regs.rdx, 8, "bsf found the lowest set bit");
assert_eq!(regs.rsi, 8, "bsr found the highest");
let pc = pc386();
pc.start_protected();
pc.write(
at::CODE0,
&[
0xb8, 0x00, 0x00, 0x00, 0xf0, 0xb9, 0x00, 0x00, 0x00, 0x0f, 0x0f, 0xa4, 0xc8, 0x04, 0xbb, 0x05, 0x00, 0x00, 0x00, 0x0f, 0xc8, 0x0f, 0xcb, 0xba, 0x0a, 0x00, 0x00, 0x00, 0x6b, 0xfa, 0x07, 0xf4,
],
);
let steps = pc.run(20);
assert!(steps < 20);
let regs = pc.regs();
assert_eq!(regs.rax.swap_bytes(), 0x0000_0000);
assert_eq!(regs.rbx, 0x0500_0000, "bswap reversed the byte order");
assert_eq!(regs.rdi, 70, "the three-operand imul");
}
#[test]
fn pusha_stores_the_stack_pointer_it_started_with_and_popa_discards_it() {
let pc = pc386();
pc.start_protected();
pc.write(
at::CODE0,
&[
0xb8, 0x11, 0x11, 0x11, 0x11, 0xbb, 0x33, 0x33, 0x33, 0x33, 0x60, 0xb8, 0x99, 0x99, 0x99, 0x99, 0x61, 0xf4,
],
);
let steps = pc.run(10);
assert!(steps < 10);
let regs = pc.regs();
assert_eq!(regs.rax, 0x1111_1111, "popad restored it");
assert_eq!(regs.rbx, 0x3333_3333);
assert_eq!(regs.rsp, at::STACK0, "and left the stack where it found it");
assert_eq!(pc.read32(at::STACK0 - 20), at::STACK0);
}
#[test]
fn enter_and_leave_build_and_unmake_a_frame() {
let pc = pc386();
pc.start_protected();
pc.write(
at::CODE0,
&[
0xbd, 0x00, 0x88, 0x00, 0x00, 0xc8, 0x10, 0x00, 0x00, 0xc9, 0xf4,
],
);
pc.cpu.step();
pc.cpu.step();
let regs = pc.regs();
assert_eq!(regs.rbp, at::STACK0 - 4, "the frame pointer is the new top");
assert_eq!(
regs.rsp,
at::STACK0 - 4 - 0x10,
"and 16 bytes were reserved"
);
assert_eq!(pc.read32(at::STACK0 - 4), 0x8800, "the old EBP was saved");
pc.cpu.step();
let regs = pc.regs();
assert_eq!(regs.rbp, 0x8800);
assert_eq!(regs.rsp, at::STACK0);
}
#[test]
fn the_shift_count_is_masked_to_five_bits_from_the_80186_on() {
let m = machine();
m.load(0x0000, 0x0100, &[0xd2, 0xe0]); m.set_regs(|r| {
r.rax = 0x00ff;
r.rcx = 32;
});
m.cpu.step();
assert_eq!(m.regs().rax & 0xff, 0, "an 8086 really shifts 32 times");
let pc = pc386();
pc.start_protected();
pc.write(
at::CODE0,
&[
0xb8, 0xff, 0x00, 0x00, 0x00, 0xb9, 0x20, 0x00, 0x00, 0x00, 0xd2, 0xe0, 0xf4,
],
);
pc.run(6);
assert_eq!(pc.regs().rax & 0xff, 0xff, "a 386 masks the count to zero");
}
#[test]
fn push_sp_stores_the_value_before_the_decrement_from_the_80286_on() {
let m = machine();
m.load(0x0000, 0x0100, &[0x54]); m.set_regs(|r| {
r.ss = 0x2000;
r.rsp = 0x0100;
});
m.cpu.step();
let pushed = u16::from(m.peek(linear(0x2000, 0x00fe)))
| (u16::from(m.peek(linear(0x2000, 0x00ff))) << 8);
assert_eq!(pushed, 0x00fe, "an 8086 pushes the decremented value");
let pc = pc386();
pc.start_protected();
pc.write(at::CODE0, &[0x54, 0xf4]); pc.cpu.step();
assert_eq!(
pc.read32(at::STACK0 - 4),
at::STACK0,
"a 386 pushes the value it had before"
);
}
#[test]
fn lar_lsl_verr_and_arpl_answer_without_faulting() {
let pc = pc386();
pc.start_protected();
pc.gdt(3, descriptor(0, 0x0fff, rights::DATA32));
pc.gdt(4, descriptor(0, 0xffff_ffff, rights::DATA32 | rights::DPL3));
pc.write(
at::CODE0,
&[
0xb8, 0x18, 0x00, 0x00, 0x00, 0x0f, 0x03, 0xd8, 0x0f, 0x02, 0xc8, 0xb8, 0x00, 0xf0, 0x00, 0x00, 0x0f, 0x03, 0xd0, 0xf4,
],
);
let steps = pc.run(10);
assert!(steps < 10);
let regs = pc.regs();
assert_eq!(regs.rbx, 0x0fff, "lsl read the limit");
assert_eq!(regs.rcx, u64::from(rights::DATA32 & ar::MASK));
assert_eq!(
regs.rdx, 0,
"a selector past the table leaves the target alone"
);
assert!(!regs.flag(flags::ZF), "and clears ZF rather than faulting");
let pc = pc386();
pc.start_protected();
pc.write(
at::CODE0,
&[
0xb8, 0x08, 0x00, 0x00, 0x00, 0xb9, 0x03, 0x00, 0x00, 0x00, 0x63, 0xc8, 0xf4,
],
);
let steps = pc.run(6);
assert!(steps < 6);
assert_eq!(pc.regs().rax & 0xffff, 0x0b, "raised to RPL 3");
assert!(pc.regs().flag(flags::ZF));
}
#[test]
fn a_double_fault_escalates_and_a_third_shuts_the_processor_down() {
let pc = pc386();
pc.start_protected();
pc.gdt(7, descriptor(0, 0xffff_ffff, rights::CODE32 & !ar::PRESENT));
pc.idt(13, gate(0x38, 0x3100, sys_type::INT_GATE32, 0));
pc.idt(8, gate(0x38, 0x3200, sys_type::INT_GATE32, 0));
pc.idt(11, gate(0x38, 0x3300, sys_type::INT_GATE32, 0));
pc.write(
at::CODE0,
&[
0x31, 0xc0, 0x8e, 0xc0, 0x26, 0x8b, 0x1d, 0x00, 0x00, 0x00, 0x00, ],
);
pc.cpu.step();
pc.cpu.step();
pc.cpu.step();
assert!(pc.cpu.is_halted());
assert_eq!(pc.cpu.step(), 0, "a shut-down core charges nothing");
}
#[test]
fn a_snapshot_round_trips_the_hidden_descriptor_caches() {
let pc = pc386();
pc.start_protected();
let mut sys = pc.cpu.sys();
sys.segs[usize::from(isa::seg::DS)].limit = 0x1234;
sys.segs[usize::from(isa::seg::FS)].base = 0xdead_0000;
sys.cr2 = 0xfeed_face;
sys.cr3 = at::PDIR;
sys.dr[0] = 0x1111_2222;
sys.ldtr = SegReg {
selector: 0x30,
base: 0x9000,
limit: 0xff,
ar: ar::PRESENT | (u32::from(sys_type::LDT) << 8),
};
pc.cpu.set_sys(sys);
pc.cpu.set_regs(Regs {
rax: 0x1234_5678,
rsi: 0x9abc_def0,
..pc.regs()
});
let regs_before = pc.regs();
let sys_before = pc.cpu.sys();
let mut shape = MachineShape::new();
shape.add_device("/cpu0", "cpu.x86").unwrap();
let mut writer = StateWriter::new(shape);
{
let mut chunk = writer.chunk("/cpu0", "cpu.x86", 2).unwrap();
pc.cpu.save(&mut chunk).unwrap();
}
let bytes = writer.to_vec().unwrap();
pc.cpu.reset(ResetKind::Cold);
assert_ne!(pc.cpu.sys(), sys_before);
let reader = crate::core::state::StateReader::new(&bytes).unwrap();
let (_, _, data) = reader.load_raw("/cpu0").unwrap();
let mut chunk = ChunkReader::new(data);
pc.cpu.load(&mut chunk).unwrap();
chunk.end().unwrap();
assert_eq!(pc.regs(), regs_before);
assert_eq!(pc.cpu.sys(), sys_before);
}
#[test]
fn the_first_sixty_four_bytes_of_a_saved_core_are_gdbs_register_block() {
let pc = pc386();
pc.start_protected();
pc.cpu.set_regs(Regs {
rax: 0x0000_0001,
rcx: 0x0000_0002,
rdx: 0x0000_0003,
rbx: 0x0000_0004,
rsp: 0x0000_0005,
rbp: 0x0000_0006,
rsi: 0x0000_0007,
rdi: 0x0000_0008,
rip: 0x0000_0009,
..pc.regs()
});
let mut shape = MachineShape::new();
shape.add_device("/cpu0", "cpu.x86").unwrap();
let mut writer = StateWriter::new(shape);
{
let mut chunk = writer.chunk("/cpu0", "cpu.x86", 2).unwrap();
pc.cpu.save(&mut chunk).unwrap();
}
let bytes = writer.to_vec().unwrap();
let reader = crate::core::state::StateReader::new(&bytes).unwrap();
let (_, _, data) = reader.load_raw("/cpu0").unwrap();
for i in 0..9u32 {
let offset = (i * 4) as usize;
let word = u32::from_le_bytes([
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
]);
assert_eq!(word, i + 1, "register {i} of gdb's i386 block");
}
let cs = u32::from_le_bytes([data[40], data[41], data[42], data[43]]);
assert_eq!(cs, 0x08);
}
#[test]
fn a_descriptor_that_changes_under_a_loaded_selector_does_not_move_the_segment() {
let pc = pc386();
pc.start_protected();
pc.gdt(3, descriptor(0x1_0000, 0xffff, rights::DATA32));
pc.write32(0x1_0000, 0xaaaa_aaaa);
pc.write32(0x2_0000, 0xbbbb_bbbb);
pc.write(
at::CODE0,
&[
0xb8, 0x18, 0x00, 0x00, 0x00, 0x8e, 0xc0, 0x26, 0x8b, 0x1d, 0x00, 0x00, 0x00, 0x00, 0xf4,
],
);
pc.cpu.step();
pc.cpu.step();
pc.cpu.step();
assert_eq!(pc.regs().rbx, 0xaaaa_aaaa);
pc.gdt(3, descriptor(0x2_0000, 0xffff, rights::DATA32));
pc.cpu.set_regs(Regs {
rip: at::CODE0 + 7,
..pc.regs()
});
pc.cpu.step();
assert_eq!(
pc.regs().rbx,
0xaaaa_aaaa,
"the cached base is what the processor uses"
);
pc.cpu.set_regs(Regs {
rip: at::CODE0,
..pc.regs()
});
pc.cpu.step();
pc.cpu.step();
pc.cpu.step();
assert_eq!(pc.regs().rbx, 0xbbbb_bbbb);
}
#[test]
fn a_null_selector_is_loadable_and_then_unusable() {
let pc = pc386();
pc.start_protected();
pc.idt(13, gate(0x08, 0x3100, sys_type::INT_GATE32, 0));
pc.write(0x3100, &[0xf4]);
pc.write(
at::CODE0,
&[
0x31, 0xc0, 0x8e, 0xc0, 0x26, 0x8b, 0x1d, 0x00, 0x00, 0x00, 0x00, 0xf4,
],
);
pc.cpu.step();
pc.cpu.step();
assert_eq!(pc.regs().es, 0, "loading it is fine");
pc.cpu.step();
assert_eq!(pc.regs().rip, 0x3100, "using it is not");
assert_eq!(pc.read32(at::STACK0 - 16), 0, "#GP(0), naming no selector");
}
#[test]
fn a_task_switch_saves_the_outgoing_task_and_loads_the_incoming_one() {
let pc = pc386();
pc.start_protected();
pub(super) const TSS_B: u64 = 0x2200;
pc.gdt(
5,
descriptor(
at::TSS,
0x67,
ar::PRESENT | (u32::from(sys_type::TSS32_AVAIL) << 8),
),
);
pc.gdt(
6,
descriptor(
TSS_B,
0x67,
ar::PRESENT | (u32::from(sys_type::TSS32_AVAIL) << 8),
),
);
pc.write32(TSS_B + tss32::EIP, 0x3300);
pc.write32(TSS_B + tss32::EFLAGS, u64::from(flags::ALWAYS_SET));
pc.write32(TSS_B + tss32::EAX, 0x4444_4444);
pc.write32(TSS_B + tss32::EAX + 16, 0x8f00); pc.write32(TSS_B + tss32::ES, 0x10);
pc.write32(TSS_B + tss32::ES + 4, 0x08); pc.write32(TSS_B + tss32::ES + 8, 0x10); pc.write32(TSS_B + tss32::ES + 12, 0x10); pc.write32(TSS_B + tss32::ES + 16, 0x10);
pc.write32(TSS_B + tss32::ES + 20, 0x10);
pc.write(0x3300, &[0xf4]);
pc.write(
at::CODE0,
&[
0xb8, 0x28, 0x00, 0x00, 0x00, 0x0f, 0x00, 0xd8, 0xb8, 0x77, 0x77, 0x77, 0x77, 0xea, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, ],
);
for _ in 0..4 {
pc.cpu.step();
}
let regs = pc.regs();
assert_eq!(regs.rax, 0x4444_4444, "the incoming task's registers");
assert_eq!(regs.rip, 0x3300);
assert_eq!(pc.cpu.sys().task.selector, 0x30);
assert_eq!(
pc.cpu.sys().cr0 & cr0::TS,
cr0::TS,
"a task switch always sets TS"
);
assert_eq!(pc.read32(at::TSS + tss32::EAX), 0x7777_7777);
assert_eq!(pc.read32(at::TSS + tss32::EIP), at::CODE0 + 20);
}
#[test]
fn an_io_port_needs_the_privilege_level_or_the_permission_bitmap() {
let pc = pc386();
pc.start_protected();
pc.gdt(3, descriptor(0, 0xffff_ffff, rights::CODE32 | rights::DPL3));
pc.gdt(4, descriptor(0, 0xffff_ffff, rights::DATA32 | rights::DPL3));
pc.gdt(
5,
descriptor(
at::TSS,
0x7f,
ar::PRESENT | (u32::from(sys_type::TSS32_AVAIL) << 8),
),
);
pc.write32(at::TSS + tss32::ESP0, at::STACK0);
pc.write32(at::TSS + tss32::SS0, 0x10);
pc.write32(at::TSS + 0x64, 0x0068_0000);
for i in 0..0x18u64 {
pc.write(at::TSS + 0x68 + i, &[0xff]);
}
pc.write(at::TSS + 0x68 + 0x0c, &[0xfe]); pc.idt(13, gate(0x08, 0x3100, sys_type::INT_GATE32, 0));
pc.write(0x3100, &[0xf4]);
pc.write(
at::CODE0,
&[
0xb8, 0x28, 0x00, 0x00, 0x00, 0x0f, 0x00, 0xd8, 0x6a, 0x23, 0x68, 0x00, 0xa0, 0x00, 0x00, 0x6a, 0x02, 0x6a, 0x1b, 0x68, 0x00, 0x40,
0x00, 0x00, 0xcf,
],
);
pc.write(
at::CODE3,
&[
0xe4, 0x60, 0xe4, 0x61, 0xf4,
],
);
for _ in 0..8 {
pc.cpu.step();
}
assert_eq!(pc.regs().cs, 0x1b);
pc.cpu.step();
assert_eq!(pc.regs().rip, at::CODE3 + 2, "port 0x60 went through");
pc.cpu.step();
assert_eq!(pc.regs().rip, 0x3100, "port 0x61 raised #GP");
}
#[test]
fn a_thirty_two_bit_listing_reads_the_way_the_assembler_wrote_it() {
use super::disasm::disassemble_as;
let cases: &[(&[u8], &str)] = &[
(&[0x0f, 0xb6, 0xc1], "movzx eax, cl"),
(&[0x0f, 0xbf, 0xc1], "movsx eax, cx"),
(&[0x0f, 0xbc, 0xc1], "bsf eax, ecx"),
(&[0x0f, 0xba, 0xe8, 0x07], "bts eax, 0x7"),
(&[0x0f, 0xa4, 0xc8, 0x04], "shld eax, ecx, 0x4"),
(&[0x0f, 0xad, 0xc8], "shrd eax, ecx, cl"),
(&[0x0f, 0x94, 0xc0], "setz al"),
(&[0x60], "pushad"),
(&[0xc8, 0x10, 0x00, 0x00], "enter 0x10, 0x0"),
(&[0x6b, 0xc1, 0x64], "imul eax, ecx, 0x64"),
(&[0x0f, 0xc8], "bswap eax"),
(&[0x0f, 0xc1, 0xc8], "xadd eax, ecx"),
(&[0x0f, 0xb1, 0x08], "cmpxchg [ds:eax], ecx"),
(&[0x0f, 0x01, 0x10], "lgdt [ds:eax]"),
(&[0x0f, 0x00, 0xd0], "lldt ax"),
(&[0x0f, 0x02, 0xc1], "lar eax, ecx"),
(&[0x0f, 0x20, 0xc0], "mov eax, cr0"),
(&[0x0f, 0x22, 0xc0], "mov cr0, eax"),
(&[0x0f, 0x21, 0xf8], "mov eax, dr7"),
(&[0x0f, 0xa0], "push fs"),
(&[0x0f, 0xb2, 0x20], "lss esp, [ds:eax]"),
(&[0x8b, 0x14, 0x88], "mov edx, [ds:eax+ecx*4]"),
(
&[0x8b, 0x94, 0xf3, 0x34, 0x12, 0x00, 0x00],
"mov edx, [ds:ebx+esi*8+0x1234]",
),
(&[0xa1, 0x78, 0x56, 0x34, 0x12], "mov eax, [ds:0x12345678]"),
(&[0x8b, 0x45, 0x00], "mov eax, [ss:ebp]"),
(&[0x8b, 0x04, 0x24], "mov eax, [ss:esp]"),
(&[0x8b, 0x44, 0x7c, 0x04], "mov eax, [ss:esp+edi*2+0x4]"),
(&[0x66, 0x05, 0x34, 0x12], "add ax, 0x1234"),
(&[0x6f], "outsd dx, [ds:esi]"),
(&[0xf3, 0xa5], "rep movsd [es:edi], [ds:esi]"),
(&[0xcf], "iretd"),
(&[0x98], "cwde"),
(&[0x99], "cdq"),
(&[0x68, 0x78, 0x56, 0x34, 0x12], "push 0x12345678"),
(
&[0xea, 0x78, 0x56, 0x34, 0x12, 0x34, 0x12],
"jmpf 0x1234:0x12345678",
),
(&[0xca, 0x04, 0x00], "retf 0x4"),
];
for (bytes, want) in cases {
let d = disassemble_as(isa::Gen::I386, isa::Bits::B32, 0, 0, bytes);
assert_eq!(alloc::format!("{d}"), *want, "for {bytes:02x?}");
assert_eq!(d.len as usize, bytes.len(), "length of {bytes:02x?}");
}
}
#[test]
fn the_386_map_reclaimed_the_encodings_the_8086_spent_on_aliases() {
use super::isa::{Gen, decode_as};
assert_eq!(decode_as(Gen::I8086, 0x60).op, Op::JO);
assert_eq!(decode_as(Gen::I386, 0x60).op, Op::PUSHA);
assert_eq!(decode_as(Gen::I8086, 0x0f).op, Op::POP);
assert_eq!(decode_as(Gen::I386, 0x0f).class, Class::Escape);
assert_eq!(decode_as(Gen::I8086, 0xc8).op, Op::RETF);
assert_eq!(decode_as(Gen::I386, 0xc8).op, Op::ENTER);
assert_eq!(
super::isa::resolve_as(Gen::I8086, decode_as(Gen::I8086, 0xfe), 2).op,
Op::CALL
);
assert_eq!(
super::isa::resolve_as(Gen::I386, decode_as(Gen::I386, 0xfe), 2).op,
Op::UD
);
}
#[test]
fn the_string_instructions_move_at_the_operand_size() {
let pc = pc386();
pc.start_protected();
for i in 0..4u64 {
pc.write32(0x8000 + i * 4, 0x1111_1111 * (i + 1));
}
pc.write(
at::CODE0,
&[
0xbe, 0x00, 0x80, 0x00, 0x00, 0xbf, 0x00, 0x81, 0x00, 0x00, 0xb9, 0x04, 0x00, 0x00, 0x00, 0xfc, 0xf3, 0xa5, 0xb8, 0xa5, 0xa5, 0xa5, 0xa5, 0xbf, 0x00, 0x82, 0x00, 0x00, 0xb9, 0x02, 0x00, 0x00, 0x00, 0xf3, 0xab, 0xbf, 0x00, 0x82, 0x00, 0x00, 0xb9, 0x04, 0x00, 0x00, 0x00, 0xf2, 0xaf, 0xf4,
],
);
let steps = pc.run(30);
assert!(steps < 30);
for i in 0..4u64 {
assert_eq!(pc.read32(0x8100 + i * 4), 0x1111_1111 * (i + 1));
}
assert_eq!(pc.read32(0x8200), 0xa5a5_a5a5);
assert_eq!(pc.read32(0x8204), 0xa5a5_a5a5);
assert_eq!(pc.read32(0x8208), 0, "and stopped after two");
let regs = pc.regs();
assert_eq!(regs.rcx, 3);
assert_eq!(regs.rdi, 0x8204);
assert!(regs.flag(flags::ZF));
}
#[test]
fn the_near_conditional_jumps_take_a_full_displacement() {
let pc = pc386();
pc.start_protected();
pc.write(
at::CODE0,
&[
0x31, 0xc0, 0x0f, 0x84, 0xf8, 0x0f, 0x00, 0x00, ],
);
pc.write(at::CODE3, &[0xbb, 0x0d, 0x60, 0x00, 0x00, 0xf4]);
let steps = pc.run(6);
assert!(steps < 6);
assert_eq!(pc.regs().rbx, 0x600d, "the near jump reached CODE3");
let pc = pc386();
pc.start_protected();
pc.write(
at::CODE0,
&[
0x31, 0xc0, 0x66, 0x0f, 0x84, 0xf9, 0x0f, ],
);
pc.write(at::CODE3, &[0xf4]);
pc.cpu.step();
pc.cpu.step();
assert_eq!(pc.regs().rip, at::CODE3);
}
#[test]
fn xadd_and_cmpxchg_are_the_486_atomics_the_manual_describes() {
let pc = pc386();
pc.start_protected();
pc.write(
at::CODE0,
&[
0xb8, 0x00, 0x10, 0x00, 0x00, 0xb9, 0x34, 0x02, 0x00, 0x00, 0x0f, 0xc1, 0xc8, 0xf4,
],
);
let steps = pc.run(6);
assert!(steps < 6);
let regs = pc.regs();
assert_eq!(regs.rax, 0x1234, "the sum");
assert_eq!(regs.rcx, 0x1000, "and the destination's old value");
let pc = pc386();
pc.start_protected();
pc.write(
at::CODE0,
&[
0xb8, 0x05, 0x00, 0x00, 0x00, 0xbb, 0x05, 0x00, 0x00, 0x00, 0xb9, 0x09, 0x00, 0x00, 0x00, 0x0f, 0xb1, 0xcb, 0xf4,
],
);
let steps = pc.run(8);
assert!(steps < 8);
let regs = pc.regs();
assert!(regs.flag(flags::ZF), "the comparison matched");
assert_eq!(regs.rbx, 9, "so the source was stored");
assert_eq!(regs.rax, 5, "and the accumulator is unchanged");
let pc = pc386();
pc.start_protected();
pc.write(
at::CODE0,
&[
0xb8, 0x07, 0x00, 0x00, 0x00, 0xbb, 0x05, 0x00, 0x00, 0x00, 0xb9, 0x09, 0x00, 0x00, 0x00, 0x0f, 0xb1, 0xcb, 0xf4,
],
);
let steps = pc.run(8);
assert!(steps < 8);
let regs = pc.regs();
assert!(!regs.flag(flags::ZF));
assert_eq!(regs.rbx, 5, "the destination is left alone");
assert_eq!(regs.rax, 5, "and the accumulator takes its value");
}
#[test]
fn the_shift_group_gained_an_immediate_count_on_the_80186() {
let pc = pc386();
pc.start_protected();
pc.write(
at::CODE0,
&[
0xb8, 0x01, 0x00, 0x00, 0x00, 0xc1, 0xe0, 0x05, 0x66, 0xbb, 0x00, 0x80, 0x66, 0xd1, 0xcb, 0xf4,
],
);
let steps = pc.run(8);
assert!(steps < 8);
let regs = pc.regs();
assert_eq!(regs.rax, 0x20);
assert_eq!(regs.rbx & 0xffff, 0x4000);
}
#[test]
fn in_and_out_transfer_at_the_operand_size() {
let pc = pc386();
pc.start_protected();
for i in 0..4u64 {
pc.ports.write_u8(0x300 + i, 0x11 * (i as u8 + 1)).unwrap();
}
pc.write(
at::CODE0,
&[
0x66, 0xba, 0x00, 0x03, 0xed, 0x66, 0xba, 0x10, 0x03, 0xef, 0xf4,
],
);
let steps = pc.run(6);
assert!(steps < 6);
assert_eq!(pc.regs().rax, 0x4433_2211);
for i in 0..4u64 {
assert_eq!(
pc.ports.read_u8(0x310 + i).unwrap(),
0x11 * (i as u8 + 1),
"byte {i} of the 32-bit port write"
);
}
}
#[test]
fn lss_loads_the_stack_and_opens_the_interrupt_shadow() {
let pc = pc386();
pc.start_protected();
pc.write32(0x8300, 0x0000_8800);
pc.write32(0x8304, 0x0000_0010);
pc.write(
at::CODE0,
&[
0x0f, 0xb2, 0x25, 0x00, 0x83, 0x00, 0x00, 0xf4,
],
);
pc.cpu.step();
let regs = pc.regs();
assert_eq!(regs.rsp, 0x8800);
assert_eq!(regs.ss, 0x10);
assert!(
pc.cpu.interrupt_shadow(),
"loading SS inhibits interrupts for one instruction, whichever \
encoding did it"
);
}
#[test]
fn a_386_in_real_mode_takes_its_vectors_through_the_idt_register() {
let pc = pc386();
pc.start_real(0, 0x1000);
pc.write32(0x2000 + 3 * 4, 0x0000_1800);
let mut sys = pc.cpu.sys();
sys.idtr.base = 0x2000;
sys.idtr.limit = 0x3ff;
pc.cpu.set_sys(sys);
pc.write(0x1000, &[0xcc, 0xf4]); pc.write(0x1800, &[0xf4]);
pc.cpu.step();
let regs = pc.regs();
assert_eq!((regs.cs, regs.rip), (0x0000, 0x1800));
}
#[test]
fn a_page_fault_restarts_the_instruction_that_caused_it() {
let pc = pc386();
pc.start_protected();
pc.write32(at::PDIR, at::PTAB | 0b111);
for page in 0..1024u64 {
pc.write32(at::PTAB + page * 4, (page << 12) | 0b111);
}
pc.write32(at::PTAB + (at::MARK >> 12) * 4, 0);
let mut sys = pc.cpu.sys();
sys.cr3 = at::PDIR;
sys.cr0 |= cr0::PG;
pc.cpu.set_sys(sys);
pc.idt(14, gate(0x08, 0x3100, sys_type::INT_GATE32, 0));
pc.write(0x3100, &[0xf4]);
pc.write(
at::CODE0,
&[
0xb8, 0x0d, 0xf0, 0xad, 0x0b, 0xa3, 0x00, 0x70, 0x00, 0x00, 0xf4,
],
);
pc.cpu.step();
pc.cpu.step();
assert_eq!(pc.regs().rip, 0x3100);
assert_eq!(pc.read32(at::STACK0 - 12), at::CODE0 + 5);
assert_eq!(pc.regs().rax, 0x0bad_f00d, "the earlier work is not undone");
pc.write32(at::PTAB + (at::MARK >> 12) * 4, at::MARK | 0b111);
pc.cpu.set_regs(Regs {
rip: at::CODE0 + 5,
rsp: at::STACK0,
..pc.regs()
});
let mut sys = pc.cpu.sys();
sys.cr2 = 0;
pc.cpu.set_sys(sys);
pc.cpu.step();
assert_eq!(pc.read32(at::MARK), 0x0bad_f00d);
}
#[test]
fn a_sixteen_bit_code_segment_in_protected_mode_runs_sixteen_bit_code() {
let pc = pc386();
pc.start_protected();
pc.gdt(3, descriptor(0, 0xffff, rights::CODE16));
pc.gdt(4, descriptor(0, 0xffff, rights::DATA16));
pc.write(
at::CODE0,
&[0xea, 0x00, 0x40, 0x00, 0x00, 0x18, 0x00], );
pc.write(
at::CODE3,
&[
0xb8, 0x34, 0x12, 0x66, 0xbb, 0x78, 0x56, 0x34, 0x12, 0xa3, 0x00, 0x70, 0xf4,
],
);
let steps = pc.run(8);
assert!(steps < 8);
let regs = pc.regs();
assert_eq!(regs.cs, 0x18);
assert!(!pc.cpu.sys().seg(isa::seg::CS).big());
assert_eq!(regs.rax & 0xffff, 0x1234);
assert_eq!(regs.rbx, 0x1234_5678);
assert_eq!(pc.read32(at::MARK) & 0xffff, 0x1234);
}
#[test]
fn a_far_call_crosses_between_sixteen_and_thirty_two_bit_segments() {
let pc = pc386();
pc.start_protected();
pc.gdt(3, descriptor(0, 0xffff, rights::CODE16));
pc.write(
at::CODE0,
&[
0x9a, 0x00, 0x40, 0x00, 0x00, 0x18, 0x00, 0xbb, 0x0d, 0x60, 0x00, 0x00, 0xf4,
],
);
pc.write(
at::CODE3,
&[
0xb8, 0x99, 0x99, 0x66, 0xcb, ],
);
let steps = pc.run(8);
assert!(steps < 8);
let regs = pc.regs();
assert_eq!(regs.rax & 0xffff, 0x9999, "the 16-bit callee ran");
assert_eq!(regs.rbx, 0x600d, "and control came back");
assert_eq!(regs.cs, 0x08);
assert_eq!(regs.rsp, at::STACK0, "the stack is balanced");
}
#[test]
fn an_interrupt_onto_a_sixteen_bit_stack_moves_the_pointer_in_sixteen_bits() {
let pc = pc386();
pc.start_protected();
pc.gdt(3, descriptor(0, 0xffff, rights::DATA16));
pc.gdt(4, descriptor(0, 0xffff, rights::CODE16));
pc.idt(0x40, gate(0x20, 0x4000, sys_type::INT_GATE16, 0));
pc.write(0x4000, &[0xf4]);
let mut sys = pc.cpu.sys();
sys.segs[usize::from(isa::seg::SS)] = SegReg {
selector: 0x18,
base: 0,
limit: 0xffff,
ar: rights::DATA16,
};
pc.cpu.set_sys(sys);
pc.cpu.set_regs(Regs {
ss: 0x18,
rsp: 0xdead_9000,
..pc.regs()
});
pc.write(at::CODE0, &[0xcd, 0x40, 0xf4]); pc.cpu.step();
let regs = pc.regs();
assert_eq!(regs.cs, 0x20);
assert_eq!(regs.rip, 0x4000);
assert_eq!(
regs.rsp, 0xdead_8ffa,
"three words pushed, and the high half of ESP untouched"
);
assert_eq!(
pc.read32(0x8ffc) & 0xffff,
0x08,
"the caller's CS, stored as a word"
);
}
#[test]
fn smsw_sldt_and_str_read_back_what_was_loaded() {
let pc = pc386();
pc.start_protected();
pc.gdt(
5,
descriptor(
at::TSS,
0x67,
ar::PRESENT | (u32::from(sys_type::TSS32_AVAIL) << 8),
),
);
pc.gdt(
6,
descriptor(0x9800, 0xff, ar::PRESENT | (u32::from(sys_type::LDT) << 8)),
);
pc.write(
at::CODE0,
&[
0xb8, 0x28, 0x00, 0x00, 0x00, 0x0f, 0x00, 0xd8, 0xb8, 0x30, 0x00, 0x00, 0x00, 0x0f, 0x00, 0xd0, 0x0f, 0x00, 0xc1, 0x0f, 0x00, 0xca, 0x0f, 0x01, 0xe3, 0xf4,
],
);
let steps = pc.run(10);
assert!(steps < 10);
let regs = pc.regs();
assert_eq!(regs.rcx & 0xffff, 0x30, "sldt");
assert_eq!(regs.rdx & 0xffff, 0x28, "str");
assert_eq!(regs.rbx & 1, 1, "smsw sees CR0.PE");
assert_eq!(pc.cpu.sys().ldtr.base, 0x9800);
assert_eq!(
(pc.read32(at::GDT + 5 * 8 + 4) >> 8) & 0xf,
u64::from(sys_type::TSS32_BUSY)
);
}
#[test]
fn two_saves_of_the_same_state_are_byte_identical() {
let pc = pc386();
pc.start_protected();
pc.write(at::CODE0, &[0xb8, 0x11, 0x22, 0x33, 0x44, 0x50, 0x58, 0xf4]);
pc.run(5);
let snapshot = |cpu: &X86| {
let mut shape = MachineShape::new();
shape.add_device("/cpu0", "cpu.x86").unwrap();
let mut writer = StateWriter::new(shape);
{
let mut chunk = writer.chunk("/cpu0", "cpu.x86", 2).unwrap();
cpu.save(&mut chunk).unwrap();
}
writer.to_vec().unwrap()
};
let first = snapshot(&pc.cpu);
pc.cpu.reset(ResetKind::Cold);
let reader = crate::core::state::StateReader::new(&first).unwrap();
let (_, _, data) = reader.load_raw("/cpu0").unwrap();
let mut chunk = ChunkReader::new(data);
pc.cpu.load(&mut chunk).unwrap();
chunk.end().unwrap();
let second = snapshot(&pc.cpu);
assert_eq!(first, second, "the snapshot does not round-trip exactly");
}
#[test]
fn the_divide_that_has_no_representable_quotient_is_a_divide_error() {
let pc = pc386();
pc.start_protected();
pc.idt(0, gate(0x08, 0x3100, sys_type::INT_GATE32, 0));
pc.write(0x3100, &[0xf4]);
pc.write(
at::CODE0,
&[
0x31, 0xc0, 0xba, 0x00, 0x00, 0x00, 0x80, 0xb9, 0xff, 0xff, 0xff, 0xff, 0xf7, 0xf9, 0xf4,
],
);
for _ in 0..4 {
pc.cpu.step();
}
assert_eq!(pc.regs().rip, 0x3100, "#DE, not a host panic");
assert_eq!(pc.read32(at::STACK0 - 12), at::CODE0 + 12);
let pc = pc386();
pc.start_protected();
pc.idt(0, gate(0x08, 0x3100, sys_type::INT_GATE32, 0));
pc.write(0x3100, &[0xf4]);
pc.write(
at::CODE0,
&[
0xb8, 0x00, 0x00, 0x00, 0x00, 0xba, 0x01, 0x00, 0x00, 0x00, 0xb9, 0x01, 0x00, 0x00, 0x00, 0xf7, 0xf1, 0xf4,
],
);
for _ in 0..4 {
pc.cpu.step();
}
assert_eq!(pc.regs().rip, 0x3100);
}
#[test]
fn a_bit_test_on_memory_with_a_register_offset_reaches_outside_the_operand() {
let pc = pc386();
pc.start_protected();
pc.write32(0x8000, 0);
pc.write32(0x8004, 0x0000_0004); pc.write32(0x7ffc, 0x8000_0000); pc.write(
at::CODE0,
&[
0xbb, 0x22, 0x00, 0x00, 0x00, 0x0f, 0xa3, 0x1d, 0x00, 0x80, 0x00, 0x00, 0x0f, 0x92, 0xc0, 0xbb, 0xff, 0xff, 0xff, 0xff, 0x0f, 0xa3, 0x1d, 0x00, 0x80, 0x00, 0x00, 0x0f, 0x92, 0xc4, 0xf4,
],
);
let steps = pc.run(10);
assert!(steps < 10);
let regs = pc.regs();
assert_eq!(regs.rax & 0xff, 1, "bit 34 is one doubleword along");
assert_eq!((regs.rax >> 8) & 0xff, 1, "bit -1 is one doubleword back");
let pc = pc386();
pc.start_protected();
pc.write32(0x8000, 0x0000_0001);
pc.write32(0x8004, 0xffff_ffff);
pc.write(
at::CODE0,
&[
0x0f, 0xba, 0x25, 0x00, 0x80, 0x00, 0x00, 0x20, 0x0f, 0x92, 0xc0, 0xf4,
],
);
let steps = pc.run(5);
assert!(steps < 5);
assert_eq!(pc.regs().rax & 0xff, 1, "bit 32 wrapped to bit 0");
}
fn rewind(pc: &Pc) {
let mut regs = pc.cpu.regs();
regs.rip = at::CODE0;
pc.cpu.set_regs(regs);
}
#[test]
fn the_a20_gate_masks_address_bit_twenty_and_nothing_else() {
let pc = pc386();
pc.start_protected();
pc.write(0x0000_0010, &[0x5a]);
pc.write(0x0010_0010, &[0xa5]);
pc.write(at::CODE0, &[0xa0, 0x10, 0x00, 0x10, 0x00]);
assert!(pc.cpu.a20_open(), "a core with no gate wired has bit 20");
pc.cpu.step();
assert_eq!(pc.regs().rax & 0xff, 0xa5, "the megabyte above");
pc.cpu.set_a20(false);
rewind(&pc);
pc.cpu.step();
assert_eq!(
pc.regs().rax & 0xff,
0x5a,
"with the gate shut, bit 20 never reaches memory"
);
pc.write(0x0020_0010, &[0x3c]);
pc.write(at::CODE0, &[0xa0, 0x10, 0x00, 0x20, 0x00]);
rewind(&pc);
pc.cpu.step();
assert_eq!(pc.regs().rax & 0xff, 0x3c);
}
#[test]
fn wiring_an_a20_pin_shuts_the_gate_because_a_fresh_net_sits_low() {
use crate::core::wire::{Level, Wire, WireId};
let pc = pc386();
assert!(pc.cpu.a20_open(), "nothing has wired a gate");
let src = WireId(1);
let pin = pc.cpu.sink("a20", &[src]).expect("an a20 pin");
assert!(
!pc.cpu.a20_open(),
"a board that has a gate starts with it shut, which is what its net \
sitting low means and what an AT does"
);
let wire = Wire::builder()
.source(src)
.sink_weak(Arc::downgrade(&pin.sink), pin.line)
.build();
wire.set(src, Level::High);
assert!(pc.cpu.a20_open());
wire.set(src, Level::Low);
assert!(!pc.cpu.a20_open());
Device::reset(&*pc.cpu, ResetKind::Cold);
assert!(!pc.cpu.a20_open());
}
#[test]
fn the_scheduler_budget_is_never_overshot_and_the_debt_is_paid_back() {
let pc = pc386();
pc.start_protected();
pc.write(at::CODE0, &[0x40, 0x40, 0x40, 0xeb, 0xfb]);
let before = pc.cpu.cycles();
let mut total = 0u64;
for _ in 0..64 {
let used = pc.cpu.run_budget(1);
assert!(used <= 1, "a budget of one tick reported {used}");
total += used;
}
assert_eq!(total, 64, "every tick of every budget was granted and used");
assert_eq!(
pc.cpu.cycles() - before,
total + pc.cpu.cycle_debt(),
"clocks executed but not yet reported are exactly the debt"
);
}
mod long_mode {
use super::*;
use crate::core::state::StateReader;
use crate::cpu::x86::Features;
use crate::cpu::x86::paging::Mode;
use crate::cpu::x86::prot::{cr4, efer, msr};
mod la {
pub(super) const PML4: u64 = 0x1_0000;
pub(super) const PDPT: u64 = 0x1_1000;
pub(super) const PD: u64 = 0x1_2000;
pub(super) const PT: u64 = 0x1_4000;
pub(super) const CODE64: u64 = 0x2_0000;
pub(super) const HANDLER: u64 = 0x2_9000;
pub(super) const HANDLER2: u64 = 0x2_a000;
pub(super) const MARK: u64 = 0x2_8000;
}
const CODE64_AR: u32 = ar::PRESENT | ar::S | ar::CODE | ar::RW | ar::L | ar::GRANULAR;
const MAP: u64 = 0b111;
const PS: u64 = 1 << 7;
impl Pc {
fn write64(&self, addr: u64, value: u64) {
for i in 0..8u64 {
self.ram
.write_u8(addr + i, (value >> (8 * i)) as u8)
.unwrap();
}
}
fn read64(&self, addr: u64) -> u64 {
let mut value = 0u64;
for i in 0..8u64 {
value |= u64::from(self.ram.read_u8(addr + i).unwrap()) << (8 * i);
}
value
}
fn idt64(&self, vector: u64, selector: u16, offset: u64) {
let base = at::IDT + vector * 16;
let low = (offset as u32 & 0xffff) | (u32::from(selector) << 16);
let high = (offset as u32 & 0xffff_0000)
| ar::PRESENT
| (u32::from(sys_type::INT_GATE32) << 8);
self.write32(base, u64::from(low));
self.write32(base + 4, u64::from(high));
self.write32(base + 8, offset >> 32);
self.write32(base + 12, 0);
}
fn prepare_long(&self) {
self.write64(la::PML4, la::PDPT | MAP);
self.write64(la::PDPT, la::PD | MAP);
self.write64(la::PD, MAP | PS);
self.write64(la::PD + 8, 0x20_0000 | MAP | PS);
self.gdt(3, descriptor(0, 0xffff_ffff, CODE64_AR));
self.gdt(4, descriptor(0, 0xffff_ffff, CODE64_AR));
let mut sys = self.cpu.sys();
sys.idtr.limit = 0xfff;
self.cpu.set_sys(sys);
}
}
fn enter_long_mode_code(target: u64) -> Vec<u8> {
let mut code = Vec::new();
code.extend_from_slice(&[0x0f, 0x20, 0xe0]);
code.push(0x0d);
code.extend_from_slice(&(cr4::PAE as u32).to_le_bytes());
code.extend_from_slice(&[0x0f, 0x22, 0xe0]);
code.push(0xb8);
code.extend_from_slice(&(la::PML4 as u32).to_le_bytes());
code.extend_from_slice(&[0x0f, 0x22, 0xd8]);
code.push(0xb9);
code.extend_from_slice(&msr::EFER.to_le_bytes());
code.extend_from_slice(&[0x0f, 0x32]);
code.push(0x0d);
code.extend_from_slice(&(efer::LME as u32).to_le_bytes());
code.extend_from_slice(&[0x0f, 0x30]);
code.extend_from_slice(&[0x0f, 0x20, 0xc0]);
code.push(0x0d);
code.extend_from_slice(&cr0::PG.to_le_bytes());
code.extend_from_slice(&[0x0f, 0x22, 0xc0]);
code.push(0xea);
code.extend_from_slice(&(target as u32).to_le_bytes());
code.extend_from_slice(&0x18u16.to_le_bytes());
code
}
fn pc64() -> Pc {
Pc::new(Variant::X86_64)
}
fn run64(code: &[u8]) -> Pc {
let pc = pc64();
pc.start_protected();
pc.prepare_long();
pc.write(at::CODE0, &enter_long_mode_code(la::CODE64));
pc.write(la::CODE64, code);
let steps = pc.run(200);
assert!(steps < 200, "the 64-bit program halted");
assert!(pc.cpu.sys().sixty_four(), "and did so in 64-bit mode");
pc
}
#[test]
fn a_guest_enters_long_mode_and_executes_64_bit_code() {
let pc = pc64();
pc.start_protected();
pc.prepare_long();
pc.write(at::CODE0, &enter_long_mode_code(la::CODE64));
let mut code = Vec::new();
code.extend_from_slice(&[0x48, 0xb8]);
code.extend_from_slice(&0x0123_4567_89ab_cdefu64.to_le_bytes());
code.extend_from_slice(&[0x48, 0x89, 0xc3]);
code.extend_from_slice(&[0x49, 0x89, 0xc7]);
code.extend_from_slice(&[0x48, 0xc7, 0xc1, 0xff, 0xff, 0xff, 0xff]);
let after = la::CODE64 + code.len() as u64 + 7;
let disp = i32::try_from(la::MARK as i64 - after as i64).expect("in range");
code.extend_from_slice(&[0x48, 0x89, 0x05]);
code.extend_from_slice(&disp.to_le_bytes());
code.push(0xf4); pc.write(la::CODE64, &code);
let steps = pc.run(40);
assert!(steps < 40, "the program reached its hlt in {steps} steps");
let sys = pc.cpu.sys();
assert!(sys.long_mode(), "EFER.LMA is set by the write to CR0.PG");
assert!(sys.sixty_four(), "and CS.L put the core in 64-bit mode");
assert_eq!(sys.paging_mode(pc.cpu.config().features), Mode::Ia32e);
assert_eq!(sys.cr3, la::PML4);
let regs = pc.regs();
assert_eq!(regs.rax, 0x0123_4567_89ab_cdef, "a 64-bit immediate");
assert_eq!(regs.rbx, 0x0123_4567_89ab_cdef, "REX.W moved all of it");
assert_eq!(regs.r[7], 0x0123_4567_89ab_cdef, "REX.B reached R15");
assert_eq!(
regs.rcx,
u64::MAX,
"an imm32 sign-extended, not zero-filled"
);
assert_eq!(
pc.read64(la::MARK),
0x0123_4567_89ab_cdef,
"a RIP-relative store landed where the displacement pointed"
);
}
#[test]
fn setting_paging_without_the_address_extension_refuses_to_enter_long_mode() {
let pc = pc64();
pc.start_protected();
pc.prepare_long();
let mut sys = pc.cpu.sys();
sys.efer |= efer::LME;
sys.cr3 = la::PML4;
pc.cpu.set_sys(sys);
pc.idt(13, gate(0x08, 0x9000, sys_type::INT_GATE32, 0));
pc.write(0x9000, &[0xf4]);
let mut code = alloc::vec![0x0f, 0x20, 0xc0, 0x0d];
code.extend_from_slice(&cr0::PG.to_le_bytes());
code.extend_from_slice(&[0x0f, 0x22, 0xc0]);
pc.write(at::CODE0, &code);
pc.run(10);
assert_eq!(pc.regs().rip, 0x9001, "the write to CR0 raised #GP");
assert!(!pc.cpu.sys().long_mode(), "and long mode was not entered");
}
#[test]
fn arming_long_mode_while_paging_is_on_is_refused() {
let pc = pc64();
pc.start_protected();
pc.prepare_long();
let mut sys = pc.cpu.sys();
sys.cr4 |= cr4::PAE;
sys.cr3 = la::PDPT;
sys.cr0 |= cr0::PG;
pc.cpu.set_sys(sys);
pc.write64(la::PDPT, la::PD | MAP);
pc.write64(la::PD, MAP | PS);
pc.write64(la::PD + 8, 0x20_0000 | MAP | PS);
pc.idt(13, gate(0x08, 0x9000, sys_type::INT_GATE32, 0));
pc.write(0x9000, &[0xf4]);
let mut code = alloc::vec![0xb9];
code.extend_from_slice(&msr::EFER.to_le_bytes());
code.extend_from_slice(&[0x0f, 0x32, 0x0d]);
code.extend_from_slice(&(efer::LME as u32).to_le_bytes());
code.extend_from_slice(&[0x0f, 0x30, 0xf4]);
pc.write(at::CODE0, &code);
pc.run(10);
assert_eq!(pc.regs().rip, 0x9001, "the WRMSR raised #GP");
assert_eq!(pc.cpu.sys().efer & efer::LME, 0);
}
#[test]
fn efer_lma_is_the_processors_bit_and_not_softwares() {
let pc = pc64();
pc.start_protected();
pc.prepare_long();
let mut code = alloc::vec![0xb9];
code.extend_from_slice(&msr::EFER.to_le_bytes());
code.push(0xb8);
code.extend_from_slice(&((efer::LMA | efer::LME) as u32).to_le_bytes());
code.extend_from_slice(&[0x31, 0xd2, 0x0f, 0x30, 0x0f, 0x32, 0xf4]);
pc.write(at::CODE0, &code);
pc.run(10);
let regs = pc.regs();
assert_eq!(regs.rax & efer::LME, efer::LME, "LME took");
assert_eq!(regs.rax & efer::LMA, 0, "LMA did not");
assert_eq!(pc.cpu.sys().efer & efer::LMA, 0);
}
#[test]
fn clearing_the_address_extension_in_long_mode_is_refused() {
let pc = pc64();
pc.start_protected();
pc.prepare_long();
pc.write(at::CODE0, &enter_long_mode_code(la::CODE64));
pc.idt64(13, 0x18, la::HANDLER);
pc.write(la::HANDLER, &[0xf4]);
let mut code = alloc::vec![0x0f, 0x20, 0xe0, 0x25];
code.extend_from_slice(&(!(cr4::PAE as u32)).to_le_bytes());
code.extend_from_slice(&[0x0f, 0x22, 0xe0, 0xf4]);
pc.write(la::CODE64, &code);
pc.run(60);
assert!(pc.cpu.sys().long_mode(), "long mode survived the attempt");
assert_ne!(pc.cpu.sys().cr4 & cr4::PAE, 0, "and so did CR4.PAE");
assert_eq!(pc.regs().rip, la::HANDLER + 1, "#GP was taken");
}
#[test]
fn a_code_segment_may_not_be_both_long_and_big() {
let pc = pc64();
pc.start_protected();
pc.prepare_long();
pc.gdt(3, descriptor(0, 0xffff_ffff, CODE64_AR | ar::DB));
pc.write(at::CODE0, &enter_long_mode_code(la::CODE64));
pc.idt64(13, 0x20, la::HANDLER);
pc.write(la::HANDLER, &[0xf4]);
pc.write(la::CODE64, &[0xf4]);
pc.run(40);
assert!(pc.cpu.sys().long_mode(), "long mode was entered");
assert_eq!(pc.regs().rip, la::HANDLER + 1, "but the far jump was #GP");
assert_eq!(
pc.cpu.sys().seg(isa::seg::CS).selector & !3,
0x20,
"and the handler ran in the segment that was still valid"
);
}
#[test]
fn a_thirty_two_bit_write_zero_extends_and_a_narrower_one_does_not() {
let pc = run64(&[
0x48, 0xb8, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x48, 0x89, 0xc3, 0x48, 0x89, 0xc1, 0x48, 0x89, 0xc2, 0xbb, 0x78, 0x56, 0x34, 0x12, 0x66, 0xb9, 0x34, 0x12, 0xb2, 0x99, 0xf4,
]);
let regs = pc.regs();
assert_eq!(regs.rbx, 0x1234_5678, "a 32-bit write cleared the top half");
assert_eq!(regs.rcx, 0xffff_ffff_ffff_1234, "a 16-bit write did not");
assert_eq!(regs.rdx, 0xffff_ffff_ffff_ff99, "nor did an 8-bit one");
}
#[test]
fn a_rex_prefix_renames_the_high_byte_registers() {
let pc = run64(&[
0x48, 0x31, 0xe4, 0xb0, 0x5a, 0x40, 0x88, 0xc4, 0x88, 0xc4, 0xf4,
]);
let regs = pc.regs();
assert_eq!(regs.rsp, 0x5a, "the REX form wrote SPL");
assert_eq!(regs.rax & 0xffff, 0x5a5a, "the bare form wrote AH");
}
#[test]
fn rip_relative_addressing_counts_from_the_end_of_the_instruction() {
let pc = pc64();
pc.start_protected();
pc.prepare_long();
pc.write(at::CODE0, &enter_long_mode_code(la::CODE64));
pc.write64(la::MARK, 0xdead_beef_cafe_f00d);
let mut code = Vec::new();
let after = la::CODE64 + 7;
let disp = i32::try_from(la::MARK as i64 - after as i64).expect("in range");
code.extend_from_slice(&[0x48, 0x8b, 0x05]); code.extend_from_slice(&disp.to_le_bytes());
code.push(0xf4);
pc.write(la::CODE64, &code);
pc.run(60);
assert_eq!(pc.regs().rax, 0xdead_beef_cafe_f00d);
}
#[test]
fn the_stack_moves_eight_bytes_at_a_time_and_a_prefix_narrows_it() {
let pc = run64(&[
0x48, 0xb8, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x48, 0x89, 0xe3, 0x50, 0x48, 0x89, 0xe1, 0x5a, 0x66, 0x50, 0x48, 0x89, 0xe6, 0x66, 0x5f, 0xf4,
]);
let regs = pc.regs();
assert_eq!(regs.rbx - regs.rcx, 8, "a bare push moved RSP by eight");
assert_eq!(regs.rdx, 0x8877_6655_4433_2211, "and popped all of it back");
assert_eq!(regs.rbx - regs.rsi, 2, "a 66-prefixed push moved it by two");
assert_eq!(regs.rdi & 0xffff, 0x2211);
}
#[test]
fn a_near_call_pushes_a_sixty_four_bit_return_address() {
let pc = run64(&[
0xe8, 0x08, 0x00, 0x00, 0x00, 0x48, 0xc7, 0xc3, 0x01, 0x00, 0x00, 0x00, 0xf4, 0x48, 0x8b, 0x04, 0x24, 0xc3, ]);
let regs = pc.regs();
assert_eq!(
regs.rax,
la::CODE64 + 5,
"the pushed return address was the full sixty-four bits"
);
assert_eq!(regs.rbx, 1, "and the return landed on the next instruction");
}
#[test]
fn movsxd_replaced_arpl_and_sign_extends_a_doubleword() {
let pc = run64(&[
0x48, 0xc7, 0xc1, 0x00, 0x00, 0x00, 0x80, 0x48, 0x63, 0xc1, 0x63, 0xd9, 0xf4,
]);
let regs = pc.regs();
assert_eq!(regs.rax, 0xffff_ffff_8000_0000, "REX.W sign-extended");
assert_eq!(regs.rbx, 0x8000_0000, "without it the result zero-extends");
}
#[test]
fn the_encodings_long_mode_reclaimed_raise_invalid_opcode() {
for (opcode, name) in [
(0x06u8, "push es"),
(0x07, "pop es"),
(0x0e, "push cs"),
(0x16, "push ss"),
(0x1e, "push ds"),
(0x27, "daa"),
(0x2f, "das"),
(0x37, "aaa"),
(0x3f, "aas"),
(0x60, "pusha"),
(0x61, "popa"),
(0x62, "bound"),
(0x82, "the 80-group alias"),
(0x9a, "call far ptr16:32"),
(0xc4, "les"),
(0xc5, "lds"),
(0xce, "into"),
(0xd4, "aam"),
(0xd5, "aad"),
(0xd6, "salc"),
(0xea, "jmp far ptr16:32"),
] {
let pc = pc64();
pc.start_protected();
pc.prepare_long();
pc.write(at::CODE0, &enter_long_mode_code(la::CODE64));
pc.write(la::CODE64, &[opcode, 0x00, 0x00, 0x00, 0x00, 0x00]);
pc.idt64(6, 0x18, la::HANDLER);
pc.write(la::HANDLER, &[0xf4]);
pc.run(60);
assert_eq!(
pc.regs().rip,
la::HANDLER + 1,
"{name} raised #UD in 64-bit mode"
);
}
}
#[test]
fn a_non_canonical_address_faults_before_the_page_tables_are_consulted() {
let pc = pc64();
pc.start_protected();
pc.prepare_long();
pc.write(at::CODE0, &enter_long_mode_code(la::CODE64));
pc.idt64(13, 0x18, la::HANDLER);
pc.write(la::HANDLER, &[0xf4]);
let mut code = alloc::vec![0x48, 0xb8];
code.extend_from_slice(&0x0001_0000_0000_0000u64.to_le_bytes());
code.extend_from_slice(&[0x48, 0x8b, 0x18, 0xf4]);
pc.write(la::CODE64, &code);
pc.run(60);
assert_eq!(pc.regs().rip, la::HANDLER + 1, "#GP, not a page fault");
assert_eq!(pc.cpu.sys().cr2, 0, "and CR2 was never latched");
}
#[test]
fn an_interrupt_in_long_mode_takes_a_sixteen_byte_gate_and_iretq_returns() {
let pc = pc64();
pc.start_protected();
pc.prepare_long();
pc.write(at::CODE0, &enter_long_mode_code(la::CODE64));
pc.idt64(0x40, 0x18, la::HANDLER);
pc.write(
la::HANDLER,
&[0x48, 0xc7, 0xc3, 0x2a, 0x00, 0x00, 0x00, 0x48, 0xcf],
);
pc.write(
la::CODE64,
&[0xcd, 0x40, 0x48, 0xc7, 0xc1, 0x07, 0x00, 0x00, 0x00, 0xf4],
);
pc.run(80);
let regs = pc.regs();
assert_eq!(regs.rbx, 42, "the handler ran");
assert_eq!(regs.rcx, 7, "and IRETQ came back to the instruction after");
assert!(pc.cpu.sys().sixty_four(), "still in 64-bit mode");
}
#[test]
fn syscall_and_sysret_cross_the_boundary_without_the_descriptor_tables() {
let pc = pc64();
pc.start_protected();
pc.prepare_long();
pc.write(at::CODE0, &enter_long_mode_code(la::CODE64));
let mut sys = pc.cpu.sys();
sys.efer |= efer::SCE;
sys.star = (0x0010u64 << 48) | (0x0018u64 << 32);
sys.lstar = la::HANDLER;
pc.cpu.set_sys(sys);
pc.write(
la::HANDLER,
&[0x48, 0xc7, 0xc3, 0x63, 0x00, 0x00, 0x00, 0x48, 0x0f, 0x07],
);
pc.write(
la::CODE64,
&[0x0f, 0x05, 0x48, 0xc7, 0xc2, 0x09, 0x00, 0x00, 0x00, 0xf4],
);
pc.run(80);
let regs = pc.regs();
assert_eq!(regs.rbx, 0x63, "the kernel entry point ran");
assert_eq!(regs.rdx, 9, "and SYSRET came back to the next instruction");
assert_eq!(regs.cs & 3, 3, "at privilege level three");
}
#[test]
fn swapgs_exchanges_the_gs_base_with_the_kernels() {
let pc = pc64();
pc.start_protected();
pc.prepare_long();
pc.write(at::CODE0, &enter_long_mode_code(la::CODE64));
let mut sys = pc.cpu.sys();
sys.gs_base = 0x1111_0000;
sys.segs[usize::from(isa::seg::GS)].base = 0x1111_0000;
sys.kernel_gs_base = la::MARK;
pc.cpu.set_sys(sys);
pc.write64(la::MARK, 0xfeed_face_0000_0001);
pc.write(
la::CODE64,
&[
0x0f, 0x01, 0xf8, 0x65, 0x48, 0x8b, 0x04, 0x25, 0, 0, 0, 0, 0x0f, 0x01, 0xf8, 0xf4,
],
);
pc.run(60);
assert_eq!(
pc.regs().rax,
0xfeed_face_0000_0001,
"GS reached the kernel area"
);
let sys = pc.cpu.sys();
assert_eq!(sys.gs_base, 0x1111_0000, "and the second swap put it back");
assert_eq!(sys.kernel_gs_base, la::MARK);
}
#[test]
fn the_no_execute_bit_stops_a_fetch_and_leaves_a_read_alone() {
let pc = pc64();
pc.start_protected();
pc.prepare_long();
pc.write64(la::PD, la::PT | MAP);
for page in 0..512u64 {
pc.write64(la::PT + page * 8, (page * 0x1000) | MAP);
}
let barred = la::HANDLER;
let index = barred / 0x1000;
pc.write64(la::PT + index * 8, barred | MAP | (1 << 63));
pc.write(at::CODE0, &enter_long_mode_code(la::CODE64));
let mut sys = pc.cpu.sys();
sys.efer |= efer::NXE;
pc.cpu.set_sys(sys);
pc.idt64(14, 0x18, la::HANDLER2);
pc.write(la::HANDLER2, &[0xf4]);
pc.write64(barred + 0x100, 0x5555_aaaa_5555_aaaa);
let mut code = alloc::vec![0x48, 0xb8];
code.extend_from_slice(&(barred + 0x100).to_le_bytes());
code.extend_from_slice(&[0x48, 0x8b, 0x18]); code.extend_from_slice(&[0x48, 0xb8]);
code.extend_from_slice(&barred.to_le_bytes());
code.extend_from_slice(&[0xff, 0xe0]); pc.write(la::CODE64, &code);
pc.run(80);
assert_eq!(
pc.regs().rbx,
0x5555_aaaa_5555_aaaa,
"the read went through"
);
assert_eq!(pc.regs().rip, la::HANDLER2 + 1, "and the fetch faulted");
assert_eq!(
pc.cpu.sys().cr2,
barred,
"CR2 names the page that was barred"
);
}
#[test]
fn a_gigabyte_page_is_mapped_by_the_pointer_table_itself() {
let pc = pc64();
pc.start_protected();
pc.prepare_long();
pc.write64(la::PDPT, MAP | PS);
pc.write(at::CODE0, &enter_long_mode_code(la::CODE64));
pc.write64(la::MARK, 0x0102_0304_0506_0708);
let mut code = alloc::vec![0x48, 0xb8];
code.extend_from_slice(&la::MARK.to_le_bytes());
code.extend_from_slice(&[0x48, 0x8b, 0x18, 0xf4]); pc.write(la::CODE64, &code);
pc.run(60);
assert_eq!(pc.regs().rbx, 0x0102_0304_0506_0708);
assert_eq!(
pc.cpu.translate_debug(la::MARK),
DebugTranslation::Mapped(la::MARK),
"and the debug walk agrees, through the same code"
);
}
#[test]
fn physical_address_extension_translates_without_long_mode() {
let pc = pc64();
pc.start_protected();
pc.write64(la::PDPT, la::PD | MAP);
pc.write64(la::PD, MAP | PS);
pc.write64(la::PD + 8, 0x20_0000 | MAP | PS);
let mut sys = pc.cpu.sys();
sys.cr4 |= cr4::PAE;
sys.cr3 = la::PDPT;
sys.cr0 |= cr0::PG;
pc.cpu.set_sys(sys);
assert_eq!(
pc.cpu.sys().paging_mode(pc.cpu.config().features),
Mode::Pae
);
pc.write32(at::MARK, 0x1234_5678);
let mut code = alloc::vec![0xa1];
code.extend_from_slice(&(at::MARK as u32).to_le_bytes());
code.push(0xf4);
pc.write(at::CODE0, &code);
pc.run(10);
assert_eq!(pc.regs().rax, 0x1234_5678);
assert_eq!(
pc.cpu.translate_debug(at::MARK),
DebugTranslation::Mapped(at::MARK)
);
}
#[test]
fn a_long_mode_core_round_trips_through_a_snapshot() {
let pc = run64(&[
0x48, 0xb8, 0xef, 0xcd, 0xab, 0x89, 0x67, 0x45, 0x23, 0x01, 0x49, 0x89, 0xc7, 0x49, 0x89, 0xc4, 0xf4,
]);
let before = pc.regs();
let sys_before = pc.cpu.sys();
assert_ne!(before.r[7], 0, "there is something to lose");
let mut shape = MachineShape::new();
shape.add_device("/cpu0", "cpu.x86").unwrap();
let mut writer = StateWriter::new(shape);
{
let mut chunk = writer.chunk("/cpu0", "cpu.x86", 4).unwrap();
pc.cpu.save(&mut chunk).unwrap();
}
let bytes = writer.to_vec().unwrap();
pc.cpu.reset(ResetKind::Cold);
assert_ne!(pc.cpu.regs(), before);
assert!(!pc.cpu.sys().long_mode(), "a reset left long mode");
let reader = StateReader::new(&bytes).unwrap();
let (_, _, data) = reader.load_raw("/cpu0").unwrap();
let mut chunk = ChunkReader::new(data);
pc.cpu.load(&mut chunk).unwrap();
chunk.end().unwrap();
assert_eq!(pc.cpu.regs(), before, "every register came back");
assert_eq!(
pc.cpu.sys(),
sys_before,
"and so did EFER, CR4 and the MSRs"
);
assert!(pc.cpu.sys().sixty_four());
let mut shape = MachineShape::new();
shape.add_device("/cpu0", "cpu.x86").unwrap();
let mut again = StateWriter::new(shape);
{
let mut chunk = again.chunk("/cpu0", "cpu.x86", 4).unwrap();
pc.cpu.save(&mut chunk).unwrap();
}
assert_eq!(bytes, again.to_vec().unwrap());
}
#[test]
fn the_disassembler_prints_the_sixty_four_bit_forms() {
use crate::cpu::x86::disasm::disassemble_as;
let at64 = |bytes: &[u8]| {
let d = disassemble_as(isa::Gen::I386, isa::Bits::B64, 0, 0x1000, bytes);
alloc::format!("{d}")
};
assert_eq!(at64(&[0x48, 0x89, 0xc3]), "mov rbx, rax");
assert_eq!(at64(&[0x49, 0x89, 0xc7]), "mov r15, rax");
assert_eq!(at64(&[0x4d, 0x89, 0xc7]), "mov r15, r8");
assert_eq!(at64(&[0x40, 0x88, 0xc4]), "mov spl, al");
assert_eq!(at64(&[0x88, 0xc4]), "mov ah, al");
assert_eq!(at64(&[0x48, 0x63, 0xc1]), "movsxd rax, ecx");
assert_eq!(at64(&[0x0f, 0x05]), "syscall");
assert_eq!(
at64(&[0x48, 0x8b, 0x05, 0x10, 0x00, 0x00, 0x00]),
"mov rax, [ds:rip+0x10]"
);
assert_eq!(at64(&[0x60]), "ud");
}
#[test]
fn the_extension_lattice_is_selected_rather_than_implied() {
let cfg = Config::X86_64.with_features(Features {
long: false,
pae: false,
nx: false,
syscall: false,
..Features::X86_64
});
let pc = Pc::new(Variant::X86_64);
let cpu = Arc::new(X86::new(cfg));
let _ = &pc;
assert!(!cpu.config().features.long);
assert!(cfg.features.validate().is_ok());
let impossible = Features {
pae: false,
..Features::X86_64
};
assert!(impossible.validate().is_err(), "long mode needs PAE");
}
}
mod fp {
use super::*;
use crate::cpu::x86::fpu::{Sse, Tag, cw, mxcsr, sw};
use crate::cpu::x86::prot::cr4;
use crate::float::x87::F80;
mod fpat {
pub(super) const DATA: u64 = 0xb000;
pub(super) const ODD: u64 = 0xb008;
pub(super) const OUT: u64 = 0xc000;
pub(super) const SAVE: u64 = 0xd000;
}
const HANDLER: u64 = 0x3800;
const HANDLED: u64 = HANDLER + 1;
fn x87pc() -> Pc {
let pc = pc386();
pc.start_protected();
pc
}
fn ssepc() -> Pc {
let pc = Pc::new(Variant::X86_64);
pc.start_protected();
let mut sys = pc.cpu.sys();
sys.cr4 |= cr4::OSFXSR | cr4::OSXMMEXCPT;
pc.cpu.set_sys(sys);
pc
}
fn disp32(reg: u8, addr: u64) -> Vec<u8> {
let mut out = alloc::vec![((reg & 7) << 3) | 0b101];
out.extend_from_slice(&(addr as u32).to_le_bytes());
out
}
fn run(pc: &Pc, code: &[u8]) {
pc.write(at::CODE0, code);
let steps = pc.run(200);
assert!(steps < 200, "the program reached its hlt");
}
fn read64(pc: &Pc, addr: u64) -> u64 {
let mut v = 0u64;
for i in 0..8u64 {
v |= u64::from(pc.ram.read_u8(addr + i).unwrap()) << (8 * i);
}
v
}
fn write64(pc: &Pc, addr: u64, value: u64) {
for i in 0..8u64 {
pc.ram.write_u8(addr + i, (value >> (8 * i)) as u8).unwrap();
}
}
#[test]
fn a_load_pushes_and_the_top_of_stack_pointer_moves_down() {
let pc = x87pc();
run(&pc, &[0xdb, 0xe3, 0xd9, 0xe8, 0xf4]); let x = pc.cpu.x87();
assert_eq!(x.top(), 7);
assert_eq!(x.raw(0), F80::new(0x3fff, 0x8000_0000_0000_0000));
assert_eq!(x.tag_at(7), Tag::Valid);
assert_eq!(x.tag_at(0), Tag::Empty);
}
#[test]
fn a_ninth_push_is_a_stack_overflow_with_c1_set() {
let pc = x87pc();
let mut code = alloc::vec![0xdb, 0xe3];
for _ in 0..9 {
code.extend_from_slice(&[0xd9, 0xe8]); }
code.push(0xf4);
run(&pc, &code);
let x = pc.cpu.x87();
assert_ne!(x.status & sw::IE, 0, "invalid operation");
assert_ne!(x.status & sw::SF, 0, "and it was a stack fault");
assert_ne!(x.status & sw::C1, 0, "an overflow, not an underflow");
assert_eq!(x.raw(0), F80::INDEFINITE);
}
#[test]
fn reading_an_empty_register_is_an_underflow_with_c1_clear() {
let pc = x87pc();
run(&pc, &[0xdb, 0xe3, 0xd9, 0xc0, 0xf4]); let x = pc.cpu.x87();
assert_ne!(x.status & sw::IE, 0);
assert_ne!(x.status & sw::SF, 0);
assert_eq!(x.status & sw::C1, 0, "an underflow sets C1 to zero");
assert_eq!(x.raw(0), F80::INDEFINITE);
}
#[test]
fn fxch_swaps_the_values_and_their_tags() {
let pc = x87pc();
run(&pc, &[0xdb, 0xe3, 0xd9, 0xee, 0xd9, 0xe8, 0xd9, 0xc9, 0xf4]);
let x = pc.cpu.x87();
assert_eq!(x.raw(0), F80::ZERO);
assert_eq!(x.raw(1), F80::new(0x3fff, 0x8000_0000_0000_0000));
assert_eq!(x.tag_at(x.phys(0)), Tag::Zero);
assert_eq!(x.tag_at(x.phys(1)), Tag::Valid);
}
#[test]
fn fincstp_leaves_the_tag_word_alone_and_a_pop_does_not() {
let pc = x87pc();
run(&pc, &[0xdb, 0xe3, 0xd9, 0xe8, 0xd9, 0xf7, 0xf4]);
assert_eq!(pc.cpu.x87().tag_at(7), Tag::Valid, "still occupied");
let pc = x87pc();
run(&pc, &[0xdb, 0xe3, 0xd9, 0xe8, 0xdd, 0xd8, 0xf4]);
assert_eq!(pc.cpu.x87().tag_at(7), Tag::Empty, "freed by the pop");
}
#[test]
fn a_double_precision_add_produces_the_exact_sum() {
let pc = x87pc();
write64(&pc, fpat::DATA, 0x3ff8_0000_0000_0000); write64(&pc, fpat::DATA + 8, 0x4002_0000_0000_0000); let mut code = alloc::vec![0xdb, 0xe3, 0xdd]; code.extend_from_slice(&disp32(0, fpat::DATA));
code.push(0xdc); code.extend_from_slice(&disp32(0, fpat::DATA + 8));
code.push(0xdd); code.extend_from_slice(&disp32(3, fpat::OUT));
code.push(0xf4);
run(&pc, &code);
assert_eq!(read64(&pc, fpat::OUT), 0x400e_0000_0000_0000, "3.75");
assert_eq!(pc.cpu.x87().status & sw::EXCEPTIONS, 0, "exactly");
}
#[test]
fn precision_control_shortens_the_significand_and_nothing_else() {
let program = |control: u16| {
let pc = x87pc();
write64(&pc, fpat::DATA, u64::from(control));
write64(&pc, fpat::DATA + 8, 0x3e10_0000_0000_0000); let mut code = alloc::vec![0xdb, 0xe3, 0xd9]; code.extend_from_slice(&disp32(5, fpat::DATA));
code.extend_from_slice(&[0xd9, 0xe8, 0xdc]); code.extend_from_slice(&disp32(0, fpat::DATA + 8));
code.push(0xdb); code.extend_from_slice(&disp32(7, fpat::OUT));
code.push(0xf4);
run(&pc, &code);
(read64(&pc, fpat::OUT), pc.cpu.x87().status)
};
let (extended, st) = program(cw::RESET);
assert_eq!(extended, 0x8000_0002_0000_0000, "2^-30 survives at PC=64");
assert_eq!(st & sw::PE, 0, "and the sum is exact");
let (single, st) = program(cw::RESET & !cw::PC);
assert_eq!(single, 0x8000_0000_0000_0000, "and vanishes at PC=24");
assert_ne!(st & sw::PE, 0, "which is inexact, and says so");
}
#[test]
fn rounding_control_changes_the_last_bit_of_one_third() {
let program = |control: u16| {
let pc = x87pc();
write64(&pc, fpat::DATA, u64::from(control));
write64(&pc, fpat::DATA + 8, 0x4008_0000_0000_0000); let mut code = alloc::vec![0xdb, 0xe3, 0xd9];
code.extend_from_slice(&disp32(5, fpat::DATA));
code.extend_from_slice(&[0xd9, 0xe8, 0xdd]); code.extend_from_slice(&disp32(0, fpat::DATA + 8));
code.extend_from_slice(&[0xde, 0xf9, 0xdb]);
code.extend_from_slice(&disp32(7, fpat::OUT));
code.push(0xf4);
run(&pc, &code);
read64(&pc, fpat::OUT)
};
assert_eq!(program(cw::RESET), 0xaaaa_aaaa_aaaa_aaab, "to nearest");
let toward_zero = cw::RESET | cw::RC;
assert_eq!(program(toward_zero), 0xaaaa_aaaa_aaaa_aaaa, "toward zero");
}
#[test]
fn the_integer_conversions_round_and_saturate_as_the_manual_says() {
let pc = x87pc();
write64(&pc, fpat::DATA, 0x4059_0000_0000_0000); let mut code = alloc::vec![0xdb, 0xe3, 0xdd];
code.extend_from_slice(&disp32(0, fpat::DATA));
code.push(0xdb); code.extend_from_slice(&disp32(3, fpat::OUT));
code.push(0xf4);
run(&pc, &code);
assert_eq!(read64(&pc, fpat::OUT) as u32, 100);
let pc = x87pc();
write64(&pc, fpat::DATA, 0x4270_0000_0000_0000); let mut code = alloc::vec![0xdb, 0xe3, 0xdd];
code.extend_from_slice(&disp32(0, fpat::DATA));
code.push(0xdb);
code.extend_from_slice(&disp32(3, fpat::OUT));
code.push(0xf4);
run(&pc, &code);
assert_eq!(read64(&pc, fpat::OUT) as u32, 0x8000_0000);
assert_ne!(pc.cpu.x87().status & sw::IE, 0);
}
#[test]
fn fsqrt_frndint_and_fscale_compute_what_they_claim() {
let pc = x87pc();
write64(&pc, fpat::DATA, 0x4010_0000_0000_0000); let mut code = alloc::vec![0xdb, 0xe3, 0xdd];
code.extend_from_slice(&disp32(0, fpat::DATA));
code.extend_from_slice(&[0xd9, 0xfa, 0xdd]); code.extend_from_slice(&disp32(3, fpat::OUT));
code.push(0xf4);
run(&pc, &code);
assert_eq!(read64(&pc, fpat::OUT), 0x4000_0000_0000_0000, "sqrt(4) = 2");
let pc = x87pc();
write64(&pc, fpat::DATA, 0x4004_0000_0000_0000); let mut code = alloc::vec![0xdb, 0xe3, 0xdd];
code.extend_from_slice(&disp32(0, fpat::DATA));
code.extend_from_slice(&[0xd9, 0xfc, 0xdd]);
code.extend_from_slice(&disp32(3, fpat::OUT));
code.push(0xf4);
run(&pc, &code);
assert_eq!(read64(&pc, fpat::OUT), 0x4000_0000_0000_0000, "2.5 -> 2");
assert_ne!(pc.cpu.x87().status & sw::PE, 0, "and it moved");
let pc = x87pc();
write64(&pc, fpat::DATA, 0x4008_0000_0000_0000); write64(&pc, fpat::DATA + 8, 0x3ff8_0000_0000_0000); let mut code = alloc::vec![0xdb, 0xe3, 0xdd];
code.extend_from_slice(&disp32(0, fpat::DATA));
code.push(0xdd);
code.extend_from_slice(&disp32(0, fpat::DATA + 8));
code.extend_from_slice(&[0xd9, 0xfd, 0xdd]); code.extend_from_slice(&disp32(3, fpat::OUT));
code.push(0xf4);
run(&pc, &code);
assert_eq!(read64(&pc, fpat::OUT), 0x4028_0000_0000_0000, "12.0");
}
#[test]
fn fprem_reduces_exactly_and_reports_the_quotient_bits() {
let pc = x87pc();
write64(&pc, fpat::DATA, 0x4010_0000_0000_0000); write64(&pc, fpat::DATA + 8, 0x402a_0000_0000_0000); let mut code = alloc::vec![0xdb, 0xe3, 0xdd];
code.extend_from_slice(&disp32(0, fpat::DATA));
code.push(0xdd);
code.extend_from_slice(&disp32(0, fpat::DATA + 8));
code.extend_from_slice(&[0xd9, 0xf8, 0xdd]); code.extend_from_slice(&disp32(3, fpat::OUT));
code.push(0xf4);
run(&pc, &code);
assert_eq!(
read64(&pc, fpat::OUT),
0x3ff0_0000_0000_0000,
"13 mod 4 = 1"
);
let st = pc.cpu.x87().status;
assert_eq!(st & sw::C2, 0, "the reduction was complete");
assert_ne!(st & sw::C1, 0, "Q0");
assert_ne!(st & sw::C3, 0, "Q1");
assert_eq!(st & sw::C0, 0, "Q2 — the quotient is 3");
}
#[test]
fn a_memory_source_compare_reads_the_operand_and_not_st_one() {
let pc = x87pc();
write64(&pc, fpat::DATA, 0x4059_0000_0000_0000); write64(&pc, fpat::DATA + 8, 0x3ff0_0000_0000_0000); pc.ram.write_u8(fpat::DATA + 16, 0x00).unwrap();
pc.ram.write_u8(fpat::DATA + 17, 0x00).unwrap();
pc.ram.write_u8(fpat::DATA + 18, 0x00).unwrap();
pc.ram.write_u8(fpat::DATA + 19, 0x40).unwrap(); let mut code = alloc::vec![0xdb, 0xe3, 0xdd];
code.extend_from_slice(&disp32(0, fpat::DATA)); code.push(0xdd);
code.extend_from_slice(&disp32(0, fpat::DATA + 8)); code.push(0xd8); code.extend_from_slice(&disp32(2, fpat::DATA + 16));
code.extend_from_slice(&[0xdf, 0xe0, 0xf4]); run(&pc, &code);
let s = (pc.regs().rax & 0xffff) as u16;
assert_ne!(s & sw::C0, 0, "1.0 < 2.0");
assert_eq!(s & sw::C3, 0);
}
#[test]
fn a_store_from_an_empty_register_writes_the_indefinite() {
let pc = x87pc();
write64(&pc, fpat::OUT, 0x1234_5678_9abc_def0);
let mut code = alloc::vec![0xdb, 0xe3, 0xdd]; code.extend_from_slice(&disp32(3, fpat::OUT));
code.push(0xf4);
run(&pc, &code);
assert_eq!(read64(&pc, fpat::OUT), 0xfff8_0000_0000_0000, "-QNaN");
let x = pc.cpu.x87();
assert_ne!(x.status & sw::IE, 0);
assert_ne!(x.status & sw::SF, 0);
}
#[test]
fn the_transcendentals_are_absent_and_say_so() {
let pc = x87pc();
pc.idt(6, gate(0x08, HANDLER as u32, sys_type::INT_GATE32, 0));
pc.write(HANDLER, &[0xf4]);
run(&pc, &[0xdb, 0xe3, 0xd9, 0xe8, 0xd9, 0xf0, 0xf4]);
assert_eq!(pc.regs().rip, HANDLED, "#UD for F2XM1");
}
#[test]
fn the_disassembler_prints_the_escapes_and_the_simd_forms() {
let pc = ssepc();
let listing = |bytes: &[u8]| {
pc.write(at::CODE0, bytes);
let out = pc.cpu.disassemble(0x08, at::CODE0, 1);
alloc::format!("{}", out[0])
};
assert!(listing(&[0xd9, 0xe8]).ends_with("fld1"), "fld1");
assert!(listing(&[0xd8, 0xc1]).ends_with("fadd st(0), st(1)"));
assert!(listing(&[0xde, 0xc1]).ends_with("faddp st(1), st(0)"));
assert!(listing(&[0xdf, 0xe0]).ends_with("fnstsw ax"));
let dq = listing(&[0xdd, 0x05, 0x00, 0xb0, 0x00, 0x00]);
assert!(dq.ends_with("fld qword [ds:0xb000]"), "{dq}");
let tb = listing(&[0xdb, 0x2d, 0x00, 0xb0, 0x00, 0x00]);
assert!(tb.ends_with("fld tbyte [ds:0xb000]"), "{tb}");
assert!(listing(&[0xf2, 0x0f, 0x58, 0xc1]).ends_with("addsd xmm0, xmm1"));
assert!(listing(&[0x66, 0x0f, 0x28, 0xc1]).ends_with("movapd xmm0, xmm1"));
assert!(listing(&[0x0f, 0x50, 0xc1]).ends_with("movmskps eax, xmm1"));
let ld = listing(&[0x0f, 0xae, 0x15, 0x00, 0xb0, 0x00, 0x00]);
assert!(ld.ends_with("ldmxcsr dword [ds:0xb000]"), "{ld}");
assert!(listing(&[0x0f, 0xae, 0xf0]).ends_with("mfence"));
let cx = listing(&[0x0f, 0xc7, 0x0d, 0x00, 0xb0, 0x00, 0x00]);
assert!(cx.ends_with("cmpxchg8b [ds:0xb000]"), "{cx}");
let ss = listing(&[0xf3, 0x0f, 0x10, 0xc1]);
assert!(ss.ends_with("movss xmm0, xmm1"), "{ss}");
assert!(!ss.contains("rep"), "{ss}");
}
#[test]
fn fcom_sets_the_three_condition_codes_the_manual_tabulates() {
let compare = |a: u64, b: u64| {
let pc = x87pc();
write64(&pc, fpat::DATA, b);
write64(&pc, fpat::DATA + 8, a);
let mut code = alloc::vec![0xdb, 0xe3, 0xdd];
code.extend_from_slice(&disp32(0, fpat::DATA));
code.push(0xdd);
code.extend_from_slice(&disp32(0, fpat::DATA + 8));
code.extend_from_slice(&[0xd8, 0xd1, 0xdf, 0xe0, 0xf4]);
run(&pc, &code);
(pc.regs().rax & 0xffff) as u16
};
let one = 0x3ff0_0000_0000_0000;
let two = 0x4000_0000_0000_0000;
let qnan = 0x7ff8_0000_0000_0000;
let s = compare(one, two);
assert_ne!(s & sw::C0, 0, "1 < 2 sets C0");
assert_eq!(s & (sw::C2 | sw::C3), 0);
let s = compare(two, one);
assert_eq!(s & (sw::C0 | sw::C2 | sw::C3), 0, "2 > 1 clears all three");
let s = compare(one, one);
assert_ne!(s & sw::C3, 0, "equal sets C3");
assert_eq!(s & (sw::C0 | sw::C2), 0);
let s = compare(one, qnan);
assert_eq!(
s & (sw::C0 | sw::C2 | sw::C3),
sw::C0 | sw::C2 | sw::C3,
"unordered sets all three"
);
assert_ne!(s & sw::IE, 0, "and FCOM signals on a quiet NaN");
}
#[test]
fn fucom_is_quiet_where_fcom_signals() {
let with = |opcode: [u8; 2]| {
let pc = x87pc();
write64(&pc, fpat::DATA, 0x7ff8_0000_0000_0000); write64(&pc, fpat::DATA + 8, 0x3ff0_0000_0000_0000);
let mut code = alloc::vec![0xdb, 0xe3, 0xdd];
code.extend_from_slice(&disp32(0, fpat::DATA));
code.push(0xdd);
code.extend_from_slice(&disp32(0, fpat::DATA + 8));
code.extend_from_slice(&opcode);
code.push(0xf4);
run(&pc, &code);
pc.cpu.x87().status
};
assert_ne!(with([0xd8, 0xd1]) & sw::IE, 0, "FCOM signals");
assert_eq!(with([0xdd, 0xe1]) & sw::IE, 0, "FUCOM does not");
}
#[test]
fn fcomi_writes_the_integer_flags_instead_of_the_condition_codes() {
let pc = x87pc();
write64(&pc, fpat::DATA, 0x4000_0000_0000_0000); write64(&pc, fpat::DATA + 8, 0x3ff0_0000_0000_0000); let mut code = alloc::vec![0xdb, 0xe3, 0xdd];
code.extend_from_slice(&disp32(0, fpat::DATA));
code.push(0xdd);
code.extend_from_slice(&disp32(0, fpat::DATA + 8));
code.extend_from_slice(&[0xdb, 0xf1, 0xf4]); run(&pc, &code);
let e = pc.regs().eflags;
assert_ne!(e & flags::CF, 0, "1 < 2");
assert_eq!(e & flags::ZF, 0);
assert_eq!(e & flags::PF, 0);
}
#[test]
fn fxam_tells_an_empty_register_from_a_zero() {
let pc = x87pc();
run(&pc, &[0xdb, 0xe3, 0xd9, 0xe5, 0xf4]); let s = pc.cpu.x87().status;
assert_ne!(s & sw::C3, 0);
assert_eq!(s & sw::C2, 0);
assert_ne!(s & sw::C0, 0, "empty");
let pc = x87pc();
run(&pc, &[0xdb, 0xe3, 0xd9, 0xee, 0xd9, 0xe5, 0xf4]);
let s = pc.cpu.x87().status;
assert_ne!(s & sw::C3, 0);
assert_eq!(s & (sw::C2 | sw::C0), 0, "a zero");
assert_eq!(s & sw::C1, 0, "and a positive one");
}
#[test]
fn a_mask_decides_whether_the_result_is_written_at_all() {
let divide = |control: u16| {
let pc = x87pc();
write64(&pc, fpat::DATA, u64::from(control));
let mut code = alloc::vec![0xdb, 0xe3, 0xd9];
code.extend_from_slice(&disp32(5, fpat::DATA));
code.extend_from_slice(&[0xd9, 0xee, 0xd9, 0xe8, 0xd8, 0xf1, 0xf4]);
run(&pc, &code);
pc.cpu.x87()
};
let x = divide(cw::RESET);
assert_ne!(x.status & sw::ZE, 0, "recorded either way");
assert_eq!(x.status & sw::ES, 0, "masked, so nothing is pending");
assert_eq!(x.raw(0), F80::INFINITY, "the standard masked response");
let x = divide(cw::RESET & !cw::ZM);
assert_ne!(x.status & sw::ZE, 0);
assert_ne!(x.status & sw::ES, 0, "unmasked, so it is pending");
assert_ne!(x.status & sw::B, 0);
assert_eq!(
x.raw(0),
F80::new(0x3fff, 0x8000_0000_0000_0000),
"and ST(0) still holds the dividend"
);
}
#[test]
fn every_mask_is_separately_effective() {
let cases: [(u16, u16, &[u8]); 4] = [
(cw::IM, sw::IE, &[0xd9, 0xe8, 0xd9, 0xe0, 0xd9, 0xfa]),
(cw::ZM, sw::ZE, &[0xd9, 0xee, 0xd9, 0xe8, 0xd8, 0xf1]),
(
cw::PM,
sw::PE,
&[0xd9, 0xe8, 0xd9, 0xe8, 0xd8, 0xc0, 0xd8, 0xc1, 0xde, 0xf9],
),
(cw::DM, sw::DE, &[]),
];
for (mask, flag, body) in cases {
for unmask in [false, true] {
let control = if unmask { cw::RESET & !mask } else { cw::RESET };
let pc = x87pc();
write64(&pc, fpat::DATA, u64::from(control));
write64(&pc, fpat::DATA + 8, 1); let mut code = alloc::vec![0xdb, 0xe3, 0xd9];
code.extend_from_slice(&disp32(5, fpat::DATA));
if body.is_empty() {
code.push(0xdd);
code.extend_from_slice(&disp32(0, fpat::DATA + 8));
} else {
code.extend_from_slice(body);
}
code.push(0xf4);
run(&pc, &code);
let st = pc.cpu.x87().status;
assert_ne!(st & flag, 0, "the flag is sticky whatever the mask");
assert_eq!(
st & sw::ES != 0,
unmask,
"mask {mask:#06x}: the summary follows the mask"
);
}
}
}
#[test]
fn an_unmasked_exception_faults_at_the_next_floating_point_instruction() {
let pc = x87pc();
let mut sys = pc.cpu.sys();
sys.cr0 |= cr0::NE;
pc.cpu.set_sys(sys);
pc.idt(16, gate(0x08, 0x3800, sys_type::INT_GATE32, 0));
pc.write(0x3800, &[0xf4]);
write64(&pc, fpat::DATA, u64::from(cw::RESET & !cw::ZM));
let mut code = alloc::vec![0xdb, 0xe3, 0xd9];
code.extend_from_slice(&disp32(5, fpat::DATA));
code.extend_from_slice(&[0xd9, 0xee, 0xd9, 0xe8, 0xd8, 0xf1]);
code.extend_from_slice(&[0xd9, 0xe8, 0xf4]);
run(&pc, &code);
assert_eq!(pc.regs().rip, HANDLED, "#MF was taken by the *next* one");
}
#[test]
fn fwait_is_the_synchronisation_point_a_program_chooses() {
let pc = x87pc();
let mut sys = pc.cpu.sys();
sys.cr0 |= cr0::NE;
pc.cpu.set_sys(sys);
pc.idt(16, gate(0x08, 0x3800, sys_type::INT_GATE32, 0));
pc.write(0x3800, &[0xf4]);
write64(&pc, fpat::DATA, u64::from(cw::RESET & !cw::ZM));
let mut code = alloc::vec![0xdb, 0xe3, 0xd9];
code.extend_from_slice(&disp32(5, fpat::DATA));
code.extend_from_slice(&[0xd9, 0xee, 0xd9, 0xe8, 0xd8, 0xf1, 0x9b, 0xf4]);
run(&pc, &code);
assert_eq!(pc.regs().rip, HANDLED, "fwait took it");
}
#[test]
fn the_no_wait_forms_run_with_an_exception_pending() {
let pc = x87pc();
let mut sys = pc.cpu.sys();
sys.cr0 |= cr0::NE;
pc.cpu.set_sys(sys);
pc.idt(16, gate(0x08, 0x3800, sys_type::INT_GATE32, 0));
pc.write(0x3800, &[0xf4]);
write64(&pc, fpat::DATA, u64::from(cw::RESET & !cw::ZM));
let mut code = alloc::vec![0xdb, 0xe3, 0xd9];
code.extend_from_slice(&disp32(5, fpat::DATA));
code.extend_from_slice(&[0xd9, 0xee, 0xd9, 0xe8, 0xd8, 0xf1]);
code.extend_from_slice(&[0xdf, 0xe0, 0xdb, 0xe2, 0xd9, 0xe8, 0xf4]);
run(&pc, &code);
assert_ne!(pc.regs().rip, HANDLED, "no #MF was taken");
assert_ne!(
(pc.regs().rax as u16) & sw::ES,
0,
"and FNSTSW saw the pending exception before FNCLEX removed it"
);
}
#[test]
fn an_escape_with_cr0_em_or_ts_set_is_a_device_not_available_fault() {
for bit in [cr0::EM, cr0::TS] {
let pc = x87pc();
let mut sys = pc.cpu.sys();
sys.cr0 |= bit;
pc.cpu.set_sys(sys);
pc.idt(7, gate(0x08, 0x3800, sys_type::INT_GATE32, 0));
pc.write(0x3800, &[0xf4]);
run(&pc, &[0xd9, 0xe8, 0xf4]);
assert_eq!(pc.regs().rip, HANDLED, "#NM with CR0 bit {bit:#x}");
}
}
#[test]
fn a_part_with_no_unit_is_a_coprocessor_socket_with_nothing_in_it() {
let pc = Pc::with_features(Variant::I80486, Features::I80486SX);
pc.start_protected();
pc.idt(7, gate(0x08, HANDLER as u32, sys_type::INT_GATE32, 0));
pc.write(HANDLER, &[0xf4]);
run(&pc, &[0xd9, 0xe8, 0xf4]);
assert_eq!(pc.regs().rip, at::CODE0 + 3, "the escape did nothing");
assert_eq!(
pc.cpu.x87(),
crate::cpu::x86::fpu::X87::new(),
"and left no state"
);
let pc = Pc::with_features(Variant::I80486, Features::I80486SX);
pc.start_protected();
let mut sys = pc.cpu.sys();
sys.cr0 |= cr0::EM;
pc.cpu.set_sys(sys);
pc.idt(7, gate(0x08, HANDLER as u32, sys_type::INT_GATE32, 0));
pc.write(HANDLER, &[0xf4]);
run(&pc, &[0xd9, 0xe8, 0xf4]);
assert_eq!(pc.regs().rip, HANDLED, "#NM with CR0.EM set");
}
#[test]
fn fnstenv_writes_the_three_words_and_masks_everything() {
let pc = x87pc();
write64(&pc, fpat::DATA, u64::from(cw::RESET & !cw::ZM));
let mut code = alloc::vec![0xdb, 0xe3, 0xd9];
code.extend_from_slice(&disp32(5, fpat::DATA));
code.extend_from_slice(&[0xd9, 0xe8, 0xd9]); code.extend_from_slice(&disp32(6, fpat::OUT));
code.push(0xf4);
run(&pc, &code);
let word = |n: u64| (read64(&pc, fpat::OUT + n * 4) & 0xffff) as u16;
assert_eq!(word(0), cw::RESET & !cw::ZM, "the control word");
assert_eq!(
word(1) & sw::TOP,
7 << sw::TOP_SHIFT,
"TOP is in the status"
);
assert_eq!(word(2), 0x3fff, "one register occupied, seven empty");
assert_eq!(
pc.cpu.x87().control & cw::MASKS,
cw::MASKS,
"and everything is masked now"
);
}
#[test]
fn fnsave_and_frstor_round_trip_the_whole_unit() {
let pc = x87pc();
write64(&pc, fpat::DATA, 0x400e_0000_0000_0000); let mut code = alloc::vec![0xdb, 0xe3, 0xdd];
code.extend_from_slice(&disp32(0, fpat::DATA));
code.extend_from_slice(&[0xd9, 0xe8, 0xdd]); code.extend_from_slice(&disp32(6, fpat::SAVE));
code.push(0xdd); code.extend_from_slice(&disp32(4, fpat::SAVE));
code.push(0xf4);
run(&pc, &code);
let x = pc.cpu.x87();
assert_eq!(x.top(), 6);
assert_eq!(x.raw(0), F80::new(0x3fff, 0x8000_0000_0000_0000));
assert_eq!(x.raw(1), F80::new(0x4000, 0xf000_0000_0000_0000), "3.75");
assert_eq!(x.tag_at(x.phys(2)), Tag::Empty);
}
#[test]
fn a_scalar_double_add_produces_the_exact_sum() {
let pc = ssepc();
write64(&pc, fpat::DATA, 0x3ff8_0000_0000_0000); write64(&pc, fpat::DATA + 8, 0x4002_0000_0000_0000); let mut code = alloc::vec![0xf2, 0x0f, 0x10]; code.extend_from_slice(&disp32(0, fpat::DATA));
code.extend_from_slice(&[0xf2, 0x0f, 0x10]); code.extend_from_slice(&disp32(1, fpat::DATA + 8));
code.extend_from_slice(&[0xf2, 0x0f, 0x58, 0xc1]); code.extend_from_slice(&[0xf2, 0x0f, 0x11]); code.extend_from_slice(&disp32(0, fpat::OUT));
code.push(0xf4);
run(&pc, &code);
assert_eq!(read64(&pc, fpat::OUT), 0x400e_0000_0000_0000, "3.75");
assert_eq!(pc.cpu.sse().mxcsr & mxcsr::EXCEPTIONS, 0);
}
#[test]
fn a_load_zeroes_the_lanes_above_a_scalar_and_a_register_move_does_not() {
let pc = ssepc();
write64(&pc, fpat::DATA, 0x1111_1111_1111_1111);
let mut sse = Sse::new();
sse.set(0, [0x2222_2222_2222_2222, 0x3333_3333_3333_3333]);
sse.set(1, [0x4444_4444_4444_4444, 0x5555_5555_5555_5555]);
pc.cpu.set_sse(sse);
let mut code = alloc::vec![0xf2, 0x0f, 0x10];
code.extend_from_slice(&disp32(0, fpat::DATA));
code.extend_from_slice(&[0xf2, 0x0f, 0x10, 0xd1, 0xf4]); run(&pc, &code);
let sse = pc.cpu.sse();
assert_eq!(sse.get(0), [0x1111_1111_1111_1111, 0], "the load zeroed");
assert_eq!(sse.get(2)[0], 0x4444_4444_4444_4444);
assert_eq!(sse.get(2)[1], 0, "xmm2 kept its own upper half");
}
#[test]
fn an_aligned_move_refuses_a_misaligned_address() {
let pc = ssepc();
pc.idt(13, gate(0x08, 0x3800, sys_type::INT_GATE32, 0));
pc.write(0x3800, &[0xf4]);
let mut code = alloc::vec![0x0f, 0x28];
code.extend_from_slice(&disp32(0, fpat::ODD));
code.push(0xf4);
run(&pc, &code);
assert_eq!(pc.regs().rip, HANDLED, "#GP on a misaligned MOVAPS");
let pc = ssepc();
let mut code = alloc::vec![0x0f, 0x10];
code.extend_from_slice(&disp32(0, fpat::ODD));
code.push(0xf4);
run(&pc, &code);
assert_eq!(
pc.regs().rip,
at::CODE0 + code.len() as u64,
"the unaligned form completed and the hlt retired"
);
}
#[test]
fn sse_needs_cr4_osfxsr_and_says_so_with_an_invalid_opcode() {
let pc = Pc::new(Variant::X86_64);
pc.start_protected();
pc.idt(6, gate(0x08, 0x3800, sys_type::INT_GATE32, 0));
pc.write(0x3800, &[0xf4]);
run(&pc, &[0x0f, 0x57, 0xc0, 0xf4]); assert_eq!(pc.regs().rip, HANDLED, "#UD with CR4.OSFXSR clear");
}
#[test]
fn cr0_em_makes_sse_invalid_rather_than_trappable() {
let pc = ssepc();
let mut sys = pc.cpu.sys();
sys.cr0 |= cr0::EM;
pc.cpu.set_sys(sys);
pc.idt(6, gate(0x08, 0x3800, sys_type::INT_GATE32, 0));
pc.idt(7, gate(0x08, 0x3900, sys_type::INT_GATE32, 0));
pc.write(0x3800, &[0xf4]);
pc.write(0x3900, &[0xf4]);
run(&pc, &[0x0f, 0x57, 0xc0, 0xf4]);
assert_eq!(pc.regs().rip, HANDLED, "#UD, not #NM");
}
#[test]
fn mxcsr_rounding_reaches_the_arithmetic() {
let divide = |rc: u32| {
let pc = ssepc();
write64(&pc, fpat::DATA, u64::from(mxcsr::RESET | rc));
write64(&pc, fpat::DATA + 8, 0x3ff0_0000_0000_0000); write64(&pc, fpat::DATA + 16, 0x4008_0000_0000_0000); let mut code = alloc::vec![0x0f, 0xae];
code.extend_from_slice(&disp32(2, fpat::DATA)); code.extend_from_slice(&[0xf2, 0x0f, 0x10]);
code.extend_from_slice(&disp32(0, fpat::DATA + 8));
code.extend_from_slice(&[0xf2, 0x0f, 0x10]);
code.extend_from_slice(&disp32(1, fpat::DATA + 16));
code.extend_from_slice(&[0xf2, 0x0f, 0x5e, 0xc1]); code.extend_from_slice(&[0xf2, 0x0f, 0x11]);
code.extend_from_slice(&disp32(0, fpat::OUT));
code.push(0xf4);
run(&pc, &code);
read64(&pc, fpat::OUT)
};
assert_eq!(divide(0), 0x3fd5_5555_5555_5555, "1/3, to nearest");
assert_eq!(
divide(3 << mxcsr::RC_SHIFT),
0x3fd5_5555_5555_5555,
"toward zero rounds the same way for this value"
);
assert_eq!(divide(2 << mxcsr::RC_SHIFT), 0x3fd5_5555_5555_5556);
}
#[test]
fn ldmxcsr_refuses_a_reserved_bit() {
let pc = ssepc();
pc.idt(13, gate(0x08, 0x3800, sys_type::INT_GATE32, 0));
pc.write(0x3800, &[0xf4]);
write64(&pc, fpat::DATA, 0x0001_0000);
let mut code = alloc::vec![0x0f, 0xae];
code.extend_from_slice(&disp32(2, fpat::DATA));
code.push(0xf4);
run(&pc, &code);
assert_eq!(pc.regs().rip, HANDLED);
}
#[test]
fn the_conversions_move_between_the_integer_and_the_simd_files() {
let pc = ssepc();
let mut code = alloc::vec![0xb8]; code.extend_from_slice(&(-7i32).to_le_bytes());
code.extend_from_slice(&[0xf2, 0x0f, 0x2a, 0xc0]); code.extend_from_slice(&[0xf2, 0x0f, 0x11]);
code.extend_from_slice(&disp32(0, fpat::OUT));
code.extend_from_slice(&[0xf2, 0x0f, 0x2c, 0xd8]); code.push(0xf4);
run(&pc, &code);
assert_eq!(read64(&pc, fpat::OUT), 0xc01c_0000_0000_0000, "-7.0");
assert_eq!(pc.regs().rbx as i32, -7, "and back again");
}
#[test]
fn ucomisd_writes_the_flags_and_comisd_signals_on_a_quiet_nan() {
let compare = |op: u8, b: u64| {
let pc = ssepc();
write64(&pc, fpat::DATA, 0x3ff0_0000_0000_0000); write64(&pc, fpat::DATA + 8, b);
let mut code = alloc::vec![0xf2, 0x0f, 0x10];
code.extend_from_slice(&disp32(0, fpat::DATA));
code.extend_from_slice(&[0xf2, 0x0f, 0x10]);
code.extend_from_slice(&disp32(1, fpat::DATA + 8));
code.extend_from_slice(&[0x66, 0x0f, op, 0xc1, 0xf4]);
run(&pc, &code);
(pc.regs().eflags, pc.cpu.sse().mxcsr)
};
let (e, _) = compare(0x2e, 0x4000_0000_0000_0000);
assert_ne!(e & flags::CF, 0, "1 < 2");
let (e, _) = compare(0x2e, 0x3ff0_0000_0000_0000);
assert_ne!(e & flags::ZF, 0, "equal");
assert_eq!(e & (flags::CF | flags::PF), 0);
let (e, m) = compare(0x2e, 0x7ff8_0000_0000_0000);
assert_eq!(
e & (flags::ZF | flags::PF | flags::CF),
flags::ZF | flags::PF | flags::CF,
"unordered"
);
assert_eq!(m & mxcsr::IE, 0, "UCOMISD is quiet about a quiet NaN");
let (_, m) = compare(0x2f, 0x7ff8_0000_0000_0000);
assert_ne!(m & mxcsr::IE, 0, "COMISD is not");
}
#[test]
fn movmskps_gathers_the_four_sign_bits() {
let pc = ssepc();
let mut sse = Sse::new();
sse.set(1, [0x0000_0000_8000_0000, 0x8000_0000_0000_0000]);
pc.cpu.set_sse(sse);
run(&pc, &[0x0f, 0x50, 0xc1, 0xf4]); assert_eq!(pc.regs().rax, 0b1001);
}
#[test]
fn the_bitwise_operations_cover_the_whole_register() {
let pc = ssepc();
let mut sse = Sse::new();
sse.set(0, [u64::MAX, 0x0f0f_0f0f_0f0f_0f0f]);
sse.set(1, [0x00ff_00ff_00ff_00ff, u64::MAX]);
pc.cpu.set_sse(sse);
run(&pc, &[0x0f, 0x54, 0xc1, 0x0f, 0x57, 0xd2, 0xf4]); let sse = pc.cpu.sse();
assert_eq!(sse.get(0), [0x00ff_00ff_00ff_00ff, 0x0f0f_0f0f_0f0f_0f0f]);
assert_eq!(
sse.get(2),
[0, 0],
"xorps with itself is the idiomatic zero"
);
}
#[test]
fn a_packed_add_touches_all_four_lanes() {
let pc = ssepc();
let mut sse = Sse::new();
sse.set(0, [0x3f80_0000_3f80_0000, 0x3f80_0000_3f80_0000]);
sse.set(1, [0x4000_0000_4000_0000, 0x4000_0000_4000_0000]);
pc.cpu.set_sse(sse);
run(&pc, &[0x0f, 0x58, 0xc1, 0xf4]); assert_eq!(
pc.cpu.sse().get(0),
[0x4040_0000_4040_0000, 0x4040_0000_4040_0000],
"3.0f in all four"
);
}
#[test]
fn shufps_selects_two_lanes_from_each_source() {
let pc = ssepc();
let mut sse = Sse::new();
sse.set(0, [0x0000_0001_0000_0000, 0x0000_0003_0000_0002]);
sse.set(1, [0x0000_0011_0000_0010, 0x0000_0013_0000_0012]);
pc.cpu.set_sse(sse);
run(&pc, &[0x0f, 0xc6, 0xc1, 0b1101_1000, 0xf4]);
assert_eq!(
pc.cpu.sse().get(0),
[0x0000_0002_0000_0000, 0x0000_0013_0000_0011]
);
}
#[test]
fn fxsave_and_fxrstor_carry_both_register_files() {
let pc = ssepc();
let mut sse = Sse::new();
sse.set(3, [0xdead_beef_cafe_babe, 0x0123_4567_89ab_cdef]);
sse.mxcsr = mxcsr::RESET | mxcsr::FTZ;
pc.cpu.set_sse(sse);
let mut code = alloc::vec![0xdb, 0xe3, 0xd9, 0xe8, 0x0f, 0xae];
code.extend_from_slice(&disp32(0, fpat::SAVE)); code.extend_from_slice(&[0xdb, 0xe3, 0x0f, 0x57, 0xdb, 0x0f, 0xae]);
code.extend_from_slice(&disp32(1, fpat::SAVE)); code.push(0xf4);
run(&pc, &code);
let x = pc.cpu.x87();
assert_eq!(x.top(), 7, "TOP came back");
assert_eq!(x.raw(0), F80::new(0x3fff, 0x8000_0000_0000_0000));
assert_eq!(x.tag_at(x.phys(1)), Tag::Empty, "and so did the tag word");
let sse = pc.cpu.sse();
assert_eq!(sse.get(3), [0xdead_beef_cafe_babe, 0x0123_4567_89ab_cdef]);
assert_eq!(sse.mxcsr, mxcsr::RESET | mxcsr::FTZ);
}
#[test]
fn an_unmasked_simd_exception_leaves_its_cause_in_mxcsr() {
let pc = ssepc();
pc.idt(19, gate(0x08, HANDLER as u32, sys_type::INT_GATE32, 0));
pc.write(HANDLER, &[0xf4]);
write64(&pc, fpat::DATA, u64::from(mxcsr::RESET & !mxcsr::ZM));
write64(&pc, fpat::DATA + 8, 0x3ff0_0000_0000_0000); write64(&pc, fpat::DATA + 16, 0); let mut code = alloc::vec![0x0f, 0xae];
code.extend_from_slice(&disp32(2, fpat::DATA)); code.extend_from_slice(&[0xf2, 0x0f, 0x10]);
code.extend_from_slice(&disp32(0, fpat::DATA + 8));
code.extend_from_slice(&[0xf2, 0x0f, 0x10]);
code.extend_from_slice(&disp32(1, fpat::DATA + 16));
code.extend_from_slice(&[0xf2, 0x0f, 0x5e, 0xc1]); code.push(0xf4);
run(&pc, &code);
assert_eq!(pc.regs().rip, HANDLED, "#XM was taken");
let m = pc.cpu.sse().mxcsr;
assert_ne!(m & mxcsr::ZE, 0, "and said which exception it was");
let cause = (!m >> mxcsr::MASK_SHIFT) & m & mxcsr::EXCEPTIONS;
assert_eq!(cause, mxcsr::ZE, "a handler can classify the trap");
assert_eq!(pc.cpu.sse().get(0), [0x3ff0_0000_0000_0000, 0]);
}
#[test]
fn without_osxmmexcpt_an_unmasked_exception_is_an_invalid_opcode() {
let pc = ssepc();
let mut sys = pc.cpu.sys();
sys.cr4 &= !cr4::OSXMMEXCPT;
pc.cpu.set_sys(sys);
pc.idt(6, gate(0x08, HANDLER as u32, sys_type::INT_GATE32, 0));
pc.write(HANDLER, &[0xf4]);
write64(&pc, fpat::DATA, u64::from(mxcsr::RESET & !mxcsr::ZM));
write64(&pc, fpat::DATA + 8, 0x3ff0_0000_0000_0000);
write64(&pc, fpat::DATA + 16, 0);
let mut code = alloc::vec![0x0f, 0xae];
code.extend_from_slice(&disp32(2, fpat::DATA));
code.extend_from_slice(&[0xf2, 0x0f, 0x10]);
code.extend_from_slice(&disp32(0, fpat::DATA + 8));
code.extend_from_slice(&[0xf2, 0x0f, 0x10]);
code.extend_from_slice(&disp32(1, fpat::DATA + 16));
code.extend_from_slice(&[0xf2, 0x0f, 0x5e, 0xc1]);
code.push(0xf4);
run(&pc, &code);
assert_eq!(pc.regs().rip, HANDLED, "#UD, not #XM");
}
#[test]
fn denormals_are_zeros_reaches_the_comparisons_too() {
let compare = |daz: bool| {
let pc = ssepc();
let value = if daz {
mxcsr::RESET | mxcsr::DAZ
} else {
mxcsr::RESET
};
write64(&pc, fpat::DATA, u64::from(value));
write64(&pc, fpat::DATA + 8, 1); write64(&pc, fpat::DATA + 16, 0); let mut code = alloc::vec![0x0f, 0xae];
code.extend_from_slice(&disp32(2, fpat::DATA));
code.extend_from_slice(&[0xf2, 0x0f, 0x10]);
code.extend_from_slice(&disp32(0, fpat::DATA + 8));
code.extend_from_slice(&[0xf2, 0x0f, 0x10]);
code.extend_from_slice(&disp32(1, fpat::DATA + 16));
code.extend_from_slice(&[0x66, 0x0f, 0x2e, 0xc1, 0xf4]); run(&pc, &code);
(pc.regs().eflags, pc.cpu.sse().mxcsr)
};
let (e, m) = compare(false);
assert_eq!(e & flags::ZF, 0, "a subnormal is not zero");
assert_ne!(m & mxcsr::DE, 0, "and it is reported");
let (e, m) = compare(true);
assert_ne!(e & flags::ZF, 0, "with DAZ it is a zero");
assert_eq!(m & mxcsr::DE, 0, "and DAZ suppresses the report");
}
#[test]
fn the_store_halves_of_movlps_and_movhps_have_no_register_form() {
let pc = ssepc();
pc.idt(6, gate(0x08, HANDLER as u32, sys_type::INT_GATE32, 0));
pc.write(HANDLER, &[0xf4]);
run(&pc, &[0x0f, 0x13, 0xc1, 0xf4]);
assert_eq!(pc.regs().rip, HANDLED, "#UD");
let pc = ssepc();
let mut sse = Sse::new();
sse.set(1, [0x1111_1111_1111_1111, 0x2222_2222_2222_2222]);
pc.cpu.set_sse(sse);
run(&pc, &[0x0f, 0x12, 0xc1, 0xf4]);
assert_eq!(pc.cpu.sse().get(0)[0], 0x2222_2222_2222_2222);
}
#[test]
fn a_control_instruction_does_not_move_the_data_pointer() {
let pc = x87pc();
write64(&pc, fpat::DATA, 0x3ff0_0000_0000_0000);
let mut code = alloc::vec![0xdb, 0xe3, 0xdd];
code.extend_from_slice(&disp32(0, fpat::DATA));
code.push(0xd9);
code.extend_from_slice(&disp32(6, fpat::OUT));
code.push(0xf4);
run(&pc, &code);
let dword = |n: u64| read64(&pc, fpat::OUT + n * 4) & 0xffff_ffff;
assert_eq!(dword(5), fpat::DATA, "FDP names the FLD's operand");
assert_eq!(pc.cpu.x87().last_dp, fpat::DATA);
let pc = x87pc();
write64(&pc, fpat::DATA, 0x3ff0_0000_0000_0000);
write64(&pc, fpat::DATA + 24, u64::from(cw::RESET));
let mut code = alloc::vec![0xdb, 0xe3, 0xdd];
code.extend_from_slice(&disp32(0, fpat::DATA));
code.push(0xd9);
code.extend_from_slice(&disp32(5, fpat::DATA + 24));
code.push(0xf4);
run(&pc, &code);
assert_eq!(pc.cpu.x87().last_dp, fpat::DATA, "FLDCW did not move it");
}
#[test]
fn a_denormal_in_a_register_tags_special_where_a_guest_can_see_it() {
let pc = x87pc();
write64(&pc, fpat::DATA, 1); write64(&pc, fpat::DATA + 8, 0); let mut code = alloc::vec![0xdb, 0xe3, 0xdb];
code.extend_from_slice(&disp32(5, fpat::DATA)); code.push(0xd9);
code.extend_from_slice(&disp32(6, fpat::OUT));
code.push(0xf4);
run(&pc, &code);
let tag = (read64(&pc, fpat::OUT + 8) & 0xffff) as u16;
assert_eq!(tag >> 14, 0b10, "Special, not Valid");
assert_eq!(pc.cpu.x87().tag_at(7), Tag::Special);
}
#[test]
fn cmpxchg8b_exchanges_on_a_match_and_loads_on_a_miss() {
let build = || {
let mut code = alloc::vec![0xb8];
code.extend_from_slice(&0x2222_2222u32.to_le_bytes()); code.push(0xba);
code.extend_from_slice(&0x1111_1111u32.to_le_bytes()); code.push(0xbb);
code.extend_from_slice(&0xdead_beefu32.to_le_bytes()); code.push(0xb9);
code.extend_from_slice(&0xcafe_babeu32.to_le_bytes()); code.extend_from_slice(&[0x0f, 0xc7]);
code.extend_from_slice(&disp32(1, fpat::DATA));
code.push(0xf4);
code
};
let pc = ssepc();
write64(&pc, fpat::DATA, 0x1111_1111_2222_2222);
run(&pc, &build());
assert_ne!(pc.regs().eflags & flags::ZF, 0, "the compare matched");
assert_eq!(read64(&pc, fpat::DATA), 0xcafe_babe_dead_beef);
let pc = ssepc();
write64(&pc, fpat::DATA, 0x3333_3333_4444_4444);
run(&pc, &build());
assert_eq!(pc.regs().eflags & flags::ZF, 0, "the compare failed");
assert_eq!(pc.regs().rax as u32, 0x4444_4444, "and memory was loaded");
assert_eq!(pc.regs().rdx as u32, 0x3333_3333);
assert_eq!(read64(&pc, fpat::DATA), 0x3333_3333_4444_4444, "untouched");
}
#[test]
fn a_part_without_cx8_says_so_and_refuses_the_instruction() {
let pc = x87pc(); pc.idt(6, gate(0x08, 0x3800, sys_type::INT_GATE32, 0));
pc.write(0x3800, &[0xf4]);
let mut code = alloc::vec![0x0f, 0xc7];
code.extend_from_slice(&disp32(1, fpat::DATA));
code.push(0xf4);
run(&pc, &code);
assert_eq!(pc.regs().rip, HANDLED);
}
#[test]
fn cpuid_now_reports_what_a_64_bit_operating_system_looks_for() {
let pc = Pc::new(Variant::X86_64);
pc.start_protected();
run(&pc, &[0xb8, 0x01, 0x00, 0x00, 0x00, 0x0f, 0xa2, 0xf4]);
let edx = pc.regs().rdx as u32;
for (bit, name) in [
(0, "FPU"),
(3, "PSE"),
(4, "TSC"),
(5, "MSR"),
(6, "PAE"),
(8, "CX8"),
(13, "PGE"),
(15, "CMOV"),
(24, "FXSR"),
(25, "SSE"),
(26, "SSE2"),
] {
assert_ne!(edx & (1u32 << bit), 0, "leaf 1 should report {name}");
}
assert_eq!(edx & (1 << 23), 0, "MMX is not implemented");
}
#[test]
fn narrowing_the_feature_set_narrows_what_cpuid_claims() {
let features = Features {
sse2: false,
long: false,
nx: false,
syscall: false,
..Features::X86_64
};
assert!(features.validate().is_ok());
let pc = Pc::with_features(Variant::X86_64, features);
pc.start_protected();
run(&pc, &[0xb8, 0x01, 0x00, 0x00, 0x00, 0x0f, 0xa2, 0xf4]);
let edx = pc.regs().rdx as u32;
assert_ne!(edx & (1 << 25), 0, "SSE");
assert_eq!(edx & (1 << 26), 0, "but not SSE2");
}
#[test]
fn a_long_mode_part_must_have_sse2() {
let impossible = Features {
sse2: false,
..Features::X86_64
};
assert!(impossible.validate().is_err());
}
#[test]
fn no_host_float_reaches_a_guest_result() {
let sources = [
("fpu.rs", include_str!("fpu.rs")),
("fpexec.rs", include_str!("fpexec.rs")),
];
let boundary = |src: &str, at: usize| {
let before = src[..at].chars().next_back();
let after = src[at..].chars().nth(3);
!before.is_some_and(|c| c.is_alphanumeric() || c == '_')
&& !after.is_some_and(|c| c.is_alphanumeric() || c == '_')
};
for (name, src) in sources {
for (n, line) in src.lines().enumerate() {
let code = match line.find("//") {
Some(i) => &line[..i],
None => line,
};
for needle in ["f32", "f64", "f16", "sqrtf", "libm"] {
let mut from = 0;
while let Some(i) = code[from..].find(needle) {
let at = from + i;
assert!(
!boundary(code, at),
"{name}:{}: host floating point: {code}",
n + 1
);
from = at + needle.len();
}
}
}
}
}
#[test]
fn the_floating_point_state_survives_a_snapshot() {
let pc = ssepc();
run(
&pc,
&[0xdb, 0xe3, 0xd9, 0xe8, 0xd9, 0xeb, 0x0f, 0x57, 0xc0, 0xf4],
);
let mut sse = pc.cpu.sse();
sse.set(5, [0x1234_5678_9abc_def0, 0x0fed_cba9_8765_4321]);
sse.mxcsr = mxcsr::RESET | mxcsr::DAZ;
pc.cpu.set_sse(sse);
let x87_before = pc.cpu.x87();
let sse_before = pc.cpu.sse();
let mut shape = MachineShape::new();
shape.add_device("/cpu0", "cpu.x86").unwrap();
let mut writer = StateWriter::new(shape);
{
let mut chunk = writer.chunk("/cpu0", "cpu.x86", 5).unwrap();
pc.cpu.save(&mut chunk).unwrap();
}
let bytes = writer.to_vec().unwrap();
pc.cpu.reset(ResetKind::Cold);
assert_ne!(pc.cpu.x87(), x87_before, "there is something to lose");
let reader = crate::core::state::StateReader::new(&bytes).unwrap();
let (_, _, data) = reader.load_raw("/cpu0").unwrap();
let mut chunk = ChunkReader::new(data);
pc.cpu.load(&mut chunk).unwrap();
chunk.end().unwrap();
assert_eq!(pc.cpu.x87(), x87_before);
assert_eq!(pc.cpu.sse(), sse_before);
let mut shape = MachineShape::new();
shape.add_device("/cpu0", "cpu.x86").unwrap();
let mut again = StateWriter::new(shape);
{
let mut chunk = again.chunk("/cpu0", "cpu.x86", 5).unwrap();
pc.cpu.save(&mut chunk).unwrap();
}
assert_eq!(bytes, again.to_vec().unwrap());
}
}
mod multiprocessor {
use super::*;
use crate::core::state::StateReader;
use crate::core::sync::{AtomicU64, Ordering};
use crate::core::wire::{LocalController, Startup};
use crate::cpu::x86::prot::{apic_base, cr4};
const PAGE: u8 = 0x08;
const HANDLER: u64 = 0x3100;
fn set_rip(pc: &Pc, rip: u64) {
let mut regs = pc.cpu.regs();
regs.rip = rip;
pc.cpu.set_regs(regs);
}
fn set_if(pc: &Pc) {
let mut regs = pc.cpu.regs();
regs.eflags |= flags::IF;
pc.cpu.set_regs(regs);
}
fn pc_msr() -> Pc {
let mut features = Variant::I80486.features();
features.cr4 = true;
features.msr = true;
Pc::with_features(Variant::I80486, features)
}
#[test]
fn an_init_is_a_reset_that_stops_at_wait_for_sipi() {
let pc = pc386();
pc.start_protected();
pc.cpu.step();
assert!(pc.cpu.sys().protected(), "there is something to lose");
let cycles = pc.cpu.cycles();
pc.cpu.request_init();
assert!(pc.cpu.init_requested());
let charged = pc.cpu.step();
assert!(charged > 0, "the sequence itself is charged for");
assert!(
!pc.cpu.sys().protected(),
"an INIT puts CR0 back to its reset value (Table 9-1)"
);
assert_eq!(pc.cpu.regs().cs, 0xf000);
assert_eq!(pc.cpu.regs().rip, 0xfff0);
assert!(
pc.cpu.cycles() > cycles,
"the time-stamp counter is not reset by an INIT, only added to"
);
assert!(
pc.cpu.is_waiting_for_startup(),
"and it stops there rather than fetching from the reset vector"
);
assert_eq!(pc.cpu.step(), 0, "which is a full stop");
}
#[test]
fn a_reset_and_an_init_differ_in_where_they_leave_the_processor() {
let reset = pc386();
reset.start_protected();
reset.cpu.request_reset();
reset.cpu.step();
assert!(!reset.cpu.is_waiting_for_startup());
assert!(reset.cpu.step() > 0, "it is fetching");
let init = pc386();
init.start_protected();
init.cpu.request_init();
init.cpu.step();
assert!(init.cpu.is_waiting_for_startup());
assert_eq!(init.cpu.step(), 0, "it is not");
}
#[test]
fn a_start_up_begins_execution_at_the_page_it_names() {
let pc = pc386();
pc.start_protected();
pc.write(u64::from(PAGE) << 12, &[0x40, 0xeb, 0xfe]);
pc.cpu.request_init();
pc.cpu.step();
pc.cpu.start_up(PAGE);
assert!(pc.cpu.step() > 0, "the Start-Up sequence runs");
assert_eq!(pc.cpu.regs().cs, u16::from(PAGE) << 8);
assert_eq!(pc.cpu.regs().rip, 0);
assert_eq!(
pc.cpu.sys().segs[usize::from(isa::seg::CS)].base,
u64::from(PAGE) << 12,
"the cached base is page << 12, so the fetch is from 000PP000H"
);
assert!(!pc.cpu.is_waiting_for_startup());
pc.cpu.step();
assert_eq!(pc.regs().rax & 0xffff_ffff, 1, "and it executed the page");
}
#[test]
fn a_start_up_to_a_processor_that_is_not_waiting_is_ignored() {
let pc = pc386();
pc.start_protected();
pc.write(at::CODE0, &[0x40, 0xeb, 0xfe]); pc.cpu.start_up(PAGE);
pc.cpu.step();
assert_eq!(pc.regs().rax & 0xffff_ffff, 1);
assert_ne!(pc.cpu.regs().cs, u16::from(PAGE) << 8);
}
#[test]
fn an_interrupt_does_not_leave_the_wait_for_sipi_state() {
let pc = pc386();
pc.start_protected();
pc.cpu.request_init();
pc.cpu.step();
pc.cpu.set_intr_vector(0x42);
pc.cpu.set_intr(true);
set_if(&pc);
assert_eq!(pc.cpu.step(), 0, "the request stays pending on the pin");
assert!(pc.cpu.is_waiting_for_startup());
pc.cpu.pulse_nmi();
assert_eq!(pc.cpu.step(), 0, "and so does an NMI");
assert!(pc.cpu.nmi_pending(), "which is still latched");
}
#[test]
fn the_init_pin_holds_the_processor_in_reset_while_it_is_asserted() {
use crate::core::wire::{Level, Wire, WireId};
let pc = pc386();
pc.start_protected();
pc.write(u64::from(PAGE) << 12, &[0x40, 0xeb, 0xfe]);
let src = WireId(1);
let pin = pc.cpu.sink("init", &[src]).expect("a 386 has an INIT pin");
assert!(
!pc.cpu.init_held(),
"a fresh net sits low, and low is de-asserted: nothing invented"
);
let wire = Wire::builder()
.source(src)
.sink_weak(Arc::downgrade(&pin.sink), pin.line)
.build();
wire.set(src, Level::High);
assert!(pc.cpu.init_held());
assert!(pc.cpu.step() > 0, "the rising edge runs the sequence");
assert!(pc.cpu.is_waiting_for_startup());
pc.cpu.start_up(PAGE);
assert_eq!(pc.cpu.step(), 0);
assert!(pc.cpu.is_waiting_for_startup());
wire.set(src, Level::Low);
assert!(!pc.cpu.init_held());
assert!(pc.cpu.step() > 0, "and now the Start-Up is taken");
assert_eq!(pc.cpu.regs().cs, u16::from(PAGE) << 8);
}
#[test]
fn a_reset_outranks_an_init_that_has_not_run_yet() {
let pc = pc386();
pc.start_protected();
pc.cpu.request_init();
Device::reset(&*pc.cpu, ResetKind::Warm);
assert!(!pc.cpu.init_requested(), "the latch went with the reset");
pc.cpu.step();
assert!(!pc.cpu.is_waiting_for_startup());
}
#[test]
fn a_sixteen_bit_part_has_no_init_pin() {
let m = machine();
assert!(
m.cpu
.sink("init", &[crate::core::wire::WireId(1)])
.is_none()
);
let pc = pc386();
assert!(
pc.cpu
.sink("init", &[crate::core::wire::WireId(1)])
.is_some()
);
}
#[test]
fn the_multiprocessor_state_round_trips_through_a_snapshot() {
let pc = pc386();
pc.start_protected();
pc.cpu.request_init();
pc.cpu.step();
pc.cpu.start_up(PAGE);
assert!(pc.cpu.is_waiting_for_startup());
let mut shape = MachineShape::new();
shape.add_device("/cpu0", "cpu.x86").unwrap();
let mut writer = StateWriter::new(shape);
{
let mut chunk = writer.chunk("/cpu0", "cpu.x86", 6).unwrap();
pc.cpu.save(&mut chunk).unwrap();
}
let bytes = writer.to_vec().unwrap();
Device::reset(&*pc.cpu, ResetKind::Cold);
assert!(!pc.cpu.is_waiting_for_startup());
let reader = StateReader::new(&bytes).unwrap();
let (_, _, data) = reader.load_raw("/cpu0").unwrap();
let mut chunk = ChunkReader::new(data);
pc.cpu.load(&mut chunk).unwrap();
chunk.end().unwrap();
assert!(pc.cpu.is_waiting_for_startup(), "still waiting");
let mut shape = MachineShape::new();
shape.add_device("/cpu0", "cpu.x86").unwrap();
let mut again = StateWriter::new(shape);
{
let mut chunk = again.chunk("/cpu0", "cpu.x86", 6).unwrap();
pc.cpu.save(&mut chunk).unwrap();
}
assert_eq!(bytes, again.to_vec().unwrap());
pc.cpu.step();
assert_eq!(pc.cpu.regs().cs, u16::from(PAGE) << 8);
}
#[test]
fn an_unimplemented_model_specific_register_faults() {
let pc = pc_msr();
pc.start_protected();
pc.idt(13, gate(0x08, HANDLER as u32, sys_type::INT_GATE32, 0));
pc.write(HANDLER, &[0xf4]); pc.write(at::CODE0, &[0xb9, 0xef, 0xbe, 0xad, 0xde, 0x0f, 0x32]);
pc.cpu.step();
pc.cpu.step();
assert_eq!(pc.cpu.regs().rip, HANDLER, "#GP(0), not a zero");
}
#[test]
fn a_write_to_an_unimplemented_model_specific_register_faults() {
let pc = pc_msr();
pc.start_protected();
pc.idt(13, gate(0x08, HANDLER as u32, sys_type::INT_GATE32, 0));
pc.write(HANDLER, &[0xf4]);
pc.write(
at::CODE0,
&[
0xb9, 0xef, 0xbe, 0xad, 0xde, 0x31, 0xc0, 0x31, 0xd2, 0x0f, 0x30,
],
);
for _ in 0..4 {
pc.cpu.step();
}
assert_eq!(pc.cpu.regs().rip, HANDLER);
}
#[test]
fn the_time_stamp_counter_is_readable_two_ways_and_writable_one() {
let pc = pc_msr();
pc.start_protected();
pc.write(at::CODE0, &[0x0f, 0x31, 0xb9, 0x10, 0, 0, 0, 0x0f, 0x32]);
pc.cpu.step();
let by_rdtsc = pc.regs().rax & 0xffff_ffff;
assert!(by_rdtsc > 0, "the counter is this core's own cycle count");
pc.cpu.step();
pc.cpu.step();
assert!(
pc.regs().rax & 0xffff_ffff > by_rdtsc,
"and `RDMSR` of 0x10 reads the same counter, which has moved on"
);
pc.write(
at::CODE0,
&[
0xb9, 0x10, 0, 0, 0, 0xb8, 0x00, 0x10, 0, 0, 0x31, 0xd2, 0x0f, 0x30, 0x0f, 0x31,
],
);
set_rip(&pc, at::CODE0);
for _ in 0..5 {
pc.cpu.step();
}
let after = pc.regs().rax & 0xffff_ffff;
assert!(
(0x1000..0x1100).contains(&after),
"the counter restarted from what was written, not from where it was"
);
}
#[test]
fn rdtsc_is_privileged_only_while_cr4_tsd_is_set() {
let pc = pc_msr();
pc.start_protected();
let mut sys = pc.cpu.sys();
sys.cr4 |= cr4::TSD;
pc.cpu.set_sys(sys);
pc.write(at::CODE0, &[0x0f, 0x31]);
pc.cpu.step();
assert!(
pc.regs().rax & 0xffff_ffff > 0,
"ring 0 reads it however `TSD` is set"
);
}
#[test]
fn a_part_with_no_model_specific_registers_raises_ud() {
let pc = pc386();
pc.start_protected();
pc.idt(6, gate(0x08, HANDLER as u32, sys_type::INT_GATE32, 0));
pc.write(HANDLER, &[0xf4]);
pc.write(at::CODE0, &[0x0f, 0x31]); pc.cpu.step();
assert_eq!(pc.cpu.regs().rip, HANDLER, "#UD");
}
#[derive(Debug)]
struct Controller {
base: AtomicU64,
}
impl LocalController for Controller {
fn take_startup(&self) -> Startup {
Startup::NONE
}
fn base_register(&self) -> u64 {
self.base.load(Ordering::Acquire)
}
fn set_base_register(&self, value: u64) {
self.base.store(value, Ordering::Release);
}
}
#[test]
fn ia32_apic_base_faults_on_a_processor_with_no_local_controller() {
let pc = pc_msr();
pc.start_protected();
pc.idt(13, gate(0x08, HANDLER as u32, sys_type::INT_GATE32, 0));
pc.write(HANDLER, &[0xf4]);
pc.write(at::CODE0, &[0xb9, 0x1b, 0, 0, 0, 0x0f, 0x32]);
pc.cpu.step();
pc.cpu.step();
assert_eq!(pc.cpu.regs().rip, HANDLER);
}
#[test]
fn ia32_apic_base_reads_and_writes_the_controller_that_owns_it() {
let pc = pc_msr();
let intc = Arc::new(Controller {
base: AtomicU64::new(0xfee0_0000 | apic_base::ENABLE | apic_base::BSP),
});
let peer: Arc<dyn LocalController> = intc.clone();
pc.cpu
.attach_local_controller("intr", Arc::downgrade(&peer));
pc.start_protected();
pc.write(at::CODE0, &[0xb9, 0x1b, 0, 0, 0, 0x0f, 0x32]);
pc.cpu.step();
pc.cpu.step();
assert_eq!(pc.regs().rax & 0xffff_ffff, 0xfee0_0900);
assert_eq!(pc.regs().rdx & 0xffff_ffff, 0);
pc.write(
at::CODE0,
&[
0xb9, 0x1b, 0, 0, 0, 0xb8, 0x00, 0x00, 0xe0, 0xfe, 0x31, 0xd2, 0x0f, 0x30,
],
);
set_rip(&pc, at::CODE0);
for _ in 0..4 {
pc.cpu.step();
}
assert_eq!(
intc.base.load(Ordering::Acquire),
0xfee0_0000 | apic_base::BSP,
"the enable bit is gone and the bootstrap flag, which is read-only, \
is not"
);
}
#[test]
fn a_reserved_bit_in_ia32_apic_base_faults() {
let pc = pc_msr();
let intc = Arc::new(Controller {
base: AtomicU64::new(0xfee0_0000 | apic_base::ENABLE),
});
let peer: Arc<dyn LocalController> = intc.clone();
pc.cpu
.attach_local_controller("intr", Arc::downgrade(&peer));
pc.start_protected();
pc.idt(13, gate(0x08, HANDLER as u32, sys_type::INT_GATE32, 0));
pc.write(HANDLER, &[0xf4]);
pc.write(
at::CODE0,
&[
0xb9, 0x1b, 0, 0, 0, 0xb8, 0x01, 0x04, 0xe0, 0xfe, 0x31, 0xd2, 0x0f, 0x30,
],
);
for _ in 0..4 {
pc.cpu.step();
}
assert_eq!(pc.cpu.regs().rip, HANDLER);
assert_eq!(
intc.base.load(Ordering::Acquire) & 1,
0,
"and nothing was written"
);
}
#[test]
fn cpuid_reports_an_apic_only_when_one_is_wired() {
let pc = pc_msr();
pc.start_protected();
pc.write(at::CODE0, &[0xb8, 1, 0, 0, 0, 0x0f, 0xa2]);
pc.cpu.step();
pc.cpu.step();
assert_eq!(pc.regs().rdx & (1 << 9), 0, "no controller, no APIC bit");
let pc = pc_msr();
let intc = Arc::new(Controller {
base: AtomicU64::new(0xfee0_0000 | apic_base::ENABLE),
});
let peer: Arc<dyn LocalController> = intc.clone();
pc.cpu
.attach_local_controller("intr", Arc::downgrade(&peer));
pc.start_protected();
pc.write(at::CODE0, &[0xb8, 1, 0, 0, 0, 0x0f, 0xa2]);
pc.cpu.step();
pc.cpu.step();
assert_ne!(pc.regs().rdx & (1 << 9), 0, "and one wired says so");
}
}