1
0
mirror of https://github.com/TomHarte/CLK.git synced 2024-07-22 00:29:41 +00:00

Merge pull request #1193 from TomHarte/8088Intentions

Work towards x86 access violations.
This commit is contained in:
Thomas Harte 2023-11-02 16:46:36 -04:00 committed by GitHub
commit 18820644b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 1061 additions and 897 deletions

View File

@ -138,7 +138,7 @@ std::pair<int, typename Decoder<model>::InstructionT> Decoder<model>::decode(con
PartialBlock(0x00, ADD); break;
case 0x06: Complete(PUSH, ES, None, data_size_); break;
case 0x07: Complete(POP, ES, None, data_size_); break;
case 0x07: Complete(POP, None, ES, data_size_); break;
PartialBlock(0x08, OR); break;
case 0x0e: Complete(PUSH, CS, None, data_size_); break;
@ -147,7 +147,7 @@ std::pair<int, typename Decoder<model>::InstructionT> Decoder<model>::decode(con
// prefixed with $0f.
case 0x0f:
if constexpr (model < Model::i80286) {
Complete(POP, CS, None, data_size_);
Complete(POP, None, CS, data_size_);
} else {
phase_ = Phase::InstructionPageF;
}
@ -155,11 +155,11 @@ std::pair<int, typename Decoder<model>::InstructionT> Decoder<model>::decode(con
PartialBlock(0x10, ADC); break;
case 0x16: Complete(PUSH, SS, None, DataSize::Word); break;
case 0x17: Complete(POP, SS, None, DataSize::Word); break;
case 0x17: Complete(POP, None, SS, DataSize::Word); break;
PartialBlock(0x18, SBB); break;
case 0x1e: Complete(PUSH, DS, None, DataSize::Word); break;
case 0x1f: Complete(POP, DS, None, DataSize::Word); break;
case 0x1f: Complete(POP, None, DS, DataSize::Word); break;
PartialBlock(0x20, AND); break;
case 0x26: segment_override_ = Source::ES; break;
@ -523,145 +523,147 @@ std::pair<int, typename Decoder<model>::InstructionT> Decoder<model>::decode(con
// MARK: - Additional F page of instructions.
if(phase_ == Phase::InstructionPageF && source != end) {
// Update the instruction acquired.
const uint8_t instr = *source;
++source;
++consumed_;
if constexpr (model >= Model::i80286) {
if(phase_ == Phase::InstructionPageF && source != end) {
// Update the instruction acquired.
const uint8_t instr = *source;
++source;
++consumed_;
// NB: to reach here, the instruction set must be at least
// that of an 80286.
switch(instr) {
default: undefined();
// NB: to reach here, the instruction set must be at least
// that of an 80286.
switch(instr) {
default: undefined();
case 0x00: MemRegReg(Invalid, MemRegSLDT_to_VERW, data_size_); break;
case 0x01: MemRegReg(Invalid, MemRegSGDT_to_LMSW, data_size_); break;
case 0x02: MemRegReg(LAR, Reg_MemReg, data_size_); break;
case 0x03: MemRegReg(LSL, Reg_MemReg, data_size_); break;
case 0x05:
Requires(i80286);
Complete(LOADALL, None, None, DataSize::Byte);
break;
case 0x06: Complete(CLTS, None, None, DataSize::Byte); break;
case 0x00: MemRegReg(Invalid, MemRegSLDT_to_VERW, data_size_); break;
case 0x01: MemRegReg(Invalid, MemRegSGDT_to_LMSW, data_size_); break;
case 0x02: MemRegReg(LAR, Reg_MemReg, data_size_); break;
case 0x03: MemRegReg(LSL, Reg_MemReg, data_size_); break;
case 0x05:
Requires(i80286);
Complete(LOADALL, None, None, DataSize::Byte);
break;
case 0x06: Complete(CLTS, None, None, DataSize::Byte); break;
case 0x20:
RequiresMin(i80386);
MemRegReg(MOVfromCr, Reg_MemReg, DataSize::DWord);
break;
case 0x21:
RequiresMin(i80386);
MemRegReg(MOVfromDr, Reg_MemReg, DataSize::DWord);
break;
case 0x22:
RequiresMin(i80386);
MemRegReg(MOVtoCr, Reg_MemReg, DataSize::DWord);
break;
case 0x23:
RequiresMin(i80386);
MemRegReg(MOVtoDr, Reg_MemReg, DataSize::DWord);
break;
case 0x24:
RequiresMin(i80386);
MemRegReg(MOVfromTr, Reg_MemReg, DataSize::DWord);
break;
case 0x26:
RequiresMin(i80386);
MemRegReg(MOVtoTr, Reg_MemReg, DataSize::DWord);
break;
case 0x20:
RequiresMin(i80386);
MemRegReg(MOVfromCr, Reg_MemReg, DataSize::DWord);
break;
case 0x21:
RequiresMin(i80386);
MemRegReg(MOVfromDr, Reg_MemReg, DataSize::DWord);
break;
case 0x22:
RequiresMin(i80386);
MemRegReg(MOVtoCr, Reg_MemReg, DataSize::DWord);
break;
case 0x23:
RequiresMin(i80386);
MemRegReg(MOVtoDr, Reg_MemReg, DataSize::DWord);
break;
case 0x24:
RequiresMin(i80386);
MemRegReg(MOVfromTr, Reg_MemReg, DataSize::DWord);
break;
case 0x26:
RequiresMin(i80386);
MemRegReg(MOVtoTr, Reg_MemReg, DataSize::DWord);
break;
case 0x70: RequiresMin(i80386); Displacement(JO, data_size_); break;
case 0x71: RequiresMin(i80386); Displacement(JNO, data_size_); break;
case 0x72: RequiresMin(i80386); Displacement(JB, data_size_); break;
case 0x73: RequiresMin(i80386); Displacement(JNB, data_size_); break;
case 0x74: RequiresMin(i80386); Displacement(JZ, data_size_); break;
case 0x75: RequiresMin(i80386); Displacement(JNZ, data_size_); break;
case 0x76: RequiresMin(i80386); Displacement(JBE, data_size_); break;
case 0x77: RequiresMin(i80386); Displacement(JNBE, data_size_); break;
case 0x78: RequiresMin(i80386); Displacement(JS, data_size_); break;
case 0x79: RequiresMin(i80386); Displacement(JNS, data_size_); break;
case 0x7a: RequiresMin(i80386); Displacement(JP, data_size_); break;
case 0x7b: RequiresMin(i80386); Displacement(JNP, data_size_); break;
case 0x7c: RequiresMin(i80386); Displacement(JL, data_size_); break;
case 0x7d: RequiresMin(i80386); Displacement(JNL, data_size_); break;
case 0x7e: RequiresMin(i80386); Displacement(JLE, data_size_); break;
case 0x7f: RequiresMin(i80386); Displacement(JNLE, data_size_); break;
case 0x70: RequiresMin(i80386); Displacement(JO, data_size_); break;
case 0x71: RequiresMin(i80386); Displacement(JNO, data_size_); break;
case 0x72: RequiresMin(i80386); Displacement(JB, data_size_); break;
case 0x73: RequiresMin(i80386); Displacement(JNB, data_size_); break;
case 0x74: RequiresMin(i80386); Displacement(JZ, data_size_); break;
case 0x75: RequiresMin(i80386); Displacement(JNZ, data_size_); break;
case 0x76: RequiresMin(i80386); Displacement(JBE, data_size_); break;
case 0x77: RequiresMin(i80386); Displacement(JNBE, data_size_); break;
case 0x78: RequiresMin(i80386); Displacement(JS, data_size_); break;
case 0x79: RequiresMin(i80386); Displacement(JNS, data_size_); break;
case 0x7a: RequiresMin(i80386); Displacement(JP, data_size_); break;
case 0x7b: RequiresMin(i80386); Displacement(JNP, data_size_); break;
case 0x7c: RequiresMin(i80386); Displacement(JL, data_size_); break;
case 0x7d: RequiresMin(i80386); Displacement(JNL, data_size_); break;
case 0x7e: RequiresMin(i80386); Displacement(JLE, data_size_); break;
case 0x7f: RequiresMin(i80386); Displacement(JNLE, data_size_); break;
#define Set(x) \
RequiresMin(i80386); \
MemRegReg(SET##x, MemRegSingleOperand, DataSize::Byte);
case 0x90: Set(O); break;
case 0x91: Set(NO); break;
case 0x92: Set(B); break;
case 0x93: Set(NB); break;
case 0x94: Set(Z); break;
case 0x95: Set(NZ); break;
case 0x96: Set(BE); break;
case 0x97: Set(NBE); break;
case 0x98: Set(S); break;
case 0x99: Set(NS); break;
case 0x9a: Set(P); break;
case 0x9b: Set(NP); break;
case 0x9c: Set(L); break;
case 0x9d: Set(NL); break;
case 0x9e: Set(LE); break;
case 0x9f: Set(NLE); break;
case 0x90: Set(O); break;
case 0x91: Set(NO); break;
case 0x92: Set(B); break;
case 0x93: Set(NB); break;
case 0x94: Set(Z); break;
case 0x95: Set(NZ); break;
case 0x96: Set(BE); break;
case 0x97: Set(NBE); break;
case 0x98: Set(S); break;
case 0x99: Set(NS); break;
case 0x9a: Set(P); break;
case 0x9b: Set(NP); break;
case 0x9c: Set(L); break;
case 0x9d: Set(NL); break;
case 0x9e: Set(LE); break;
case 0x9f: Set(NLE); break;
#undef Set
case 0xa0: RequiresMin(i80386); Complete(PUSH, FS, None, data_size_); break;
case 0xa1: RequiresMin(i80386); Complete(POP, FS, None, data_size_); break;
case 0xa3: RequiresMin(i80386); MemRegReg(BT, MemReg_Reg, data_size_); break;
case 0xa4:
RequiresMin(i80386);
MemRegReg(SHLDimm, Reg_MemReg, data_size_);
operand_size_ = DataSize::Byte;
break;
case 0xa5:
RequiresMin(i80386);
MemRegReg(SHLDCL, MemReg_Reg, data_size_);
break;
case 0xa8: RequiresMin(i80386); Complete(PUSH, GS, None, data_size_); break;
case 0xa9: RequiresMin(i80386); Complete(POP, GS, None, data_size_); break;
case 0xab: RequiresMin(i80386); MemRegReg(BTS, MemReg_Reg, data_size_); break;
case 0xac:
RequiresMin(i80386);
MemRegReg(SHRDimm, Reg_MemReg, data_size_);
operand_size_ = DataSize::Byte;
break;
case 0xad:
RequiresMin(i80386);
MemRegReg(SHRDCL, MemReg_Reg, data_size_);
break;
case 0xaf:
RequiresMin(i80386);
MemRegReg(IMUL_2, Reg_MemReg, data_size_);
break;
case 0xa0: RequiresMin(i80386); Complete(PUSH, FS, None, data_size_); break;
case 0xa1: RequiresMin(i80386); Complete(POP, None, FS, data_size_); break;
case 0xa3: RequiresMin(i80386); MemRegReg(BT, MemReg_Reg, data_size_); break;
case 0xa4:
RequiresMin(i80386);
MemRegReg(SHLDimm, Reg_MemReg, data_size_);
operand_size_ = DataSize::Byte;
break;
case 0xa5:
RequiresMin(i80386);
MemRegReg(SHLDCL, MemReg_Reg, data_size_);
break;
case 0xa8: RequiresMin(i80386); Complete(PUSH, GS, None, data_size_); break;
case 0xa9: RequiresMin(i80386); Complete(POP, None, GS, data_size_); break;
case 0xab: RequiresMin(i80386); MemRegReg(BTS, MemReg_Reg, data_size_); break;
case 0xac:
RequiresMin(i80386);
MemRegReg(SHRDimm, Reg_MemReg, data_size_);
operand_size_ = DataSize::Byte;
break;
case 0xad:
RequiresMin(i80386);
MemRegReg(SHRDCL, MemReg_Reg, data_size_);
break;
case 0xaf:
RequiresMin(i80386);
MemRegReg(IMUL_2, Reg_MemReg, data_size_);
break;
case 0xb2: RequiresMin(i80386); MemRegReg(LSS, Reg_MemReg, data_size_); break;
case 0xb3: RequiresMin(i80386); MemRegReg(BTR, MemReg_Reg, data_size_); break;
case 0xb4: RequiresMin(i80386); MemRegReg(LFS, Reg_MemReg, data_size_); break;
case 0xb5: RequiresMin(i80386); MemRegReg(LGS, Reg_MemReg, data_size_); break;
case 0xb6:
RequiresMin(i80386);
MemRegReg(MOVZX, Reg_MemReg, DataSize::Byte);
break;
case 0xb7:
RequiresMin(i80386);
MemRegReg(MOVZX, Reg_MemReg, DataSize::Word);
break;
case 0xba: RequiresMin(i80386); MemRegReg(Invalid, MemRegBT_to_BTC, data_size_); break;
case 0xbb: RequiresMin(i80386); MemRegReg(BTC, MemReg_Reg, data_size_); break;
case 0xbc: RequiresMin(i80386); MemRegReg(BSF, MemReg_Reg, data_size_); break;
case 0xbd: RequiresMin(i80386); MemRegReg(BSR, MemReg_Reg, data_size_); break;
case 0xbe:
RequiresMin(i80386);
MemRegReg(MOVSX, Reg_MemReg, DataSize::Byte);
break;
case 0xbf:
RequiresMin(i80386);
MemRegReg(MOVSX, Reg_MemReg, DataSize::Word);
break;
case 0xb2: RequiresMin(i80386); MemRegReg(LSS, Reg_MemReg, data_size_); break;
case 0xb3: RequiresMin(i80386); MemRegReg(BTR, MemReg_Reg, data_size_); break;
case 0xb4: RequiresMin(i80386); MemRegReg(LFS, Reg_MemReg, data_size_); break;
case 0xb5: RequiresMin(i80386); MemRegReg(LGS, Reg_MemReg, data_size_); break;
case 0xb6:
RequiresMin(i80386);
MemRegReg(MOVZX, Reg_MemReg, DataSize::Byte);
break;
case 0xb7:
RequiresMin(i80386);
MemRegReg(MOVZX, Reg_MemReg, DataSize::Word);
break;
case 0xba: RequiresMin(i80386); MemRegReg(Invalid, MemRegBT_to_BTC, data_size_); break;
case 0xbb: RequiresMin(i80386); MemRegReg(BTC, MemReg_Reg, data_size_); break;
case 0xbc: RequiresMin(i80386); MemRegReg(BSF, MemReg_Reg, data_size_); break;
case 0xbd: RequiresMin(i80386); MemRegReg(BSR, MemReg_Reg, data_size_); break;
case 0xbe:
RequiresMin(i80386);
MemRegReg(MOVSX, Reg_MemReg, DataSize::Byte);
break;
case 0xbf:
RequiresMin(i80386);
MemRegReg(MOVSX, Reg_MemReg, DataSize::Word);
break;
}
}
}
@ -979,18 +981,20 @@ std::pair<int, typename Decoder<model>::InstructionT> Decoder<model>::decode(con
// MARK: - ScaleIndexBase
if(phase_ == Phase::ScaleIndexBase && source != end) {
sib_ = *source;
++source;
++consumed_;
if constexpr (is_32bit(model)) {
if(phase_ == Phase::ScaleIndexBase && source != end) {
sib_ = *source;
++source;
++consumed_;
// Potentially record the lack of a base.
if(displacement_size_ == DataSize::None && (uint8_t(sib_)&7) == 5) {
source_ = (source_ == Source::Indirect) ? Source::IndirectNoBase : source_;
destination_ = (destination_ == Source::Indirect) ? Source::IndirectNoBase : destination_;
// Potentially record the lack of a base.
if(displacement_size_ == DataSize::None && (uint8_t(sib_)&7) == 5) {
source_ = (source_ == Source::Indirect) ? Source::IndirectNoBase : source_;
destination_ = (destination_ == Source::Indirect) ? Source::IndirectNoBase : destination_;
}
phase_ = (displacement_size_ != DataSize::None || operand_size_ != DataSize::None) ? Phase::DisplacementOrOperand : Phase::ReadyToPost;
}
phase_ = (displacement_size_ != DataSize::None || operand_size_ != DataSize::None) ? Phase::DisplacementOrOperand : Phase::ReadyToPost;
}
// MARK: - Displacement and operand.
@ -1041,6 +1045,18 @@ std::pair<int, typename Decoder<model>::InstructionT> Decoder<model>::decode(con
// MARK: - Check for completion.
if(phase_ == Phase::ReadyToPost) {
// TODO: map to #UD where applicable; build LOCK into the Operation type, buying an extra bit for the operation?
//
// As of the P6 Intel stipulates that:
//
// "The LOCK prefix can be prepended only to the following instructions and to those forms of the instructions
// that use a memory operand: ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR,
// XADD, and XCHG."
//
// ... and the #UD exception will be raised if LOCK is encountered elsewhere. So adding 17 additional
// operations would unlock an extra bit of storage for a net gain of 239 extra operation types and thereby
// alleviating any concerns over whether there'll be space to handle MMX, floating point extensions, etc.
const auto result = std::make_pair(
consumed_,
InstructionT(

File diff suppressed because it is too large Load Diff

View File

@ -163,7 +163,7 @@ enum class Operation: uint8_t {
XOR,
/// NOP; no further fields.
NOP,
/// POP from the stack to source.
/// POP from the stack to destination.
POP,
/// POP from the stack to the flags register.
POPF,

View File

@ -15,24 +15,59 @@
namespace InstructionSet::x86 {
/// Explains the type of access that `perform` intends to perform; is provided as a template parameter to whatever
/// the caller supplies as `MemoryT` and `RegistersT` when obtaining a reference to whatever the processor
/// intends to reference.
///
/// `perform` guarantees to validate all accesses before modifying any state, giving the caller opportunity to generate
/// any exceptions that might be applicable.
enum class AccessType {
/// The requested value will be read from.
Read,
/// The requested value will be written to.
Write,
/// The requested value will be read from and then written to.
ReadModifyWrite,
/// The requested value has already been authorised for whatever form of access is now intended, so there's no
/// need further to inspect. This is done e.g. by operations that will push multiple values to the stack to verify that
/// all necessary stack space is available ahead of pushing anything, though each individual push will then result in
/// a further `Preauthorised` access.
PreauthorisedRead,
PreauthorisedWrite,
};
template <
Model model_,
typename FlowControllerT,
typename RegistersT,
typename MemoryT,
typename IOT
> struct ExecutionContext {
FlowControllerT flow_controller;
Status status;
RegistersT registers;
MemoryT memory;
IOT io;
static constexpr Model model = model_;
};
/// Performs @c instruction querying @c registers and/or @c memory as required, using @c io for port input/output,
/// and providing any flow control effects to @c flow_controller.
///
/// Any change in processor status will be applied to @c status.
template <
Model model,
typename InstructionT,
typename FlowControllerT,
typename RegistersT,
typename MemoryT,
typename IOT
typename ContextT
> void perform(
const InstructionT &instruction,
Status &status,
FlowControllerT &flow_controller,
RegistersT &registers,
MemoryT &memory,
IOT &io
ContextT &context
);
template <
typename ContextT
> void interrupt(
int index,
ContextT &context
);
}

View File

@ -14,6 +14,8 @@
#include <iostream>
#include <sstream>
#include <fstream>
#include <unordered_map>
#include <unordered_set>
#include "NSData+dataWithContentsOfGZippedFile.h"
@ -91,110 +93,193 @@ struct Registers {
}
};
struct Memory {
enum class Tag {
Seeded,
AccessExpected,
Accessed,
FlagsL,
FlagsH
};
public:
using AccessType = InstructionSet::x86::AccessType;
std::unordered_map<uint32_t, Tag> tags;
std::vector<uint8_t> memory;
const Registers &registers_;
template <typename IntT, AccessType type> struct ReturnType;
Memory(Registers &registers) : registers_(registers) {
memory.resize(1024*1024);
}
// Reads: return a value directly.
template <typename IntT> struct ReturnType<IntT, AccessType::Read> { using type = IntT; };
template <typename IntT> struct ReturnType<IntT, AccessType::PreauthorisedRead> { using type = IntT; };
void clear() {
tags.clear();
}
// Writes: return a reference.
template <typename IntT> struct ReturnType<IntT, AccessType::Write> { using type = IntT &; };
template <typename IntT> struct ReturnType<IntT, AccessType::ReadModifyWrite> { using type = IntT &; };
template <typename IntT> struct ReturnType<IntT, AccessType::PreauthorisedWrite> { using type = IntT &; };
void seed(uint32_t address, uint8_t value) {
memory[address] = value;
tags[address] = Tag::Seeded;
}
void touch(uint32_t address) {
tags[address] = Tag::AccessExpected;
}
uint32_t segment_base(InstructionSet::x86::Source segment) {
uint32_t physical_address;
using Source = InstructionSet::x86::Source;
switch(segment) {
default: physical_address = registers_.ds_; break;
case Source::ES: physical_address = registers_.es_; break;
case Source::CS: physical_address = registers_.cs_; break;
case Source::SS: physical_address = registers_.ss_; break;
// Constructor.
Memory(Registers &registers) : registers_(registers) {
memory.resize(1024*1024);
}
return physical_address << 4;
}
// Entry point used by the flow controller so that it can mark up locations at which the flags were written,
// so that defined-flag-only masks can be applied while verifying RAM contents.
template <typename IntT> IntT &access(InstructionSet::x86::Source segment, uint16_t address, Tag tag) {
const uint32_t physical_address = (segment_base(segment) + address) & 0xf'ffff;
return access<IntT>(physical_address, tag);
}
// Initialisation.
void clear() {
tags.clear();
}
// An additional entry point for the flow controller; on the original 8086 interrupt vectors aren't relative
// to a selector, they're just at an absolute location.
template <typename IntT> IntT &access(uint32_t address, Tag tag) {
// Check for address wraparound
if(address >= 0x10'0001 - sizeof(IntT)) {
if constexpr (std::is_same_v<IntT, uint8_t>) {
address &= 0xf'ffff;
} else {
if(address == 0xf'ffff) {
// This is a 16-bit access comprising the final byte in memory and the first.
write_back_address_[0] = address;
write_back_address_[1] = 0;
void seed(uint32_t address, uint8_t value) {
memory[address] = value;
tags[address] = Tag::Seeded;
}
void touch(uint32_t address) {
tags[address] = Tag::AccessExpected;
}
// Preauthorisation call-ins.
void preauthorise_stack_write(uint32_t length) {
uint16_t sp = registers_.sp_;
while(length--) {
--sp;
preauthorise(InstructionSet::x86::Source::SS, sp);
}
}
void preauthorise_stack_read(uint32_t length) {
uint16_t sp = registers_.sp_;
while(length--) {
preauthorise(InstructionSet::x86::Source::SS, sp);
++sp;
}
}
void preauthorise_read(InstructionSet::x86::Source segment, uint16_t start, uint32_t length) {
while(length--) {
preauthorise(segment, start);
++start;
}
}
void preauthorise_read(uint32_t start, uint32_t length) {
while(length--) {
preauthorise(start);
++start;
}
}
// Access call-ins.
// Accesses an address based on segment:offset.
template <typename IntT, AccessType type>
typename ReturnType<IntT, type>::type &access(InstructionSet::x86::Source segment, uint32_t address) {
if constexpr (std::is_same_v<IntT, uint16_t>) {
// If this is a 16-bit access that runs past the end of the segment, it'll wrap back
// to the start. So the 16-bit value will need to be a local cache.
if(address == 0xffff) {
write_back_address_[0] = (segment_base(segment) + address) & 0xf'ffff;
write_back_address_[1] = (write_back_address_[0] - 65535) & 0xf'ffff;
write_back_value_ = memory[write_back_address_[0]] | (memory[write_back_address_[1]] << 8);
return write_back_value_;
} else {
address &= 0xf'ffff;
}
}
auto &value = access<IntT, type>(segment, address, Tag::Accessed);
// If the CPU has indicated a write, it should be safe to fuzz the value now.
if(type == AccessType::Write) {
value = IntT(~0);
}
return value;
}
// Accesses an address based on physical location.
template <typename IntT, AccessType type>
typename ReturnType<IntT, type>::type &access(uint32_t address) {
return access<IntT, type>(address, Tag::Accessed);
}
template <typename IntT>
void write_back() {
if constexpr (std::is_same_v<IntT, uint16_t>) {
if(write_back_address_[0] != NoWriteBack) {
memory[write_back_address_[0]] = write_back_value_ & 0xff;
memory[write_back_address_[1]] = write_back_value_ >> 8;
write_back_address_[0] = 0;
}
}
}
if(tags.find(address) == tags.end()) {
printf("Access to unexpected RAM address");
}
tags[address] = tag;
return *reinterpret_cast<IntT *>(&memory[address]);
}
private:
enum class Tag {
Seeded,
AccessExpected,
Accessed,
};
// Entry point for the 8086; simply notes that memory was accessed.
template <typename IntT> IntT &access([[maybe_unused]] InstructionSet::x86::Source segment, uint32_t address) {
if constexpr (std::is_same_v<IntT, uint16_t>) {
// If this is a 16-bit access that runs past the end of the segment, it'll wrap back
// to the start. So the 16-bit value will need to be a local cache.
if(address == 0xffff) {
write_back_address_[0] = (segment_base(segment) + address) & 0xf'ffff;
write_back_address_[1] = (write_back_address_[0] - 65535) & 0xf'ffff;
write_back_value_ = memory[write_back_address_[0]] | (memory[write_back_address_[1]] << 8);
return write_back_value_;
std::unordered_set<uint32_t> preauthorisations;
std::unordered_map<uint32_t, Tag> tags;
std::vector<uint8_t> memory;
const Registers &registers_;
void preauthorise(uint32_t address) {
preauthorisations.insert(address);
}
void preauthorise(InstructionSet::x86::Source segment, uint16_t address) {
preauthorise((segment_base(segment) + address) & 0xf'ffff);
}
bool test_preauthorisation(uint32_t address) {
auto authorisation = preauthorisations.find(address);
if(authorisation == preauthorisations.end()) {
return false;
}
preauthorisations.erase(authorisation);
return true;
}
return access<IntT>(segment, address, Tag::Accessed);
}
template <typename IntT>
void write_back() {
if constexpr (std::is_same_v<IntT, uint16_t>) {
if(write_back_address_[0] != NoWriteBack) {
memory[write_back_address_[0]] = write_back_value_ & 0xff;
memory[write_back_address_[1]] = write_back_value_ >> 8;
write_back_address_[0] = 0;
uint32_t segment_base(InstructionSet::x86::Source segment) {
uint32_t physical_address;
using Source = InstructionSet::x86::Source;
switch(segment) {
default: physical_address = registers_.ds_; break;
case Source::ES: physical_address = registers_.es_; break;
case Source::CS: physical_address = registers_.cs_; break;
case Source::SS: physical_address = registers_.ss_; break;
}
return physical_address << 4;
}
}
static constexpr uint32_t NoWriteBack = 0; // A low byte address of 0 can't require write-back.
uint32_t write_back_address_[2] = {NoWriteBack, NoWriteBack};
uint16_t write_back_value_;
// Entry point used by the flow controller so that it can mark up locations at which the flags were written,
// so that defined-flag-only masks can be applied while verifying RAM contents.
template <typename IntT, AccessType type>
typename ReturnType<IntT, type>::type &access(InstructionSet::x86::Source segment, uint16_t address, Tag tag) {
const uint32_t physical_address = (segment_base(segment) + address) & 0xf'ffff;
return access<IntT, type>(physical_address, tag);
}
// An additional entry point for the flow controller; on the original 8086 interrupt vectors aren't relative
// to a selector, they're just at an absolute location.
template <typename IntT, AccessType type>
typename ReturnType<IntT, type>::type &access(uint32_t address, Tag tag) {
if constexpr (type == AccessType::PreauthorisedRead || type == AccessType::PreauthorisedWrite) {
if(!test_preauthorisation(address)) {
printf("Non preauthorised access\n");
}
}
// Check for address wraparound
if(address > 0x10'0000 - sizeof(IntT)) {
if constexpr (std::is_same_v<IntT, uint8_t>) {
address &= 0xf'ffff;
} else {
address &= 0xf'ffff;
if(address == 0xf'ffff) {
// This is a 16-bit access comprising the final byte in memory and the first.
write_back_address_[0] = address;
write_back_address_[1] = 0;
write_back_value_ = memory[write_back_address_[0]] | (memory[write_back_address_[1]] << 8);
return write_back_value_;
}
}
}
if(tags.find(address) == tags.end()) {
printf("Access to unexpected RAM address\n");
}
tags[address] = tag;
return *reinterpret_cast<IntT *>(&memory[address]);
}
static constexpr uint32_t NoWriteBack = 0; // A low byte address of 0 can't require write-back.
uint32_t write_back_address_[2] = {NoWriteBack, NoWriteBack};
uint16_t write_back_value_;
};
struct IO {
template <typename IntT> void out([[maybe_unused]] uint16_t port, [[maybe_unused]] IntT value) {}
@ -205,39 +290,7 @@ class FlowController {
FlowController(Memory &memory, Registers &registers, Status &status) :
memory_(memory), registers_(registers), status_(status) {}
void did_iret() {}
void did_near_ret() {}
void did_far_ret() {}
void interrupt(int index) {
const uint16_t address = static_cast<uint16_t>(index) << 2;
const uint16_t new_ip = memory_.access<uint16_t>(address, Memory::Tag::Accessed);
const uint16_t new_cs = memory_.access<uint16_t>(address + 2, Memory::Tag::Accessed);
push(status_.get(), true);
using Flag = InstructionSet::x86::Flag;
status_.set_from<Flag::Interrupt, Flag::Trap>(0);
// Push CS and IP.
push(registers_.cs_);
push(registers_.ip_);
registers_.cs_ = new_cs;
registers_.ip_ = new_ip;
}
void call(uint16_t address) {
push(registers_.ip_);
jump(address);
}
void call(uint16_t segment, uint16_t offset) {
push(registers_.cs_);
push(registers_.ip_);
jump(segment, offset);
}
// Requirements for perform.
void jump(uint16_t address) {
registers_.ip_ = address;
}
@ -250,12 +303,14 @@ class FlowController {
void halt() {}
void wait() {}
void begin_instruction() {
should_repeat_ = false;
}
void repeat_last() {
should_repeat_ = true;
}
// Other actions.
void begin_instruction() {
should_repeat_ = false;
}
bool should_repeat() const {
return should_repeat_;
}
@ -265,23 +320,6 @@ class FlowController {
Registers &registers_;
Status &status_;
bool should_repeat_ = false;
void push(uint16_t value, bool is_flags = false) {
// Perform the push in two steps because it's possible for SP to underflow, and so that FlagsL and
// FlagsH can be set separately.
--registers_.sp_;
memory_.access<uint8_t>(
InstructionSet::x86::Source::SS,
registers_.sp_,
is_flags ? Memory::Tag::FlagsH : Memory::Tag::Accessed
) = value >> 8;
--registers_.sp_;
memory_.access<uint8_t>(
InstructionSet::x86::Source::SS,
registers_.sp_,
is_flags ? Memory::Tag::FlagsL : Memory::Tag::Accessed
) = value & 0xff;
}
};
struct ExecutionSupport {
@ -290,8 +328,9 @@ struct ExecutionSupport {
Memory memory;
FlowController flow_controller;
IO io;
static constexpr auto model = InstructionSet::x86::Model::i8086;
ExecutionSupport() : memory(registers), flow_controller(memory, registers, status) {}
ExecutionSupport(): memory(registers), flow_controller(memory, registers, status) {}
void clear() {
memory.clear();
@ -310,8 +349,8 @@ struct FailedExecution {
@end
@implementation i8088Tests {
ExecutionSupport execution_support;
std::vector<FailedExecution> execution_failures;
ExecutionSupport execution_support;
}
- (NSArray<NSString *> *)testFiles {
@ -383,6 +422,8 @@ struct FailedExecution {
return hexInstruction;
};
EACCES;
const auto decoded = decoder.decode(data.data(), data.size());
const bool sizeMatched = decoded.first == data.size();
if(assert) {
@ -522,13 +563,9 @@ struct FailedExecution {
execution_support.registers.ip_ += decoded.first;
do {
execution_support.flow_controller.begin_instruction();
InstructionSet::x86::perform<InstructionSet::x86::Model::i8086>(
InstructionSet::x86::perform(
decoded.second,
execution_support.status,
execution_support.flow_controller,
execution_support.registers,
execution_support.memory,
execution_support.io
execution_support
);
} while (execution_support.flow_controller.should_repeat());
@ -537,21 +574,32 @@ struct FailedExecution {
InstructionSet::x86::Status intended_status;
bool ramEqual = true;
int mask_position = 0;
for(NSArray<NSNumber *> *ram in final_state[@"ram"]) {
const uint32_t address = [ram[0] intValue];
const auto value = execution_support.memory.access<uint8_t, Memory::AccessType::Read>(address);
uint8_t mask = 0xff;
if(const auto tag = execution_support.memory.tags.find(address); tag != execution_support.memory.tags.end()) {
switch(tag->second) {
default: break;
case Memory::Tag::FlagsH: mask = flags_mask >> 8; break;
case Memory::Tag::FlagsL: mask = flags_mask & 0xff; break;
if((mask_position != 1) && value == [ram[1] intValue]) {
continue;
}
// Consider whether this apparent mismatch might be because flags have been written to memory;
// allow only one use of the [16-bit] mask per test.
bool matched_with_mask = false;
while(mask_position < 2) {
const uint8_t mask = mask_position ? (flags_mask >> 8) : (flags_mask & 0xff);
++mask_position;
if((value & mask) == ([ram[1] intValue] & mask)) {
matched_with_mask = true;
break;
}
}
if((execution_support.memory.memory[address] & mask) != ([ram[1] intValue] & mask)) {
ramEqual = false;
if(matched_with_mask) {
continue;
}
ramEqual = false;
break;
}
[self populate:intended_registers status:intended_status value:final_state[@"regs"]];
@ -663,7 +711,15 @@ struct FailedExecution {
}
// Lock in current failure rate.
XCTAssertLessThanOrEqual(execution_failures.size(), 1654);
XCTAssertLessThanOrEqual(execution_failures.size(), 4138);
// Current accepted failures:
// * 65 instances of DAA with invalid BCD input, and 64 of DAS;
// * 2484 instances of LEA from a register, which officially has undefined results;
// * 42 instances of AAM 00h for which I can't figure out what to do with flags; and
// * 1486 instances of IDIV, most either with a rep or repne that on the 8086 specifically negatives the result,
// but some admittedly still unexplained (primarily setting overflow even though the result doesn't overflow;
// a couple of online 8086 emulators also didn't throw so maybe this is an 8086 quirk?)
for(const auto &failure: execution_failures) {
NSLog(@"Failed %s — %s", failure.test_name.c_str(), failure.reason.c_str());