// // // PerformImplementation.hpp // Clock Signal // // Created by Thomas Harte on 05/10/2023. // Copyright © 2023 Thomas Harte. All rights reserved. // #pragma once #include "Arithmetic.hpp" #include "BCD.hpp" #include "FlowControl.hpp" #include "InOut.hpp" #include "LoadStore.hpp" #include "Logical.hpp" #include "Repetition.hpp" #include "Resolver.hpp" #include "ShiftRoll.hpp" #include "Stack.hpp" #include "InstructionSets/x86/AccessType.hpp" #include "InstructionSets/x86/Descriptors.hpp" #include "InstructionSets/x86/Exceptions.hpp" #include "InstructionSets/x86/MachineStatus.hpp" #include // // Comments throughout headers above come from the 1997 edition of the // Intel Architecture Software Developer’s Manual; that year all such // definitions still fitted within a single volume, Volume 2. // // Order Number 243191; e.g. https://www.ardent-tool.com/CPU/docs/Intel/IA/243191-002.pdf // namespace InstructionSet::x86 { template < DataSize data_size, AddressSize address_size, typename InstructionT, typename ContextT > void perform( const InstructionT &instruction, ContextT &context ) { using IntT = typename DataSizeType::type; using AddressT = typename AddressSizeType::type; // Establish source() and destination() shorthands to fetch data if necessary. // // C++17, which this project targets at the time of writing, does not provide templatised lambdas. // So the following division is in part a necessity. // // (though GCC offers C++20 syntax as an extension, and Clang seems to follow along, so maybe I'm overthinking) IntT immediate; const auto source_r = [&]() -> read_t { return resolve( instruction, instruction.source().source(), instruction.source(), context, nullptr, &immediate); }; const auto source_rmw = [&]() -> modify_t { return resolve( instruction, instruction.source().source(), instruction.source(), context, nullptr, &immediate); }; const auto destination_r = [&]() -> read_t { return resolve( instruction, instruction.destination().source(), instruction.destination(), context, nullptr, &immediate); }; const auto destination_w = [&]() -> write_t { return resolve( instruction, instruction.destination().source(), instruction.destination(), context, nullptr, &immediate); }; const auto destination_rmw = [&]() -> modify_t { return resolve( instruction, instruction.destination().source(), instruction.destination(), context, nullptr, &immediate); }; // Performs a displacement jump only if @c condition is true. const auto jcc = [&](bool condition) { Primitive::jump( condition, instruction.displacement(), context); }; const auto shift_count = [&]() -> uint8_t { static constexpr uint8_t mask = (ContextT::model != Model::i8086) ? 0x1f : 0xff; switch(instruction.source().source()) { case Source::None: return 1; case Source::Immediate: return uint8_t(instruction.operand()) & mask; default: return context.registers.cl() & mask; } }; // Currently a special case for descriptor loading; assumes an indirect operand and returns the // address indicated. Unlike [source/destination]_r it doesn't read an IntT from that address, // since those instructions load an atypical six bytes. const auto source_indirect = [&]() -> AddressT { return AddressT( address(instruction, instruction.source(), context) ); }; // Some instructions use a pair of registers as an extended accumulator — DX:AX or EDX:EAX. // The two following return the high and low parts of that pair; they also work in Byte mode to return AH:AL, // i.e. AX split into high and low parts. const auto pair_high = [&]() -> IntT& { if constexpr (data_size == DataSize::Byte) return context.registers.ah(); else if constexpr (data_size == DataSize::Word) return context.registers.dx(); else if constexpr (data_size == DataSize::DWord) return context.registers.edx(); }; const auto pair_low = [&]() -> IntT& { if constexpr (data_size == DataSize::Byte) return context.registers.al(); else if constexpr (data_size == DataSize::Word) return context.registers.ax(); else if constexpr (data_size == DataSize::DWord) return context.registers.eax(); }; // For the string operations, evaluate to either SI and DI or ESI and EDI, depending on the address size. const auto eSI = [&]() -> AddressT& { if constexpr (std::is_same_v) { return context.registers.si(); } else { return context.registers.esi(); } }; const auto eDI = [&]() -> AddressT& { if constexpr (std::is_same_v) { return context.registers.di(); } else { return context.registers.edi(); } }; // For counts, provide either eCX or CX depending on address size. const auto eCX = [&]() -> AddressT& { if constexpr (std::is_same_v) { return context.registers.cx(); } else { return context.registers.ecx(); } }; // Gets the port for an IN or OUT; these are always 16-bit. const auto port = [&](Source source) -> uint16_t { switch(source) { case Source::DirectAddress: return instruction.offset(); default: return context.registers.dx(); } }; // Guide to the below: // // * use hard-coded register names where appropriate, otherwise use the source_X() and destination_X() lambdas; // * return directly if there is definitely no possible write back to RAM; // * break if there's a chance of writeback. switch(instruction.operation()) { default: // If execution gets here then the decoder recognised an operation that I have yet to implement. // This is definitely an oversight on my part. It cannot possibly be a problem with the underlying software. assert(false); [[fallthrough]]; case Operation::NOP: return; case Operation::Invalid: if constexpr (!uses_8086_exceptions(ContextT::model)) { throw Exception::exception(); } return; case Operation::ESC: if constexpr (!uses_8086_exceptions(ContextT::model)) { const auto should_throw = context.registers.msw() & MachineStatus::EmulateProcessorExtension; if(should_throw) { throw Exception::exception(); } } return; case Operation::AAM: Primitive::aam(context.registers.axp(), uint8_t(instruction.operand()), context); return; case Operation::AAD: Primitive::aad(context.registers.axp(), uint8_t(instruction.operand()), context); return; case Operation::AAA: Primitive::aaas(context.registers.axp(), context); return; case Operation::AAS: Primitive::aaas(context.registers.axp(), context); return; case Operation::DAA: Primitive::daas(context.registers.al(), context); return; case Operation::DAS: Primitive::daas(context.registers.al(), context); return; case Operation::CBW: Primitive::cbw(pair_low()); return; case Operation::CWD: Primitive::cwd(pair_high(), pair_low()); return; case Operation::HLT: context.flow_controller.halt(); return; case Operation::WAIT: context.flow_controller.wait(); return; case Operation::ADC: Primitive::add(destination_rmw(), source_r(), context); break; case Operation::ADD: Primitive::add(destination_rmw(), source_r(), context); break; case Operation::SBB: Primitive::sub(destination_rmw(), source_r(), context); break; case Operation::SUB: Primitive::sub(destination_rmw(), source_r(), context); break; case Operation::CMP: Primitive::sub(destination_r(), source_r(), context); return; case Operation::TEST: Primitive::test(destination_r(), source_r(), context); return; case Operation::MUL: Primitive::mul(pair_high(), pair_low(), source_r(), context); return; case Operation::IMUL_1: Primitive::imul_double(pair_high(), pair_low(), source_r(), context); return; case Operation::IMUL_3: Primitive::imul_single(destination_w(), source_r(), IntT(instruction.operand()), context); return; case Operation::DIV: Primitive::div(pair_high(), pair_low(), source_r(), context); return; case Operation::IDIV: Primitive::idiv(pair_high(), pair_low(), source_r(), context); return; case Operation::IDIV_REP: if constexpr (ContextT::model == Model::i8086) { Primitive::idiv(pair_high(), pair_low(), source_r(), context); break; } else { static_assert(int(Operation::IDIV_REP) == int(Operation::LEAVE)); if constexpr (std::is_same_v || std::is_same_v) { Primitive::leave(context); } } return; case Operation::INC: Primitive::inc(destination_rmw(), context); break; case Operation::DEC: Primitive::dec(destination_rmw(), context); break; case Operation::AND: Primitive::and_(destination_rmw(), source_r(), context); break; case Operation::OR: Primitive::or_(destination_rmw(), source_r(), context); break; case Operation::XOR: Primitive::xor_(destination_rmw(), source_r(), context); break; case Operation::NEG: Primitive::neg(source_rmw(), context); break; // TODO: should be a destination. case Operation::NOT: Primitive::not_(source_rmw()); break; // TODO: should be a destination. case Operation::CALLrel: Primitive::call_relative(instruction.displacement(), context); return; case Operation::CALLabs: Primitive::call_absolute(destination_r(), context); return; case Operation::CALLfar: Primitive::call_far(instruction, context); return; case Operation::JMPrel: jcc(true); return; case Operation::JMPabs: Primitive::jump_absolute(destination_r(), context); return; case Operation::JMPfar: Primitive::jump_far(instruction, context); return; case Operation::JCXZ: jcc(!eCX()); return; case Operation::LOOP: Primitive::loop(eCX(), instruction.offset(), context); return; case Operation::LOOPE: Primitive::loope(eCX(), instruction.offset(), context); return; case Operation::LOOPNE: Primitive::loopne(eCX(), instruction.offset(), context); return; case Operation::IRET: Primitive::iret(context); return; case Operation::RETnear: Primitive::ret_near(instruction, context); return; case Operation::RETfar: Primitive::ret_far(instruction, context); return; case Operation::INT: interrupt(Exception::interrupt(uint8_t(instruction.operand())), context); return; case Operation::INTO: Primitive::into(context); return; case Operation::SAHF: Primitive::sahf(context.registers.ah(), context); return; case Operation::LAHF: Primitive::lahf(context.registers.ah(), context); return; case Operation::LDS: if constexpr (data_size == DataSize::Word) { Primitive::ld(instruction, destination_w(), context); } return; case Operation::LES: if constexpr (data_size == DataSize::Word) { Primitive::ld(instruction, destination_w(), context); } return; case Operation::LEA: Primitive::lea(instruction, destination_w(), context); return; case Operation::MOV: { const auto source = source_r(); const auto segment = instruction.destination().source(); if(is_segment_register(segment)) { context.segments.preauthorise(segment, source); Primitive::mov(destination_w(), source); context.segments.did_update(segment); } else { Primitive::mov(destination_w(), source); } } break; case Operation::SMSW: if constexpr (ContextT::model >= Model::i80286 && std::is_same_v) { Primitive::smsw(destination_w(), context); } else { assert(false); } break; case Operation::LMSW: if constexpr (ContextT::model >= Model::i80286 && std::is_same_v) { Primitive::lmsw(source_r(), context); } else { assert(false); } return; case Operation::LIDT: if constexpr (ContextT::model >= Model::i80286) { Primitive::ldt(source_indirect(), instruction, context); } else { assert(false); } return; case Operation::LGDT: if constexpr (ContextT::model >= Model::i80286) { Primitive::ldt(source_indirect(), instruction, context); } else { assert(false); } return; case Operation::LLDT: if constexpr (ContextT::model >= Model::i80286) { Primitive::lldt(source_r(), context); } else { assert(false); } return; case Operation::SIDT: if constexpr (ContextT::model >= Model::i80286) { Primitive::sdt(source_indirect(), instruction, context); } else { assert(false); } break; case Operation::SGDT: if constexpr (ContextT::model >= Model::i80286) { Primitive::sdt(source_indirect(), instruction, context); } else { assert(false); } break; case Operation::SLDT: // TODO: // "When the destination operand is a memory location, the segment selector is written to memory as a // 16-bit quantity, regardless of the operand size." if constexpr (ContextT::model >= Model::i80286) { Primitive::sldt(destination_w(), context); } else { assert(false); } break; case Operation::JO: jcc(context.flags.template condition()); return; case Operation::JNO: jcc(!context.flags.template condition()); return; case Operation::JB: jcc(context.flags.template condition()); return; case Operation::JNB: jcc(!context.flags.template condition()); return; case Operation::JZ: jcc(context.flags.template condition()); return; case Operation::JNZ: jcc(!context.flags.template condition()); return; case Operation::JBE: jcc(context.flags.template condition()); return; case Operation::JNBE: jcc(!context.flags.template condition()); return; case Operation::JS: jcc(context.flags.template condition()); return; case Operation::JNS: jcc(!context.flags.template condition()); return; case Operation::JP: jcc(!context.flags.template condition()); return; case Operation::JNP: jcc(context.flags.template condition()); return; case Operation::JL: jcc(context.flags.template condition()); return; case Operation::JNL: jcc(!context.flags.template condition()); return; case Operation::JLE: jcc(context.flags.template condition()); return; case Operation::JNLE: jcc(!context.flags.template condition()); return; case Operation::RCL: Primitive::rcl(destination_rmw(), shift_count(), context); break; case Operation::RCR: Primitive::rcr(destination_rmw(), shift_count(), context); break; case Operation::ROL: Primitive::rol(destination_rmw(), shift_count(), context); break; case Operation::ROR: Primitive::ror(destination_rmw(), shift_count(), context); break; case Operation::SAL: Primitive::sal(destination_rmw(), shift_count(), context); break; case Operation::SAR: Primitive::sar(destination_rmw(), shift_count(), context); break; case Operation::SHR: Primitive::shr(destination_rmw(), shift_count(), context); break; case Operation::CLC: Primitive::clc(context); return; case Operation::CLD: Primitive::cld(context); return; case Operation::CLI: Primitive::cli(context); return; case Operation::STC: Primitive::stc(context); return; case Operation::STD: Primitive::std(context); return; case Operation::STI: Primitive::sti(context); return; case Operation::CMC: Primitive::cmc(context); return; case Operation::XCHG: Primitive::xchg(destination_rmw(), source_rmw()); break; case Operation::SALC: Primitive::salc(context.registers.al(), context); return; case Operation::SETMO: if constexpr (ContextT::model == Model::i8086) { Primitive::setmo(destination_w(), context); break; } else { static_assert(int(Operation::SETMO) == int(Operation::ENTER)); Primitive::enter(instruction, context); } return; case Operation::SETMOC: if constexpr (ContextT::model == Model::i8086) { // Test CL out here to avoid taking a reference to memory if // no write is going to occur. if(context.registers.cl()) { Primitive::setmo(destination_w(), context); } break; } else { static_assert(int(Operation::SETMOC) == int(Operation::BOUND)); Primitive::bound(instruction, destination_r(), source_r(), context); } return; case Operation::OUT: Primitive::out(port(instruction.destination().source()), pair_low(), context); return; case Operation::IN: Primitive::in(port(instruction.source().source()), pair_low(), context); return; case Operation::XLAT: Primitive::xlat(instruction, context); return; case Operation::POP: { const auto value = Primitive::pop(context); const auto segment = instruction.destination().source(); if(is_segment_register(segment)) { context.segments.preauthorise(segment, value); destination_w() = value; context.segments.did_update(segment); } else { destination_w() = value; } } break; case Operation::PUSH: if constexpr (ContextT::model >= Model::i80286) { Primitive::push(source_r(), context); } else { Primitive::push(source_rmw(), context); // Prior to the 286, PUSH SP modifies SP // before pushing it; hence PUSH is // sometimes read-modify-write. } return; case Operation::POPF: if constexpr (std::is_same_v || std::is_same_v) { Primitive::popf(context); } else { assert(false); } return; case Operation::PUSHF: if constexpr (std::is_same_v || std::is_same_v) { Primitive::pushf(context); } else { assert(false); } return; case Operation::POPA: if constexpr (std::is_same_v || std::is_same_v) { Primitive::popa(context); } else { assert(false); } return; case Operation::PUSHA: if constexpr (std::is_same_v || std::is_same_v) { Primitive::pusha(context); } else { assert(false); } return; case Operation::CMPS: Primitive::cmps(instruction, eCX(), eSI(), eDI(), context); return; case Operation::CMPS_REPE: Primitive::cmps(instruction, eCX(), eSI(), eDI(), context); return; case Operation::CMPS_REPNE: Primitive::cmps(instruction, eCX(), eSI(), eDI(), context); return; case Operation::SCAS: Primitive::scas(eCX(), eDI(), pair_low(), context); return; case Operation::SCAS_REPE: Primitive::scas(eCX(), eDI(), pair_low(), context); return; case Operation::SCAS_REPNE: Primitive::scas(eCX(), eDI(), pair_low(), context); return; case Operation::LODS: Primitive::lods(instruction, eCX(), eSI(), pair_low(), context); return; case Operation::LODS_REP: Primitive::lods(instruction, eCX(), eSI(), pair_low(), context); return; case Operation::MOVS: Primitive::movs(instruction, eCX(), eSI(), eDI(), context); break; case Operation::MOVS_REP: Primitive::movs(instruction, eCX(), eSI(), eDI(), context); break; case Operation::STOS: Primitive::stos(eCX(), eDI(), pair_low(), context); break; case Operation::STOS_REP: Primitive::stos(eCX(), eDI(), pair_low(), context); break; case Operation::OUTS: Primitive::outs( instruction, eCX(), context.registers.dx(), eSI(), context); return; case Operation::OUTS_REP: Primitive::outs( instruction, eCX(), context.registers.dx(), eSI(), context); return; case Operation::INS: Primitive::ins(eCX(), context.registers.dx(), eDI(), context); break; case Operation::INS_REP: Primitive::ins(eCX(), context.registers.dx(), eDI(), context); break; case Operation::ARPL: if constexpr (ContextT::model >= Model::i80286 && std::is_same_v) { if(is_real(context.cpu_control.mode())) { throw Exception::exception(); return; } Primitive::arpl(destination_rmw(), source_r(), context); } else { assert(false); } break; case Operation::CLTS: if constexpr (ContextT::model >= Model::i80286) { Primitive::clts(context); } else { assert(false); } break; // TODO to reach a full 80286: // // LAR // VERR // VERW // LSL // LTR // STR // LOADALL } // Write to memory if required to complete this operation. // // This is not currently handled via RAII because of the amount of context that would need to place onto the stack; // instead code has been set up to make sure there is only at most one writeable target on loan for potential // write back. I might flip-flop on this, especially if I can verify whether extra stack context is easily // optimised out. context.memory.template write_back(); } template < InstructionType type, typename ContextT > requires is_context void perform( const Instruction &instruction, ContextT &context ) { const auto size = [](const DataSize operation_size, const AddressSize address_size) constexpr -> int { return int(operation_size) + (int(address_size) << 2); }; static constexpr bool supports_32bit = type != InstructionType::Bits16; // Dispatch to a function specialised on data and address size. switch(size(instruction.operation_size(), instruction.address_size())) { // 16-bit combinations. case size(DataSize::Byte, AddressSize::b16): perform(instruction, context); return; case size(DataSize::Word, AddressSize::b16): perform(instruction, context); return; // 32-bit combinations. // // The if constexprs below ensure that `perform` isn't compiled for incompatible data or address size and // model combinations. So if a caller nominates a 16-bit model it can supply registers and memory objects // that don't implement 32-bit registers or accesses. case size(DataSize::Byte, AddressSize::b32): assert(supports_32bit); if constexpr (supports_32bit) { perform(instruction, context); return; } break; case size(DataSize::Word, AddressSize::b32): assert(supports_32bit); if constexpr (supports_32bit) { perform(instruction, context); return; } break; case size(DataSize::DWord, AddressSize::b16): assert(supports_32bit); if constexpr (supports_32bit) { perform(instruction, context); return; } break; case size(DataSize::DWord, AddressSize::b32): assert(supports_32bit); if constexpr (supports_32bit) { perform(instruction, context); return; } break; default: break; } // This is reachable only if the data and address size combination in use isn't available // on the processor model nominated. assert(false); } // // Public function; just indirects into a trampoline into a version of perform templated on data and address size. // // Which, yes, means there's an outer switch leading to an inner switch, which could be reduced to one big switch. // It'd be a substantial effort to find the most neat expression of that, I think, so it is not currently done. // template < InstructionType type, typename ContextT > requires is_context void perform( const Instruction &instruction, ContextT &context, uint32_t source_ip ) { if constexpr (uses_8086_exceptions(ContextT::model)) { InstructionSet::x86::perform( instruction, context ); } else { try { InstructionSet::x86::perform( instruction, context ); return; } catch (const InstructionSet::x86::Exception exception) { context.flow_controller.cancel_repetition(); fault(exception, context, source_ip); } } } template < typename ContextT > requires is_context void interrupt( const Exception exception, ContextT &context ) { const auto table_pointer = [&] { if constexpr (ContextT::model >= Model::i80286) { return context.registers.template get(); } return DescriptorTablePointer{ .limit = 1024, .base = 0 }; } (); const auto far_call = [&](const uint16_t segment, const uint16_t offset) { context.memory.preauthorise_stack_write(sizeof(uint16_t) * 3); auto flags = context.flags.get(); if(ContextT::model >= Model::i80286 && exception.code_type == Exception::CodeType::Internal) { // TODO: set OP flags, nested flag, etc, properly. flags &= 0xfff; } Primitive::push(flags, context); // Push CS and IP. Primitive::push(context.registers.cs(), context); Primitive::push(context.registers.ip(), context); // Set new destination. context.flow_controller.jump(segment, offset); }; if constexpr (ContextT::model >= Model::i80286) { if(context.registers.msw() & MachineStatus::ProtectedModeEnable) { const auto call_gate = descriptor_at( context.linear_memory, table_pointer, uint16_t(exception.vector << 3)); if(!call_gate.present()) { printf("TODO: should throw for non-present IDT entry\n"); assert(false); } if( call_gate.type() != InterruptDescriptor::Type::Interrupt16 && call_gate.type() != InterruptDescriptor::Type::Trap16 ) { printf("TODO: unknown or unhandled call gate type\n"); assert(false); } far_call(call_gate.segment(), static_cast(call_gate.offset())); if(call_gate.type() == InterruptDescriptor::Type::Interrupt16) { context.flags.template set_from(0); } return; } } const uint32_t address = static_cast(table_pointer.base + exception.vector) << 2; context.linear_memory.preauthorise_read(address, sizeof(uint16_t) * 2); // TODO: I think (?) these are always physical addresses, not linear ones. // Indicate that when fetching. const uint16_t ip = context.linear_memory.template read(address); const uint16_t cs = context.linear_memory.template read(address + 2); far_call(cs, ip); context.flags.template set_from(0); } template < typename ContextT > requires is_context void fault( const Exception exception, ContextT &context, const uint32_t source_ip ) { if constexpr (uses_8086_exceptions(ContextT::model)) { InstructionSet::x86::interrupt( exception, context ); return; } if( exception.code_type == Exception::CodeType::Internal && !posts_next_ip(InstructionSet::x86::Vector(exception.vector)) ) { context.registers.ip() = source_ip; } try { InstructionSet::x86::interrupt( exception, context ); } catch (const InstructionSet::x86::Exception exception) { // TODO: unsure about this. Probably just recurse? printf("DOUBLE FAULT TODO!"); } } }