velxio/test/test_intel/test_8086/8086.c

1487 lines
57 KiB
C

/*
* Intel 8086 emulator — clean-room implementation as a velxio custom chip.
*
* Source (in autosearch/pdfs/):
* [I86] Intel 8086 Family User's Manual, October 1979 (order 9800722-03).
* All citations are PDF-index pages; manual section numbers in parens.
* See autosearch/15_8086_authoritative_spec.md for the digested spec and
* autosearch/16_8086_reference_implementations.md for cross-validation
* sources (8086tiny / MartyPC / YJDoc2 — all permissively licensed).
*
* Scope of this initial implementation:
* - 40-pin minimum-mode pin contract.
* - Reset state: CS=0xFFFF, IP=0, all other segs=0; first fetch at the
* physical address 0xFFFF0 with ALE strobing.
* - Bus cycle T1-T4 (instruction-per-tick collapse): drive AD0..AD15 with
* low 16 addr, A16..A19 with high 4 addr, ALE pulse, then either
* RD̅ for reads or WR̅ for writes with M/IO and DT/R̅ properly set.
* - 20-bit physical addressing: (segment<<4)+offset, wrap at 1 MB.
* - Register file (AX/BX/CX/DX with high/low halves, SI/DI/BP/SP, IP,
* CS/DS/ES/SS, FLAGS).
* - ModR/M decode for memory operands (Table 4-10).
* - Subset of ISA: NOP, HLT, MOV reg/imm, MOV r/m, ADD/SUB/AND/OR/XOR/CMP,
* INC/DEC, PUSH/POP, JMP near/short, conditional jumps, CALL/RET, INT 3.
*
* Out of scope (deferred):
* - String ops (MOVS/CMPS/SCAS/LODS/STOS) with REP prefix.
* - MUL/DIV/IMUL/IDIV.
* - BCD adjust (DAA/DAS/AAA/AAS/AAM/AAD).
* - Port I/O instructions (IN/OUT).
* - Hardware interrupts (NMI/INTR vectoring).
* - Maximum-mode bus protocol.
* - Cycle-accurate prefetch queue.
* - Undocumented opcodes (POP CS, SALC).
*/
#include "velxio-chip.h"
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
/* ─── Flag bits ────────────────────────────────────────────────────────── */
#define F_CF 0x0001
#define F_PF 0x0004
#define F_AF 0x0010
#define F_ZF 0x0040
#define F_SF 0x0080
#define F_TF 0x0100
#define F_IF 0x0200
#define F_DF 0x0400
#define F_OF 0x0800
/* Reserved bits per [I86] Fig 2-9: bit 1 reads as 1, bits 12-15 as 1
per SingleStepTests canonicalisation. Bit 3, 5 read as 0. */
#define F_RESERVED_ON (0xF002)
#define F_RESERVED_OFF (0x0028)
/* Segment-register codes (matches SR field encoding, [I86] Table 4-11) */
#define SEG_ES 0
#define SEG_CS 1
#define SEG_SS 2
#define SEG_DS 3
/* Status byte values for M/IO, DT/R̅ during a cycle:
M/IO: 1 = memory, 0 = I/O ([I86] PDF p.249 — 8086 polarity) */
typedef struct {
/* Pin handles */
vx_pin ad[16]; /* AD0..AD15 — multiplexed addr/data */
vx_pin a[4]; /* A16..A19 — multiplexed addr/status */
vx_pin ale;
vx_pin rd, wr;
vx_pin mio;
vx_pin dtr;
vx_pin den;
vx_pin hold, hlda;
vx_pin intr, nmi, inta;
vx_pin reset_, ready, test_;
vx_pin clk;
vx_pin mnmx;
vx_pin bhe;
vx_pin vcc, gnd;
vx_timer cycle_timer;
/* Register file. Pairs aliased via union for byte access. */
union { struct { uint8_t al, ah; }; uint16_t ax; };
union { struct { uint8_t cl, ch; }; uint16_t cx; };
union { struct { uint8_t dl, dh; }; uint16_t dx; };
union { struct { uint8_t bl, bh; }; uint16_t bx; };
uint16_t sp, bp, si, di;
uint16_t cs, ds, es, ss;
uint16_t ip;
uint16_t flags;
/* State */
bool halted;
bool reset_active;
bool driving_ad;
/* Segment override for the current instruction (-1 = none) */
int seg_override;
/* Last-cycle latched address (for status drives) */
uint32_t last_phys;
/* REP prefix state — set by F2/F3 prefix bytes, cleared at end of
string-op execution. */
int rep_kind; /* -1 = none, 0 = REPNE/REPNZ (F2), 1 = REP/REPE/REPZ (F3) */
/* Hardware-interrupt pending flags. Set by pin watchers and
serviced at instruction boundaries. */
bool nmi_pending;
bool intr_line;
} cpu_t;
static cpu_t G;
/* ─── AD/A bus helpers ──────────────────────────────────────────────────── */
static void drive_ad(uint16_t v) {
for (int i = 0; i < 16; i++) {
vx_pin_set_mode(G.ad[i], VX_OUTPUT);
vx_pin_write(G.ad[i], (v >> i) & 1);
}
G.driving_ad = true;
}
static void release_ad(void) {
if (!G.driving_ad) return;
for (int i = 0; i < 16; i++) vx_pin_set_mode(G.ad[i], VX_INPUT);
G.driving_ad = false;
}
static void drive_a_high(uint8_t hi4) {
for (int i = 0; i < 4; i++) {
vx_pin_write(G.a[i], (hi4 >> i) & 1);
}
}
static uint16_t read_ad(void) {
uint16_t v = 0;
for (int i = 0; i < 16; i++) if (vx_pin_read(G.ad[i])) v |= (1u << i);
return v;
}
/* Compute 20-bit physical address from segment:offset, mod 1 MB. */
static uint32_t physical(uint16_t segment, uint16_t offset) {
return (((uint32_t)segment << 4) + offset) & 0xFFFFF;
}
/* ─── Bus cycle: read one byte at physical address ─────────────────────── */
static uint8_t bus_read_byte(uint32_t paddr, bool is_io) {
/* T1: drive AD with low 16 bits, A high pins with bits 16..19, pulse
ALE high so external 8282 latches the address; deassert ALE. */
drive_ad(paddr & 0xFFFF);
drive_a_high((paddr >> 16) & 0xF);
vx_pin_write(G.bhe, (paddr & 1) ? 1 : 0); /* BHE̅ low if low byte not used */
vx_pin_write(G.mio, is_io ? 0 : 1);
vx_pin_write(G.dtr, 0); /* receive */
vx_pin_write(G.ale, 1);
vx_pin_write(G.ale, 0);
/* T2..T3: switch AD to input, assert RD̅ + DEN̅. */
release_ad();
vx_pin_write(G.den, 0);
vx_pin_write(G.rd, 0);
/* Sample. The AD0..AD15 lines now carry data; for byte read at an
even address use AD0..AD7, for odd address use AD8..AD15. */
uint16_t bus = read_ad();
uint8_t byte = (paddr & 1) ? (uint8_t)(bus >> 8) : (uint8_t)bus;
/* T4: deassert. */
vx_pin_write(G.rd, 1);
vx_pin_write(G.den, 1);
G.last_phys = paddr;
return byte;
}
static uint16_t bus_read_word(uint32_t paddr, bool is_io) {
/* For aligned even addresses we could do this in one cycle (BHE̅+A0=00).
For the simple model we just do two byte reads. */
uint8_t lo = bus_read_byte(paddr, is_io);
uint8_t hi = bus_read_byte(paddr + 1, is_io);
return lo | ((uint16_t)hi << 8);
}
static void bus_write_byte(uint32_t paddr, uint8_t data, bool is_io) {
drive_ad(paddr & 0xFFFF);
drive_a_high((paddr >> 16) & 0xF);
vx_pin_write(G.bhe, (paddr & 1) ? 1 : 0);
vx_pin_write(G.mio, is_io ? 0 : 1);
vx_pin_write(G.dtr, 1); /* transmit */
vx_pin_write(G.ale, 1);
vx_pin_write(G.ale, 0);
/* Drive data on AD bus. For odd addr put byte on AD8..AD15. */
uint16_t out = (paddr & 1) ? ((uint16_t)data << 8) : data;
drive_ad(out);
vx_pin_write(G.den, 0);
vx_pin_write(G.wr, 0);
vx_pin_write(G.wr, 1); /* rising edge — external latches */
vx_pin_write(G.den, 1);
G.last_phys = paddr;
}
static void bus_write_word(uint32_t paddr, uint16_t data, bool is_io) {
bus_write_byte(paddr, (uint8_t)data, is_io);
bus_write_byte(paddr + 1, (uint8_t)(data >> 8), is_io);
}
/* ─── Memory accessors with default-segment selection ──────────────────── */
static uint16_t* seg_reg(int code) {
switch (code) {
case SEG_ES: return &G.es;
case SEG_CS: return &G.cs;
case SEG_SS: return &G.ss;
default: return &G.ds;
}
}
static int default_seg(int seg_code) {
/* Resolve segment override if active; else use the supplied default. */
if (G.seg_override >= 0) return G.seg_override;
return seg_code;
}
static uint8_t mem_read_byte(int default_seg_code, uint16_t off) {
int seg = default_seg(default_seg_code);
return bus_read_byte(physical(*seg_reg(seg), off), false);
}
static uint16_t mem_read_word(int default_seg_code, uint16_t off) {
int seg = default_seg(default_seg_code);
return bus_read_word(physical(*seg_reg(seg), off), false);
}
static void mem_write_byte(int default_seg_code, uint16_t off, uint8_t v) {
int seg = default_seg(default_seg_code);
bus_write_byte(physical(*seg_reg(seg), off), v, false);
}
static void mem_write_word(int default_seg_code, uint16_t off, uint16_t v) {
int seg = default_seg(default_seg_code);
bus_write_word(physical(*seg_reg(seg), off), v, false);
}
/* ─── Code fetch (always uses CS:IP) ───────────────────────────────────── */
static uint8_t fetch_byte(void) {
uint8_t v = bus_read_byte(physical(G.cs, G.ip), false);
G.ip++;
return v;
}
static uint16_t fetch_word(void) {
uint8_t lo = fetch_byte();
uint8_t hi = fetch_byte();
return lo | ((uint16_t)hi << 8);
}
/* ─── ModR/M decode ────────────────────────────────────────────────────── */
/* 8-bit registers indexed by REG/RM bits 000..111 ([I86] Table 4-9 w=0):
AL CL DL BL AH CH DH BH */
static uint8_t* reg8_ptr(uint8_t code) {
switch (code & 7) {
case 0: return &G.al;
case 1: return &G.cl;
case 2: return &G.dl;
case 3: return &G.bl;
case 4: return &G.ah;
case 5: return &G.ch;
case 6: return &G.dh;
default: return &G.bh;
}
}
/* 16-bit registers (w=1): AX CX DX BX SP BP SI DI */
static uint16_t* reg16_ptr(uint8_t code) {
switch (code & 7) {
case 0: return &G.ax;
case 1: return &G.cx;
case 2: return &G.dx;
case 3: return &G.bx;
case 4: return &G.sp;
case 5: return &G.bp;
case 6: return &G.si;
default: return &G.di;
}
}
/* Effective-address calc + default-segment selection per Table 4-10.
Returns the EA and writes the default segment code via *out_seg. */
static uint16_t calc_ea(uint8_t mod, uint8_t rm, int* out_seg) {
int16_t disp = 0;
int seg = SEG_DS;
if (mod == 1) disp = (int8_t)fetch_byte();
else if (mod == 2) disp = (int16_t)fetch_word();
uint16_t ea = 0;
switch (rm & 7) {
case 0: ea = G.bx + G.si; seg = SEG_DS; break;
case 1: ea = G.bx + G.di; seg = SEG_DS; break;
case 2: ea = G.bp + G.si; seg = SEG_SS; break;
case 3: ea = G.bp + G.di; seg = SEG_SS; break;
case 4: ea = G.si; seg = SEG_DS; break;
case 5: ea = G.di; seg = SEG_DS; break;
case 6:
if (mod == 0) {
/* disp16 absolute, default DS */
disp = (int16_t)fetch_word();
ea = 0;
seg = SEG_DS;
} else {
ea = G.bp;
seg = SEG_SS;
}
break;
case 7: ea = G.bx; seg = SEG_DS; break;
}
ea += disp;
*out_seg = seg;
return ea;
}
/* Read/write an r/m operand (8-bit or 16-bit). For mod=11 the operand is a
register; otherwise it's a memory location at the computed EA. */
static uint8_t rm8_read(uint8_t modrm) {
uint8_t mod = (modrm >> 6) & 3;
uint8_t rm = modrm & 7;
if (mod == 3) return *reg8_ptr(rm);
int seg;
uint16_t ea = calc_ea(mod, rm, &seg);
return mem_read_byte(seg, ea);
}
static void rm8_write(uint8_t modrm, uint8_t v) {
uint8_t mod = (modrm >> 6) & 3;
uint8_t rm = modrm & 7;
if (mod == 3) { *reg8_ptr(rm) = v; return; }
int seg;
uint16_t ea = calc_ea(mod, rm, &seg);
mem_write_byte(seg, ea, v);
}
static uint16_t rm16_read(uint8_t modrm) {
uint8_t mod = (modrm >> 6) & 3;
uint8_t rm = modrm & 7;
if (mod == 3) return *reg16_ptr(rm);
int seg;
uint16_t ea = calc_ea(mod, rm, &seg);
return mem_read_word(seg, ea);
}
static void rm16_write(uint8_t modrm, uint16_t v) {
uint8_t mod = (modrm >> 6) & 3;
uint8_t rm = modrm & 7;
if (mod == 3) { *reg16_ptr(rm) = v; return; }
int seg;
uint16_t ea = calc_ea(mod, rm, &seg);
mem_write_word(seg, ea, v);
}
/* ─── Flag helpers ──────────────────────────────────────────────────────── */
static bool parity8(uint8_t v) { v ^= v >> 4; v ^= v >> 2; v ^= v >> 1; return (v & 1) == 0; }
static void set_szp8(uint8_t v) {
G.flags = (G.flags & ~(F_SF | F_ZF | F_PF))
| (v & 0x80 ? F_SF : 0)
| (v == 0 ? F_ZF : 0)
| (parity8(v) ? F_PF : 0);
}
static void set_szp16(uint16_t v) {
G.flags = (G.flags & ~(F_SF | F_ZF | F_PF))
| (v & 0x8000 ? F_SF : 0)
| (v == 0 ? F_ZF : 0)
| (parity8(v & 0xff) ? F_PF : 0);
}
static uint8_t alu_add8(uint8_t a, uint8_t b, bool with_carry) {
uint16_t cin = (with_carry && (G.flags & F_CF)) ? 1 : 0;
uint16_t r = a + b + cin;
bool c = (r & 0x100) != 0;
bool h = (((a & 0xF) + (b & 0xF) + cin) & 0x10) != 0;
bool ov = (~(a ^ b) & (a ^ (uint8_t)r) & 0x80) != 0;
G.flags = (G.flags & ~(F_CF | F_AF | F_OF))
| (c ? F_CF : 0) | (h ? F_AF : 0) | (ov ? F_OF : 0);
set_szp8((uint8_t)r);
return (uint8_t)r;
}
static uint16_t alu_add16(uint16_t a, uint16_t b, bool with_carry) {
uint32_t cin = (with_carry && (G.flags & F_CF)) ? 1 : 0;
uint32_t r = a + b + cin;
bool c = (r & 0x10000) != 0;
bool h = (((a & 0xF) + (b & 0xF) + cin) & 0x10) != 0;
bool ov = (~(a ^ b) & (a ^ (uint16_t)r) & 0x8000) != 0;
G.flags = (G.flags & ~(F_CF | F_AF | F_OF))
| (c ? F_CF : 0) | (h ? F_AF : 0) | (ov ? F_OF : 0);
set_szp16((uint16_t)r);
return (uint16_t)r;
}
static uint8_t alu_sub8(uint8_t a, uint8_t b, bool with_borrow, bool store) {
uint16_t cin = (with_borrow && (G.flags & F_CF)) ? 1 : 0;
uint16_t r = a - b - cin;
bool c = (r & 0x100) != 0;
bool h = (((a & 0xF) - (b & 0xF) - cin) & 0x10) != 0;
bool ov = ((a ^ b) & (a ^ (uint8_t)r) & 0x80) != 0;
G.flags = (G.flags & ~(F_CF | F_AF | F_OF))
| (c ? F_CF : 0) | (h ? F_AF : 0) | (ov ? F_OF : 0);
set_szp8((uint8_t)r);
(void)store;
return (uint8_t)r;
}
static uint16_t alu_sub16(uint16_t a, uint16_t b, bool with_borrow, bool store) {
uint32_t cin = (with_borrow && (G.flags & F_CF)) ? 1 : 0;
uint32_t r = a - b - cin;
bool c = (r & 0x10000) != 0;
bool h = (((a & 0xF) - (b & 0xF) - cin) & 0x10) != 0;
bool ov = ((a ^ b) & (a ^ (uint16_t)r) & 0x8000) != 0;
G.flags = (G.flags & ~(F_CF | F_AF | F_OF))
| (c ? F_CF : 0) | (h ? F_AF : 0) | (ov ? F_OF : 0);
set_szp16((uint16_t)r);
(void)store;
return (uint16_t)r;
}
static uint8_t alu_and8(uint8_t a, uint8_t b) {
uint8_t r = a & b;
G.flags = (G.flags & ~(F_CF | F_OF | F_AF));
set_szp8(r);
return r;
}
static uint16_t alu_and16(uint16_t a, uint16_t b) {
uint16_t r = a & b;
G.flags = (G.flags & ~(F_CF | F_OF | F_AF));
set_szp16(r);
return r;
}
static uint8_t alu_or8(uint8_t a, uint8_t b) {
uint8_t r = a | b;
G.flags = (G.flags & ~(F_CF | F_OF | F_AF));
set_szp8(r);
return r;
}
static uint16_t alu_or16(uint16_t a, uint16_t b) {
uint16_t r = a | b;
G.flags = (G.flags & ~(F_CF | F_OF | F_AF));
set_szp16(r);
return r;
}
static uint8_t alu_xor8(uint8_t a, uint8_t b) {
uint8_t r = a ^ b;
G.flags = (G.flags & ~(F_CF | F_OF | F_AF));
set_szp8(r);
return r;
}
static uint16_t alu_xor16(uint16_t a, uint16_t b) {
uint16_t r = a ^ b;
G.flags = (G.flags & ~(F_CF | F_OF | F_AF));
set_szp16(r);
return r;
}
/* ─── Stack helpers ─────────────────────────────────────────────────────── */
static void push16(uint16_t v) {
G.sp -= 2;
bus_write_word(physical(G.ss, G.sp), v, false);
}
static uint16_t pop16(void) {
uint16_t v = bus_read_word(physical(G.ss, G.sp), false);
G.sp += 2;
return v;
}
/* ─── Shift/rotate helpers (Group 2) ────────────────────────────────────── */
/* Per [I86] PDF p.262 (manual Table 4-12): the REG field of ModR/M
selects the operation: 0=ROL, 1=ROR, 2=RCL, 3=RCR, 4=SHL/SAL,
5=SHR, 6=undefined (treated as SHL), 7=SAR. The count comes from
either an immediate 1 (opcodes D0/D1) or CL (D2/D3). On 8086 the
shift count is NOT masked to 5 bits — that's an 80186+ change. */
static uint8_t shift_op8(uint8_t op_sel, uint8_t v, uint8_t count) {
if (count == 0) return v;
/* For shifts with count > 0, OF is set only when count == 1. */
bool count_was_1 = count == 1;
while (count--) {
uint8_t old_msb = (v >> 7) & 1;
uint8_t old_lsb = v & 1;
uint8_t cf;
switch (op_sel) {
case 0: /* ROL */ cf = old_msb; v = (uint8_t)((v << 1) | cf); break;
case 1: /* ROR */ cf = old_lsb; v = (uint8_t)((v >> 1) | (cf << 7)); break;
case 2: /* RCL */ cf = old_msb; v = (uint8_t)((v << 1) | (G.flags & F_CF ? 1 : 0)); break;
case 3: /* RCR */ cf = old_lsb; v = (uint8_t)((v >> 1) | ((G.flags & F_CF ? 1 : 0) << 7)); break;
case 4: case 6: /* SHL / SAL */ cf = old_msb; v <<= 1; break;
case 5: /* SHR */ cf = old_lsb; v >>= 1; break;
default: /* SAR */ cf = old_lsb; v = (uint8_t)((v >> 1) | (v & 0x80)); break;
}
G.flags = (G.flags & ~F_CF) | (cf ? F_CF : 0);
}
/* Set S/Z/P from result for shifts (4..7); rotates leave them alone
per the manual but most refs set them. We set them for shifts. */
if (op_sel >= 4) {
set_szp8(v);
}
if (count_was_1) {
/* OF for count==1: rotate variants set OF as XOR of two
highest carry-relevant bits; shifts have specific rules. */
bool of;
switch (op_sel) {
case 0: of = ((v >> 7) & 1) != ((G.flags & F_CF) ? 1 : 0); break;
case 1: of = ((v >> 7) & 1) != (((v >> 6) & 1)); break;
case 2: of = ((v >> 7) & 1) != ((G.flags & F_CF) ? 1 : 0); break;
case 3: of = ((v >> 7) & 1) != (((v >> 6) & 1)); break;
case 4: case 6: of = ((v >> 7) & 1) != ((G.flags & F_CF) ? 1 : 0); break;
case 5: of = ((v >> 7) & 1) != 0; break; /* high bit changed → 0 */
default: of = false; break; /* SAR: OF = 0 */
}
G.flags = (G.flags & ~F_OF) | (of ? F_OF : 0);
}
return v;
}
static uint16_t shift_op16(uint8_t op_sel, uint16_t v, uint8_t count) {
if (count == 0) return v;
bool count_was_1 = count == 1;
while (count--) {
uint8_t old_msb = (v >> 15) & 1;
uint8_t old_lsb = v & 1;
uint8_t cf;
switch (op_sel) {
case 0: cf = old_msb; v = (uint16_t)((v << 1) | cf); break;
case 1: cf = old_lsb; v = (uint16_t)((v >> 1) | ((uint16_t)cf << 15)); break;
case 2: cf = old_msb; v = (uint16_t)((v << 1) | (G.flags & F_CF ? 1 : 0)); break;
case 3: cf = old_lsb; v = (uint16_t)((v >> 1) | ((uint16_t)(G.flags & F_CF ? 1 : 0) << 15)); break;
case 4: case 6: cf = old_msb; v <<= 1; break;
case 5: cf = old_lsb; v >>= 1; break;
default: cf = old_lsb; v = (uint16_t)((v >> 1) | (v & 0x8000)); break;
}
G.flags = (G.flags & ~F_CF) | (cf ? F_CF : 0);
}
if (op_sel >= 4) set_szp16(v);
if (count_was_1) {
bool of;
switch (op_sel) {
case 0: of = ((v >> 15) & 1) != ((G.flags & F_CF) ? 1 : 0); break;
case 1: of = ((v >> 15) & 1) != (((v >> 14) & 1)); break;
case 2: of = ((v >> 15) & 1) != ((G.flags & F_CF) ? 1 : 0); break;
case 3: of = ((v >> 15) & 1) != (((v >> 14) & 1)); break;
case 4: case 6: of = ((v >> 15) & 1) != ((G.flags & F_CF) ? 1 : 0); break;
case 5: of = ((v >> 15) & 1) != 0; break;
default: of = false; break;
}
G.flags = (G.flags & ~F_OF) | (of ? F_OF : 0);
}
return v;
}
/* ─── BCD adjust ────────────────────────────────────────────────────────── */
/* DAA: decimal-adjust AL after BCD addition. Per [I86] PDF p.58. */
static void daa_op(void) {
uint8_t old_al = G.al;
bool old_cf = (G.flags & F_CF) != 0;
bool new_cf = old_cf;
bool new_af = (G.flags & F_AF) != 0;
if ((G.al & 0x0F) > 9 || (G.flags & F_AF)) {
uint16_t r = G.al + 6;
G.al = (uint8_t)r;
new_af = true;
if (r & 0x100) new_cf = true;
}
if (old_al > 0x99 || old_cf) {
G.al += 0x60;
new_cf = true;
}
G.flags = (G.flags & ~(F_CF | F_AF | F_SF | F_ZF | F_PF))
| (new_cf ? F_CF : 0)
| (new_af ? F_AF : 0);
set_szp8(G.al);
}
/* DAS: decimal-adjust AL after BCD subtraction. */
static void das_op(void) {
uint8_t old_al = G.al;
bool old_cf = (G.flags & F_CF) != 0;
bool new_cf = old_cf;
bool new_af = (G.flags & F_AF) != 0;
if ((G.al & 0x0F) > 9 || (G.flags & F_AF)) {
int r = G.al - 6;
G.al = (uint8_t)r;
new_af = true;
if (r < 0) new_cf = true;
}
if (old_al > 0x99 || old_cf) {
G.al -= 0x60;
new_cf = true;
}
G.flags = (G.flags & ~(F_CF | F_AF | F_SF | F_ZF | F_PF))
| (new_cf ? F_CF : 0)
| (new_af ? F_AF : 0);
set_szp8(G.al);
}
/* AAA: ASCII-adjust AL after addition. */
static void aaa_op(void) {
if ((G.al & 0x0F) > 9 || (G.flags & F_AF)) {
G.ax += 0x106;
G.flags |= (F_AF | F_CF);
} else {
G.flags &= ~(F_AF | F_CF);
}
G.al &= 0x0F;
}
static void aas_op(void) {
if ((G.al & 0x0F) > 9 || (G.flags & F_AF)) {
G.al -= 6;
G.ah -= 1;
G.flags |= (F_AF | F_CF);
} else {
G.flags &= ~(F_AF | F_CF);
}
G.al &= 0x0F;
}
/* AAM: ASCII-adjust AL after multiply. Operand is the divisor (typically 10). */
static void aam_op(uint8_t base) {
if (base == 0) {
/* Real 8086: divide-error exception — we treat as halt. */
G.halted = true;
return;
}
G.ah = G.al / base;
G.al = G.al % base;
set_szp8(G.al);
}
/* AAD: ASCII-adjust AX before division. */
static void aad_op(uint8_t base) {
G.al = (uint8_t)(G.ah * base + G.al);
G.ah = 0;
set_szp8(G.al);
}
/* ─── String op helpers ─────────────────────────────────────────────────── */
static int string_dir(void) { return (G.flags & F_DF) ? -1 : 1; }
/* MUL r/m8: AX = AL * src. CF=OF set if AH != 0. */
static void mul8(uint8_t v) {
G.ax = (uint16_t)G.al * v;
bool of = G.ah != 0;
G.flags = (G.flags & ~(F_CF | F_OF)) | (of ? (F_CF | F_OF) : 0);
}
/* MUL r/m16: DX:AX = AX * src. CF=OF set if DX != 0. */
static void mul16(uint16_t v) {
uint32_t r = (uint32_t)G.ax * v;
G.ax = (uint16_t)r;
G.dx = (uint16_t)(r >> 16);
bool of = G.dx != 0;
G.flags = (G.flags & ~(F_CF | F_OF)) | (of ? (F_CF | F_OF) : 0);
}
/* IMUL r/m8: AX = (signed)AL * (signed)src. */
static void imul8(uint8_t v) {
int16_t r = (int16_t)(int8_t)G.al * (int8_t)v;
G.ax = (uint16_t)r;
bool of = (int8_t)G.al != (int16_t)r; /* OF set if result doesn't fit in AL */
G.flags = (G.flags & ~(F_CF | F_OF)) | (of ? (F_CF | F_OF) : 0);
}
static void imul16(uint16_t v) {
int32_t r = (int32_t)(int16_t)G.ax * (int16_t)v;
G.ax = (uint16_t)r;
G.dx = (uint16_t)(r >> 16);
bool of = (int16_t)G.ax != (int32_t)r;
G.flags = (G.flags & ~(F_CF | F_OF)) | (of ? (F_CF | F_OF) : 0);
}
/* DIV r/m8: AX / src → AL = quotient, AH = remainder. */
static void div8(uint8_t v) {
if (v == 0) { G.halted = true; return; }
uint16_t q = G.ax / v;
if (q > 0xFF) { G.halted = true; return; }
G.ah = G.ax % v;
G.al = (uint8_t)q;
}
static void div16(uint16_t v) {
if (v == 0) { G.halted = true; return; }
uint32_t dividend = ((uint32_t)G.dx << 16) | G.ax;
uint32_t q = dividend / v;
if (q > 0xFFFF) { G.halted = true; return; }
G.dx = (uint16_t)(dividend % v);
G.ax = (uint16_t)q;
}
/* IDIV (signed). */
static void idiv8(uint8_t v) {
if (v == 0) { G.halted = true; return; }
int16_t dividend = (int16_t)G.ax;
int16_t divisor = (int8_t)v;
int16_t q = dividend / divisor;
if (q > 127 || q < -128) { G.halted = true; return; }
G.ah = (uint8_t)(dividend % divisor);
G.al = (uint8_t)q;
}
static void idiv16(uint16_t v) {
if (v == 0) { G.halted = true; return; }
int32_t dividend = (int32_t)(((uint32_t)G.dx << 16) | G.ax);
int32_t divisor = (int16_t)v;
int32_t q = dividend / divisor;
if (q > 32767 || q < -32768) { G.halted = true; return; }
G.dx = (uint16_t)(dividend % divisor);
G.ax = (uint16_t)q;
}
/* INT n: push flags, push CS, push IP; clear IF and TF; jump to vector
table entry at 0:(n*4). */
static void do_int(uint8_t n) {
push16(G.flags);
push16(G.cs);
push16(G.ip);
G.flags &= ~(F_IF | F_TF);
uint32_t va = (uint32_t)n * 4;
uint16_t off = bus_read_word(va, false);
uint16_t seg = bus_read_word(va + 2, false);
G.ip = off;
G.cs = seg;
}
/* ─── Conditional jump test ([I86] Table 2-13) ─────────────────────────── */
static bool cond_jcc(uint8_t op) {
/* op encodes condition in low 4 bits of the byte (op = 0x70..0x7F) */
bool r;
switch (op & 0x0F) {
case 0x0: r = (G.flags & F_OF) != 0; break; /* JO */
case 0x1: r = (G.flags & F_OF) == 0; break; /* JNO */
case 0x2: r = (G.flags & F_CF) != 0; break; /* JB / JNAE / JC */
case 0x3: r = (G.flags & F_CF) == 0; break; /* JNB / JAE / JNC */
case 0x4: r = (G.flags & F_ZF) != 0; break; /* JE / JZ */
case 0x5: r = (G.flags & F_ZF) == 0; break; /* JNE / JNZ */
case 0x6: r = (G.flags & (F_CF | F_ZF)) != 0; break; /* JBE / JNA */
case 0x7: r = (G.flags & (F_CF | F_ZF)) == 0; break; /* JNBE / JA */
case 0x8: r = (G.flags & F_SF) != 0; break; /* JS */
case 0x9: r = (G.flags & F_SF) == 0; break; /* JNS */
case 0xA: r = (G.flags & F_PF) != 0; break; /* JP / JPE */
case 0xB: r = (G.flags & F_PF) == 0; break; /* JNP / JPO */
case 0xC: r = ((G.flags & F_SF) != 0) != ((G.flags & F_OF) != 0); break;
/* JL / JNGE */
case 0xD: r = ((G.flags & F_SF) != 0) == ((G.flags & F_OF) != 0); break;
/* JNL / JGE */
case 0xE: r = (G.flags & F_ZF) ||
(((G.flags & F_SF) != 0) != ((G.flags & F_OF) != 0));
break; /* JLE / JNG */
default: r = !(G.flags & F_ZF) &&
(((G.flags & F_SF) != 0) == ((G.flags & F_OF) != 0));
break; /* JNLE / JG */
}
return r;
}
/* ─── Group 1/3/4/5 sub-opcode dispatch ─────────────────────────────────── */
/* Group 1 (opcodes 0x80..0x83): ADD/OR/ADC/SBB/AND/SUB/XOR/CMP r/m, imm. */
static void exec_group1(uint8_t op) {
uint8_t modrm = fetch_byte();
uint8_t sub = (modrm >> 3) & 7;
bool w = (op & 1);
bool s = (op & 2) != 0; /* sign-extend imm8 to imm16 */
if (!w) {
uint8_t a = rm8_read(modrm);
uint8_t b = fetch_byte();
uint8_t r = a;
switch (sub) {
case 0: r = alu_add8(a, b, false); break; /* ADD */
case 1: r = alu_or8(a, b); break; /* OR */
case 2: r = alu_add8(a, b, true); break; /* ADC */
case 3: r = alu_sub8(a, b, true, true); break; /* SBB */
case 4: r = alu_and8(a, b); break; /* AND */
case 5: r = alu_sub8(a, b, false, true); break; /* SUB */
case 6: r = alu_xor8(a, b); break; /* XOR */
case 7: alu_sub8(a, b, false, false); return; /* CMP — no store */
}
rm8_write(modrm, r);
} else {
uint16_t a = rm16_read(modrm);
uint16_t b;
if (s) b = (int16_t)(int8_t)fetch_byte();
else b = fetch_word();
uint16_t r = a;
switch (sub) {
case 0: r = alu_add16(a, b, false); break;
case 1: r = alu_or16(a, b); break;
case 2: r = alu_add16(a, b, true); break;
case 3: r = alu_sub16(a, b, true, true); break;
case 4: r = alu_and16(a, b); break;
case 5: r = alu_sub16(a, b, false, true); break;
case 6: r = alu_xor16(a, b); break;
case 7: alu_sub16(a, b, false, false); return;
}
rm16_write(modrm, r);
}
}
/* Group 5 (0xFF) — INC/DEC/CALL/JMP/PUSH on r/m16. */
static void exec_group5_word(uint8_t modrm) {
uint8_t sub = (modrm >> 3) & 7;
uint16_t a = rm16_read(modrm);
switch (sub) {
case 0: { /* INC */
bool old_cf = G.flags & F_CF;
uint16_t r = alu_add16(a, 1, false);
G.flags = (G.flags & ~F_CF) | (old_cf ? F_CF : 0);
rm16_write(modrm, r);
break;
}
case 1: { /* DEC */
bool old_cf = G.flags & F_CF;
uint16_t r = alu_sub16(a, 1, false, true);
G.flags = (G.flags & ~F_CF) | (old_cf ? F_CF : 0);
rm16_write(modrm, r);
break;
}
case 2: /* CALL near indirect */
push16(G.ip);
G.ip = a;
break;
case 4: /* JMP near indirect */
G.ip = a;
break;
case 6: /* PUSH */
push16(a);
break;
default: break;
}
}
/* ─── Group 3 (0xF6/0xF7) — TEST/NOT/NEG/MUL/IMUL/DIV/IDIV r/m ─────────── */
static void exec_group3_byte(uint8_t modrm) {
uint8_t sub = (modrm >> 3) & 7;
uint8_t v = rm8_read(modrm);
switch (sub) {
case 0: case 1: { /* TEST r/m8, imm8 */
uint8_t imm = fetch_byte();
(void)alu_and8(v, imm); /* sets flags, discards result */
break;
}
case 2: rm8_write(modrm, (uint8_t)~v); break; /* NOT */
case 3: { /* NEG */
bool cf = v != 0;
uint8_t r = (uint8_t)(0 - v);
bool h = (v & 0x0F) != 0;
bool ov = v == 0x80;
G.flags = (G.flags & ~(F_CF | F_AF | F_OF))
| (cf ? F_CF : 0) | (h ? F_AF : 0) | (ov ? F_OF : 0);
set_szp8(r);
rm8_write(modrm, r);
break;
}
case 4: mul8(v); break;
case 5: imul8(v); break;
case 6: div8(v); break;
case 7: idiv8(v); break;
}
}
static void exec_group3_word(uint8_t modrm) {
uint8_t sub = (modrm >> 3) & 7;
uint16_t v = rm16_read(modrm);
switch (sub) {
case 0: case 1: { /* TEST r/m16, imm16 */
uint16_t imm = fetch_word();
(void)alu_and16(v, imm);
break;
}
case 2: rm16_write(modrm, (uint16_t)~v); break;
case 3: {
bool cf = v != 0;
uint16_t r = (uint16_t)(0 - v);
bool h = (v & 0x000F) != 0;
bool ov = v == 0x8000;
G.flags = (G.flags & ~(F_CF | F_AF | F_OF))
| (cf ? F_CF : 0) | (h ? F_AF : 0) | (ov ? F_OF : 0);
set_szp16(r);
rm16_write(modrm, r);
break;
}
case 4: mul16(v); break;
case 5: imul16(v); break;
case 6: div16(v); break;
case 7: idiv16(v); break;
}
}
/* ─── Group 4 (0xFE) — INC/DEC r/m8 ─────────────────────────────────────── */
static void exec_group4(uint8_t modrm) {
uint8_t sub = (modrm >> 3) & 7;
uint8_t v = rm8_read(modrm);
bool old_cf = (G.flags & F_CF) != 0;
if (sub == 0) {
v = alu_add8(v, 1, false);
} else if (sub == 1) {
v = alu_sub8(v, 1, false, true);
}
G.flags = (G.flags & ~F_CF) | (old_cf ? F_CF : 0);
rm8_write(modrm, v);
}
/* ─── Group 2 — shift/rotate r/m by 1 (D0/D1) or by CL (D2/D3) ─────────── */
static void exec_shift_byte(uint8_t modrm, uint8_t count) {
uint8_t sub = (modrm >> 3) & 7;
uint8_t v = rm8_read(modrm);
rm8_write(modrm, shift_op8(sub, v, count));
}
static void exec_shift_word(uint8_t modrm, uint8_t count) {
uint8_t sub = (modrm >> 3) & 7;
uint16_t v = rm16_read(modrm);
rm16_write(modrm, shift_op16(sub, v, count));
}
/* ─── String-op step (one iteration, called from step() under REP) ───── */
static bool string_step(uint8_t op) {
int dir = string_dir();
bool word = (op & 1) != 0;
/* Returns true if the REP prefix should EXIT (e.g. zf check failed). */
bool exit_rep = false;
switch (op) {
case 0xA4: { /* MOVSB */
uint8_t v = mem_read_byte(SEG_DS, G.si);
int dst_seg = SEG_ES; /* ES is fixed for string destinations */
G.seg_override = -1; /* override doesn't affect ES dest */
bus_write_byte(physical(G.es, G.di), v, false);
(void)dst_seg;
G.si = (uint16_t)(G.si + dir);
G.di = (uint16_t)(G.di + dir);
break;
}
case 0xA5: { /* MOVSW */
uint16_t v = mem_read_word(SEG_DS, G.si);
bus_write_word(physical(G.es, G.di), v, false);
G.si = (uint16_t)(G.si + dir * 2);
G.di = (uint16_t)(G.di + dir * 2);
break;
}
case 0xA6: { /* CMPSB */
uint8_t a = mem_read_byte(SEG_DS, G.si);
uint8_t b = bus_read_byte(physical(G.es, G.di), false);
(void)alu_sub8(a, b, false, false);
G.si = (uint16_t)(G.si + dir);
G.di = (uint16_t)(G.di + dir);
break;
}
case 0xA7: { /* CMPSW */
uint16_t a = mem_read_word(SEG_DS, G.si);
uint16_t b = bus_read_word(physical(G.es, G.di), false);
(void)alu_sub16(a, b, false, false);
G.si = (uint16_t)(G.si + dir * 2);
G.di = (uint16_t)(G.di + dir * 2);
break;
}
case 0xAA: /* STOSB */
bus_write_byte(physical(G.es, G.di), G.al, false);
G.di = (uint16_t)(G.di + dir);
break;
case 0xAB: /* STOSW */
bus_write_word(physical(G.es, G.di), G.ax, false);
G.di = (uint16_t)(G.di + dir * 2);
break;
case 0xAC: /* LODSB */
G.al = mem_read_byte(SEG_DS, G.si);
G.si = (uint16_t)(G.si + dir);
break;
case 0xAD: /* LODSW */
G.ax = mem_read_word(SEG_DS, G.si);
G.si = (uint16_t)(G.si + dir * 2);
break;
case 0xAE: { /* SCASB */
uint8_t b = bus_read_byte(physical(G.es, G.di), false);
(void)alu_sub8(G.al, b, false, false);
G.di = (uint16_t)(G.di + dir);
break;
}
case 0xAF: { /* SCASW */
uint16_t b = bus_read_word(physical(G.es, G.di), false);
(void)alu_sub16(G.ax, b, false, false);
G.di = (uint16_t)(G.di + dir * 2);
break;
}
}
/* For CMPS / SCAS the REP prefix checks ZF. */
if (op == 0xA6 || op == 0xA7 || op == 0xAE || op == 0xAF) {
if (G.rep_kind == 1 && (G.flags & F_ZF) == 0) exit_rep = true;
if (G.rep_kind == 0 && (G.flags & F_ZF) != 0) exit_rep = true;
}
return exit_rep;
}
/* ─── One-instruction step ──────────────────────────────────────────────── */
static void step(void) {
/* Service hardware interrupts at instruction boundaries. NMI is
edge-triggered and always serviced; INTR is level + IF-gated. */
if (G.nmi_pending) {
G.nmi_pending = false;
do_int(2);
G.halted = false;
return;
}
if (G.intr_line && (G.flags & F_IF)) {
/* Approximate: read the vector byte from the data bus during
an INTA cycle. The actual external 8259 PIC would jam the
vector. Here we synthesise INT 0 if no fixture drives the
bus (reset state); a test fixture can override by driving
the data bus when our chip asserts INTA̅ low. */
vx_pin_write(G.inta, 0);
uint8_t vec = bus_read_byte(0, false); /* dummy read for cycle */
vx_pin_write(G.inta, 1);
do_int(vec);
G.halted = false;
G.flags &= ~F_IF;
return;
}
if (G.halted) return;
G.seg_override = -1;
G.rep_kind = -1;
/* Handle prefix bytes (segment override + REP). */
while (1) {
uint8_t prefix = bus_read_byte(physical(G.cs, G.ip), false);
if (prefix == 0x26) { G.seg_override = SEG_ES; G.ip++; continue; }
if (prefix == 0x2E) { G.seg_override = SEG_CS; G.ip++; continue; }
if (prefix == 0x36) { G.seg_override = SEG_SS; G.ip++; continue; }
if (prefix == 0x3E) { G.seg_override = SEG_DS; G.ip++; continue; }
if (prefix == 0xF0) { G.ip++; continue; } /* LOCK — ignored */
if (prefix == 0xF2) { G.rep_kind = 0; G.ip++; continue; }
if (prefix == 0xF3) { G.rep_kind = 1; G.ip++; continue; }
break;
}
uint8_t op = fetch_byte();
/* MOV r8, imm8 — opcodes 0xB0..0xB7 */
if (op >= 0xB0 && op <= 0xB7) {
*reg8_ptr(op & 7) = fetch_byte();
return;
}
/* MOV r16, imm16 — opcodes 0xB8..0xBF */
if (op >= 0xB8 && op <= 0xBF) {
*reg16_ptr(op & 7) = fetch_word();
return;
}
/* INC r16 — opcodes 0x40..0x47 */
if (op >= 0x40 && op <= 0x47) {
uint16_t* r = reg16_ptr(op & 7);
bool old_cf = G.flags & F_CF;
*r = alu_add16(*r, 1, false);
G.flags = (G.flags & ~F_CF) | (old_cf ? F_CF : 0);
return;
}
/* DEC r16 — 0x48..0x4F */
if (op >= 0x48 && op <= 0x4F) {
uint16_t* r = reg16_ptr(op & 7);
bool old_cf = G.flags & F_CF;
*r = alu_sub16(*r, 1, false, true);
G.flags = (G.flags & ~F_CF) | (old_cf ? F_CF : 0);
return;
}
/* PUSH r16 — 0x50..0x57 */
if (op >= 0x50 && op <= 0x57) {
push16(*reg16_ptr(op & 7));
return;
}
/* POP r16 — 0x58..0x5F */
if (op >= 0x58 && op <= 0x5F) {
*reg16_ptr(op & 7) = pop16();
return;
}
/* Conditional short jumps Jcc — 0x70..0x7F */
if (op >= 0x70 && op <= 0x7F) {
int8_t disp = (int8_t)fetch_byte();
if (cond_jcc(op)) G.ip = (uint16_t)(G.ip + disp);
return;
}
/* String ops with optional REP/REPE/REPNE prefix */
if (op >= 0xA4 && op <= 0xAF && (op & 0xFC) != 0xA8) {
if (G.rep_kind >= 0) {
while (G.cx != 0) {
bool exit_rep = string_step(op);
G.cx--;
if (exit_rep) break;
}
} else {
(void)string_step(op);
}
return;
}
/* XCHG AX, r16 — opcodes 0x91..0x97 (0x90 is NOP) */
if (op >= 0x91 && op <= 0x97) {
uint16_t* r = reg16_ptr(op & 7);
uint16_t t = G.ax; G.ax = *r; *r = t;
return;
}
switch (op) {
case 0x90: /* NOP (XCHG AX, AX) */ break;
/* MOV r/m8, r8 — 0x88; r/m16, r16 — 0x89; r8, r/m8 — 0x8A;
r16, r/m16 — 0x8B */
case 0x88: { uint8_t modrm = fetch_byte(); rm8_write(modrm, *reg8_ptr((modrm >> 3) & 7)); break; }
case 0x89: { uint8_t modrm = fetch_byte(); rm16_write(modrm, *reg16_ptr((modrm >> 3) & 7)); break; }
case 0x8A: { uint8_t modrm = fetch_byte(); *reg8_ptr((modrm >> 3) & 7) = rm8_read(modrm); break; }
case 0x8B: { uint8_t modrm = fetch_byte(); *reg16_ptr((modrm >> 3) & 7) = rm16_read(modrm); break; }
/* MOV r/m, imm — 0xC6 / 0xC7. Encoding: opcode + modrm + disp + imm.
Crucially the disp bytes (consumed by calc_ea) come BEFORE the
immediate, so we must compute the EA first, then fetch imm. */
case 0xC6: {
uint8_t modrm = fetch_byte();
uint8_t mod = (modrm >> 6) & 3;
uint8_t rm = modrm & 7;
if (mod == 3) {
uint8_t imm = fetch_byte();
*reg8_ptr(rm) = imm;
} else {
int seg;
uint16_t ea = calc_ea(mod, rm, &seg);
uint8_t imm = fetch_byte();
mem_write_byte(seg, ea, imm);
}
break;
}
case 0xC7: {
uint8_t modrm = fetch_byte();
uint8_t mod = (modrm >> 6) & 3;
uint8_t rm = modrm & 7;
if (mod == 3) {
uint16_t imm = fetch_word();
*reg16_ptr(rm) = imm;
} else {
int seg;
uint16_t ea = calc_ea(mod, rm, &seg);
uint16_t imm = fetch_word();
mem_write_word(seg, ea, imm);
}
break;
}
/* MOV r/m16, sreg — 0x8C / MOV sreg, r/m16 — 0x8E */
case 0x8C: { uint8_t modrm = fetch_byte(); rm16_write(modrm, *seg_reg((modrm >> 3) & 3)); break; }
case 0x8E: { uint8_t modrm = fetch_byte(); *seg_reg((modrm >> 3) & 3) = rm16_read(modrm); break; }
/* MOV AL,[addr] / AX,[addr] / [addr],AL / [addr],AX */
case 0xA0: { uint16_t a = fetch_word(); G.al = mem_read_byte(SEG_DS, a); break; }
case 0xA1: { uint16_t a = fetch_word(); G.ax = mem_read_word(SEG_DS, a); break; }
case 0xA2: { uint16_t a = fetch_word(); mem_write_byte(SEG_DS, a, G.al); break; }
case 0xA3: { uint16_t a = fetch_word(); mem_write_word(SEG_DS, a, G.ax); break; }
/* ADD/SUB/AND/OR/XOR/CMP r/m, r — and reverse — and AL/AX,imm.
00 /op /, 02 /op /reverse, 04 /op /AL+imm8, 05 /op /AX+imm16.
op encoded in bits 5..3 of the leading byte. */
case 0x00: case 0x08: case 0x10: case 0x18:
case 0x20: case 0x28: case 0x30: case 0x38: {
uint8_t modrm = fetch_byte();
uint8_t* dst = reg8_ptr((modrm >> 3) & 7);
uint8_t a = rm8_read(modrm);
uint8_t b = *dst;
uint8_t r = a;
switch ((op >> 3) & 7) {
case 0: r = alu_add8(a, b, false); break; /* ADD */
case 1: r = alu_or8(a, b); break;
case 2: r = alu_add8(a, b, true); break;
case 3: r = alu_sub8(a, b, true, true); break;
case 4: r = alu_and8(a, b); break;
case 5: r = alu_sub8(a, b, false, true); break;
case 6: r = alu_xor8(a, b); break;
case 7: alu_sub8(a, b, false, false); return; /* CMP */
}
rm8_write(modrm, r);
break;
}
case 0x01: case 0x09: case 0x11: case 0x19:
case 0x21: case 0x29: case 0x31: case 0x39: {
uint8_t modrm = fetch_byte();
uint16_t* dst = reg16_ptr((modrm >> 3) & 7);
uint16_t a = rm16_read(modrm);
uint16_t b = *dst;
uint16_t r = a;
switch ((op >> 3) & 7) {
case 0: r = alu_add16(a, b, false); break;
case 1: r = alu_or16(a, b); break;
case 2: r = alu_add16(a, b, true); break;
case 3: r = alu_sub16(a, b, true, true); break;
case 4: r = alu_and16(a, b); break;
case 5: r = alu_sub16(a, b, false, true); break;
case 6: r = alu_xor16(a, b); break;
case 7: alu_sub16(a, b, false, false); return;
}
rm16_write(modrm, r);
break;
}
case 0x02: case 0x0A: case 0x12: case 0x1A:
case 0x22: case 0x2A: case 0x32: case 0x3A: {
uint8_t modrm = fetch_byte();
uint8_t* dst = reg8_ptr((modrm >> 3) & 7);
uint8_t a = *dst;
uint8_t b = rm8_read(modrm);
uint8_t r = a;
switch ((op >> 3) & 7) {
case 0: r = alu_add8(a, b, false); break;
case 1: r = alu_or8(a, b); break;
case 2: r = alu_add8(a, b, true); break;
case 3: r = alu_sub8(a, b, true, true); break;
case 4: r = alu_and8(a, b); break;
case 5: r = alu_sub8(a, b, false, true); break;
case 6: r = alu_xor8(a, b); break;
case 7: alu_sub8(a, b, false, false); return;
}
*dst = r;
break;
}
case 0x03: case 0x0B: case 0x13: case 0x1B:
case 0x23: case 0x2B: case 0x33: case 0x3B: {
uint8_t modrm = fetch_byte();
uint16_t* dst = reg16_ptr((modrm >> 3) & 7);
uint16_t a = *dst;
uint16_t b = rm16_read(modrm);
uint16_t r = a;
switch ((op >> 3) & 7) {
case 0: r = alu_add16(a, b, false); break;
case 1: r = alu_or16(a, b); break;
case 2: r = alu_add16(a, b, true); break;
case 3: r = alu_sub16(a, b, true, true); break;
case 4: r = alu_and16(a, b); break;
case 5: r = alu_sub16(a, b, false, true); break;
case 6: r = alu_xor16(a, b); break;
case 7: alu_sub16(a, b, false, false); return;
}
*dst = r;
break;
}
case 0x04: G.al = alu_add8(G.al, fetch_byte(), false); break;
case 0x05: G.ax = alu_add16(G.ax, fetch_word(), false); break;
case 0x0C: G.al = alu_or8(G.al, fetch_byte()); break;
case 0x0D: G.ax = alu_or16(G.ax, fetch_word()); break;
case 0x14: G.al = alu_add8(G.al, fetch_byte(), true); break;
case 0x15: G.ax = alu_add16(G.ax, fetch_word(), true); break;
case 0x1C: G.al = alu_sub8(G.al, fetch_byte(), true, true); break;
case 0x1D: G.ax = alu_sub16(G.ax, fetch_word(), true, true); break;
case 0x24: G.al = alu_and8(G.al, fetch_byte()); break;
case 0x25: G.ax = alu_and16(G.ax, fetch_word()); break;
case 0x2C: G.al = alu_sub8(G.al, fetch_byte(), false, true); break;
case 0x2D: G.ax = alu_sub16(G.ax, fetch_word(), false, true); break;
case 0x34: G.al = alu_xor8(G.al, fetch_byte()); break;
case 0x35: G.ax = alu_xor16(G.ax, fetch_word()); break;
case 0x3C: alu_sub8(G.al, fetch_byte(), false, false); break;
case 0x3D: alu_sub16(G.ax, fetch_word(), false, false); break;
/* Group 1: ADD/OR/ADC/SBB/AND/SUB/XOR/CMP r/m, imm */
case 0x80: case 0x81: case 0x82: case 0x83:
exec_group1(op);
break;
/* JMP near (0xE9, 16-bit displacement); JMP short (0xEB, 8-bit) */
case 0xE9: { int16_t d = (int16_t)fetch_word(); G.ip = (uint16_t)(G.ip + d); break; }
case 0xEB: { int8_t d = (int8_t) fetch_byte(); G.ip = (uint16_t)(G.ip + d); break; }
/* JMP far (0xEA) */
case 0xEA: { uint16_t off = fetch_word(); uint16_t seg = fetch_word();
G.ip = off; G.cs = seg; break; }
/* CALL near (0xE8, 16-bit displacement) */
case 0xE8: { int16_t d = (int16_t)fetch_word(); push16(G.ip); G.ip = (uint16_t)(G.ip + d); break; }
/* CALL far (0x9A): push CS, push IP, jump CS:IP */
case 0x9A: { uint16_t off = fetch_word(); uint16_t seg = fetch_word();
push16(G.cs); push16(G.ip); G.cs = seg; G.ip = off; break; }
/* RET near (0xC3) / RET imm (0xC2) */
case 0xC3: G.ip = pop16(); break;
case 0xC2: { uint16_t n = fetch_word(); G.ip = pop16(); G.sp += n; break; }
/* RET far (0xCB) / RETF imm (0xCA) */
case 0xCB: G.ip = pop16(); G.cs = pop16(); break;
case 0xCA: { uint16_t n = fetch_word(); G.ip = pop16(); G.cs = pop16(); G.sp += n; break; }
/* PUSHF / POPF */
case 0x9C: push16(G.flags); break;
case 0x9D: G.flags = (pop16() | F_RESERVED_ON) & ~F_RESERVED_OFF; break;
/* CLC / STC / CLI / STI / CLD / STD / CMC */
case 0xF8: G.flags &= ~F_CF; break;
case 0xF9: G.flags |= F_CF; break;
case 0xFA: G.flags &= ~F_IF; break;
case 0xFB: G.flags |= F_IF; break;
case 0xFC: G.flags &= ~F_DF; break;
case 0xFD: G.flags |= F_DF; break;
case 0xF5: G.flags ^= F_CF; break;
/* HLT */
case 0xF4: G.halted = true; break;
/* XCHG r/m, r */
case 0x86: { uint8_t modrm = fetch_byte(); uint8_t* r = reg8_ptr((modrm>>3)&7);
uint8_t a = rm8_read(modrm); uint8_t b = *r;
rm8_write(modrm, b); *r = a; break; }
case 0x87: { uint8_t modrm = fetch_byte(); uint16_t* r = reg16_ptr((modrm>>3)&7);
uint16_t a = rm16_read(modrm); uint16_t b = *r;
rm16_write(modrm, b); *r = a; break; }
/* TEST r/m, r — same as AND but discards result */
case 0x84: { uint8_t modrm = fetch_byte(); uint8_t a = rm8_read(modrm);
uint8_t b = *reg8_ptr((modrm>>3)&7);
(void)alu_and8(a, b); break; }
case 0x85: { uint8_t modrm = fetch_byte(); uint16_t a = rm16_read(modrm);
uint16_t b = *reg16_ptr((modrm>>3)&7);
(void)alu_and16(a, b); break; }
case 0xA8: (void)alu_and8(G.al, fetch_byte()); break; /* TEST AL, imm8 */
case 0xA9: (void)alu_and16(G.ax, fetch_word()); break; /* TEST AX, imm16 */
/* LDS / LES — load far pointer DS:r16 / ES:r16 from r/m32 */
case 0xC4: case 0xC5: {
uint8_t modrm = fetch_byte();
uint8_t mod = (modrm >> 6) & 3;
uint8_t rm = modrm & 7;
if (mod == 3) break; /* invalid; manual: undefined */
int seg;
uint16_t ea = calc_ea(mod, rm, &seg);
uint16_t off = mem_read_word(seg, ea);
uint16_t s = mem_read_word(seg, ea + 2);
*reg16_ptr((modrm >> 3) & 7) = off;
if (op == 0xC4) G.es = s; else G.ds = s;
break;
}
/* LAHF / SAHF */
case 0x9F: G.ah = (uint8_t)(G.flags & 0xFF); break;
case 0x9E: G.flags = (G.flags & ~0xFF) | G.ah; break;
/* XLAT — AL ← [BX + AL] (DS, override-respecting) */
case 0xD7: G.al = mem_read_byte(SEG_DS, (uint16_t)(G.bx + G.al)); break;
/* BCD adjust */
case 0x27: daa_op(); break;
case 0x2F: das_op(); break;
case 0x37: aaa_op(); break;
case 0x3F: aas_op(); break;
case 0xD4: aam_op(fetch_byte()); break;
case 0xD5: aad_op(fetch_byte()); break;
/* Port I/O */
case 0xE4: G.al = bus_read_byte(fetch_byte(), true); break;
case 0xE5: G.ax = bus_read_word(fetch_byte(), true); break;
case 0xE6: bus_write_byte(fetch_byte(), G.al, true); break;
case 0xE7: bus_write_word(fetch_byte(), G.ax, true); break;
case 0xEC: G.al = bus_read_byte(G.dx, true); break;
case 0xED: G.ax = bus_read_word(G.dx, true); break;
case 0xEE: bus_write_byte(G.dx, G.al, true); break;
case 0xEF: bus_write_word(G.dx, G.ax, true); break;
/* Software / hardware interrupts */
case 0xCC: do_int(3); break;
case 0xCD: do_int(fetch_byte()); break;
case 0xCE: if (G.flags & F_OF) do_int(4); break; /* INTO */
case 0xCF: { /* IRET */
G.ip = pop16();
G.cs = pop16();
G.flags = (pop16() | F_RESERVED_ON) & ~F_RESERVED_OFF;
break;
}
/* Group 2: shift/rotate r/m by 1 or CL */
case 0xD0: { uint8_t modrm = fetch_byte(); exec_shift_byte(modrm, 1); break; }
case 0xD1: { uint8_t modrm = fetch_byte(); exec_shift_word(modrm, 1); break; }
case 0xD2: { uint8_t modrm = fetch_byte(); exec_shift_byte(modrm, G.cl); break; }
case 0xD3: { uint8_t modrm = fetch_byte(); exec_shift_word(modrm, G.cl); break; }
/* Group 3: TEST/NOT/NEG/MUL/IMUL/DIV/IDIV */
case 0xF6: { uint8_t modrm = fetch_byte(); exec_group3_byte(modrm); break; }
case 0xF7: { uint8_t modrm = fetch_byte(); exec_group3_word(modrm); break; }
/* Group 4: INC/DEC r/m8 */
case 0xFE: { uint8_t modrm = fetch_byte(); exec_group4(modrm); break; }
/* Undocumented (8086 only) */
case 0x0F: G.cs = pop16(); break; /* POP CS */
case 0xD6: G.al = (G.flags & F_CF) ? 0xFF : 0x00; break; /* SALC */
/* PUSH segment regs */
case 0x06: push16(G.es); break;
case 0x0E: push16(G.cs); break;
case 0x16: push16(G.ss); break;
case 0x1E: push16(G.ds); break;
/* POP segment regs */
case 0x07: G.es = pop16(); break;
case 0x17: G.ss = pop16(); break;
case 0x1F: G.ds = pop16(); break;
/* Group 5 (0xFF) — INC/DEC/CALL/JMP/PUSH r/m16 */
case 0xFF: { uint8_t modrm = fetch_byte(); exec_group5_word(modrm); break; }
/* LOOP / LOOPE / LOOPNE / JCXZ — 0xE0..0xE3 */
case 0xE0: { int8_t d = (int8_t)fetch_byte(); G.cx--; if (G.cx != 0 && !(G.flags & F_ZF)) G.ip = (uint16_t)(G.ip + d); break; }
case 0xE1: { int8_t d = (int8_t)fetch_byte(); G.cx--; if (G.cx != 0 && (G.flags & F_ZF)) G.ip = (uint16_t)(G.ip + d); break; }
case 0xE2: { int8_t d = (int8_t)fetch_byte(); G.cx--; if (G.cx != 0) G.ip = (uint16_t)(G.ip + d); break; }
case 0xE3: { int8_t d = (int8_t)fetch_byte(); if (G.cx == 0) G.ip = (uint16_t)(G.ip + d); break; }
default:
/* Unimplemented opcode — treat as NOP. Logged as a TODO via
vx_log so users know which features remain. */
break;
}
}
/* ─── Reset / pin watchers / clock ──────────────────────────────────────── */
static void reset_state(void) {
G.cs = 0xFFFF;
G.ds = G.ss = G.es = 0;
G.ip = 0;
G.flags = F_RESERVED_ON;
G.ax = G.bx = G.cx = G.dx = 0;
G.sp = G.bp = G.si = G.di = 0;
G.halted = false;
G.seg_override = -1;
/* Idle bus */
vx_pin_write(G.ale, 0);
vx_pin_write(G.rd, 1);
vx_pin_write(G.wr, 1);
vx_pin_write(G.den, 1);
vx_pin_write(G.dtr, 1);
vx_pin_write(G.mio, 1);
vx_pin_write(G.bhe, 1);
vx_pin_write(G.hlda, 0);
vx_pin_write(G.inta, 1);
release_ad();
}
static void on_reset(void* user_data, vx_pin pin, int value) {
(void)user_data; (void)pin;
if (value) {
G.reset_active = true;
reset_state();
} else {
G.reset_active = false;
}
}
static void on_nmi(void* user_data, vx_pin pin, int value) {
(void)user_data; (void)pin; (void)value;
G.nmi_pending = true; /* edge-triggered; latched until serviced */
}
static void on_intr(void* user_data, vx_pin pin, int value) {
(void)user_data; (void)pin;
G.intr_line = (value != 0); /* level-triggered */
}
static void on_clock(void* user_data) {
(void)user_data;
if (G.reset_active) return;
if (G.halted) return;
if (vx_pin_read(G.ready) == 0) return; /* wait state */
if (vx_pin_read(G.hold) == 1) { /* bus hold */
vx_pin_write(G.hlda, 1);
return;
}
vx_pin_write(G.hlda, 0);
step();
}
void chip_setup(void) {
char name[6];
for (int i = 0; i < 16; i++) {
name[0]='A'; name[1]='D';
if (i<10) { name[2]='0'+i; name[3]=0; }
else { name[2]='1'; name[3]='0'+(i-10); name[4]=0; }
G.ad[i] = vx_pin_register(name, VX_INPUT);
}
for (int i = 0; i < 4; i++) {
name[0]='A'; name[1]='1'; name[2]='6'+i; name[3]=0;
G.a[i] = vx_pin_register(name, VX_OUTPUT_LOW);
}
G.ale = vx_pin_register("ALE", VX_OUTPUT_LOW);
G.rd = vx_pin_register("RD", VX_OUTPUT_HIGH);
G.wr = vx_pin_register("WR", VX_OUTPUT_HIGH);
G.mio = vx_pin_register("MIO", VX_OUTPUT_HIGH);
G.dtr = vx_pin_register("DTR", VX_OUTPUT_HIGH);
G.den = vx_pin_register("DEN", VX_OUTPUT_HIGH);
G.hold = vx_pin_register("HOLD", VX_INPUT);
G.hlda = vx_pin_register("HLDA", VX_OUTPUT_LOW);
G.intr = vx_pin_register("INTR", VX_INPUT);
G.nmi = vx_pin_register("NMI", VX_INPUT);
G.inta = vx_pin_register("INTA", VX_OUTPUT_HIGH);
G.reset_= vx_pin_register("RESET", VX_INPUT);
G.ready = vx_pin_register("READY", VX_INPUT);
G.test_ = vx_pin_register("TEST", VX_INPUT);
G.clk = vx_pin_register("CLK", VX_INPUT);
G.mnmx = vx_pin_register("MNMX", VX_INPUT);
G.bhe = vx_pin_register("BHE", VX_OUTPUT_HIGH);
G.vcc = vx_pin_register("VCC", VX_INPUT);
G.gnd = vx_pin_register("GND", VX_INPUT);
reset_state();
G.reset_active = true;
vx_pin_watch(G.reset_, VX_EDGE_BOTH, on_reset, 0);
vx_pin_watch(G.nmi, VX_EDGE_RISING, on_nmi, 0);
vx_pin_watch(G.intr, VX_EDGE_BOTH, on_intr, 0);
/* Run an instruction per timer fire. 200 ns ≈ 5 MHz pseudo-clock; the
test's CLOCK_NS matches. */
G.cycle_timer = vx_timer_create(on_clock, 0);
vx_timer_start(G.cycle_timer, 200, true);
}