diff --git a/src/Spice86.Core/CLI/Configuration.cs b/src/Spice86.Core/CLI/Configuration.cs index 62d46f7024..d347eeb5bd 100644 --- a/src/Spice86.Core/CLI/Configuration.cs +++ b/src/Spice86.Core/CLI/Configuration.cs @@ -33,6 +33,21 @@ public sealed class Configuration : CommandSettings { [CommandOption("--A20Gate")] public bool A20Gate { get; init; } + /// + /// Total RAM size, in KB, from physical address 0. Backs conventional memory, the HMA, and the + /// unified extended-memory pool shared by XMS and EMS. Default is 16MB (16384), can be raised up + /// to 64MB (65536) for demanding DOS extenders. + /// + [CommandOption("--RamSizeKb ")] + [DefaultValue(RamSizeDefaultKb)] + public int RamSizeKb { get; init; } = RamSizeDefaultKb; + + /// Default value for : 16MB. + public const int RamSizeDefaultKb = 16 * 1024; + + /// Maximum allowed value for : 64MB. + public const int RamSizeMaxKb = 64 * 1024; + /// /// Gets if the program will be paused on start and stop. If is set, the program will be paused anyway. /// diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Ast/Builder/AstBuilder.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Ast/Builder/AstBuilder.cs index b157b1efe4..b3d569404e 100644 --- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Ast/Builder/AstBuilder.cs +++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Ast/Builder/AstBuilder.cs @@ -43,8 +43,8 @@ public DataType UType(BitWidth bitWidth) { return DataType.UnsignedFromBitWidth(bitWidth); } - public DataType AddressType(CfgInstruction instruction) { - return instruction.AddressSize32Prefix == null ? DataType.UINT16 : DataType.UINT32; + public DataType AddressType(BitWidth addressWidthFromPrefixes) { + return UType(addressWidthFromPrefixes); } /// diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Ast/Builder/StackAstBuilder.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Ast/Builder/StackAstBuilder.cs index c2dea24011..f1be58dd21 100644 --- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Ast/Builder/StackAstBuilder.cs +++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Ast/Builder/StackAstBuilder.cs @@ -1,6 +1,7 @@ namespace Spice86.Core.Emulator.CPU.CfgCpu.Ast.Builder; using System.Collections.Generic; + using Spice86.Core.Emulator.CPU; using Spice86.Core.Emulator.CPU.CfgCpu.Ast.Instruction; using Spice86.Core.Emulator.CPU.CfgCpu.Ast.Operations; @@ -43,19 +44,12 @@ public MethodCallValueNode Peek(BitWidth bitWidth) { } /// - /// Creates an AST node for 32-bit LEAVE stack semantics. - /// - /// MethodCallNode representing 32-bit LEAVE. - public MethodCallNode Leave32() { - return new MethodCallNode("Stack", nameof(Stack.Leave32)); - } - - /// - /// Creates an AST node for 16-bit LEAVE stack semantics. + /// Creates an AST node for LEAVE stack semantics. /// - /// MethodCallNode representing 16-bit LEAVE. - public MethodCallNode Leave16() { - return new MethodCallNode("Stack", nameof(Stack.Leave16)); + /// Constant node for whether the operand size is 32-bit. + /// MethodCallNode representing Stack.Leave(operandSize32). + public MethodCallNode Leave(ValueNode operandSize32Node) { + return new MethodCallNode("Stack", nameof(Stack.Leave), operandSize32Node); } /// @@ -103,12 +97,12 @@ public void PopValues(List statements, DataType dataType, par /// /// Dispatcher method for LEAVE stack semantics based on bit width. - /// For 16-bit, calls Leave16. - /// For 32-bit, calls Leave32. + /// Builds a call to , passing the operand size as a constant. /// - /// The bit width determining which LEAVE to use - /// MethodCallNode representing the appropriate Stack.LeaveN method + /// The bit width determining the LEAVE operand size. + /// MethodCallNode representing Stack.Leave(operandSize32). public MethodCallNode Leave(BitWidth bitWidth) { - return bitWidth == BitWidth.DWORD_32 ? Leave32() : Leave16(); + return Leave(new ConstantNode(DataType.BOOL, bitWidth == BitWidth.DWORD_32 ? 1UL : 0UL)); } + } diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Ast/Instruction/InstructionOperation.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Ast/Instruction/InstructionOperation.cs index 45e193434b..50077964c4 100644 --- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Ast/Instruction/InstructionOperation.cs +++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Ast/Instruction/InstructionOperation.cs @@ -8,6 +8,7 @@ public enum InstructionOperation { ADC, ADD, AND, + ARPL, BOUND, BSF, BSR, @@ -91,18 +92,25 @@ public enum InstructionOperation { JMP_NEAR, JMP_SHORT, LAHF, + LAR, LDS, LEA, LEAVE, LEAVEW, LES, LFS, + LGDT, LGS, + LIDT, + LLDT, + LMSW, LODS, LOOP, LOOPE, LOOPNE, + LSL, LSS, + LTR, MOV, MOVS, MOVSX, @@ -152,16 +160,23 @@ public enum InstructionOperation { SETO, SETP, SETS, + SGDT, SHL, SHLD, SHRD, SHR, + SIDT, + SLDT, + SMSW, STC, STD, STI, STOS, + STR, SUB, TEST, + VERR, + VERW, XADD, XCHG, XLAT, diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/InstructionExecutor/Expressions/AstExpressionBuilder.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/InstructionExecutor/Expressions/AstExpressionBuilder.cs index e53d8dc3ab..3ec17814cf 100644 --- a/src/Spice86.Core/Emulator/CPU/CfgCpu/InstructionExecutor/Expressions/AstExpressionBuilder.cs +++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/InstructionExecutor/Expressions/AstExpressionBuilder.cs @@ -164,7 +164,7 @@ private UnaryExpression ToExpression(UnaryOperation unaryOperation, Expression v _ => throw new InvalidOperationException($"Unhandled Operation: {unaryOperation}") }; } - + private T EnsureNonNull(T? argument) { ArgumentNullException.ThrowIfNull(argument); return argument; @@ -193,7 +193,7 @@ private Type ToMemoryIndexerType(DataType dataType) { private PropertyInfo FindSingleParameterIndexer(Type type) { return EnsureNonNull(type.GetProperty("Item", [typeof(uint)])); } - + private PropertyInfo FindSegmentedIndexer(Type type) { return EnsureNonNull(type.GetProperty("Item", [typeof(ushort), typeof(uint), typeof(SegmentAccessKind)])); } @@ -209,7 +209,7 @@ private IndexExpression ToMemoryIndexer(DataType dataType, Expression indexExpre PropertyInfo indexer = FindSingleParameterIndexer(ToMemoryIndexerType(dataType)); return Expression.Property(indexerProperty, indexer, indexExpression); } - + private IndexExpression ToMemoryIndexer(DataType dataType, Expression segmentExpression, Expression offsetExpression, bool isStackSegment) { if (segmentExpression.Type != typeof(ushort)) { segmentExpression = Expression.Convert(segmentExpression, typeof(ushort)); @@ -222,7 +222,7 @@ private IndexExpression ToMemoryIndexer(DataType dataType, Expression segmentExp SegmentAccessKind accessKind = isStackSegment ? SegmentAccessKind.Stack : SegmentAccessKind.Data; return Expression.Property(indexerProperty, indexer, segmentExpression, offsetExpression, Expression.Constant(accessKind)); } - + private MemberExpression ToRegisterProperty(int registerIndex, DataType dataType, bool isSegmentRegister) { string name = isSegmentRegister ? _registerRenderer.ToStringSegmentRegister(registerIndex) : _registerRenderer.ToStringRegister(dataType.BitWidth, registerIndex); PropertyInfo stateRegisterProperty = EnsureNonNull(typeof(State).GetProperty(name)); @@ -230,7 +230,7 @@ private MemberExpression ToRegisterProperty(int registerIndex, DataType dataType } public Expression VisitSegmentRegisterNode(SegmentRegisterNode node) { - return ToRegisterProperty(node.RegisterIndex, node.DataType, true); + return ToRegisterProperty(node.RegisterIndex, node.DataType, true); } public Expression VisitSegmentedPointer(SegmentedPointerNode node) { @@ -279,10 +279,10 @@ public Expression VisitFlagRegisterNode(FlagRegisterNode node) { Expression flagsObject = Expression.Property(_stateParameter, flagsProperty); // Select the appropriate property based on the DataType - string propertyName = node.DataType.BitWidth == BitWidth.WORD_16 - ? nameof(Flags.FlagRegister16) + string propertyName = node.DataType.BitWidth == BitWidth.WORD_16 + ? nameof(Flags.FlagRegister16) : nameof(Flags.FlagRegister); - + PropertyInfo flagRegisterProperty = EnsureNonNull(typeof(Flags).GetProperty(propertyName)); return Expression.Property(flagsObject, flagRegisterProperty); } @@ -299,24 +299,24 @@ public Expression VisitSegmentedAddressNode(SegmentedAddressNode node) { ConstructorInfo constructorInfo = EnsureNonNull(typeof(SegmentedAddress).GetConstructor([typeof(ushort), typeof(ushort)])); return Expression.New(constructorInfo, segment, offset); } - + public Expression VisitBinaryOperationNode(BinaryOperationNode node) { Expression left = node.Left.Accept(this); Expression right = node.Right.Accept(this); return ToExpression(node.BinaryOperation, left, right, node.DataType); } - + public Expression VisitUnaryOperationNode(UnaryOperationNode node) { Expression value = node.Value.Accept(this); return ToExpression(node.UnaryOperation, value); } - + public Expression VisitTypeConversionNode(TypeConversionNode node) { Expression value = node.Value.Accept(this); Type targetType = FromDataType(node.DataType); return Expression.Convert(value, targetType); } - + public Expression VisitInstructionNode(InstructionNode node) { throw new InvalidOperationException( $"InstructionNode is for assembly parsing and rendering, not execution. " + @@ -343,7 +343,7 @@ private static object ToConstantValue(ConstantNode node) { _ => throw new UnsupportedBitWidthException(node.DataType.BitWidth) }; } - + public Expression VisitNearAddressNode(NearAddressNode node) { return VisitConstantNode(node); } @@ -356,19 +356,19 @@ public Expression VisitMethodCallValueNode(MethodCallValueNode node) { public Expression> ToAction(Expression expression) { return Expression.Lambda>(expression, _allParameters); } - + public Expression> ToFuncUInt8(Expression expression) { return Expression.Lambda>(expression, _allParameters); } - + public Expression> ToFuncInt8(Expression expression) { return Expression.Lambda>(expression, _allParameters); } - + public Expression> ToFuncUInt16(Expression expression) { return Expression.Lambda>(expression, _allParameters); } - + public Expression> ToFuncInt16(Expression expression) { return Expression.Lambda>(expression, _allParameters); } @@ -377,11 +377,11 @@ public Expression> ToFuncUInt32(Expression expression) Expression converted = Expression.Convert(expression, typeof(uint)); return Expression.Lambda>(converted, _allParameters); } - + public Expression> ToFuncInt32(Expression expression) { return Expression.Lambda>(expression, _allParameters); } - + public Expression> ToFuncBool(Expression expression) { return Expression.Lambda>(expression, _allParameters); } @@ -553,15 +553,16 @@ public Expression VisitMoveIpNextNode(MoveIpNextNode node) { csExpression, nextIpAsUint, Expression.Constant(1u), - Expression.Constant(SegmentAccessKind.Data)); + Expression.Constant(SegmentAccessKind.Data), + Expression.Constant(false)); return Expression.Block(assignIp, checkAccess); } public Expression VisitCallNearNode(CallNearNode node) { // helper.NearCallWithReturnIpNextInstructionXX(instruction, targetIp) - string methodName = node.CallBitWidth == BitWidth.WORD_16 - ? nameof(InstructionExecutionHelper.NearCallWithReturnIpNextInstruction16) + string methodName = node.CallBitWidth == BitWidth.WORD_16 + ? nameof(InstructionExecutionHelper.NearCallWithReturnIpNextInstruction16) : nameof(InstructionExecutionHelper.NearCallWithReturnIpNextInstruction32); return CallHelperMethodWithInstruction(methodName, node, @@ -571,8 +572,8 @@ public Expression VisitCallNearNode(CallNearNode node) { public Expression VisitCallFarNode(CallFarNode node) { Expression targetAddress = node.TargetAddress.Accept(this); - string methodName = node.CallBitWidth == BitWidth.WORD_16 - ? nameof(InstructionExecutionHelper.FarCallWithReturnIpNextInstruction16) + string methodName = node.CallBitWidth == BitWidth.WORD_16 + ? nameof(InstructionExecutionHelper.FarCallWithReturnIpNextInstruction16) : nameof(InstructionExecutionHelper.FarCallWithReturnIpNextInstruction32); return CallHelperMethodWithInstruction(methodName, node, @@ -583,7 +584,7 @@ public Expression VisitReturnNearNode(ReturnNearNode node) { string methodName = node.RetBitWidth == BitWidth.WORD_16 ? nameof(InstructionExecutionHelper.HandleNearRet16) : nameof(InstructionExecutionHelper.HandleNearRet32); - + return CallHelperMethodWithInstruction(methodName, node, node.BytesToPop.Accept(this)); } @@ -592,7 +593,7 @@ public Expression VisitReturnFarNode(ReturnFarNode node) { string methodName = node.RetBitWidth == BitWidth.WORD_16 ? nameof(InstructionExecutionHelper.HandleFarRet16) : nameof(InstructionExecutionHelper.HandleFarRet32); - + return CallHelperMethodWithInstruction(methodName, node, node.BytesToPop.Accept(this)); } @@ -601,7 +602,7 @@ public Expression VisitJumpNearNode(JumpNearNode node) { return CallHelperMethodWithInstruction(nameof(InstructionExecutionHelper.JumpNear), node, node.Ip.Accept(this)); } - + public Expression VisitJumpFarNode(JumpFarNode node) { return CallHelperMethodWithInstruction(nameof(InstructionExecutionHelper.JumpFar), node, node.TargetAddress.Segment.Accept(this), @@ -647,7 +648,7 @@ private Expression CallHelperMethodWithInstruction(string methodName, CfgInstruc Expression[] allArgs = new Expression[args.Length + 1]; allArgs[0] = Expression.Constant(node.Instruction, node.Instruction.GetType()); Array.Copy(args, 0, allArgs, 1, args.Length); - + return CallHelperMethod(methodName, allArgs); } @@ -659,16 +660,16 @@ private Expression CallHelperMethod(string methodName, params Expression[] argum if (method == null) { method = typeof(InstructionExecutionHelper).GetMethods() .FirstOrDefault(m => m.Name == methodName && m.GetParameters().Length == arguments.Length); - + if (method != null && method.IsGenericMethodDefinition) { - method = method.MakeGenericMethod(arguments[0].Type); + method = method.MakeGenericMethod(arguments[0].Type); } } if (method == null) { - throw new InvalidOperationException($"Method {methodName} not found on InstructionExecutionHelper with {arguments.Length} arguments"); + throw new InvalidOperationException($"Method {methodName} not found on InstructionExecutionHelper with {arguments.Length} arguments"); } - + return Expression.Call(_helperParameter, method, arguments); } diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/InstructionExecutor/InstructionExecutionHelper.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/InstructionExecutor/InstructionExecutionHelper.cs index c4f187a547..e569efac04 100644 --- a/src/Spice86.Core/Emulator/CPU/CfgCpu/InstructionExecutor/InstructionExecutionHelper.cs +++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/InstructionExecutor/InstructionExecutionHelper.cs @@ -8,6 +8,7 @@ namespace Spice86.Core.Emulator.CPU.CfgCpu.InstructionExecutor; using Spice86.Core.Emulator.CPU.CfgCpu.Linker; using Spice86.Core.Emulator.CPU.CfgCpu.ParsedInstruction; using Spice86.Core.Emulator.CPU.CfgCpu.ParsedInstruction.Prefix; +using Spice86.Core.Emulator.CPU.DescriptorTables; using Spice86.Core.Emulator.CPU.Exceptions; using Spice86.Core.Emulator.CPU.Registers; using Spice86.Core.Emulator.Errors; @@ -70,10 +71,135 @@ public InstructionExecutionHelper(State state, // Real mode: jump targets are already truncated to 16-bit IP by the parser/AST public void JumpFar(CfgInstruction instruction, ushort cs, ushort ip) { - State.CS = cs; + if (ProtectedModeCallGateDispatcher.TryReadCallGate(State, Memory, cs, out RawGateDescriptor gate)) { + ProtectedModeCallGateDispatcher.DispatchJump(State, Memory, gate, cs); + return; + } + PrivilegeChecks.ValidateFarCodeSegmentTransfer(State, Memory, cs); + LoadSegmentRegister((uint)SegmentRegisterIndex.CsIndex, cs); State.IP = ip; } + /// + /// Loads a raw selector value into a segment register and refreshes its descriptor cache: the + /// real-mode synthesized cache (base = selector*16) outside protected mode, or the decoded GDT/LDT + /// descriptor once is active. This is the single path every + /// segment-register write (MOV Sreg, POP Sreg, far transfers) goes through. + /// + public void LoadSegmentRegister(uint segmentRegisterIndex, ushort selector) { + SegmentAndControlRegisterOperations.LoadSegmentRegister(State, Memory, segmentRegisterIndex, selector); + } + + /// LGDT: loads GDTR from a 6-byte memory pointer (2-byte limit, 4-byte base). + public void LoadGdtr(ushort segment, uint offset) { + SegmentAndControlRegisterOperations.LoadGdtr(State, Memory, segment, offset); + } + + /// SGDT: stores GDTR to a 6-byte memory pointer (2-byte limit, 4-byte base). + public void StoreGdtr(ushort segment, uint offset) { + SegmentAndControlRegisterOperations.StoreGdtr(State, Memory, segment, offset); + } + + /// LIDT: loads IDTR from a 6-byte memory pointer (2-byte limit, 4-byte base). + public void LoadIdtr(ushort segment, uint offset) { + SegmentAndControlRegisterOperations.LoadIdtr(State, Memory, segment, offset); + } + + /// SIDT: stores IDTR to a 6-byte memory pointer (2-byte limit, 4-byte base). + public void StoreIdtr(ushort segment, uint offset) { + SegmentAndControlRegisterOperations.StoreIdtr(State, Memory, segment, offset); + } + + /// MOV r32, CRn: reads CR0/CR2/CR3/CR4. + public uint ReadControlRegister(uint crNumber) { + return SegmentAndControlRegisterOperations.ReadControlRegister(State, crNumber); + } + + /// MOV CRn, r32: writes CR0/CR2/CR3/CR4. + public void WriteControlRegister(uint crNumber, uint value) { + SegmentAndControlRegisterOperations.WriteControlRegister(State, crNumber, value); + } + + /// SMSW: reads the low 16 bits of CR0. + public ushort ReadMachineStatusWord() { + return SegmentAndControlRegisterOperations.ReadMachineStatusWord(State); + } + + /// LMSW: writes the low 4 bits of CR0 (PE, MP, EM, TS). + public void LoadMachineStatusWord(ushort value) { + SegmentAndControlRegisterOperations.LoadMachineStatusWord(State, value); + } + + /// CLTS: clears CR0.TS. + public void Clts() { + SegmentAndControlRegisterOperations.Clts(State); + } + + /// Throws #GP if CPL/IOPL do not permit `IN`/`OUT`/`CLI`/`STI`. + public void EnsureIoPrivilege() { + PrivilegeChecks.EnsureIoPrivilege(State); + } + + /// LLDT: loads LDTR from a GDT selector. + public void LoadLdtr(ushort selector) { + SegmentAndControlRegisterOperations.LoadLdtr(State, Memory, selector); + } + + /// SLDT: reads the current LDTR selector. + public ushort StoreLdtr() { + return SegmentAndControlRegisterOperations.StoreLdtr(State); + } + + /// LTR: loads the Task Register from a GDT selector. + public void LoadTr(ushort selector) { + SegmentAndControlRegisterOperations.LoadTr(State, Memory, selector); + } + + /// STR: reads the current Task Register selector. + public ushort StoreTr() { + return SegmentAndControlRegisterOperations.StoreTr(State); + } + + /// ARPL: returns the r/m operand with its RPL raised to the register operand's RPL if lower. + public ushort AdjustRequestedPrivilegeLevel(ushort rmSelector, ushort regSelector) { + return SegmentAndControlRegisterOperations.AdjustRequestedPrivilegeLevel(rmSelector, regSelector); + } + + /// ARPL: whether the r/m operand's RPL was raised (sets ZF). + public bool WasPrivilegeLevelAdjusted(ushort rmSelector, ushort regSelector) { + return SegmentAndControlRegisterOperations.WasPrivilegeLevelAdjusted(rmSelector, regSelector); + } + + /// LAR: whether a selector resolves to a present descriptor (sets ZF). + public bool IsSelectorValidForLar(ushort selector) { + return SegmentAndControlRegisterOperations.IsSelectorValidForLar(State, Memory, selector); + } + + /// LAR: loads the packed access-rights doubleword for a selector. + public uint LoadAccessRights(ushort selector) { + return SegmentAndControlRegisterOperations.LoadAccessRights(State, Memory, selector); + } + + /// LSL: whether a selector resolves to a present segment descriptor (sets ZF). + public bool IsSelectorValidForLsl(ushort selector) { + return SegmentAndControlRegisterOperations.IsSelectorValidForLsl(State, Memory, selector); + } + + /// LSL: loads the granularity-scaled limit for a selector. + public uint LoadSegmentLimit(ushort selector) { + return SegmentAndControlRegisterOperations.LoadSegmentLimit(State, Memory, selector); + } + + /// VERR: whether a selector is a present, readable data or code segment. + public bool VerifyReadable(ushort selector) { + return SegmentAndControlRegisterOperations.VerifyReadable(State, Memory, selector); + } + + /// VERW: whether a selector is a present, writable data segment. + public bool VerifyWritable(ushort selector) { + return SegmentAndControlRegisterOperations.VerifyWritable(State, Memory, selector); + } + public void JumpNear(CfgInstruction instruction, ushort ip) { State.IP = ip; } @@ -95,14 +221,30 @@ public void NearCallWithReturnIpNextInstruction32(CfgInstruction instruction, us [MethodImpl(MethodImplOptions.AggressiveInlining)] public void FarCallWithReturnIpNextInstruction16(CfgInstruction instruction, SegmentedAddress target) { SegmentedAddress returnAddress = instruction.NextInMemoryAddress32.ToSegmentedAddress(); + if (TaskSwitchOperations.TryReadAvailableTss(State, Memory, target.Segment)) { + SegmentedAddress taskTarget = TaskSwitchOperations.SwitchToNewTask(State, Memory, target.Segment, returnAddress.Offset); + CurrentFunctionHandler.Call(CallType.FAR16, taskTarget, returnAddress, instruction); + return; + } + if (ProtectedModeCallGateDispatcher.TryReadCallGate(State, Memory, target.Segment, out RawGateDescriptor gate)) { + SegmentedAddress gateTarget = ProtectedModeCallGateDispatcher.Dispatch(State, Memory, Stack, gate, target.Segment, returnAddress); + CurrentFunctionHandler.Call(CallType.FAR16, gateTarget, returnAddress, instruction); + return; + } Stack.PushSegmentedAddress(returnAddress); HandleCall(instruction, CallType.FAR16, returnAddress, target); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void FarCallWithReturnIpNextInstruction32(CfgInstruction instruction, SegmentedAddress32 target) { + SegmentedAddress returnAddress = instruction.NextInMemoryAddress32.ToSegmentedAddress(); + if (ProtectedModeCallGateDispatcher.TryReadCallGate(State, Memory, target.Segment, out RawGateDescriptor gate)) { + SegmentedAddress gateTarget = ProtectedModeCallGateDispatcher.Dispatch(State, Memory, Stack, gate, target.Segment, returnAddress); + CurrentFunctionHandler.Call(CallType.FAR32, gateTarget, returnAddress, instruction); + return; + } Stack.PushFarPointer32(instruction.NextInMemoryAddress32); - HandleCall(instruction, CallType.FAR32, instruction.NextInMemoryAddress32.ToSegmentedAddress(), target.ToSegmentedAddress()); + HandleCall(instruction, CallType.FAR32, returnAddress, target.ToSegmentedAddress()); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -110,7 +252,10 @@ public void HandleCall(CfgInstruction instruction, CallType callType, SegmentedAddress returnAddress, SegmentedAddress target) { - State.CS = target.Segment; + if (callType is CallType.FAR16 or CallType.FAR32) { + PrivilegeChecks.ValidateFarCodeSegmentTransfer(State, Memory, target.Segment); + } + LoadSegmentRegister((uint)SegmentRegisterIndex.CsIndex, target.Segment); State.IP = target.Offset; CurrentFunctionHandler.Call(callType, target, returnAddress, instruction); } @@ -126,7 +271,7 @@ public void HandleInterruptInstruction(CfgInstruction instruction, byte vectorNu // This ensures the debugger sees State.IP pointing to the INT instruction _emulatorBreakpointsManager.InterruptBreakPoints.TriggerMatchingBreakPoints(vectorNumber); MoveIpToEndOfInstruction(instruction); - (SegmentedAddress target, SegmentedAddress expectedReturn) = DoInterruptWithoutBreakpoint(vectorNumber); + (SegmentedAddress target, SegmentedAddress expectedReturn) = DoInterruptWithoutBreakpoint(vectorNumber, checkGateDpl: true); CurrentFunctionHandler.ICall(target, expectedReturn, instruction, vectorNumber); } @@ -136,12 +281,27 @@ public void HandleInterruptCall(CfgInstruction instruction, byte vectorNumber) { CurrentFunctionHandler.ICall(target, expectedReturn, instruction, vectorNumber); } - public (SegmentedAddress, SegmentedAddress) DoInterrupt(byte vectorNumber) { + public (SegmentedAddress, SegmentedAddress) DoInterrupt(byte vectorNumber, ushort? errorCode = null) { _emulatorBreakpointsManager.InterruptBreakPoints.TriggerMatchingBreakPoints(vectorNumber); - return DoInterruptWithoutBreakpoint(vectorNumber); + return DoInterruptWithoutBreakpoint(vectorNumber, checkGateDpl: false, errorCode); } - private (SegmentedAddress, SegmentedAddress) DoInterruptWithoutBreakpoint(byte vectorNumber) { + /// + /// Dispatches an interrupt or exception. Real mode semantics are unchanged (the real-mode IVT); + /// protected mode AND Virtual-8086 mode both walk the IDT instead (see + /// ) - on real hardware, V86 code always reflects + /// interrupts/exceptions to the protected-mode monitor rather than handling them directly, since CPL + /// is 3 in V86 and the monitor's handlers live at DPL 0, forcing the same escalation-via-TSS path + /// used by ordinary CPL3-to-CPL0 protected-mode dispatch. is true only + /// for a software `INT n`: hardware interrupts and CPU exceptions bypass the gate's DPL. + /// + private (SegmentedAddress, SegmentedAddress) DoInterruptWithoutBreakpoint(byte vectorNumber, bool checkGateDpl, ushort? errorCode = null) { + if (State.CpuMode is CpuMode.Protected or CpuMode.Virtual8086) { + SegmentedAddress expectedReturnBeforeDispatch = State.IpSegmentedAddress; + SegmentedAddress protectedModeTarget = ProtectedModeInterruptDispatcher.Dispatch( + State, Memory, Stack, vectorNumber, checkGateDpl, errorCode, expectedReturnBeforeDispatch); + return (protectedModeTarget, expectedReturnBeforeDispatch); + } SegmentedAddress target = InterruptVectorTable[vectorNumber]; if (target.Segment == 0 && target.Offset == 0 && !_allowIvtAddress0) { throw new UnhandledOperationException(State, @@ -152,20 +312,30 @@ public void HandleInterruptCall(CfgInstruction instruction, byte vectorNumber) { Stack.PushSegmentedAddress(expectedReturn); State.InterruptFlag = false; State.IP = target.Offset; - State.CS = target.Segment; + LoadSegmentRegister((uint)SegmentRegisterIndex.CsIndex, target.Segment); return (target, expectedReturn); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void HandleInterruptRet(CfgInstruction instruction) { CurrentFunctionHandler.Ret(CallType.INTERRUPT, instruction); - _returnOperationsHelper.InterruptRet(); + if (State.CpuMode == CpuMode.Protected) { + ProtectedModeInterruptDispatcher.InterruptReturn16(State, Memory, Stack); + } else { + _returnOperationsHelper.InterruptRet(); + LoadSegmentRegister((uint)SegmentRegisterIndex.CsIndex, State.CS); + } } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void HandleInterruptRet32(CfgInstruction instruction) { CurrentFunctionHandler.Ret(CallType.INTERRUPT, instruction); + if (State.CpuMode == CpuMode.Protected) { + ProtectedModeInterruptDispatcher.InterruptReturn32(State, Memory, Stack); + return; + } _returnOperationsHelper.InterruptRet32(); + LoadSegmentRegister((uint)SegmentRegisterIndex.CsIndex, State.CS); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -183,13 +353,23 @@ public void HandleNearRet32(CfgInstruction instruction, ushort numberOfBytesToPo [MethodImpl(MethodImplOptions.AggressiveInlining)] public void HandleFarRet16(CfgInstruction instruction, ushort numberOfBytesToPop = 0) { CurrentFunctionHandler.Ret(CallType.FAR16, instruction); - _returnOperationsHelper.FarRet16(numberOfBytesToPop); + if (State.CpuMode == CpuMode.Protected) { + ProtectedModeInterruptDispatcher.FarReturn16(State, Memory, Stack, numberOfBytesToPop); + } else { + _returnOperationsHelper.FarRet16(numberOfBytesToPop); + LoadSegmentRegister((uint)SegmentRegisterIndex.CsIndex, State.CS); + } } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void HandleFarRet32(CfgInstruction instruction, ushort numberOfBytesToPop = 0) { CurrentFunctionHandler.Ret(CallType.FAR32, instruction); + if (State.CpuMode == CpuMode.Protected) { + ProtectedModeInterruptDispatcher.FarReturn32(State, Memory, Stack, numberOfBytesToPop); + return; + } _returnOperationsHelper.FarRet32(numberOfBytesToPop); + LoadSegmentRegister((uint)SegmentRegisterIndex.CsIndex, State.CS); } public void MoveIpToEndOfInstruction(CfgInstruction instruction) { @@ -197,6 +377,7 @@ public void MoveIpToEndOfInstruction(CfgInstruction instruction) { } public void ExecuteHlt(CfgInstruction instruction) { + PrivilegeChecks.EnsureCpl0(State, "HLT"); State.IsRunning = false; MoveIpToEndOfInstruction(instruction); } @@ -228,13 +409,13 @@ public void HandleCpuException(CfgInstruction instruction, CpuException cpuExcep if (_loggerService.IsEnabled(LogLevel.Debug)) { _loggerService.LogDebug(cpuException, "{ExceptionType} in {MethodName}", nameof(CpuException), nameof(HandleCpuException)); } - // Real-mode interrupts do NOT push an error code on the stack — that is a - // protected-mode behavior. Spice86 is real-mode only, so any error code - // carried by the exception object is informational and must not be pushed. + // Real mode has no error-code concept; only protected-mode dispatch (DoInterrupt) actually + // pushes it, and only when the gate/frame layout supports it. try { // Link to the interrupt handler will likely need to be added instruction.IncreaseMaxSuccessorsCount(InterruptVectorTable[cpuException.InterruptVector]); - HandleInterruptCall(instruction, cpuException.InterruptVector); + (SegmentedAddress target, SegmentedAddress expectedReturn) = DoInterrupt(cpuException.InterruptVector, cpuException.ErrorCode); + CurrentFunctionHandler.ICall(target, expectedReturn, instruction, cpuException.InterruptVector); CurrentExecutionContext.CpuFault = true; } catch (UnhandledOperationException e) { throw new AggregateException(cpuException, e); diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/InstructionParser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/InstructionParser.cs index 25d2b7adc1..82baf08946 100644 --- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/InstructionParser.cs +++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/InstructionParser.cs @@ -29,6 +29,7 @@ public class InstructionParser { private readonly ParsingTools _parsingTools; private readonly AluOperationParser _aluOperationParser; + private readonly ArplParser _arplParser; private readonly BcdAdjustParser _bcdAdjustParser; private readonly BitScanRmParser _bitScanBsfParser; private readonly BitScanRmParser _bitScanBsrParser; @@ -39,7 +40,9 @@ public class InstructionParser { private readonly CallParser _callParser; private readonly CbwParser _cbwParser; private readonly CmpxchgRmParser _cmpxchgRmParser; + private readonly ControlRegisterParser _controlRegisterParser; private readonly CwdParser _cwdParser; + private readonly DescriptorTableParser _descriptorTableParser; private readonly EnterParser _enterParser; private readonly FlagControlParser _flagControlParser; private readonly FlagTransferParser _flagTransferParser; @@ -59,6 +62,7 @@ public class InstructionParser { private readonly JccParser _jccParser; private readonly JcxzParser _jcxzParser; private readonly JmpParser _jmpParser; + private readonly LarLslParser _larLslParser; private readonly LeaParser _leaParser; private readonly LeaveParser _leaveParser; private readonly LoopParser _loopParser; @@ -85,6 +89,7 @@ public class InstructionParser { private readonly SetRmccParser _setRmccParser; private readonly ShxdRmParser _shxdRmParser; private readonly SimpleInstructionParser _simpleInstructionParser; + private readonly SystemSegmentParser _systemSegmentParser; private readonly TestAccImmParser _testAccImmParser; private readonly TestRmRegParser _testRmRegParser; private readonly XaddRmParser _xaddRmParser; @@ -94,6 +99,7 @@ public class InstructionParser { public InstructionParser(IIndexable memory, State state, SequentialIdAllocator idAllocator) { _parsingTools = new(memory, state, idAllocator); _aluOperationParser = new(_parsingTools); + _arplParser = new(_parsingTools); _bcdAdjustParser = new(_parsingTools); _bitScanBsfParser = new(_parsingTools, InstructionOperation.BSF, "BitScanForward"); _bitScanBsrParser = new(_parsingTools, InstructionOperation.BSR, "BitScanReverse"); @@ -104,7 +110,9 @@ public InstructionParser(IIndexable memory, State state, SequentialIdAllocator i _callParser = new(_parsingTools); _cbwParser = new(_parsingTools); _cmpxchgRmParser = new CmpxchgRmParser(_parsingTools); + _controlRegisterParser = new(_parsingTools); _cwdParser = new(_parsingTools); + _descriptorTableParser = new(_parsingTools); _enterParser = new(_parsingTools); _flagControlParser = new(_parsingTools); _flagTransferParser = new(_parsingTools); @@ -124,6 +132,7 @@ public InstructionParser(IIndexable memory, State state, SequentialIdAllocator i _jccParser = new(_parsingTools); _jcxzParser = new(_parsingTools); _jmpParser = new(_parsingTools); + _larLslParser = new(_parsingTools); _leaParser = new(_parsingTools); _leaveParser = new(_parsingTools); _loopParser = new(_parsingTools); @@ -150,6 +159,7 @@ public InstructionParser(IIndexable memory, State state, SequentialIdAllocator i _setRmccParser = new(_parsingTools); _shxdRmParser = new(_parsingTools); _simpleInstructionParser = new(_parsingTools); + _systemSegmentParser = new(_parsingTools); _testAccImmParser = new(_parsingTools); _testRmRegParser = new(_parsingTools); _xaddRmParser = new(_parsingTools); @@ -171,7 +181,8 @@ public CfgInstruction ParseInstructionAt(SegmentedAddress address) { _parsingTools.InstructionReader.InstructionReaderAddressSource.InstructionAddress = address; List prefixes = ParsePrefixes(); InstructionField opcodeField = ReadOpcode(); - ParsingContext context = new(address, opcodeField, prefixes); + bool codeSegmentDefaultBig = _parsingTools.State.SegmentDescriptorCaches[SegmentRegisterIndex.CsIndex].DefaultBig; + ParsingContext context = new(address, opcodeField, prefixes, codeSegmentDefaultBig); try { CfgInstruction parsed = ParseCfgInstruction(context); ValidateLockPrefix(parsed, prefixes); @@ -270,6 +281,7 @@ private void PopulateSingleByteHandlers() { _handlers[0x60] = _pushaParser.Parse; _handlers[0x61] = _popaParser.Parse; _handlers[0x62] = _boundParser.Parse; + _handlers[0x63] = _arplParser.Parse; _handlers[0x68] = ctx => _pushImmParser.Parse(ctx, imm8SignExtended: false); _handlers[0x69] = ctx => _imulImmRmParser.Parse(ctx, imm8: false); _handlers[0x6A] = ctx => _pushImmParser.Parse(ctx, imm8SignExtended: true); @@ -498,7 +510,13 @@ private void PopulateMovRegImmHandlers() { } private void Populate0FHandlers() { + _handlers0F[0x01] = _descriptorTableParser.Parse; + _handlers0F[0x00] = _systemSegmentParser.Parse; + _handlers0F[0x02] = _larLslParser.ParseLar; + _handlers0F[0x03] = _larLslParser.ParseLsl; _handlers0F[0x06] = _simpleInstructionParser.ParseClts; + _handlers0F[0x20] = _controlRegisterParser.ParseMovRegFromCr; + _handlers0F[0x22] = _controlRegisterParser.ParseMovCrFromReg; // Jcc near: 16 condition codes (same encoding as Jcc short) for (int cc = 0; cc < 16; cc++) { diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/ParsingContext.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/ParsingContext.cs index d31c7322b2..e6054c514e 100644 --- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/ParsingContext.cs +++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/ParsingContext.cs @@ -19,14 +19,23 @@ public class ParsingContext : ModRmParsingContext { /// Returns when operand-size prefix is active, otherwise. public BitWidth DefaultWordOperandBitWidth { get; } + /// The address of the instruction being parsed. + /// The instruction's opcode field. + /// The instruction's decoded prefixes. + /// + /// The CURRENT code segment's D/B bit (32-bit default operand/address size). Real mode and 16-bit + /// protected-mode segments pass false. The 0x66/0x67 prefixes TOGGLE relative to this default + /// rather than unconditionally selecting 32-bit, matching real hardware: in a 32-bit-default segment, + /// 0x66 present means 16-bit, and 0x66 absent means 32-bit. + /// public ParsingContext(SegmentedAddress address, InstructionField opcodeField, - List prefixes) { + List prefixes, bool codeSegmentDefaultBig) { Address = address; OpcodeField = opcodeField; Prefixes = prefixes; - AddressWidthFromPrefixes = ComputeAddressSize(prefixes); + AddressWidthFromPrefixes = ComputeAddressSize(prefixes, codeSegmentDefaultBig); SegmentOverrideFromPrefixes = ComputeSegmentOverrideIndex(prefixes); - HasOperandSize32 = ComputeHasOperandSize32(prefixes); + HasOperandSize32 = ComputeHasOperandSize32(prefixes, codeSegmentDefaultBig); DefaultWordOperandBitWidth = HasOperandSize32 ? BitWidth.DWORD_32 : BitWidth.WORD_16; } @@ -36,12 +45,13 @@ public ParsingContext(SegmentedAddress address, InstructionField opcodeF return overridePrefix?.SegmentRegisterIndexValue; } - private static BitWidth ComputeAddressSize(List prefixes) { - AddressSize32Prefix? addressSize32Prefix = prefixes.OfType().FirstOrDefault(); - return addressSize32Prefix == null ? BitWidth.WORD_16 : BitWidth.DWORD_32; + private static BitWidth ComputeAddressSize(List prefixes, bool codeSegmentDefaultBig) { + bool addressSize32PrefixPresent = prefixes.OfType().Any(); + return (codeSegmentDefaultBig ^ addressSize32PrefixPresent) ? BitWidth.DWORD_32 : BitWidth.WORD_16; } - private static bool ComputeHasOperandSize32(IList prefixes) { - return prefixes.Any(p => p is OperandSize32Prefix); + private static bool ComputeHasOperandSize32(IList prefixes, bool codeSegmentDefaultBig) { + bool operandSize32PrefixPresent = prefixes.Any(p => p is OperandSize32Prefix); + return codeSegmentDefaultBig ^ operandSize32PrefixPresent; } -} \ No newline at end of file +} diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/ArplParser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/ArplParser.cs new file mode 100644 index 0000000000..3ad6165b66 --- /dev/null +++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/ArplParser.cs @@ -0,0 +1,44 @@ +namespace Spice86.Core.Emulator.CPU.CfgCpu.Parser.SpecificParsers; + +using Spice86.Core.Emulator.CPU.CfgCpu.Parser; + +using Spice86.Core.Emulator.CPU.CfgCpu.Ast; +using Spice86.Core.Emulator.CPU.CfgCpu.Ast.Instruction; +using Spice86.Core.Emulator.CPU.CfgCpu.Ast.Operations; +using Spice86.Core.Emulator.CPU.CfgCpu.Ast.Value; +using Spice86.Core.Emulator.CPU.CfgCpu.InstructionExecutor; +using Spice86.Core.Emulator.CPU.CfgCpu.ParsedInstruction; +using Spice86.Core.Emulator.CPU.CfgCpu.ParsedInstruction.ModRm; + +/// ARPL Ew, Gw: raises the r/m selector's RPL to the register selector's RPL if lower, sets ZF if changed. +public class ArplParser : BaseInstructionParser { + public ArplParser(ParsingTools parsingTools) : base(parsingTools) { + } + + public CfgInstruction Parse(ParsingContext context) { + (CfgInstruction instr, ModRmContext modRmContext) = ParseModRmBase(context, 1); + ValueNode rmNode = _astBuilder.ModRm.RmToNode(DataType.UINT16, modRmContext); + ValueNode regNode = _astBuilder.ModRm.RToNode(DataType.UINT16, modRmContext); + ValueNode zeroFlagNode = _astBuilder.Flag.Zero(); + + MethodCallValueNode wasAdjustedCall = new MethodCallValueNode(DataType.BOOL, null, + nameof(InstructionExecutionHelper.WasPrivilegeLevelAdjusted), rmNode, regNode); + MethodCallValueNode adjustCall = new MethodCallValueNode(DataType.UINT16, null, + nameof(InstructionExecutionHelper.AdjustRequestedPrivilegeLevel), rmNode, regNode); + + // Real hardware never writes the r/m operand back to memory unless the RPL was actually + // raised - an unconditional write-back would incorrectly fault on a read-only destination + // segment even when the value doesn't change. + BlockNode trueCase = new BlockNode( + _astBuilder.Assign(DataType.BOOL, zeroFlagNode, _astBuilder.Constant.ToNode(DataType.BOOL, 1UL)), + _astBuilder.Assign(DataType.UINT16, rmNode, adjustCall)); + BinaryOperationNode falseCase = _astBuilder.Assign(DataType.BOOL, zeroFlagNode, _astBuilder.Constant.ToNode(DataType.BOOL, 0UL)); + + IfElseNode ifElse = new IfElseNode(wasAdjustedCall, trueCase, falseCase); + + InstructionNode displayAst = new InstructionNode(InstructionOperation.ARPL, rmNode, regNode); + IVisitableAstNode execAst = _astBuilder.WithIpAdvancement(instr, ifElse); + instr.AttachAsts(displayAst, execAst); + return instr; + } +} diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/BaseInstructionParser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/BaseInstructionParser.cs index 362f028f44..4b2d7ae872 100644 --- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/BaseInstructionParser.cs +++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/BaseInstructionParser.cs @@ -87,20 +87,20 @@ protected void RegisterModRmFields(CfgInstruction instr, ModRmContext modRmConte protected ValueNode ReadUnsignedImmediate(CfgInstruction instr, BitWidth bitWidth) { switch (bitWidth) { case BitWidth.BYTE_8: { - InstructionField field = _instructionReader.UInt8.NextField(false); - instr.AddField(field); - return _astBuilder.InstructionField.ToNode(field); - } + InstructionField field = _instructionReader.UInt8.NextField(false); + instr.AddField(field); + return _astBuilder.InstructionField.ToNode(field); + } case BitWidth.WORD_16: { - InstructionField field = _instructionReader.UInt16.NextField(false); - instr.AddField(field); - return _astBuilder.InstructionField.ToNode(field); - } + InstructionField field = _instructionReader.UInt16.NextField(false); + instr.AddField(field); + return _astBuilder.InstructionField.ToNode(field); + } case BitWidth.DWORD_32: { - InstructionField field = _instructionReader.UInt32.NextField(false); - instr.AddField(field); - return _astBuilder.InstructionField.ToNode(field); - } + InstructionField field = _instructionReader.UInt32.NextField(false); + instr.AddField(field); + return _astBuilder.InstructionField.ToNode(field); + } default: throw CreateUnsupportedBitWidthException(bitWidth); } @@ -120,17 +120,17 @@ protected ValueNode ReadSignedImmediate(CfgInstruction instr, BitWidth bitWidth) private (int signedValue, FieldWithValue field, ValueNode node) ReadSignedField(BitWidth width, bool isDiscriminant) { switch (width) { case BitWidth.BYTE_8: { - InstructionField field = _instructionReader.Int8.NextField(isDiscriminant); - return (field.Value, field, _astBuilder.InstructionField.ToNode(field)); - } + InstructionField field = _instructionReader.Int8.NextField(isDiscriminant); + return (field.Value, field, _astBuilder.InstructionField.ToNode(field)); + } case BitWidth.WORD_16: { - InstructionField field = _instructionReader.Int16.NextField(isDiscriminant); - return (field.Value, field, _astBuilder.InstructionField.ToNode(field)); - } + InstructionField field = _instructionReader.Int16.NextField(isDiscriminant); + return (field.Value, field, _astBuilder.InstructionField.ToNode(field)); + } case BitWidth.DWORD_32: { - InstructionField field = _instructionReader.Int32.NextField(isDiscriminant); - return (field.Value, field, _astBuilder.InstructionField.ToNode(field)); - } + InstructionField field = _instructionReader.Int32.NextField(isDiscriminant); + return (field.Value, field, _astBuilder.InstructionField.ToNode(field)); + } default: throw CreateUnsupportedBitWidthException(width); } @@ -164,15 +164,16 @@ protected ValueNode ReadSignedImmediate(CfgInstruction instr, BitWidth bitWidth) CfgInstruction instr, DataType dataType, ValueNode portNode, ValueNode accumulator, bool isInput) { InstructionNode displayAst; IVisitableAstNode execAst; + MethodCallNode ensureIoPrivilege = new MethodCallNode(null, nameof(InstructionExecutor.InstructionExecutionHelper.EnsureIoPrivilege)); if (isInput) { MethodCallValueNode ioRead = _astBuilder.Io.IoRead(dataType, portNode); displayAst = new InstructionNode(InstructionOperation.IN, accumulator, portNode); execAst = _astBuilder.WithIpAdvancement(instr, - _astBuilder.Assign(dataType, accumulator, ioRead)); + ensureIoPrivilege, _astBuilder.Assign(dataType, accumulator, ioRead)); } else { MethodCallNode ioWrite = _astBuilder.Io.IoWrite(dataType, portNode, accumulator); displayAst = new InstructionNode(InstructionOperation.OUT, portNode, accumulator); - execAst = _astBuilder.WithIpAdvancement(instr, ioWrite); + execAst = _astBuilder.WithIpAdvancement(instr, ensureIoPrivilege, ioWrite); } return (displayAst, execAst); } diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/BoundParser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/BoundParser.cs index 0cbf49c850..fb27a02090 100644 --- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/BoundParser.cs +++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/BoundParser.cs @@ -22,7 +22,7 @@ public CfgInstruction Parse(ParsingContext context) { BitWidth bitWidth = context.DefaultWordOperandBitWidth; int elementSize = bitWidth.ToBytes(); DataType signedType = _astBuilder.SType(bitWidth); - DataType addrType = _astBuilder.UType(BitWidth.WORD_16); + DataType addrType = _astBuilder.AddressType(context.AddressWidthFromPrefixes); ValueNode indexNode = _astBuilder.TypeConversion.Convert(signedType, _astBuilder.ModRm.RToNode(_astBuilder.UType(bitWidth), modRmContext)); @@ -40,7 +40,7 @@ public CfgInstruction Parse(ParsingContext context) { ValueNode displayIndexNode = _astBuilder.ModRm.RToNode(signedType, modRmContext); ValueNode displayLowerPointer = _astBuilder.ModRm.ToMemoryAddressNode(signedType, modRmContext); ValueNode offset = _astBuilder.ModRm.MemoryOffsetToNode(modRmContext); - ValueNode upperOffset = _astBuilder.Constant.AddConstant(_astBuilder.AddressType(instr), offset, elementSize); + ValueNode upperOffset = _astBuilder.Constant.AddConstant(addrType, offset, elementSize); ValueNode displayUpperPointer = _astBuilder.Pointer.ToSegmentedPointer(signedType, (SegmentRegisterIndex)modRmContext.SegmentIndex.Value, upperOffset); InstructionNode displayAst = new InstructionNode( InstructionOperation.BOUND, diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/ControlRegisterParser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/ControlRegisterParser.cs new file mode 100644 index 0000000000..6a18ad8f5d --- /dev/null +++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/ControlRegisterParser.cs @@ -0,0 +1,59 @@ +namespace Spice86.Core.Emulator.CPU.CfgCpu.Parser.SpecificParsers; + +using Spice86.Core.Emulator.CPU.CfgCpu.Parser; + +using Spice86.Core.Emulator.CPU.CfgCpu.Ast; +using Spice86.Core.Emulator.CPU.CfgCpu.Ast.Instruction; +using Spice86.Core.Emulator.CPU.CfgCpu.Ast.Value; +using Spice86.Core.Emulator.CPU.CfgCpu.InstructionExecutor; +using Spice86.Core.Emulator.CPU.CfgCpu.ParsedInstruction; +using Spice86.Core.Emulator.CPU.CfgCpu.ParsedInstruction.ModRm; +using Spice86.Core.Emulator.CPU.Exceptions; +using Spice86.Core.Emulator.CPU.Registers; + +/// +/// MOV to/from a control register (0F 20 / 0F 22). The ModRM reg field selects the control register +/// number (only CR0/CR2/CR3/CR4 are valid); the mod field is ignored on real hardware and the r/m +/// field always names a general-purpose register. +/// +public class ControlRegisterParser : BaseInstructionParser { + public ControlRegisterParser(ParsingTools parsingTools) : base(parsingTools) { + } + + /// MOV r32, CRn (0F 20): reads a control register into a general-purpose register. + public CfgInstruction ParseMovRegFromCr(ParsingContext context) { + return Parse(context, isLoad: true); + } + + /// MOV CRn, r32 (0F 22): writes a general-purpose register into a control register. + public CfgInstruction ParseMovCrFromReg(ParsingContext context) { + return Parse(context, isLoad: false); + } + + private CfgInstruction Parse(ParsingContext context, bool isLoad) { + (CfgInstruction instr, ModRmContext modRmContext) = ParseModRmBase(context, 1); + int crNumber = modRmContext.RegisterIndex; + if (crNumber is not (0 or 2 or 3 or 4)) { + throw new CpuInvalidOpcodeException($"MOV to/from CR{crNumber} is not supported"); + } + + // The mod field is ignored for MOV to/from control registers: r/m always names a GP register. + ValueNode gpRegNode = _astBuilder.Register.Reg32((RegisterIndex)modRmContext.RegisterMemoryIndex); + ValueNode crNumberNode = _astBuilder.Constant.ToNode((uint)crNumber); + InstructionNode displayAst; + IVisitableAstNode execAst; + if (isLoad) { + MethodCallValueNode readCr = new MethodCallValueNode(DataType.UINT32, null, + nameof(InstructionExecutionHelper.ReadControlRegister), crNumberNode); + displayAst = new InstructionNode(InstructionOperation.MOV, gpRegNode, crNumberNode); + execAst = _astBuilder.WithIpAdvancement(instr, _astBuilder.Assign(DataType.UINT32, gpRegNode, readCr)); + } else { + MethodCallNode writeCr = new MethodCallNode(null, + nameof(InstructionExecutionHelper.WriteControlRegister), crNumberNode, gpRegNode); + displayAst = new InstructionNode(InstructionOperation.MOV, crNumberNode, gpRegNode); + execAst = _astBuilder.WithIpAdvancement(instr, writeCr); + } + instr.AttachAsts(displayAst, execAst); + return instr; + } +} diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/DescriptorTableParser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/DescriptorTableParser.cs new file mode 100644 index 0000000000..ce189c3af0 --- /dev/null +++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/DescriptorTableParser.cs @@ -0,0 +1,69 @@ +namespace Spice86.Core.Emulator.CPU.CfgCpu.Parser.SpecificParsers; + +using Spice86.Core.Emulator.CPU.CfgCpu.Parser; + +using Spice86.Core.Emulator.CPU.CfgCpu.Ast; +using Spice86.Core.Emulator.CPU.CfgCpu.Ast.Instruction; +using Spice86.Core.Emulator.CPU.CfgCpu.Ast.Value; +using Spice86.Core.Emulator.CPU.CfgCpu.InstructionExecutor; +using Spice86.Core.Emulator.CPU.CfgCpu.ParsedInstruction; +using Spice86.Core.Emulator.CPU.CfgCpu.ParsedInstruction.ModRm; +using Spice86.Core.Emulator.CPU.Exceptions; + +/// +/// Group 0F 01: LGDT, SGDT, LIDT, SIDT, SMSW, LMSW. The ModRM reg field selects the sub-operation. +/// LGDT/SGDT/LIDT/SIDT require a memory r/m operand (a 6-byte pointer: 2-byte limit + 4-byte base); +/// SMSW/LMSW accept a register or memory r/m16 operand. +/// +public class DescriptorTableParser : BaseGrpOperationParser { + public DescriptorTableParser(ParsingTools parsingTools) : base(parsingTools) { + } + + protected override CfgInstruction Parse(ParsingContext context, ModRmContext modRmContext, int groupIndex) { + return groupIndex switch { + 0 => Build(context, modRmContext, nameof(InstructionExecutionHelper.StoreGdtr), InstructionOperation.SGDT), + 1 => Build(context, modRmContext, nameof(InstructionExecutionHelper.StoreIdtr), InstructionOperation.SIDT), + 2 => Build(context, modRmContext, nameof(InstructionExecutionHelper.LoadGdtr), InstructionOperation.LGDT), + 3 => Build(context, modRmContext, nameof(InstructionExecutionHelper.LoadIdtr), InstructionOperation.LIDT), + 4 => BuildSmsw(context, modRmContext), + 6 => BuildLmsw(context, modRmContext), + _ => throw new CpuInvalidOpcodeException($"Group 0F 01 /{groupIndex} is not supported") + }; + } + + private CfgInstruction BuildSmsw(ParsingContext context, ModRmContext modRmContext) { + CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1); + RegisterModRmFields(instr, modRmContext); + ValueNode destNode = _astBuilder.ModRm.RmToNode(DataType.UINT16, modRmContext); + MethodCallValueNode readMsw = new MethodCallValueNode(DataType.UINT16, null, nameof(InstructionExecutionHelper.ReadMachineStatusWord)); + InstructionNode displayAst = new InstructionNode(InstructionOperation.SMSW, destNode); + IVisitableAstNode execAst = _astBuilder.WithIpAdvancement(instr, _astBuilder.Assign(DataType.UINT16, destNode, readMsw)); + instr.AttachAsts(displayAst, execAst); + return instr; + } + + private CfgInstruction BuildLmsw(ParsingContext context, ModRmContext modRmContext) { + CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1); + RegisterModRmFields(instr, modRmContext); + ValueNode sourceNode = _astBuilder.ModRm.RmToNode(DataType.UINT16, modRmContext); + MethodCallNode loadMsw = new MethodCallNode(null, nameof(InstructionExecutionHelper.LoadMachineStatusWord), sourceNode); + InstructionNode displayAst = new InstructionNode(InstructionOperation.LMSW, sourceNode); + IVisitableAstNode execAst = _astBuilder.WithIpAdvancement(instr, loadMsw); + instr.AttachAsts(displayAst, execAst); + return instr; + } + + private CfgInstruction Build(ParsingContext context, ModRmContext modRmContext, string methodName, InstructionOperation displayOp) { + _modRmParser.EnsureNotMode3(modRmContext); + CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1); + RegisterModRmFields(instr, modRmContext); + ValueNode segmentNode = new SegmentRegisterNode(modRmContext.SegmentIndex + ?? throw new CpuInvalidOpcodeException("Memory operand is missing a segment index")); + ValueNode offsetNode = _astBuilder.TypeConversion.Convert(DataType.UINT32, _astBuilder.ModRm.MemoryOffsetToNode(modRmContext)); + MethodCallNode call = new MethodCallNode(null, methodName, segmentNode, offsetNode); + InstructionNode displayAst = new InstructionNode(displayOp, _astBuilder.ModRm.RmToNode(DataType.UINT32, modRmContext)); + IVisitableAstNode execAst = _astBuilder.WithIpAdvancement(instr, call); + instr.AttachAsts(displayAst, execAst); + return instr; + } +} diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/EnterParser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/EnterParser.cs index fcb827d403..794474c8de 100644 --- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/EnterParser.cs +++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/EnterParser.cs @@ -4,11 +4,8 @@ namespace Spice86.Core.Emulator.CPU.CfgCpu.Parser.SpecificParsers; using Spice86.Core.Emulator.CPU.CfgCpu.Ast; using Spice86.Core.Emulator.CPU.CfgCpu.Ast.Instruction; -using Spice86.Core.Emulator.CPU.CfgCpu.Ast.Operations; using Spice86.Core.Emulator.CPU.CfgCpu.Ast.Value; using Spice86.Core.Emulator.CPU.CfgCpu.ParsedInstruction; -using Spice86.Core.Emulator.CPU.Registers; -using Spice86.Shared.Emulator.Memory; /// ENTER public class EnterParser : BaseInstructionParser { @@ -21,75 +18,18 @@ public CfgInstruction Parse(ParsingContext context) { CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1); instr.AddField(storageField); instr.AddField(levelField); - BitWidth bitWidth = GetBitWidth(false, context.HasOperandSize32); - DataType stackType = _astBuilder.UType(bitWidth); - // SP/BP arithmetic always uses the 16-bit stack pointer in real mode, - // even with the 0x66 (operand-size 32) prefix. Only the push/copy - // widths and the destination register width depend on bitWidth, so - // ESP[31:16] survives the instruction unchanged. - DataType spType = _astBuilder.UType(BitWidth.WORD_16); - ValueNode storageNodeRaw = _astBuilder.InstructionField.ToNode(storageField); - ValueNode levelNodeRaw = _astBuilder.InstructionField.ToNode(levelField); - ValueNode maskedLevelValue = new BinaryOperationNode( - DataType.UINT8, levelNodeRaw, BinaryOperation.BITWISE_AND, _astBuilder.Constant.ToNode((byte)0x1F)); - VariableDeclarationNode levelDeclaration = _astBuilder.DeclareVariable(DataType.UINT8, "level", maskedLevelValue); - VariableReferenceNode levelReference = levelDeclaration.Reference; - ValueNode pointerSize = _astBuilder.Constant.ToNode(spType, (ulong)bitWidth.ToBytes()); - VariableDeclarationNode oldBasePointerDeclaration = _astBuilder.DeclareVariable( - stackType, "oldBasePointer", _astBuilder.Register.Reg(stackType, RegisterIndex.BpIndex)); - VariableReferenceNode oldBasePointerReference = oldBasePointerDeclaration.Reference; - VariableDeclarationNode oldStackPointerDeclaration = _astBuilder.DeclareVariable( - spType, "oldStackPointer", _astBuilder.Register.Reg(spType, RegisterIndex.SpIndex)); - VariableReferenceNode oldStackPointerReference = oldStackPointerDeclaration.Reference; - ValueNode initialSpIndexValue = new BinaryOperationNode( - spType, oldStackPointerReference, BinaryOperation.MINUS, pointerSize); - VariableDeclarationNode spIndexDeclaration = _astBuilder.DeclareVariable(spType, "spIndex", initialSpIndexValue); - VariableReferenceNode spIndexReference = spIndexDeclaration.Reference; - ValueNode ssRegister = _astBuilder.Register.SReg(SegmentRegisterIndex.SsIndex); - ValueNode stackAtSp = _astBuilder.Pointer.ToSegmentedPointer(stackType, ssRegister, spIndexReference); - BinaryOperationNode pushOldBasePointer = _astBuilder.Assign(stackType, stackAtSp, oldBasePointerReference); - VariableDeclarationNode framePointerDeclaration = _astBuilder.DeclareVariable(spType, "framePtr", spIndexReference); - VariableReferenceNode framePointerReference = framePointerDeclaration.Reference; - ValueNode oldBasePointerAsWord = _astBuilder.TypeConversion.Convert(spType, oldBasePointerReference); - VariableDeclarationNode bpIndexDeclaration = _astBuilder.DeclareVariable(spType, "bpIndex", oldBasePointerAsWord); - VariableReferenceNode bpIndexReference = bpIndexDeclaration.Reference; - VariableDeclarationNode loopIndexDeclaration = _astBuilder.DeclareVariable(DataType.INT32, "i", _astBuilder.Constant.ToNode(1)); - VariableReferenceNode loopIndexReference = loopIndexDeclaration.Reference; - ValueNode levelAsInt = _astBuilder.TypeConversion.Convert(DataType.INT32, levelReference); - ValueNode loopCondition = new BinaryOperationNode(DataType.BOOL, loopIndexReference, BinaryOperation.LESS_THAN, levelAsInt); - BinaryOperationNode decrementBpIndex = _astBuilder.Assign(spType, bpIndexReference, - new BinaryOperationNode(spType, bpIndexReference, BinaryOperation.MINUS, pointerSize)); - BinaryOperationNode decrementSpIndex = _astBuilder.Assign(spType, spIndexReference, - new BinaryOperationNode(spType, spIndexReference, BinaryOperation.MINUS, pointerSize)); - ValueNode sourcePointer = _astBuilder.Pointer.ToSegmentedPointer(stackType, ssRegister, bpIndexReference); - ValueNode destinationPointer = _astBuilder.Pointer.ToSegmentedPointer(stackType, ssRegister, spIndexReference); - BinaryOperationNode copyFrameValue = _astBuilder.Assign(stackType, destinationPointer, sourcePointer); - BlockNode loopBody = new BlockNode(decrementBpIndex, decrementSpIndex, copyFrameValue); - BinaryOperationNode incrementLoopIndex = _astBuilder.Assign(DataType.INT32, loopIndexReference, - _astBuilder.Constant.AddConstant(DataType.INT32, loopIndexReference, 1)); - BlockNode forLoop = _astBuilder.ControlFlow.For(loopIndexDeclaration, loopCondition, incrementLoopIndex, loopBody); - BinaryOperationNode decrementSpForFramePointer = _astBuilder.Assign(spType, spIndexReference, - new BinaryOperationNode(spType, spIndexReference, BinaryOperation.MINUS, pointerSize)); - ValueNode destinationFramePointer = _astBuilder.Pointer.ToSegmentedPointer(stackType, ssRegister, spIndexReference); - ValueNode framePointerAsStackType = _astBuilder.TypeConversion.Convert(stackType, framePointerReference); - BinaryOperationNode pushFramePointer = _astBuilder.Assign(stackType, destinationFramePointer, framePointerAsStackType); - BlockNode levelNotZeroBlock = new BlockNode(bpIndexDeclaration, forLoop, decrementSpForFramePointer, pushFramePointer); - ValueNode levelNotZeroCondition = new BinaryOperationNode(DataType.BOOL, levelReference, BinaryOperation.NOT_EQUAL, - _astBuilder.Constant.ToNode((byte)0)); - IfElseNode handleNestingLevel = _astBuilder.ControlFlow.If(levelNotZeroCondition, levelNotZeroBlock); - ValueNode framePointerForBp = _astBuilder.TypeConversion.Convert(stackType, framePointerReference); - BinaryOperationNode setBasePointerToFrame = _astBuilder.Assign(stackType, - _astBuilder.Register.Reg(stackType, RegisterIndex.BpIndex), framePointerForBp); - BinaryOperationNode subtractStorage = _astBuilder.Assign(spType, spIndexReference, - new BinaryOperationNode(spType, spIndexReference, BinaryOperation.MINUS, storageNodeRaw)); - BinaryOperationNode setStackPointer = _astBuilder.Assign(spType, - _astBuilder.Register.Reg(spType, RegisterIndex.SpIndex), spIndexReference); - InstructionOperation enterOp = bitWidth == BitWidth.DWORD_32 ? InstructionOperation.ENTERW : InstructionOperation.ENTER; - InstructionNode displayAst = new InstructionNode(enterOp, storageNodeRaw, levelNodeRaw); - IVisitableAstNode execAst = _astBuilder.WithIpAdvancement(instr, - levelDeclaration, oldBasePointerDeclaration, oldStackPointerDeclaration, spIndexDeclaration, - pushOldBasePointer, framePointerDeclaration, handleNestingLevel, - setBasePointerToFrame, subtractStorage, setStackPointer); + + ValueNode storageNode = _astBuilder.InstructionField.ToNode(storageField); + ValueNode levelNode = _astBuilder.InstructionField.ToNode(levelField); + ValueNode operandSize32Node = _astBuilder.Constant.ToNode(DataType.BOOL, context.HasOperandSize32 ? 1UL : 0UL); + + // The frame-pointer register width and stack-pointer address width both depend on SS's D/B + // bit, which - unlike CS's - can legitimately differ between calls to the same code address, + // so Stack.Enter resolves them fresh every call instead of baking a choice in at parse time. + MethodCallNode enterCall = new("Stack", nameof(Stack.Enter), storageNode, levelNode, operandSize32Node); + + InstructionNode displayAst = new InstructionNode(context.HasOperandSize32 ? InstructionOperation.ENTERW : InstructionOperation.ENTER, storageNode, levelNode); + IVisitableAstNode execAst = _astBuilder.WithIpAdvancement(instr, enterCall); instr.AttachAsts(displayAst, execAst); return instr; } diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/FlagControlParser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/FlagControlParser.cs index 1eb4f3eabe..4bd58d55f2 100644 --- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/FlagControlParser.cs +++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/FlagControlParser.cs @@ -6,6 +6,7 @@ namespace Spice86.Core.Emulator.CPU.CfgCpu.Parser.SpecificParsers; using Spice86.Core.Emulator.CPU.CfgCpu.Ast.Instruction; using Spice86.Core.Emulator.CPU.CfgCpu.Ast.Operations; using Spice86.Core.Emulator.CPU.CfgCpu.Ast.Value; +using Spice86.Core.Emulator.CPU.CfgCpu.InstructionExecutor; using Spice86.Core.Emulator.CPU.CfgCpu.ParsedInstruction; using Spice86.Shared.Emulator.Memory; @@ -29,7 +30,10 @@ public CfgInstruction ParseFlagControl(ParsingContext context, CpuFlagNode flagN CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1); BinaryOperationNode flagAssignment = _astBuilder.Assign(DataType.BOOL, flagNode, _astBuilder.Constant.ToNode(DataType.BOOL, value)); InstructionNode displayAst = new InstructionNode(displayOp); - IVisitableAstNode execAst = _astBuilder.WithIpAdvancement(instr, flagAssignment); + // CLI requires IOPL clearance, like STI; the other flag-control ops (CLC/STC/CLD/STD) don't. + IVisitableAstNode execAst = displayOp == InstructionOperation.CLI + ? _astBuilder.WithIpAdvancement(instr, new MethodCallNode(null, nameof(InstructionExecutionHelper.EnsureIoPrivilege)), flagAssignment) + : _astBuilder.WithIpAdvancement(instr, flagAssignment); instr.AttachAsts(displayAst, execAst); // CLI (opcode 0xFA) must start a new CfgBlock so external interrupt delivery // happens at the boundary just before interrupts are disabled. @@ -42,10 +46,11 @@ public CfgInstruction ParseFlagControl(ParsingContext context, CpuFlagNode flagN public CfgInstruction ParseSti(ParsingContext context) { CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1); CpuFlagNode flagNode = _astBuilder.Flag.Interrupt(); + MethodCallNode ensureIoPrivilege = new MethodCallNode(null, nameof(InstructionExecutionHelper.EnsureIoPrivilege)); BinaryOperationNode flagAssignment = _astBuilder.Assign(DataType.BOOL, flagNode, _astBuilder.Constant.ToNode(DataType.BOOL, 1UL)); IVisitableAstNode setInterruptShadowing = _astBuilder.Flag.SetInterruptShadowingIfInterruptDisabled(); InstructionNode displayAst = new InstructionNode(InstructionOperation.STI); - IVisitableAstNode execAst = _astBuilder.WithIpAdvancement(instr, setInterruptShadowing, flagAssignment); + IVisitableAstNode execAst = _astBuilder.WithIpAdvancement(instr, ensureIoPrivilege, setInterruptShadowing, flagAssignment); instr.AttachAsts(displayAst, execAst); // STI must terminate its CfgBlock so external interrupt delivery happens at the // boundary just after interrupts are enabled (after the one-instruction shadow). diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/IoStringParser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/IoStringParser.cs index a5eca43d21..b26ce22de4 100644 --- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/IoStringParser.cs +++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/IoStringParser.cs @@ -6,6 +6,7 @@ namespace Spice86.Core.Emulator.CPU.CfgCpu.Parser.SpecificParsers; using Spice86.Core.Emulator.CPU.CfgCpu.Ast.Instruction; using Spice86.Core.Emulator.CPU.CfgCpu.Ast.Operations; using Spice86.Core.Emulator.CPU.CfgCpu.Ast.Value; +using Spice86.Core.Emulator.CPU.CfgCpu.InstructionExecutor; using Spice86.Core.Emulator.CPU.CfgCpu.ParsedInstruction; using Spice86.Core.Emulator.CPU.Registers; using Spice86.Shared.Emulator.Memory; @@ -22,8 +23,9 @@ public CfgInstruction Parse(ParsingContext context, bool isInput) { BitWidth bitWidth = GetBitWidth(context.OpcodeField, context.HasOperandSize32); DataType dataType = _astBuilder.UType(bitWidth); CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1); - DataType addressType = _astBuilder.AddressType(instr); + DataType addressType = _astBuilder.AddressType(context.AddressWidthFromPrefixes); ValueNode dx = _astBuilder.Register.Reg16(RegisterIndex.DxIndex); + MethodCallNode ensureIoPrivilege = new MethodCallNode(null, nameof(InstructionExecutor.InstructionExecutionHelper.EnsureIoPrivilege)); BlockNode coreOperation; RepPrefix? repPrefix = _astBuilder.Rep(instr.RepPrefix, false); InstructionNode displayAst; @@ -32,7 +34,7 @@ public CfgInstruction Parse(ParsingContext context, bool isInput) { ValueNode destPointer = _astBuilder.StringOperation.DestPointerDi(dataType, addressType); BinaryOperationNode storeOperation = _astBuilder.Assign(dataType, destPointer, ioRead); BinaryOperationNode advanceDi = _astBuilder.StringOperation.AdvanceDi(addressType, (int)bitWidth); - coreOperation = new BlockNode(storeOperation, advanceDi); + coreOperation = new BlockNode(ensureIoPrivilege, storeOperation, advanceDi); displayAst = new InstructionNode(repPrefix, InstructionOperation.INS, _astBuilder.Pointer.ToSegmentedPointer(dataType, SegmentRegisterIndex.EsIndex, _astBuilder.Register.Reg(addressType, RegisterIndex.DiIndex))); @@ -42,7 +44,7 @@ public CfgInstruction Parse(ParsingContext context, bool isInput) { segmentRegisterIndex, (int)SegmentRegisterIndex.DsIndex); MethodCallNode ioWrite = new MethodCallNode(null, $"Out{(int)bitWidth}", dx, sourcePointer); BinaryOperationNode advanceSi = _astBuilder.StringOperation.AdvanceSi(addressType, (int)bitWidth); - coreOperation = new BlockNode(ioWrite, advanceSi); + coreOperation = new BlockNode(ensureIoPrivilege, ioWrite, advanceSi); displayAst = new InstructionNode(repPrefix, InstructionOperation.OUTS, _astBuilder.Pointer.ToSegmentedPointer(dataType, segmentRegisterIndex, (int)SegmentRegisterIndex.DsIndex, _astBuilder.Register.Reg(addressType, RegisterIndex.SiIndex))); diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/LarLslParser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/LarLslParser.cs new file mode 100644 index 0000000000..a2a16bb0b7 --- /dev/null +++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/LarLslParser.cs @@ -0,0 +1,51 @@ +namespace Spice86.Core.Emulator.CPU.CfgCpu.Parser.SpecificParsers; + +using Spice86.Core.Emulator.CPU.CfgCpu.Parser; + +using Spice86.Core.Emulator.CPU.CfgCpu.Ast; +using Spice86.Core.Emulator.CPU.CfgCpu.Ast.Instruction; +using Spice86.Core.Emulator.CPU.CfgCpu.Ast.Operations; +using Spice86.Core.Emulator.CPU.CfgCpu.Ast.Value; +using Spice86.Core.Emulator.CPU.CfgCpu.InstructionExecutor; +using Spice86.Core.Emulator.CPU.CfgCpu.ParsedInstruction; +using Spice86.Core.Emulator.CPU.CfgCpu.ParsedInstruction.ModRm; + +/// LAR (0F 02) / LSL (0F 03): reg <- info about the r/m selector, ZF set if valid. +public class LarLslParser : BaseInstructionParser { + public LarLslParser(ParsingTools parsingTools) : base(parsingTools) { + } + + public CfgInstruction ParseLar(ParsingContext context) { + return Parse(context, nameof(InstructionExecutionHelper.IsSelectorValidForLar), + nameof(InstructionExecutionHelper.LoadAccessRights), InstructionOperation.LAR); + } + + public CfgInstruction ParseLsl(ParsingContext context) { + return Parse(context, nameof(InstructionExecutionHelper.IsSelectorValidForLsl), + nameof(InstructionExecutionHelper.LoadSegmentLimit), InstructionOperation.LSL); + } + + private CfgInstruction Parse(ParsingContext context, string isValidMethodName, string loadMethodName, InstructionOperation displayOp) { + (CfgInstruction instr, ModRmContext modRmContext) = ParseModRmBase(context, 1); + ValueNode selectorNode = _astBuilder.ModRm.RmToNode(DataType.UINT16, modRmContext); + DataType destType = context.HasOperandSize32 ? DataType.UINT32 : DataType.UINT16; + ValueNode destNode = _astBuilder.ModRm.RToNode(destType, modRmContext); + + MethodCallValueNode isValidCall = new MethodCallValueNode(DataType.BOOL, null, isValidMethodName, selectorNode); + MethodCallValueNode loadCall = new MethodCallValueNode(DataType.UINT32, null, loadMethodName, selectorNode); + ValueNode convertedLoad = _astBuilder.TypeConversion.Convert(destType, loadCall); + BinaryOperationNode assignDest = _astBuilder.Assign(destType, destNode, convertedLoad); + BinaryOperationNode setZeroTrue = _astBuilder.Assign(DataType.BOOL, _astBuilder.Flag.Zero(), _astBuilder.Constant.ToNode(true)); + BlockNode trueCase = new BlockNode(assignDest, setZeroTrue); + + BinaryOperationNode setZeroFalse = _astBuilder.Assign(DataType.BOOL, _astBuilder.Flag.Zero(), _astBuilder.Constant.ToNode(false)); + BlockNode falseCase = new BlockNode(setZeroFalse); + + IfElseNode ifElseNode = new IfElseNode(isValidCall, trueCase, falseCase); + + InstructionNode displayAst = new InstructionNode(displayOp, destNode, selectorNode); + IVisitableAstNode execAst = _astBuilder.WithIpAdvancement(instr, ifElseNode); + instr.AttachAsts(displayAst, execAst); + return instr; + } +} diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/LxsParser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/LxsParser.cs index 1e6f99f677..bbeda341bc 100644 --- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/LxsParser.cs +++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/LxsParser.cs @@ -7,6 +7,7 @@ namespace Spice86.Core.Emulator.CPU.CfgCpu.Parser.SpecificParsers; using Spice86.Core.Emulator.CPU.CfgCpu.Ast.Operations; using Spice86.Core.Emulator.CPU.CfgCpu.Ast.Value; using Spice86.Core.Emulator.CPU.CfgCpu.Ast.Value.Constant; +using Spice86.Core.Emulator.CPU.CfgCpu.InstructionExecutor; using Spice86.Core.Emulator.CPU.CfgCpu.ParsedInstruction; using Spice86.Core.Emulator.CPU.CfgCpu.ParsedInstruction.ModRm; using Spice86.Core.Emulator.CPU.Exceptions; @@ -27,7 +28,7 @@ public CfgInstruction Parse(ParsingContext context, InstructionOperation display throw new CpuInvalidOpcodeException($"{displayOp} with register source operand is invalid"); } - DataType addrType = _astBuilder.AddressType(instr); + DataType addrType = _astBuilder.AddressType(context.AddressWidthFromPrefixes); (VariableDeclarationNode cachedOffset, ValueNode memValue) = _astBuilder.ModRm.ToMemoryAddressNodeWithCachedOffset(dataType, addrType, modRmContext, "lxsOffset"); @@ -41,17 +42,17 @@ public CfgInstruction Parse(ParsingContext context, InstructionOperation display ValueNode adjustedOffset = new BinaryOperationNode(addrType, cachedOffset.Reference, BinaryOperation.PLUS, sizeInBytes); ValueNode segPointer = _astBuilder.ModRm.ToMemoryAddressNodeWithCustomOffset(DataType.UINT16, modRmContext, adjustedOffset); VariableDeclarationNode segmentValue = _astBuilder.DeclareVariable(DataType.UINT16, "lxsSegment", segPointer); - ValueNode segRegNode = _astBuilder.Register.SReg(segmentRegisterIndex); - BinaryOperationNode assignSeg = _astBuilder.Assign(DataType.UINT16, segRegNode, segmentValue.Reference); + ValueNode segIndexNode = _astBuilder.Constant.ToNode((uint)segmentRegisterIndex); + MethodCallNode loadSegment = new MethodCallNode(null, nameof(InstructionExecutionHelper.LoadSegmentRegister), segIndexNode, segmentValue.Reference); InstructionNode displayAst = new InstructionNode(displayOp, rNode, _astBuilder.ModRm.RmToNode(dataType, modRmContext)); IVisitableAstNode execAst; if (segmentRegisterIndex == SegmentRegisterIndex.SsIndex) { IVisitableAstNode setInterruptShadowingNode = _astBuilder.Flag.SetInterruptShadowing(); - execAst = _astBuilder.WithIpAdvancement(instr, cachedOffset, offsetValue, segmentValue, assignR, assignSeg, setInterruptShadowingNode); + execAst = _astBuilder.WithIpAdvancement(instr, cachedOffset, offsetValue, segmentValue, assignR, loadSegment, setInterruptShadowingNode); } else { - execAst = _astBuilder.WithIpAdvancement(instr, cachedOffset, offsetValue, segmentValue, assignR, assignSeg); + execAst = _astBuilder.WithIpAdvancement(instr, cachedOffset, offsetValue, segmentValue, assignR, loadSegment); } instr.AttachAsts(displayAst, execAst); return instr; diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/MemoryStringOpParser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/MemoryStringOpParser.cs index 4320e52337..748c89cee7 100644 --- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/MemoryStringOpParser.cs +++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/MemoryStringOpParser.cs @@ -31,7 +31,7 @@ public CfgInstruction Parse(ParsingContext context, MemoryStringOpKind kind) { BitWidth bitWidth = GetBitWidth(context.OpcodeField, context.HasOperandSize32); DataType dataType = _astBuilder.UType(bitWidth); CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1); - DataType addressType = _astBuilder.AddressType(instr); + DataType addressType = _astBuilder.AddressType(context.AddressWidthFromPrefixes); bool usesEqualityRep = kind is MemoryStringOpKind.Cmps or MemoryStringOpKind.Scas; RepPrefix? repPrefix = _astBuilder.Rep(instr.RepPrefix, usesEqualityRep); @@ -40,77 +40,77 @@ public CfgInstruction Parse(ParsingContext context, MemoryStringOpKind kind) { switch (kind) { case MemoryStringOpKind.Movs: { - int segReg = GetSegmentRegisterOverrideOrDs(context); - ValueNode src = _astBuilder.StringOperation.SourcePointerSi(dataType, addressType, - segReg, (int)SegmentRegisterIndex.DsIndex); - ValueNode dest = _astBuilder.StringOperation.DestPointerDi(dataType, addressType); - coreOperation = new BlockNode( - _astBuilder.Assign(dataType, dest, src), - _astBuilder.StringOperation.AdvanceSi(addressType, (int)bitWidth), - _astBuilder.StringOperation.AdvanceDi(addressType, (int)bitWidth)); - displayAst = new InstructionNode(repPrefix, InstructionOperation.MOVS, - _astBuilder.Pointer.ToSegmentedPointer(dataType, SegmentRegisterIndex.EsIndex, - _astBuilder.Register.Reg(addressType, RegisterIndex.DiIndex)), - _astBuilder.Pointer.ToSegmentedPointer(dataType, segReg, (int)SegmentRegisterIndex.DsIndex, - _astBuilder.Register.Reg(addressType, RegisterIndex.SiIndex))); - break; - } + int segReg = GetSegmentRegisterOverrideOrDs(context); + ValueNode src = _astBuilder.StringOperation.SourcePointerSi(dataType, addressType, + segReg, (int)SegmentRegisterIndex.DsIndex); + ValueNode dest = _astBuilder.StringOperation.DestPointerDi(dataType, addressType); + coreOperation = new BlockNode( + _astBuilder.Assign(dataType, dest, src), + _astBuilder.StringOperation.AdvanceSi(addressType, (int)bitWidth), + _astBuilder.StringOperation.AdvanceDi(addressType, (int)bitWidth)); + displayAst = new InstructionNode(repPrefix, InstructionOperation.MOVS, + _astBuilder.Pointer.ToSegmentedPointer(dataType, SegmentRegisterIndex.EsIndex, + _astBuilder.Register.Reg(addressType, RegisterIndex.DiIndex)), + _astBuilder.Pointer.ToSegmentedPointer(dataType, segReg, (int)SegmentRegisterIndex.DsIndex, + _astBuilder.Register.Reg(addressType, RegisterIndex.SiIndex))); + break; + } case MemoryStringOpKind.Cmps: { - int segReg = GetSegmentRegisterOverrideOrDs(context); - ValueNode src = _astBuilder.StringOperation.SourcePointerSi(dataType, addressType, - segReg, (int)SegmentRegisterIndex.DsIndex); - ValueNode dest = _astBuilder.StringOperation.DestPointerDi(dataType, addressType); - MethodCallValueNode aluCall = _astBuilder.AluCall(DataType.UINT16, bitWidth, "Sub", src, dest); - coreOperation = new BlockNode( - aluCall, - _astBuilder.StringOperation.AdvanceSi(addressType, (int)bitWidth), - _astBuilder.StringOperation.AdvanceDi(addressType, (int)bitWidth)); - displayAst = new InstructionNode(repPrefix, InstructionOperation.CMPS, - _astBuilder.Pointer.ToSegmentedPointer(dataType, segReg, (int)SegmentRegisterIndex.DsIndex, - _astBuilder.Register.Reg(addressType, RegisterIndex.SiIndex)), - _astBuilder.Pointer.ToSegmentedPointer(dataType, SegmentRegisterIndex.EsIndex, - _astBuilder.Register.Reg(addressType, RegisterIndex.DiIndex))); - break; - } + int segReg = GetSegmentRegisterOverrideOrDs(context); + ValueNode src = _astBuilder.StringOperation.SourcePointerSi(dataType, addressType, + segReg, (int)SegmentRegisterIndex.DsIndex); + ValueNode dest = _astBuilder.StringOperation.DestPointerDi(dataType, addressType); + MethodCallValueNode aluCall = _astBuilder.AluCall(DataType.UINT16, bitWidth, "Sub", src, dest); + coreOperation = new BlockNode( + aluCall, + _astBuilder.StringOperation.AdvanceSi(addressType, (int)bitWidth), + _astBuilder.StringOperation.AdvanceDi(addressType, (int)bitWidth)); + displayAst = new InstructionNode(repPrefix, InstructionOperation.CMPS, + _astBuilder.Pointer.ToSegmentedPointer(dataType, segReg, (int)SegmentRegisterIndex.DsIndex, + _astBuilder.Register.Reg(addressType, RegisterIndex.SiIndex)), + _astBuilder.Pointer.ToSegmentedPointer(dataType, SegmentRegisterIndex.EsIndex, + _astBuilder.Register.Reg(addressType, RegisterIndex.DiIndex))); + break; + } case MemoryStringOpKind.Lods: { - int segReg = GetSegmentRegisterOverrideOrDs(context); - ValueNode src = _astBuilder.StringOperation.SourcePointerSi(dataType, addressType, - segReg, (int)SegmentRegisterIndex.DsIndex); - ValueNode accumulator = _astBuilder.Register.Accumulator(dataType); - coreOperation = new BlockNode( - _astBuilder.Assign(dataType, accumulator, src), - _astBuilder.StringOperation.AdvanceSi(addressType, (int)bitWidth)); - displayAst = new InstructionNode(repPrefix, InstructionOperation.LODS, - accumulator, - _astBuilder.Pointer.ToSegmentedPointer(dataType, segReg, (int)SegmentRegisterIndex.DsIndex, - _astBuilder.Register.Reg(addressType, RegisterIndex.SiIndex))); - break; - } + int segReg = GetSegmentRegisterOverrideOrDs(context); + ValueNode src = _astBuilder.StringOperation.SourcePointerSi(dataType, addressType, + segReg, (int)SegmentRegisterIndex.DsIndex); + ValueNode accumulator = _astBuilder.Register.Accumulator(dataType); + coreOperation = new BlockNode( + _astBuilder.Assign(dataType, accumulator, src), + _astBuilder.StringOperation.AdvanceSi(addressType, (int)bitWidth)); + displayAst = new InstructionNode(repPrefix, InstructionOperation.LODS, + accumulator, + _astBuilder.Pointer.ToSegmentedPointer(dataType, segReg, (int)SegmentRegisterIndex.DsIndex, + _astBuilder.Register.Reg(addressType, RegisterIndex.SiIndex))); + break; + } case MemoryStringOpKind.Stos: { - ValueNode accumulator = _astBuilder.Register.Accumulator(dataType); - ValueNode dest = _astBuilder.StringOperation.DestPointerDi(dataType, addressType); - coreOperation = new BlockNode( - _astBuilder.Assign(dataType, dest, accumulator), - _astBuilder.StringOperation.AdvanceDi(addressType, (int)bitWidth)); - displayAst = new InstructionNode(repPrefix, InstructionOperation.STOS, - _astBuilder.Pointer.ToSegmentedPointer(dataType, SegmentRegisterIndex.EsIndex, - _astBuilder.Register.Reg(addressType, RegisterIndex.DiIndex)), - accumulator); - break; - } + ValueNode accumulator = _astBuilder.Register.Accumulator(dataType); + ValueNode dest = _astBuilder.StringOperation.DestPointerDi(dataType, addressType); + coreOperation = new BlockNode( + _astBuilder.Assign(dataType, dest, accumulator), + _astBuilder.StringOperation.AdvanceDi(addressType, (int)bitWidth)); + displayAst = new InstructionNode(repPrefix, InstructionOperation.STOS, + _astBuilder.Pointer.ToSegmentedPointer(dataType, SegmentRegisterIndex.EsIndex, + _astBuilder.Register.Reg(addressType, RegisterIndex.DiIndex)), + accumulator); + break; + } case MemoryStringOpKind.Scas: { - ValueNode accumulator = _astBuilder.Register.Accumulator(dataType); - ValueNode dest = _astBuilder.StringOperation.DestPointerDi(dataType, addressType); - MethodCallValueNode aluCall = _astBuilder.AluCall(DataType.UINT16, bitWidth, "Sub", accumulator, dest); - coreOperation = new BlockNode( - aluCall, - _astBuilder.StringOperation.AdvanceDi(addressType, (int)bitWidth)); - displayAst = new InstructionNode(repPrefix, InstructionOperation.SCAS, - accumulator, - _astBuilder.Pointer.ToSegmentedPointer(dataType, SegmentRegisterIndex.EsIndex, - _astBuilder.Register.Reg(addressType, RegisterIndex.DiIndex))); - break; - } + ValueNode accumulator = _astBuilder.Register.Accumulator(dataType); + ValueNode dest = _astBuilder.StringOperation.DestPointerDi(dataType, addressType); + MethodCallValueNode aluCall = _astBuilder.AluCall(DataType.UINT16, bitWidth, "Sub", accumulator, dest); + coreOperation = new BlockNode( + aluCall, + _astBuilder.StringOperation.AdvanceDi(addressType, (int)bitWidth)); + displayAst = new InstructionNode(repPrefix, InstructionOperation.SCAS, + accumulator, + _astBuilder.Pointer.ToSegmentedPointer(dataType, SegmentRegisterIndex.EsIndex, + _astBuilder.Register.Reg(addressType, RegisterIndex.DiIndex))); + break; + } default: throw new InvalidOperationException($"Unknown string operation kind: {kind}"); } diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/MovSregRm16Parser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/MovSregRm16Parser.cs index 48b312a75d..c80895d2c8 100644 --- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/MovSregRm16Parser.cs +++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/MovSregRm16Parser.cs @@ -6,6 +6,7 @@ namespace Spice86.Core.Emulator.CPU.CfgCpu.Parser.SpecificParsers; using Spice86.Core.Emulator.CPU.CfgCpu.Ast.Instruction; using Spice86.Core.Emulator.CPU.CfgCpu.Ast.Operations; using Spice86.Core.Emulator.CPU.CfgCpu.Ast.Value; +using Spice86.Core.Emulator.CPU.CfgCpu.InstructionExecutor; using Spice86.Core.Emulator.CPU.CfgCpu.ParsedInstruction; using Spice86.Core.Emulator.CPU.CfgCpu.ParsedInstruction.ModRm; using Spice86.Core.Emulator.CPU.Exceptions; @@ -27,14 +28,15 @@ public CfgInstruction Parse(ParsingContext context) { } ValueNode sregNode = _astBuilder.Register.SReg(modRmContext.RegisterIndex); ValueNode rmNode = _astBuilder.ModRm.RmToNode(DataType.UINT16, modRmContext); - BinaryOperationNode assignment = _astBuilder.Assign(DataType.UINT16, sregNode, rmNode); + ValueNode segIndexNode = _astBuilder.Constant.ToNode((uint)modRmContext.RegisterIndex); + MethodCallNode loadSegment = new MethodCallNode(null, nameof(InstructionExecutionHelper.LoadSegmentRegister), segIndexNode, rmNode); InstructionNode displayAst = new InstructionNode(InstructionOperation.MOV, sregNode, rmNode); IVisitableAstNode execAst; if (modRmContext.RegisterIndex == (uint)SegmentRegisterIndex.SsIndex) { IVisitableAstNode setInterruptShadowing = _astBuilder.Flag.SetInterruptShadowing(); - execAst = _astBuilder.WithIpAdvancement(instr, assignment, setInterruptShadowing); + execAst = _astBuilder.WithIpAdvancement(instr, loadSegment, setInterruptShadowing); } else { - execAst = _astBuilder.WithIpAdvancement(instr, assignment); + execAst = _astBuilder.WithIpAdvancement(instr, loadSegment); } instr.AttachAsts(displayAst, execAst); return instr; diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/PushaParser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/PushaParser.cs index b7471374b9..86537a2d5a 100644 --- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/PushaParser.cs +++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/PushaParser.cs @@ -17,7 +17,6 @@ public PushaParser(ParsingTools parsingTools) : base(parsingTools) { public CfgInstruction Parse(ParsingContext context) { BitWidth bitWidth = GetBitWidth(false, context.HasOperandSize32); DataType dataType = _astBuilder.UType(bitWidth); - DataType addressType = _astBuilder.UType(BitWidth.WORD_16); CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1); string methodName = bitWidth == BitWidth.DWORD_32 ? nameof(Stack.PushAll32) : nameof(Stack.PushAll16); @@ -27,7 +26,7 @@ public CfgInstruction Parse(ParsingContext context) { _astBuilder.Register.Reg(dataType, RegisterIndex.CxIndex), _astBuilder.Register.Reg(dataType, RegisterIndex.DxIndex), _astBuilder.Register.Reg(dataType, RegisterIndex.BxIndex), - _astBuilder.Register.StackPointer(addressType), + _astBuilder.Register.StackPointer(dataType), _astBuilder.Register.Reg(dataType, RegisterIndex.BpIndex), _astBuilder.Register.Reg(dataType, RegisterIndex.SiIndex), _astBuilder.Register.Reg(dataType, RegisterIndex.DiIndex)); diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/SegRegPushPopParser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/SegRegPushPopParser.cs index 50108659bf..849df8ca4b 100644 --- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/SegRegPushPopParser.cs +++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/SegRegPushPopParser.cs @@ -6,6 +6,7 @@ namespace Spice86.Core.Emulator.CPU.CfgCpu.Parser.SpecificParsers; using Spice86.Core.Emulator.CPU.CfgCpu.Ast.Instruction; using Spice86.Core.Emulator.CPU.CfgCpu.Ast.Operations; using Spice86.Core.Emulator.CPU.CfgCpu.Ast.Value; +using Spice86.Core.Emulator.CPU.CfgCpu.InstructionExecutor; using Spice86.Core.Emulator.CPU.CfgCpu.ParsedInstruction; using Spice86.Core.Emulator.CPU.Registers; using Spice86.Shared.Emulator.Memory; @@ -30,15 +31,13 @@ public CfgInstruction ParsePushSReg(ParsingContext context, int segRegIndex) { public CfgInstruction ParsePopSReg(ParsingContext context, int segRegIndex) { CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1); ValueNode regNode = _astBuilder.Register.SReg(segRegIndex); - DataType addressType = DataType.UINT16; ushort slotSize = context.HasOperandSize32 ? (ushort)4 : (ushort)2; - ValueNode stackPointer = _astBuilder.Register.StackPointer(addressType); - ValueNode popValue = _astBuilder.Pointer.ToSegmentedPointer(DataType.UINT16, SegmentRegisterIndex.SsIndex, stackPointer); - ValueNode nextSp = _astBuilder.Constant.AddConstant(addressType, stackPointer, slotSize); - BinaryOperationNode assign = new BinaryOperationNode(DataType.UINT16, regNode, BinaryOperation.ASSIGN, popValue); - BinaryOperationNode advanceStackPointer = _astBuilder.Assign(addressType, stackPointer, nextSp); + ValueNode popValue = new MethodCallValueNode(DataType.UINT16, "Stack", nameof(Stack.PopSegmentSelector), + _astBuilder.Constant.ToNode((uint)slotSize)); + ValueNode segIndexNode = _astBuilder.Constant.ToNode((uint)segRegIndex); + MethodCallNode loadSegment = new MethodCallNode(null, nameof(InstructionExecutionHelper.LoadSegmentRegister), segIndexNode, popValue); InstructionNode displayAst = new InstructionNode(InstructionOperation.POP, regNode); - IVisitableAstNode execAst = _astBuilder.WithIpAdvancement(instr, assign, advanceStackPointer); + IVisitableAstNode execAst = _astBuilder.WithIpAdvancement(instr, loadSegment); instr.AttachAsts(displayAst, execAst); return instr; } diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/SimpleInstructionParser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/SimpleInstructionParser.cs index 6d0f6a5984..98f5139c4d 100644 --- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/SimpleInstructionParser.cs +++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/SimpleInstructionParser.cs @@ -45,14 +45,12 @@ public CfgInstruction ParseCpuid(ParsingContext context) { return instr; } - /// - /// CLTS — Clear Task-Switched flag in CR0. Treated as NOP since CR0 is not emulated in real mode. - /// Encoded as a 2-byte 0F 06 instruction. - /// + /// CLTS — Clear Task-Switched flag in CR0. Encoded as a 2-byte 0F 06 instruction. public CfgInstruction ParseClts(ParsingContext context) { CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1); + MethodCallNode cltsCall = new MethodCallNode(null, nameof(Spice86.Core.Emulator.CPU.CfgCpu.InstructionExecutor.InstructionExecutionHelper.Clts)); InstructionNode displayAst = new InstructionNode(InstructionOperation.CLTS); - IVisitableAstNode execAst = _astBuilder.WithIpAdvancement(instr); + IVisitableAstNode execAst = _astBuilder.WithIpAdvancement(instr, cltsCall); instr.AttachAsts(displayAst, execAst); return instr; } diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/SystemSegmentParser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/SystemSegmentParser.cs new file mode 100644 index 0000000000..a3f126f060 --- /dev/null +++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/SystemSegmentParser.cs @@ -0,0 +1,66 @@ +namespace Spice86.Core.Emulator.CPU.CfgCpu.Parser.SpecificParsers; + +using Spice86.Core.Emulator.CPU.CfgCpu.Parser; + +using Spice86.Core.Emulator.CPU.CfgCpu.Ast; +using Spice86.Core.Emulator.CPU.CfgCpu.Ast.Instruction; +using Spice86.Core.Emulator.CPU.CfgCpu.Ast.Value; +using Spice86.Core.Emulator.CPU.CfgCpu.InstructionExecutor; +using Spice86.Core.Emulator.CPU.CfgCpu.ParsedInstruction; +using Spice86.Core.Emulator.CPU.CfgCpu.ParsedInstruction.ModRm; +using Spice86.Core.Emulator.CPU.Exceptions; + +/// +/// Group 0F 00: SLDT, STR, LLDT, LTR, VERR, VERW. The ModRM reg field selects the sub-operation; the +/// r/m field is a 16-bit selector, in a register or in memory. +/// +public class SystemSegmentParser : BaseGrpOperationParser { + public SystemSegmentParser(ParsingTools parsingTools) : base(parsingTools) { + } + + protected override CfgInstruction Parse(ParsingContext context, ModRmContext modRmContext, int groupIndex) { + return groupIndex switch { + 0 => BuildStore(context, modRmContext, nameof(InstructionExecutionHelper.StoreLdtr), InstructionOperation.SLDT), + 1 => BuildStore(context, modRmContext, nameof(InstructionExecutionHelper.StoreTr), InstructionOperation.STR), + 2 => BuildLoad(context, modRmContext, nameof(InstructionExecutionHelper.LoadLdtr), InstructionOperation.LLDT), + 3 => BuildLoad(context, modRmContext, nameof(InstructionExecutionHelper.LoadTr), InstructionOperation.LTR), + 4 => BuildVerify(context, modRmContext, nameof(InstructionExecutionHelper.VerifyReadable), InstructionOperation.VERR), + 5 => BuildVerify(context, modRmContext, nameof(InstructionExecutionHelper.VerifyWritable), InstructionOperation.VERW), + _ => throw new CpuInvalidOpcodeException($"Group 0F 00 /{groupIndex} is not supported") + }; + } + + private CfgInstruction BuildStore(ParsingContext context, ModRmContext modRmContext, string methodName, InstructionOperation displayOp) { + CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1); + RegisterModRmFields(instr, modRmContext); + ValueNode destNode = _astBuilder.ModRm.RmToNode(DataType.UINT16, modRmContext); + MethodCallValueNode storeCall = new MethodCallValueNode(DataType.UINT16, null, methodName); + InstructionNode displayAst = new InstructionNode(displayOp, destNode); + IVisitableAstNode execAst = _astBuilder.WithIpAdvancement(instr, _astBuilder.Assign(DataType.UINT16, destNode, storeCall)); + instr.AttachAsts(displayAst, execAst); + return instr; + } + + private CfgInstruction BuildLoad(ParsingContext context, ModRmContext modRmContext, string methodName, InstructionOperation displayOp) { + CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1); + RegisterModRmFields(instr, modRmContext); + ValueNode sourceNode = _astBuilder.ModRm.RmToNode(DataType.UINT16, modRmContext); + MethodCallNode loadCall = new MethodCallNode(null, methodName, sourceNode); + InstructionNode displayAst = new InstructionNode(displayOp, sourceNode); + IVisitableAstNode execAst = _astBuilder.WithIpAdvancement(instr, loadCall); + instr.AttachAsts(displayAst, execAst); + return instr; + } + + private CfgInstruction BuildVerify(ParsingContext context, ModRmContext modRmContext, string methodName, InstructionOperation displayOp) { + CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1); + RegisterModRmFields(instr, modRmContext); + ValueNode selectorNode = _astBuilder.ModRm.RmToNode(DataType.UINT16, modRmContext); + MethodCallValueNode verifyCall = new MethodCallValueNode(DataType.BOOL, null, methodName, selectorNode); + InstructionNode displayAst = new InstructionNode(displayOp, selectorNode); + IVisitableAstNode execAst = _astBuilder.WithIpAdvancement(instr, + _astBuilder.Assign(DataType.BOOL, _astBuilder.Flag.Zero(), verifyCall)); + instr.AttachAsts(displayAst, execAst); + return instr; + } +} diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/XchgRmParser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/XchgRmParser.cs index 9ae25ea1bd..b048f1afd1 100644 --- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/XchgRmParser.cs +++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/XchgRmParser.cs @@ -21,7 +21,7 @@ protected override void BuildAsts(CfgInstruction instr, DataType dataType, ModRm List nodes = new(); ValueNode rmNode; if (modRmContext.MemoryAddressType != MemoryAddressType.NONE) { - DataType addrType = _astBuilder.AddressType(instr); + DataType addrType = _astBuilder.AddressType(modRmContext.AddressSize); (VariableDeclarationNode cachedOffset, rmNode) = _astBuilder.ModRm.ToMemoryAddressNodeWithCachedOffset(dataType, addrType, modRmContext, "xchgOffset"); nodes.Add(cachedOffset); diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/XlatParser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/XlatParser.cs index e139f6e733..4331a75436 100644 --- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/XlatParser.cs +++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/XlatParser.cs @@ -19,7 +19,7 @@ public CfgInstruction Parse(ParsingContext context) { CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1); int segRegIndex = GetSegmentRegisterOverrideOrDs(context); int defaultSegRegIndex = (int)SegmentRegisterIndex.DsIndex; - DataType addrType = _astBuilder.AddressType(instr); + DataType addrType = _astBuilder.AddressType(context.AddressWidthFromPrefixes); ValueNode bxNode = _astBuilder.Register.Reg(addrType, RegisterIndex.BxIndex); ValueNode alNode = _astBuilder.Register.Accumulator(DataType.UINT8); BinaryOperationNode displayOffset = new BinaryOperationNode(addrType, bxNode, BinaryOperation.PLUS, alNode); diff --git a/src/Spice86.Core/Emulator/CPU/CpuMode.cs b/src/Spice86.Core/Emulator/CPU/CpuMode.cs new file mode 100644 index 0000000000..0e25b875d3 --- /dev/null +++ b/src/Spice86.Core/Emulator/CPU/CpuMode.cs @@ -0,0 +1,16 @@ +namespace Spice86.Core.Emulator.CPU; + +/// +/// The addressing/execution mode the CPU is currently operating in, derived from +/// and the EFLAGS VM bit. +/// +public enum CpuMode { + /// Real mode: 16-bit segmented addressing, no protection. + Real, + + /// Protected mode: descriptor-table-based segmentation with privilege checks. + Protected, + + /// Virtual-8086 mode: real-mode-style execution inside a protected-mode task. + Virtual8086 +} diff --git a/src/Spice86.Core/Emulator/CPU/DescriptorTables/DescriptorTableReader.cs b/src/Spice86.Core/Emulator/CPU/DescriptorTables/DescriptorTableReader.cs new file mode 100644 index 0000000000..598d2fd1c3 --- /dev/null +++ b/src/Spice86.Core/Emulator/CPU/DescriptorTables/DescriptorTableReader.cs @@ -0,0 +1,55 @@ +namespace Spice86.Core.Emulator.CPU.DescriptorTables; + +using Spice86.Core.Emulator.CPU.Exceptions; +using Spice86.Core.Emulator.CPU.Registers; + +/// +/// Shared GDT/LDT segment descriptor decoding, used both by the protected-mode MMU (which only has a +/// raw byte reader available) and by segment-load instruction execution (which has full memory +/// access). Reused instead of duplicated so table-limit and selector-decoding logic has one home. +/// +public static class DescriptorTableReader { + /// + /// Decodes the segment descriptor for from the GDT or LDT, depending + /// on the selector's table indicator bit. + /// + /// The raw selector value being resolved. + /// The linear base address of the GDT. + /// The byte limit of the GDT. + /// The linear base address of the currently loaded LDT. + /// The byte limit of the currently loaded LDT. + /// Reads one linear/physical byte of memory (no MMU translation applied). + /// The selector is null or outside its table's limit. + public static SegmentDescriptorCache ReadDescriptor(ushort selector, uint gdtBase, uint gdtLimit, + uint ldtBase, uint ldtLimit, Func readByte) { + if (!TryReadDescriptor(selector, gdtBase, gdtLimit, ldtBase, ldtLimit, readByte, out SegmentDescriptorCache descriptor)) { + throw new CpuGeneralProtectionFaultException($"Selector 0x{selector:X4} is outside its descriptor table limit"); + } + return descriptor; + } + + /// + /// Attempts to decode the segment descriptor for , without faulting. + /// Used by LAR/LSL/VERR/VERW, which report an invalid selector via the zero flag rather than an + /// exception. + /// + /// false if the selector is null or outside its descriptor table's limit. + public static bool TryReadDescriptor(ushort selector, uint gdtBase, uint gdtLimit, + uint ldtBase, uint ldtLimit, Func readByte, out SegmentDescriptorCache descriptor) { + SegmentSelector segmentSelector = new(selector); + uint tableBase = segmentSelector.ReferencesLocalDescriptorTable ? ldtBase : gdtBase; + uint tableLimit = segmentSelector.ReferencesLocalDescriptorTable ? ldtLimit : gdtLimit; + uint entryOffset = (uint)segmentSelector.Index * 8u; + if (segmentSelector.IsNull || entryOffset + 7u > tableLimit) { + descriptor = default; + return false; + } + + Span descriptorBytes = stackalloc byte[8]; + for (int i = 0; i < 8; i++) { + descriptorBytes[i] = readByte(tableBase + entryOffset + (uint)i); + } + descriptor = new RawSegmentDescriptor(descriptorBytes).ToSegmentDescriptorCache(); + return true; + } +} diff --git a/src/Spice86.Core/Emulator/CPU/DescriptorTables/GateType.cs b/src/Spice86.Core/Emulator/CPU/DescriptorTables/GateType.cs new file mode 100644 index 0000000000..9ed11f241d --- /dev/null +++ b/src/Spice86.Core/Emulator/CPU/DescriptorTables/GateType.cs @@ -0,0 +1,28 @@ +namespace Spice86.Core.Emulator.CPU.DescriptorTables; + +/// +/// The type of an IDT/GDT gate descriptor or a GDT/LDT system descriptor, decoded from the low 4 +/// bits of its type byte. Values match the Intel-defined type-field encodings. +/// +public enum GateType { + /// 16-bit call gate. + CallGate16 = 0x4, + + /// Task gate (shared encoding between the 286 and 386). + TaskGate = 0x5, + + /// 16-bit interrupt gate. + InterruptGate16 = 0x6, + + /// 16-bit trap gate. + TrapGate16 = 0x7, + + /// 32-bit call gate. + CallGate32 = 0xC, + + /// 32-bit interrupt gate: clears the interrupt flag on entry. + InterruptGate32 = 0xE, + + /// 32-bit trap gate: leaves the interrupt flag unchanged on entry. + TrapGate32 = 0xF +} diff --git a/src/Spice86.Core/Emulator/CPU/DescriptorTables/PrivilegeChecks.cs b/src/Spice86.Core/Emulator/CPU/DescriptorTables/PrivilegeChecks.cs new file mode 100644 index 0000000000..2d4f489caa --- /dev/null +++ b/src/Spice86.Core/Emulator/CPU/DescriptorTables/PrivilegeChecks.cs @@ -0,0 +1,151 @@ +namespace Spice86.Core.Emulator.CPU.DescriptorTables; + +using Spice86.Core.Emulator.CPU.Exceptions; +using Spice86.Core.Emulator.CPU.Registers; +using Spice86.Core.Emulator.Errors; +using Spice86.Core.Emulator.Memory; + +/// +/// Privilege-level validation shared by both execution paths (InstructionExecutionHelper and +/// CSharpOverrideHelper): IOPL gating for I/O and flag-control instructions, and DPL/RPL checks +/// for data/stack segment loads. Code-segment (CS) transfer privilege rules (call gates, conforming +/// vs non-conforming checks) are validated separately, alongside gate dispatch. +/// +public static class PrivilegeChecks { + /// + /// Throws #GP if the current privilege level is not allowed to execute `IN`/`OUT`/`CLI`/`STI`: + /// outside Virtual-8086 mode, CPL must be <= IOPL; inside it, IOPL must be exactly 3. + /// + public static void EnsureIoPrivilege(State state) { + if (state.CpuMode == CpuMode.Real) { + return; + } + bool isVirtual8086 = state.CpuMode == CpuMode.Virtual8086; + byte iopl = state.IoPrivilegeLevel; + bool violatesIoPrivilege = isVirtual8086 ? iopl < 3 : iopl < state.Cpl; + if (violatesIoPrivilege) { + throw new CpuGeneralProtectionFaultException( + $"IOPL check failed: CPL={state.Cpl}, IOPL={iopl}, VM={isVirtual8086}"); + } + } + + /// + /// Throws #GP if executed anywhere but CPL 0 (e.g. `HLT`, `LGDT`/`LIDT`, `MOV CRn`). A no-op in real + /// mode, where CPL is always effectively 0. + /// + public static void EnsureCpl0(State state, string instructionName) { + if (state.CpuMode is CpuMode.Protected or CpuMode.Virtual8086 && state.Cpl != 0) { + throw new CpuGeneralProtectionFaultException($"{instructionName} requires CPL 0, current CPL is {state.Cpl}"); + } + } + + private static readonly SegmentRegisterIndex[] DataSegmentIndices = [ + SegmentRegisterIndex.DsIndex, SegmentRegisterIndex.EsIndex, + SegmentRegisterIndex.FsIndex, SegmentRegisterIndex.GsIndex + ]; + + /// + /// After a privilege-decreasing return (IRET/RETF raising CPL), real hardware automatically loads + /// the null selector into any of DS/ES/FS/GS whose current segment is no longer accessible at the + /// new, less-privileged CPL: a data segment (or non-conforming code segment) whose descriptor DPL is + /// less than the new CPL. This must be called AFTER the new CS/CPL is already in effect. A no-op + /// outside protected mode. + /// + public static void NullifyInaccessibleDataSegments(State state) { + if (state.CpuMode != CpuMode.Protected) { + return; + } + byte newCpl = state.Cpl; + foreach (SegmentRegisterIndex index in DataSegmentIndices) { + SegmentDescriptorCache cache = state.SegmentDescriptorCaches[index]; + bool conformingCode = cache.IsCode && cache.IsConforming; + if (!conformingCode && cache.DescriptorPrivilegeLevel < newCpl) { + state.SegmentRegisters.UInt16[(uint)index] = 0; + state.SegmentDescriptorCaches[index] = default; + } + } + } + + /// + /// Validates a data or stack segment load against DPL/RPL rules once its descriptor has been + /// decoded. A no-op outside protected mode or for CS (validated separately). + /// + public static void ValidateDataSegmentLoad(State state, SegmentRegisterIndex index, ushort selector, SegmentDescriptorCache descriptor) { + if (state.CpuMode != CpuMode.Protected || index == SegmentRegisterIndex.CsIndex) { + return; + } + + byte cpl = state.Cpl; + byte rpl = new SegmentSelector(selector).RequestedPrivilegeLevel; + + if (index == SegmentRegisterIndex.SsIndex) { + ValidateStackSegmentLoad(selector, descriptor, cpl, rpl); + return; + } + + if (!descriptor.Present) { + throw new CpuSegmentNotPresentException($"Selector 0x{selector:X4} is not present", new SegmentSelector(selector).ErrorCode); + } + bool isDataOrReadableCode = descriptor.IsCodeOrDataSegment && (!descriptor.IsCode || descriptor.IsReadWriteBitSet); + if (!isDataOrReadableCode) { + throw new CpuGeneralProtectionFaultException( + $"Selector 0x{selector:X4} is not a data segment or readable code segment", new SegmentSelector(selector).ErrorCode); + } + if (!descriptor.IsConforming && Math.Max(rpl, cpl) > descriptor.DescriptorPrivilegeLevel) { + throw new CpuGeneralProtectionFaultException( + $"Selector 0x{selector:X4}: max(RPL={rpl}, CPL={cpl}) exceeds DPL={descriptor.DescriptorPrivilegeLevel}", new SegmentSelector(selector).ErrorCode); + } + } + + private static void ValidateStackSegmentLoad(ushort selector, SegmentDescriptorCache descriptor, byte cpl, byte rpl) { + if (!descriptor.Present) { + throw new CpuStackSegmentFaultException($"Selector 0x{selector:X4} is not present", new SegmentSelector(selector).ErrorCode); + } + bool isWritableData = descriptor.IsCodeOrDataSegment && !descriptor.IsCode && descriptor.IsReadWriteBitSet; + if (!isWritableData) { + throw new CpuGeneralProtectionFaultException($"Selector 0x{selector:X4} is not a writable data segment", new SegmentSelector(selector).ErrorCode); + } + if (rpl != cpl || descriptor.DescriptorPrivilegeLevel != cpl) { + throw new CpuGeneralProtectionFaultException( + $"Selector 0x{selector:X4}: RPL={rpl} and DPL={descriptor.DescriptorPrivilegeLevel} must both equal CPL={cpl}", new SegmentSelector(selector).ErrorCode); + } + } + + /// + /// Validates a direct (non-gate) far JMP/CALL code-segment transfer: present, actually a code + /// segment (a selector resolving to a system/gate descriptor means call-gate dispatch is needed, + /// which is not yet implemented), and DPL/RPL rules (conforming segments require DPL <= CPL; + /// non-conforming segments require DPL == CPL and RPL <= CPL). CPL never changes for a direct + /// transfer. A no-op outside protected mode. + /// + public static void ValidateFarCodeSegmentTransfer(State state, IMemory memory, ushort selector) { + if (state.CpuMode != CpuMode.Protected) { + return; + } + if (!DescriptorTableReader.TryReadDescriptor(selector, state.Gdtr.Base, state.Gdtr.Limit, + state.Ldtr.DescriptorCache.Base, state.Ldtr.DescriptorCache.Limit, address => memory[memory.Mmu.TranslateLinearAddress(address, isWrite: false)], out SegmentDescriptorCache descriptor)) { + throw new CpuGeneralProtectionFaultException($"Selector 0x{selector:X4} is outside its descriptor table limit", new SegmentSelector(selector).ErrorCode); + } + if (!descriptor.Present) { + throw new CpuSegmentNotPresentException($"Selector 0x{selector:X4} is not present", new SegmentSelector(selector).ErrorCode); + } + if (!descriptor.IsCodeOrDataSegment) { + throw new UnhandledOperationException(state, + $"Selector 0x{selector:X4} is a system descriptor (call/task/interrupt/trap gate); gate dispatch via direct far JMP/CALL is not yet supported"); + } + if (!descriptor.IsCode) { + throw new CpuGeneralProtectionFaultException($"Selector 0x{selector:X4} is not a code segment", new SegmentSelector(selector).ErrorCode); + } + byte cpl = state.Cpl; + byte rpl = new SegmentSelector(selector).RequestedPrivilegeLevel; + byte dpl = descriptor.DescriptorPrivilegeLevel; + if (descriptor.IsConforming) { + if (dpl > cpl) { + throw new CpuGeneralProtectionFaultException($"Selector 0x{selector:X4}: conforming DPL={dpl} > CPL={cpl}", new SegmentSelector(selector).ErrorCode); + } + } else if (dpl != cpl || rpl > cpl) { + throw new CpuGeneralProtectionFaultException( + $"Selector 0x{selector:X4}: non-conforming DPL={dpl} must equal CPL={cpl} and RPL={rpl} must be <= CPL", new SegmentSelector(selector).ErrorCode); + } + } +} diff --git a/src/Spice86.Core/Emulator/CPU/DescriptorTables/ProtectedModeCallGateDispatcher.cs b/src/Spice86.Core/Emulator/CPU/DescriptorTables/ProtectedModeCallGateDispatcher.cs new file mode 100644 index 0000000000..2bfed19119 --- /dev/null +++ b/src/Spice86.Core/Emulator/CPU/DescriptorTables/ProtectedModeCallGateDispatcher.cs @@ -0,0 +1,162 @@ +namespace Spice86.Core.Emulator.CPU.DescriptorTables; + +using Spice86.Core.Emulator.CPU.Exceptions; +using Spice86.Core.Emulator.CPU.Registers; +using Spice86.Core.Emulator.Memory; +using Spice86.Shared.Emulator.Memory; + +/// +/// Protected-mode CALL-gate dispatch: a far CALL whose target selector resolves to a GDT/LDT system +/// descriptor of type or (rather +/// than an ordinary code segment) is redirected through the gate to its real target. Distinct from +/// 's IDT gates: the gate lives in the GDT/LDT, and access +/// requires the caller's CPL and the call selector's RPL to both be <= the gate's DPL, while the +/// target code segment's DPL must be <= CPL (a call gate can only enter equally- or more-privileged +/// code). Task gates reached via a direct CALL/JMP (as opposed to an IDT vector) are not yet supported. +/// +public static class ProtectedModeCallGateDispatcher { + /// + /// Attempts to decode as a 16-bit call-gate descriptor in the GDT/LDT. + /// Returns false (with no side effects) when the selector is null, out of table bounds, or + /// resolves to an ordinary code/data segment or a different system-descriptor type - callers should + /// fall back to direct far-call/jump handling in every such case. + /// + public static bool TryReadCallGate(State state, IMemory memory, ushort selector, out RawGateDescriptor gate) { + gate = default; + if (state.CpuMode != CpuMode.Protected) { + return false; + } + SegmentSelector segmentSelector = new(selector); + if (segmentSelector.IsNull) { + return false; + } + uint tableBase = segmentSelector.ReferencesLocalDescriptorTable ? state.Ldtr.DescriptorCache.Base : state.Gdtr.Base; + uint tableLimit = segmentSelector.ReferencesLocalDescriptorTable ? state.Ldtr.DescriptorCache.Limit : state.Gdtr.Limit; + uint entryOffset = (uint)segmentSelector.Index * 8u; + if (entryOffset + 7u > tableLimit) { + return false; + } + Span descriptorBytes = stackalloc byte[8]; + for (int i = 0; i < 8; i++) { + descriptorBytes[i] = memory[memory.Mmu.TranslateLinearAddress(tableBase + entryOffset + (uint)i, isWrite: false)]; + } + // Access-byte bit 4 (S) distinguishes a code/data descriptor (S=1) from a system descriptor + // (S=0, e.g. a gate); it occupies the same byte position in both raw descriptor layouts. + if ((descriptorBytes[5] & 0b0001_0000) != 0) { + return false; + } + RawGateDescriptor candidate = new(descriptorBytes); + if (candidate.GateType is not (GateType.CallGate16 or GateType.CallGate32)) { + return false; + } + gate = candidate; + return true; + } + + /// + /// Validates access to from a far CALL through selector + /// (gate must be present; gate DPL must be >= CPL and >= the + /// call selector's RPL), resolves and validates the gate's target code segment (present, a code + /// segment, DPL <= CPL), switches to the target ring's stack via SS0:ESP0 from the current TSS + /// when escalating, and pushes the return frame (old SS:SP if escalating, then the return address). + /// Returns the resolved target address. + /// + public static SegmentedAddress Dispatch(State state, IMemory memory, Stack stack, RawGateDescriptor gate, + ushort callSelector, SegmentedAddress expectedReturn) { + byte cpl = state.Cpl; + byte callSelectorRpl = new SegmentSelector(callSelector).RequestedPrivilegeLevel; + bool is32Bit = gate.GateType == GateType.CallGate32; + if (!gate.Present) { + throw new CpuGeneralProtectionFaultException($"Call gate 0x{callSelector:X4} is not present", new SegmentSelector(callSelector).ErrorCode); + } + if (gate.DescriptorPrivilegeLevel < cpl || gate.DescriptorPrivilegeLevel < callSelectorRpl) { + throw new CpuGeneralProtectionFaultException( + $"Call gate 0x{callSelector:X4}: DPL {gate.DescriptorPrivilegeLevel} must be >= CPL {cpl} and >= RPL {callSelectorRpl}", new SegmentSelector(callSelector).ErrorCode); + } + if (!DescriptorTableReader.TryReadDescriptor(gate.Selector, state.Gdtr.Base, state.Gdtr.Limit, + state.Ldtr.DescriptorCache.Base, state.Ldtr.DescriptorCache.Limit, address => memory[memory.Mmu.TranslateLinearAddress(address, isWrite: false)], out SegmentDescriptorCache targetCode)) { + throw new CpuGeneralProtectionFaultException($"Call gate target selector 0x{gate.Selector:X4} is invalid", new SegmentSelector(callSelector).ErrorCode); + } + if (!targetCode.Present) { + throw new CpuSegmentNotPresentException($"Call gate target selector 0x{gate.Selector:X4} is not present", new SegmentSelector(gate.Selector).ErrorCode); + } + if (!targetCode.IsCode) { + throw new CpuGeneralProtectionFaultException($"Call gate target selector 0x{gate.Selector:X4} is not a code segment", new SegmentSelector(gate.Selector).ErrorCode); + } + if (targetCode.DescriptorPrivilegeLevel > cpl) { + throw new CpuGeneralProtectionFaultException( + $"Call gate target DPL {targetCode.DescriptorPrivilegeLevel} is less privileged than CPL {cpl}", new SegmentSelector(gate.Selector).ErrorCode); + } + + if (targetCode.DescriptorPrivilegeLevel < cpl) { + // CS must be loaded before SS: state.Cpl (used to validate the new stack segment's RPL/DPL) + // is derived from CS, so SS validation must see the NEW (more privileged) CPL, not the old one. + (ushort newSs, ushort newSp) = ProtectedModeInterruptDispatcher.ReadRing0StackFromTss(state, memory); + ushort oldSs = state.SS; + ushort oldSp = state.SP; + ushort escalatedCs = (ushort)((gate.Selector & 0xFFFC) | targetCode.DescriptorPrivilegeLevel); + SegmentAndControlRegisterOperations.LoadSegmentRegister(state, memory, (uint)SegmentRegisterIndex.CsIndex, escalatedCs); + SegmentAndControlRegisterOperations.LoadSegmentRegister(state, memory, (uint)SegmentRegisterIndex.SsIndex, newSs); + state.SP = newSp; + if (is32Bit) { + stack.Push32(oldSs); + stack.Push32(oldSp); + } else { + stack.Push16(oldSs); + stack.Push16(oldSp); + } + } else { + // Same-privilege dispatch (conforming target, or DPL == CPL): CPL is unchanged, so the loaded + // CS's RPL must match the CURRENT CPL, not the raw (RPL=0) selector baked into the gate. + ushort sameLevelCs = (ushort)((gate.Selector & 0xFFFC) | cpl); + SegmentAndControlRegisterOperations.LoadSegmentRegister(state, memory, (uint)SegmentRegisterIndex.CsIndex, sameLevelCs); + } + if (is32Bit) { + stack.PushFarPointer32(new SegmentedAddress32(expectedReturn.Segment, expectedReturn.Offset)); + state.EIP = gate.Offset; + } else { + stack.PushSegmentedAddress(expectedReturn); + state.IP = (ushort)gate.Offset; + } + return new SegmentedAddress(gate.Selector, (ushort)gate.Offset); + } + + /// + /// Validates access to from a direct far JMP through selector + /// (gate must be present; gate DPL must be >= CPL and >= the + /// call selector's RPL), resolves and validates the gate's target code segment (present, a code + /// segment). Unlike , a JMP through a gate can NEVER change CPL: a + /// non-conforming target must have DPL exactly equal to CPL, and a conforming target still requires + /// DPL <= CPL - there is no stack switch and no return address is pushed. Returns the resolved + /// target address. + /// + public static SegmentedAddress DispatchJump(State state, IMemory memory, RawGateDescriptor gate, ushort callSelector) { + byte cpl = state.Cpl; + byte callSelectorRpl = new SegmentSelector(callSelector).RequestedPrivilegeLevel; + if (!gate.Present) { + throw new CpuGeneralProtectionFaultException($"Call gate 0x{callSelector:X4} is not present", new SegmentSelector(callSelector).ErrorCode); + } + if (gate.DescriptorPrivilegeLevel < cpl || gate.DescriptorPrivilegeLevel < callSelectorRpl) { + throw new CpuGeneralProtectionFaultException( + $"Call gate 0x{callSelector:X4}: DPL {gate.DescriptorPrivilegeLevel} must be >= CPL {cpl} and >= RPL {callSelectorRpl}", new SegmentSelector(callSelector).ErrorCode); + } + if (!DescriptorTableReader.TryReadDescriptor(gate.Selector, state.Gdtr.Base, state.Gdtr.Limit, + state.Ldtr.DescriptorCache.Base, state.Ldtr.DescriptorCache.Limit, address => memory[memory.Mmu.TranslateLinearAddress(address, isWrite: false)], out SegmentDescriptorCache targetCode)) { + throw new CpuGeneralProtectionFaultException($"Call gate target selector 0x{gate.Selector:X4} is invalid", new SegmentSelector(callSelector).ErrorCode); + } + if (!targetCode.Present) { + throw new CpuSegmentNotPresentException($"Call gate target selector 0x{gate.Selector:X4} is not present", new SegmentSelector(gate.Selector).ErrorCode); + } + if (!targetCode.IsCode) { + throw new CpuGeneralProtectionFaultException($"Call gate target selector 0x{gate.Selector:X4} is not a code segment", new SegmentSelector(gate.Selector).ErrorCode); + } + if (targetCode.IsConforming ? targetCode.DescriptorPrivilegeLevel > cpl : targetCode.DescriptorPrivilegeLevel != cpl) { + throw new CpuGeneralProtectionFaultException( + $"JMP via call gate: target DPL {targetCode.DescriptorPrivilegeLevel} is not reachable without a privilege change from CPL {cpl}", new SegmentSelector(gate.Selector).ErrorCode); + } + ushort sameLevelCs = (ushort)((gate.Selector & 0xFFFC) | cpl); + SegmentAndControlRegisterOperations.LoadSegmentRegister(state, memory, (uint)SegmentRegisterIndex.CsIndex, sameLevelCs); + state.IP = (ushort)gate.Offset; + return new SegmentedAddress(gate.Selector, (ushort)gate.Offset); + } +} diff --git a/src/Spice86.Core/Emulator/CPU/DescriptorTables/ProtectedModeInterruptDispatcher.cs b/src/Spice86.Core/Emulator/CPU/DescriptorTables/ProtectedModeInterruptDispatcher.cs new file mode 100644 index 0000000000..5259c67f54 --- /dev/null +++ b/src/Spice86.Core/Emulator/CPU/DescriptorTables/ProtectedModeInterruptDispatcher.cs @@ -0,0 +1,275 @@ +namespace Spice86.Core.Emulator.CPU.DescriptorTables; + +using Spice86.Core.Emulator.CPU; +using Spice86.Core.Emulator.CPU.Exceptions; +using Spice86.Core.Emulator.CPU.Registers; +using Spice86.Core.Emulator.Errors; +using Spice86.Core.Emulator.Memory; +using Spice86.Shared.Emulator.Memory; + +/// +/// Protected-mode interrupt/exception dispatch through the IDT, shared by both execution paths +/// (InstructionExecutionHelper and CSharpOverrideHelper) so software `INT n`, hardware +/// interrupts, and CPU exceptions resolve to the same target and push the same return frame whether +/// the destination runs interpreted or as a generated/hand-written C# override. Real and Virtual-8086 +/// mode keep using the real-mode IVT and are not routed through this class. +/// 16-bit and 32-bit interrupt/trap gates are both supported. 32-bit gates use the real 32-bit stack +/// frame width (EIP/CS/EFLAGS pushed as dwords, matching 's layout, +/// and a dword error code) since some CPU-conformance tests check the exact byte offsets of that frame; +/// the pushed EIP's high word is always 0 since tracks only a 16-bit instruction +/// pointer throughout this codebase - a genuine EIP-tracking implementation remains out of scope. +/// IDT task gates are supported too: dispatch redirects to +/// instead of pushing an interrupt frame, exactly like a CALL to a TSS selector - the interrupted task's +/// resume point is saved into its own TSS rather than onto its stack, and any exception error code is +/// pushed onto the NEW task's stack once the switch completes, matching real hardware. GDT/LDT task-gate +/// descriptors (reached via a direct CALL/JMP rather than an IDT vector) are not yet implemented. +/// +public static class ProtectedModeInterruptDispatcher { + /// + /// Decodes the IDT gate for , validates it, and either performs a task + /// switch (see ) if the gate is a task gate, or + /// switches to the target ring's stack via SS0:ESP0 read directly from the current TSS on privilege + /// escalation and pushes the return frame (old SS:SP if escalating, then FLAGS, then + /// , then the error code if any) and sets CS:IP to the gate's target. + /// Returns the resolved target address. + /// + /// + /// The address to resume at once the handler returns. Passed explicitly rather than read from + /// state.IpSegmentedAddress because generated/override code does not keep State.IP + /// continuously in sync the way the interpreter does. + /// + public static SegmentedAddress Dispatch(State state, IMemory memory, Stack stack, byte vectorNumber, bool checkGateDpl, + ushort? errorCode, SegmentedAddress expectedReturn) { + RawGateDescriptor gate = ReadIdtGate(state, memory, vectorNumber); + if (checkGateDpl && gate.DescriptorPrivilegeLevel < state.Cpl) { + throw new CpuGeneralProtectionFaultException( + $"Vector 0x{vectorNumber:X2} gate DPL {gate.DescriptorPrivilegeLevel} < CPL {state.Cpl}", IdtErrorCode(vectorNumber)); + } + if (gate.GateType == GateType.TaskGate) { + return DispatchViaTaskGate(state, memory, stack, gate.Selector, errorCode, expectedReturn, vectorNumber); + } + bool is32Bit = gate.GateType is GateType.InterruptGate32 or GateType.TrapGate32; + if (gate.GateType is not (GateType.InterruptGate16 or GateType.TrapGate16 or GateType.InterruptGate32 or GateType.TrapGate32)) { + throw new UnhandledOperationException(state, $"IDT gate type {gate.GateType} is not yet supported"); + } + if (!DescriptorTableReader.TryReadDescriptor(gate.Selector, state.Gdtr.Base, state.Gdtr.Limit, + state.Ldtr.DescriptorCache.Base, state.Ldtr.DescriptorCache.Limit, address => memory[memory.Mmu.TranslateLinearAddress(address, isWrite: false)], out SegmentDescriptorCache targetCode)) { + throw new CpuGeneralProtectionFaultException($"Gate selector 0x{gate.Selector:X4} is invalid", IdtErrorCode(vectorNumber)); + } + if (!targetCode.IsConforming && targetCode.DescriptorPrivilegeLevel > state.Cpl) { + throw new CpuGeneralProtectionFaultException( + $"Gate target DPL {targetCode.DescriptorPrivilegeLevel} is less privileged than CPL {state.Cpl}", IdtErrorCode(vectorNumber)); + } + + if (targetCode.DescriptorPrivilegeLevel < state.Cpl) { + // CS must be loaded before SS: state.Cpl (used to validate the new stack segment's RPL/DPL) + // is derived from CS, so SS validation must see the NEW (more privileged) CPL, not the old one. + (ushort newSs, ushort newSp) = ReadRing0StackFromTss(state, memory); + ushort oldSs = state.SS; + ushort oldSp = state.SP; + // Reflecting from V86 mode always leaves it (real hardware treats the handler as running in + // ordinary protected mode): clear VM only now, after every CPL-dependent check above has + // already read the V86-implies-CPL3 value, so the CS/SS loads below resolve through the GDT + // instead of re-synthesizing a real-mode-style cache from the raw selector. + state.Flags.SetFlag(Flags.Virtual8086Mode, false); + ushort escalatedCs = (ushort)((gate.Selector & 0xFFFC) | targetCode.DescriptorPrivilegeLevel); + SegmentAndControlRegisterOperations.LoadSegmentRegister(state, memory, (uint)SegmentRegisterIndex.CsIndex, escalatedCs); + SegmentAndControlRegisterOperations.LoadSegmentRegister(state, memory, (uint)SegmentRegisterIndex.SsIndex, newSs); + state.SP = newSp; + if (is32Bit) { + stack.Push32(oldSs); + stack.Push32(oldSp); + } else { + stack.Push16(oldSs); + stack.Push16(oldSp); + } + } else { + // Same-privilege dispatch (conforming target, or DPL == CPL): CPL is unchanged, so the loaded + // CS's RPL must match the CURRENT CPL, not the raw (RPL=0) selector baked into the gate. + ushort sameLevelCs = (ushort)((gate.Selector & 0xFFFC) | state.Cpl); + SegmentAndControlRegisterOperations.LoadSegmentRegister(state, memory, (uint)SegmentRegisterIndex.CsIndex, sameLevelCs); + } + if (is32Bit) { + stack.Push32(state.Flags.FlagRegister); + stack.PushFarPointer32(new SegmentedAddress32(expectedReturn.Segment, expectedReturn.Offset)); + if (errorCode.HasValue) { + stack.Push32(errorCode.Value); + } + } else { + stack.Push16(state.Flags.FlagRegister16); + stack.PushSegmentedAddress(expectedReturn); + if (errorCode.HasValue) { + stack.Push16(errorCode.Value); + } + } + if (gate.GateType is GateType.InterruptGate16 or GateType.InterruptGate32) { + state.InterruptFlag = false; + } + state.IP = (ushort)gate.Offset; + return new SegmentedAddress(gate.Selector, (ushort)gate.Offset); + } + + /// + /// Dispatches an interrupt/exception that vectors through an IDT task gate: switches to the task + /// referenced by (saving the interrupted task's resume point, + /// , into its own TSS rather than pushing it onto its stack), then + /// pushes any exception error code onto the NEW task's stack once the switch has completed, matching + /// real hardware's task-gate error-code delivery. + /// + private static SegmentedAddress DispatchViaTaskGate(State state, IMemory memory, Stack stack, ushort tssSelector, + ushort? errorCode, SegmentedAddress expectedReturn, byte vectorNumber) { + if (!TaskSwitchOperations.TryReadAvailableTss(state, memory, tssSelector)) { + throw new CpuGeneralProtectionFaultException($"Task gate TSS selector 0x{tssSelector:X4} is invalid", IdtErrorCode(vectorNumber)); + } + SegmentedAddress target = TaskSwitchOperations.SwitchToNewTask(state, memory, tssSelector, expectedReturn.Offset); + if (errorCode.HasValue) { + stack.Push16(errorCode.Value); + } + return target; + } + + /// + /// Protected-mode 32-bit IRETD: if EFLAGS.NT is set, performs a task switch back to the calling task + /// via its TSS back-link () instead of an + /// ordinary return. Otherwise pops EIP, CS (padded to a dword, matching + /// 's layout) and EFLAGS; if the popped CS's RPL is less + /// privileged than the current CPL (i.e. returning outward from a privilege escalation), also pops + /// ESP and SS (each pushed as a dword by the escalating dispatch path). + /// + public static void InterruptReturn32(State state, IMemory memory, Stack stack) { + if (state.Flags.GetFlag(Flags.NestedTask)) { + TaskSwitchOperations.SwitchBackViaBackLink(state, memory); + return; + } + byte cplBeforeReturn = state.Cpl; + SegmentedAddress32 poppedCsEip = stack.PopSegmentedAddress32(); + uint poppedEflags = stack.Pop32(); + byte returningRpl = new SegmentSelector(poppedCsEip.Segment).RequestedPrivilegeLevel; + state.EIP = poppedCsEip.Offset; + SegmentAndControlRegisterOperations.LoadSegmentRegister(state, memory, (uint)SegmentRegisterIndex.CsIndex, poppedCsEip.Segment); + state.Flags.FlagRegister = poppedEflags; + if (returningRpl > cplBeforeReturn) { + uint poppedEsp = stack.Pop32(); + uint poppedSs = stack.Pop32(); + SegmentAndControlRegisterOperations.LoadSegmentRegister(state, memory, (uint)SegmentRegisterIndex.SsIndex, (ushort)poppedSs); + state.ESP = poppedEsp; + PrivilegeChecks.NullifyInaccessibleDataSegments(state); + } + } + + /// + /// Protected-mode 16-bit IRET: if EFLAGS.NT is set, performs a task switch back to the calling task + /// via its TSS back-link () instead of an + /// ordinary return. Otherwise pops IP, CS, and FLAGS; if the popped CS's RPL is less privileged + /// than the current CPL (i.e. returning outward from a privilege escalation), also pops SP and SS. + /// + public static void InterruptReturn16(State state, IMemory memory, Stack stack) { + if (state.Flags.GetFlag(Flags.NestedTask)) { + TaskSwitchOperations.SwitchBackViaBackLink(state, memory); + return; + } + byte cplBeforeReturn = state.Cpl; + ushort poppedIp = stack.Pop16(); + ushort poppedCs = stack.Pop16(); + ushort poppedFlags = stack.Pop16(); + byte returningRpl = new SegmentSelector(poppedCs).RequestedPrivilegeLevel; + state.IP = poppedIp; + SegmentAndControlRegisterOperations.LoadSegmentRegister(state, memory, (uint)SegmentRegisterIndex.CsIndex, poppedCs); + state.Flags.FlagRegister16 = poppedFlags; + if (returningRpl > cplBeforeReturn) { + ushort poppedSp = stack.Pop16(); + ushort poppedSs = stack.Pop16(); + SegmentAndControlRegisterOperations.LoadSegmentRegister(state, memory, (uint)SegmentRegisterIndex.SsIndex, poppedSs); + state.SP = poppedSp; + PrivilegeChecks.NullifyInaccessibleDataSegments(state); + } + } + + /// + /// Protected-mode 16-bit far RET: pops IP and CS; a target RPL less privileged than the current CPL + /// (i.e. returning outward across a privilege boundary) additionally pops SS:SP after discarding + /// from the callee stack, then adds it again to the restored + /// SP (no call-gate parameter copying is implemented, so this only matters for `RETF imm16` cleanup + /// conventions, not genuine copied parameters). Returning to a MORE privileged level is a #GP. + /// + public static void FarReturn16(State state, IMemory memory, Stack stack, ushort numberOfBytesToPop) { + byte cplBeforeReturn = state.Cpl; + ushort poppedIp = stack.Pop16(); + ushort poppedCs = stack.Pop16(); + byte returningRpl = new SegmentSelector(poppedCs).RequestedPrivilegeLevel; + if (returningRpl < cplBeforeReturn) { + throw new CpuGeneralProtectionFaultException( + $"RETF cannot return to a more privileged level (target RPL {returningRpl} < CPL {cplBeforeReturn})", new SegmentSelector(poppedCs).ErrorCode); + } + state.IP = poppedIp; + SegmentAndControlRegisterOperations.LoadSegmentRegister(state, memory, (uint)SegmentRegisterIndex.CsIndex, poppedCs); + stack.Discard(numberOfBytesToPop); + if (returningRpl > cplBeforeReturn) { + ushort poppedSp = stack.Pop16(); + ushort poppedSs = stack.Pop16(); + SegmentAndControlRegisterOperations.LoadSegmentRegister(state, memory, (uint)SegmentRegisterIndex.SsIndex, poppedSs); + state.SP = (ushort)(poppedSp + numberOfBytesToPop); + PrivilegeChecks.NullifyInaccessibleDataSegments(state); + } + } + + /// + /// Protected-mode 32-bit RETF: pops EIP and CS (padded to a dword, matching + /// 's layout); a target RPL less privileged than the current CPL + /// (i.e. returning outward across a privilege boundary) additionally pops ESP:SS after discarding + /// from the callee stack, then adds it again to the restored + /// ESP. Returning to a MORE privileged level is a #GP. + /// + public static void FarReturn32(State state, IMemory memory, Stack stack, ushort numberOfBytesToPop) { + byte cplBeforeReturn = state.Cpl; + SegmentedAddress32 poppedCsEip = stack.PopSegmentedAddress32(); + byte returningRpl = new SegmentSelector(poppedCsEip.Segment).RequestedPrivilegeLevel; + if (returningRpl < cplBeforeReturn) { + throw new CpuGeneralProtectionFaultException( + $"RETF cannot return to a more privileged level (target RPL {returningRpl} < CPL {cplBeforeReturn})", new SegmentSelector(poppedCsEip.Segment).ErrorCode); + } + state.EIP = poppedCsEip.Offset; + SegmentAndControlRegisterOperations.LoadSegmentRegister(state, memory, (uint)SegmentRegisterIndex.CsIndex, poppedCsEip.Segment); + stack.Discard(numberOfBytesToPop); + if (returningRpl > cplBeforeReturn) { + uint poppedEsp = stack.Pop32(); + uint poppedSs = stack.Pop32(); + SegmentAndControlRegisterOperations.LoadSegmentRegister(state, memory, (uint)SegmentRegisterIndex.SsIndex, (ushort)poppedSs); + state.ESP = poppedEsp + numberOfBytesToPop; + PrivilegeChecks.NullifyInaccessibleDataSegments(state); + } + } + + private static RawGateDescriptor ReadIdtGate(State state, IMemory memory, byte vectorNumber) { + uint entryOffset = (uint)vectorNumber * 8; + if (entryOffset + 7 > state.Idtr.Limit) { + throw new CpuGeneralProtectionFaultException($"Vector 0x{vectorNumber:X2} is outside the IDT limit", IdtErrorCode(vectorNumber)); + } + Span gateBytes = stackalloc byte[8]; + for (int i = 0; i < 8; i++) { + gateBytes[i] = memory[memory.Mmu.TranslateLinearAddress(state.Idtr.Base + entryOffset + (uint)i, isWrite: false)]; + } + RawGateDescriptor gate = new(gateBytes); + if (!gate.Present) { + throw new CpuGeneralProtectionFaultException($"Vector 0x{vectorNumber:X2} gate is not present", IdtErrorCode(vectorNumber)); + } + return gate; + } + + private static ushort IdtErrorCode(byte vectorNumber) { + // Selector-like error code: bit 1 set means the index refers to the IDT. + return (ushort)((vectorNumber * 8) | 0b10); + } + + /// + /// Reads SS0:ESP0 (the ring-0 stack pointer) from the standard 32-bit TSS layout. Shared with + /// , which needs the identical ring-0-stack lookup for + /// privilege-escalating CALLs through a call gate. + /// + internal static (ushort ss0, ushort sp0) ReadRing0StackFromTss(State state, IMemory memory) { + uint tssBase = state.Tr.DescriptorCache.Base; + uint esp0 = memory.UInt32[memory.Mmu.TranslateLinearAddress(tssBase + 4, isWrite: false)]; + ushort ss0 = memory.UInt16[memory.Mmu.TranslateLinearAddress(tssBase + 8, isWrite: false)]; + return (ss0, (ushort)esp0); + } +} diff --git a/src/Spice86.Core/Emulator/CPU/DescriptorTables/RawGateDescriptor.cs b/src/Spice86.Core/Emulator/CPU/DescriptorTables/RawGateDescriptor.cs new file mode 100644 index 0000000000..1b418440c0 --- /dev/null +++ b/src/Spice86.Core/Emulator/CPU/DescriptorTables/RawGateDescriptor.cs @@ -0,0 +1,43 @@ +namespace Spice86.Core.Emulator.CPU.DescriptorTables; + +/// +/// The raw 8-byte layout of an IDT gate descriptor (interrupt gate, trap gate, or task gate). +/// Distinct from : a gate redirects control transfer rather than +/// describing an addressable memory segment. +/// +public readonly struct RawGateDescriptor { + /// + /// Decodes a gate descriptor from its raw 8-byte in-memory representation. + /// + /// The 8 raw descriptor bytes, in the order they appear in memory. + public RawGateDescriptor(ReadOnlySpan descriptorBytes) { + if (descriptorBytes.Length != 8) { + throw new ArgumentException("A gate descriptor is exactly 8 bytes.", nameof(descriptorBytes)); + } + + ushort offsetLow = (ushort)(descriptorBytes[0] | (descriptorBytes[1] << 8)); + ushort offsetHigh = (ushort)(descriptorBytes[6] | (descriptorBytes[7] << 8)); + byte typeByte = descriptorBytes[5]; + + Selector = (ushort)(descriptorBytes[2] | (descriptorBytes[3] << 8)); + Offset = (uint)(offsetLow | (offsetHigh << 16)); + GateType = (GateType)(typeByte & 0x0F); + DescriptorPrivilegeLevel = (byte)((typeByte >> 5) & 0b11); + Present = (typeByte & 0x80) != 0; + } + + /// The 32-bit offset of the handler entry point within its code segment. + public uint Offset { get; } + + /// The code segment selector (interrupt/trap gates) or TSS selector (task gates). + public ushort Selector { get; } + + /// The gate type (call, interrupt, trap, or task gate). + public GateType GateType { get; } + + /// The descriptor privilege level required to invoke this gate via a software INT. + public byte DescriptorPrivilegeLevel { get; } + + /// Whether the gate's present bit is set. + public bool Present { get; } +} diff --git a/src/Spice86.Core/Emulator/CPU/DescriptorTables/RawSegmentDescriptor.cs b/src/Spice86.Core/Emulator/CPU/DescriptorTables/RawSegmentDescriptor.cs new file mode 100644 index 0000000000..a35e3fbd29 --- /dev/null +++ b/src/Spice86.Core/Emulator/CPU/DescriptorTables/RawSegmentDescriptor.cs @@ -0,0 +1,70 @@ +namespace Spice86.Core.Emulator.CPU.DescriptorTables; + +using Spice86.Core.Emulator.CPU.Registers; + +/// +/// The raw 8-byte layout of a GDT/LDT segment descriptor, decoded from memory. Distinct from +/// , which is what a CPU segment register caches once a +/// descriptor has been read and validated by a segment load. +/// +public readonly struct RawSegmentDescriptor { + /// + /// Decodes a segment descriptor from its raw 8-byte in-memory representation. + /// + /// The 8 raw descriptor bytes, in the order they appear in memory. + public RawSegmentDescriptor(ReadOnlySpan descriptorBytes) { + if (descriptorBytes.Length != 8) { + throw new ArgumentException("A segment descriptor is exactly 8 bytes.", nameof(descriptorBytes)); + } + + ushort limitLow = (ushort)(descriptorBytes[0] | (descriptorBytes[1] << 8)); + uint baseLow = (uint)(descriptorBytes[2] | (descriptorBytes[3] << 8) | (descriptorBytes[4] << 16)); + byte limitHighAndFlags = descriptorBytes[6]; + byte baseHigh = descriptorBytes[7]; + + AccessByte = descriptorBytes[5]; + Available = (limitHighAndFlags & 0x10) != 0; + DefaultBig = (limitHighAndFlags & 0x40) != 0; + Granularity4K = (limitHighAndFlags & 0x80) != 0; + + byte limitHigh = (byte)(limitHighAndFlags & 0x0F); + uint rawLimit = (uint)((limitHigh << 16) | limitLow); + Limit = Granularity4K ? (rawLimit << 12) | 0xFFF : rawLimit; + Base = baseLow | ((uint)baseHigh << 24); + } + + /// The 32-bit linear base address encoded in the descriptor. + public uint Base { get; } + + /// The segment limit, already scaled to a byte value when granularity is 4K pages. + public uint Limit { get; } + + /// The raw access byte (present, DPL, S, type). + public byte AccessByte { get; } + + /// Whether the AVL (software-available) bit is set. + public bool Available { get; } + + /// Whether the segment defaults to 32-bit operands/addressing (D/B bit). + public bool DefaultBig { get; } + + /// Whether the limit is scaled in 4K pages (G bit) rather than bytes. + public bool Granularity4K { get; } + + /// Whether the descriptor's present bit (access byte bit 7) is set. + public bool Present => (AccessByte & 0x80) != 0; + + /// The descriptor privilege level (access byte bits 5-6). + public byte DescriptorPrivilegeLevel => (byte)((AccessByte >> 5) & 0b11); + + /// Whether this is a code-or-data descriptor (S bit set) rather than a system descriptor. + public bool IsCodeOrDataSegment => (AccessByte & 0x10) != 0; + + /// The type field (access byte bits 0-3). Meaning depends on . + public byte Type => (byte)(AccessByte & 0x0F); + + /// Converts this raw descriptor into the cache a segment register loads it into. + public SegmentDescriptorCache ToSegmentDescriptorCache() { + return new SegmentDescriptorCache(Base, Limit, AccessByte, DefaultBig, Granularity4K, Present); + } +} diff --git a/src/Spice86.Core/Emulator/CPU/DescriptorTables/SegmentAndControlRegisterOperations.cs b/src/Spice86.Core/Emulator/CPU/DescriptorTables/SegmentAndControlRegisterOperations.cs new file mode 100644 index 0000000000..400df05753 --- /dev/null +++ b/src/Spice86.Core/Emulator/CPU/DescriptorTables/SegmentAndControlRegisterOperations.cs @@ -0,0 +1,259 @@ +namespace Spice86.Core.Emulator.CPU.DescriptorTables; + +using Spice86.Core.Emulator.CPU.Exceptions; +using Spice86.Core.Emulator.CPU.Registers; +using Spice86.Core.Emulator.Memory; + +/// +/// Shared segment-load, GDTR/IDTR, and control-register operations used by both interpreted execution +/// (InstructionExecutionHelper) and generated/hand-written C# overrides +/// (CSharpOverrideHelper), so the two execution paths behave identically. +/// +public static class SegmentAndControlRegisterOperations { + /// + /// Loads a raw selector value into a segment register and refreshes its descriptor cache: the + /// real-mode synthesized cache (base = selector*16) outside protected mode, or the decoded GDT/LDT + /// descriptor once is active. This is the single path every + /// segment-register write (MOV Sreg, POP Sreg, far transfers) goes through. A null selector is + /// allowed for DS/ES/FS/GS (real hardware only faults when it is later used), but not for SS. + /// DPL/RPL privilege rules are validated for every register except CS (validated separately + /// alongside code-segment/gate transfer rules). + /// + public static void LoadSegmentRegister(State state, IMemory memory, uint segmentRegisterIndex, ushort selector) { + SegmentRegisterIndex index = (SegmentRegisterIndex)segmentRegisterIndex; + SegmentDescriptorCache descriptorCache; + if (state.CpuMode != CpuMode.Protected) { + descriptorCache = SegmentDescriptorCache.CreateRealMode(selector); + } else if (new SegmentSelector(selector).IsNull) { + if (index == SegmentRegisterIndex.SsIndex) { + throw new CpuGeneralProtectionFaultException("Cannot load a null selector into SS"); + } + descriptorCache = default; + } else if (!DescriptorTableReader.TryReadDescriptor(selector, state.Gdtr.Base, state.Gdtr.Limit, + state.Ldtr.DescriptorCache.Base, state.Ldtr.DescriptorCache.Limit, address => memory[memory.Mmu.TranslateLinearAddress(address, isWrite: false)], out descriptorCache)) { + throw new CpuGeneralProtectionFaultException($"Selector 0x{selector:X4} is outside its descriptor table limit", new SegmentSelector(selector).ErrorCode); + } else { + if (Environment.GetEnvironmentVariable("SPICE86_TRACE_EXC") is not null) { + System.IO.Directory.CreateDirectory("tmp"); + SegmentSelector ss = new(selector); + uint tblBase = ss.ReferencesLocalDescriptorTable ? state.Ldtr.DescriptorCache.Base : state.Gdtr.Base; + uint entryOff = (uint)ss.Index * 8u; + byte[] raw = new byte[8]; + for (int i = 0; i < 8; i++) { raw[i] = memory[tblBase + entryOff + (uint)i]; } + System.IO.File.AppendAllText("tmp/seg_trace.txt", + $"reg={index} sel=0x{selector:X4} TI={ss.ReferencesLocalDescriptorTable} idx={ss.Index} tblBase=0x{tblBase:X} descBase=0x{descriptorCache.Base:X} LdtrBase=0x{state.Ldtr.DescriptorCache.Base:X} LdtrLimit=0x{state.Ldtr.DescriptorCache.Limit:X} raw={Convert.ToHexString(raw)} present={descriptorCache.Present} PG={state.ControlRegisters.PagingEnable} CR3=0x{state.ControlRegisters.Cr3:X}\n"); + } + PrivilegeChecks.ValidateDataSegmentLoad(state, index, selector, descriptorCache); + } + state.SegmentRegisters.UInt16[segmentRegisterIndex] = selector; + state.SegmentDescriptorCaches[index] = descriptorCache; + } + + /// LGDT: loads GDTR from a 6-byte memory pointer (2-byte limit, 4-byte base). + public static void LoadGdtr(State state, IMemory memory, ushort segment, uint offset) { + state.Gdtr.Limit = memory.UInt16[segment, offset]; + state.Gdtr.Base = memory.UInt32[segment, offset + 2]; + } + + /// SGDT: stores GDTR to a 6-byte memory pointer (2-byte limit, 4-byte base). + public static void StoreGdtr(State state, IMemory memory, ushort segment, uint offset) { + memory.UInt16[segment, offset] = state.Gdtr.Limit; + memory.UInt32[segment, offset + 2] = state.Gdtr.Base; + } + + /// LIDT: loads IDTR from a 6-byte memory pointer (2-byte limit, 4-byte base). + public static void LoadIdtr(State state, IMemory memory, ushort segment, uint offset) { + state.Idtr.Limit = memory.UInt16[segment, offset]; + state.Idtr.Base = memory.UInt32[segment, offset + 2]; + } + + /// SIDT: stores IDTR to a 6-byte memory pointer (2-byte limit, 4-byte base). + public static void StoreIdtr(State state, IMemory memory, ushort segment, uint offset) { + memory.UInt16[segment, offset] = state.Idtr.Limit; + memory.UInt32[segment, offset + 2] = state.Idtr.Base; + } + + /// MOV r32, CRn: reads CR0/CR2/CR3/CR4. + public static uint ReadControlRegister(State state, uint crNumber) { + return crNumber switch { + 0 => state.ControlRegisters.Cr0, + 2 => state.ControlRegisters.Cr2, + 3 => state.ControlRegisters.Cr3, + 4 => state.ControlRegisters.Cr4, + _ => throw new CpuInvalidOpcodeException($"MOV from CR{crNumber} is not supported") + }; + } + + /// MOV CRn, r32: writes CR0/CR2/CR3/CR4. + public static void WriteControlRegister(State state, uint crNumber, uint value) { + switch (crNumber) { + case 0: WriteCr0(state, value); break; + case 2: state.ControlRegisters.Cr2 = value; break; + case 3: state.ControlRegisters.Cr3 = value; break; + case 4: state.ControlRegisters.Cr4 = value; break; + default: throw new CpuInvalidOpcodeException($"MOV to CR{crNumber} is not supported"); + } + } + + /// SMSW: reads the low 16 bits of CR0 (the legacy 80286 "machine status word"). + public static ushort ReadMachineStatusWord(State state) { + return (ushort)state.ControlRegisters.Cr0; + } + + /// + /// LMSW: writes the low 4 bits of CR0 (PE, MP, EM, TS) from a 16-bit machine status word. Matches + /// real hardware: bits above 3 are ignored, and PE can only be set, never cleared, by LMSW. + /// + public static void LoadMachineStatusWord(State state, ushort value) { + uint newLowBits = (uint)(value & 0xF) | (state.ControlRegisters.ProtectionEnable ? 1u : 0u); + uint newCr0 = (state.ControlRegisters.Cr0 & ~0xFu) | newLowBits; + WriteCr0(state, newCr0); + } + + /// CLTS: clears CR0.TS (Task Switched), set by the CPU on every task switch. + public static void Clts(State state) { + state.ControlRegisters.TaskSwitched = false; + } + + private static void WriteCr0(State state, uint value) { + bool enteringProtectedMode = !state.ControlRegisters.ProtectionEnable && (value & 1) != 0; + state.ControlRegisters.Cr0 = value; + if (enteringProtectedMode) { + RefreshDescriptorCachesForRealModeTransition(state); + } + } + + /// + /// LLDT: loads LDTR from a GDT selector and caches its descriptor. A null selector is allowed (it + /// means "no LDT is loaded"), matching real hardware and 's + /// null-selector handling for DS/ES/FS/GS. + /// + public static void LoadLdtr(State state, IMemory memory, ushort selector) { + state.Ldtr.Selector = selector; + state.Ldtr.DescriptorCache = new SegmentSelector(selector).IsNull + ? default + : DescriptorTableReader.ReadDescriptor(selector, state.Gdtr.Base, state.Gdtr.Limit, + state.Ldtr.DescriptorCache.Base, state.Ldtr.DescriptorCache.Limit, address => memory[memory.Mmu.TranslateLinearAddress(address, isWrite: false)]); + } + + /// SLDT: reads the current LDTR selector. + public static ushort StoreLdtr(State state) { + return state.Ldtr.Selector; + } + + /// LTR: loads the Task Register from a GDT selector and caches its (TSS) descriptor. + public static void LoadTr(State state, IMemory memory, ushort selector) { + state.Tr.Selector = selector; + state.Tr.DescriptorCache = DescriptorTableReader.ReadDescriptor(selector, state.Gdtr.Base, state.Gdtr.Limit, + state.Ldtr.DescriptorCache.Base, state.Ldtr.DescriptorCache.Limit, address => memory[memory.Mmu.TranslateLinearAddress(address, isWrite: false)]); + } + + /// STR: reads the current Task Register selector. + public static ushort StoreTr(State state) { + return state.Tr.Selector; + } + + /// + /// ARPL: returns the r/m operand with its RPL raised to the register operand's RPL if it was lower. + /// + public static ushort AdjustRequestedPrivilegeLevel(ushort rmSelector, ushort regSelector) { + byte rmRpl = new SegmentSelector(rmSelector).RequestedPrivilegeLevel; + byte regRpl = new SegmentSelector(regSelector).RequestedPrivilegeLevel; + return rmRpl < regRpl ? (ushort)((rmSelector & ~0b11) | regRpl) : rmSelector; + } + + /// ARPL: whether the r/m operand's RPL was raised (sets ZF). + public static bool WasPrivilegeLevelAdjusted(ushort rmSelector, ushort regSelector) { + return new SegmentSelector(rmSelector).RequestedPrivilegeLevel < new SegmentSelector(regSelector).RequestedPrivilegeLevel; + } + + /// LAR: whether resolves to a present descriptor (sets ZF). + public static bool IsSelectorValidForLar(State state, IMemory memory, ushort selector) { + return TryReadDescriptorForVerification(state, memory, selector, out SegmentDescriptorCache descriptor) + && descriptor.Present; + } + + /// + /// LAR: loads the packed access-rights doubleword for (only meaningful + /// when is true; the destination is left unchanged otherwise). + /// Bits 8-15 are the raw access byte (type/S/DPL/P); bits 20-23 are AVL/reserved/D-B/G. + /// + public static uint LoadAccessRights(State state, IMemory memory, ushort selector) { + TryReadDescriptorForVerification(state, memory, selector, out SegmentDescriptorCache descriptor); + uint flagsNibble = (descriptor.Granularity4K ? 0x8u : 0) | (descriptor.DefaultBig ? 0x4u : 0); + return ((uint)descriptor.AccessRights << 8) | (flagsNibble << 20); + } + + /// LSL: whether resolves to a present segment descriptor (sets ZF). + public static bool IsSelectorValidForLsl(State state, IMemory memory, ushort selector) { + return TryReadDescriptorForVerification(state, memory, selector, out SegmentDescriptorCache descriptor) + && descriptor.Present && descriptor.IsCodeOrDataSegment; + } + + /// + /// LSL: loads the (already granularity-scaled) limit for (only + /// meaningful when is true). + /// + public static uint LoadSegmentLimit(State state, IMemory memory, ushort selector) { + TryReadDescriptorForVerification(state, memory, selector, out SegmentDescriptorCache descriptor); + return descriptor.Limit; + } + + /// VERR: whether is a present, readable data or code segment + /// accessible from the current privilege level. + public static bool VerifyReadable(State state, IMemory memory, ushort selector) { + if (!TryReadDescriptorForVerification(state, memory, selector, out SegmentDescriptorCache descriptor) + || !descriptor.Present || !descriptor.IsCodeOrDataSegment) { + return false; + } + // Code segments are readable only when the readable bit (access byte bit 1) is set. + bool readableBit = (descriptor.AccessRights & 0b10) != 0; + if (descriptor.IsCode && !readableBit) { + return false; + } + // A conforming code segment is accessible from any privilege level; every other segment + // (data, or non-conforming code) requires max(RPL, CPL) <= DPL, same as a normal segment load. + if (descriptor.IsCode && descriptor.IsConforming) { + return true; + } + byte rpl = new SegmentSelector(selector).RequestedPrivilegeLevel; + return Math.Max(rpl, state.Cpl) <= descriptor.DescriptorPrivilegeLevel; + } + + /// VERW: whether is a present, writable data segment + /// accessible from the current privilege level. + public static bool VerifyWritable(State state, IMemory memory, ushort selector) { + if (!TryReadDescriptorForVerification(state, memory, selector, out SegmentDescriptorCache descriptor) + || !descriptor.Present || !descriptor.IsCodeOrDataSegment || descriptor.IsCode) { + return false; + } + // Data segments are writable only when the writable bit (access byte bit 1) is set. + if ((descriptor.AccessRights & 0b10) == 0) { + return false; + } + // Data segments are never conforming, so the privilege check always applies. + byte rpl = new SegmentSelector(selector).RequestedPrivilegeLevel; + return Math.Max(rpl, state.Cpl) <= descriptor.DescriptorPrivilegeLevel; + } + + + private static bool TryReadDescriptorForVerification(State state, IMemory memory, ushort selector, out SegmentDescriptorCache descriptor) { + return DescriptorTableReader.TryReadDescriptor(selector, state.Gdtr.Base, state.Gdtr.Limit, + state.Ldtr.DescriptorCache.Base, state.Ldtr.DescriptorCache.Limit, address => memory[memory.Mmu.TranslateLinearAddress(address, isWrite: false)], out descriptor); + } + + /// + /// Real hardware always keeps a segment register's hidden descriptor cache in sync with its raw + /// value while in real mode, even when that value was set by something other than a segment-load + /// instruction (e.g. a loader writing CS/DS directly at boot). This emulator only refreshes the + /// cache on explicit loads (), so the moment CR0.PE transitions + /// to 1 - before the mandatory far jump reloads CS - any register whose cache was never refreshed + /// this way would resolve through a stale, mismatched cache. Snapshotting every cache to its + /// real-mode equivalent here restores the invariant real hardware never breaks. + /// + private static void RefreshDescriptorCachesForRealModeTransition(State state) { + foreach (SegmentRegisterIndex index in Enum.GetValues()) { + state.SegmentDescriptorCaches[index] = SegmentDescriptorCache.CreateRealMode(state.SegmentRegisters.UInt16[(uint)index]); + } + } +} diff --git a/src/Spice86.Core/Emulator/CPU/DescriptorTables/TaskStateSegment.cs b/src/Spice86.Core/Emulator/CPU/DescriptorTables/TaskStateSegment.cs new file mode 100644 index 0000000000..ea0ac5f960 --- /dev/null +++ b/src/Spice86.Core/Emulator/CPU/DescriptorTables/TaskStateSegment.cs @@ -0,0 +1,106 @@ +namespace Spice86.Core.Emulator.CPU.DescriptorTables; + +using Spice86.Core.Emulator.Memory; + +/// +/// The raw 32-bit TSS (Task State Segment) field layout used by hardware task switching, and the +/// save/load operations that copy CPU state to/from it. Field offsets match the Intel-defined 386 TSS +/// (a 104-byte structure ending with the LDT selector); the I/O permission bitmap fields beyond that +/// are not used since I/O bitmap enforcement is not yet implemented. +/// +public static class TaskStateSegment { + /// Offset of the back-link (previous task's TSS selector) field. + public const uint LinkOffset = 0; + + /// Offset of the ring-0 ESP field. + public const uint Esp0Offset = 4; + + /// Offset of the ring-0 SS field. + public const uint Ss0Offset = 8; + + private const uint CrThreeOffset = 28; + private const uint EipOffset = 32; + private const uint EflagsOffset = 36; + private const uint EaxOffset = 40; + private const uint EcxOffset = 44; + private const uint EdxOffset = 48; + private const uint EbxOffset = 52; + private const uint EspOffset = 56; + private const uint EbpOffset = 60; + private const uint EsiOffset = 64; + private const uint EdiOffset = 68; + private const uint EsOffset = 72; + private const uint CsOffset = 76; + private const uint SsOffset = 80; + private const uint DsOffset = 84; + private const uint FsOffset = 88; + private const uint GsOffset = 92; + private const uint LdtSelectorOffset = 96; + + /// The segment/EIP/LDT-selector fields read back from a TSS by a task switch, applied by + /// the caller via full segment-load validation (which needs the already-updated CPL to order the + /// loads correctly - CS before SS). + public readonly record struct TssSnapshot(uint Eip, ushort Es, ushort Cs, ushort Ss, ushort Ds, ushort Fs, ushort Gs, ushort LdtSelector); + + /// + /// Writes the current CPU state into the TSS at (the task being left), + /// using as its resume point (the instruction after the CALL for a forward + /// switch, or the current IP for a switch back via IRET). + /// + public static void SaveState(State state, IMemory memory, uint tssBase, uint eip) { + memory.UInt32[Lin(memory, tssBase + EipOffset, isWrite: true)] = eip; + memory.UInt32[Lin(memory, tssBase + EflagsOffset, isWrite: true)] = state.Flags.FlagRegister; + memory.UInt32[Lin(memory, tssBase + EaxOffset, isWrite: true)] = state.EAX; + memory.UInt32[Lin(memory, tssBase + EcxOffset, isWrite: true)] = state.ECX; + memory.UInt32[Lin(memory, tssBase + EdxOffset, isWrite: true)] = state.EDX; + memory.UInt32[Lin(memory, tssBase + EbxOffset, isWrite: true)] = state.EBX; + memory.UInt32[Lin(memory, tssBase + EspOffset, isWrite: true)] = state.ESP; + memory.UInt32[Lin(memory, tssBase + EbpOffset, isWrite: true)] = state.EBP; + memory.UInt32[Lin(memory, tssBase + EsiOffset, isWrite: true)] = state.ESI; + memory.UInt32[Lin(memory, tssBase + EdiOffset, isWrite: true)] = state.EDI; + memory.UInt16[Lin(memory, tssBase + EsOffset, isWrite: true)] = state.ES; + memory.UInt16[Lin(memory, tssBase + CsOffset, isWrite: true)] = state.CS; + memory.UInt16[Lin(memory, tssBase + SsOffset, isWrite: true)] = state.SS; + memory.UInt16[Lin(memory, tssBase + DsOffset, isWrite: true)] = state.DS; + memory.UInt16[Lin(memory, tssBase + FsOffset, isWrite: true)] = state.FS; + memory.UInt16[Lin(memory, tssBase + GsOffset, isWrite: true)] = state.GS; + memory.UInt16[Lin(memory, tssBase + LdtSelectorOffset, isWrite: true)] = state.Ldtr.Selector; + memory.UInt32[Lin(memory, tssBase + CrThreeOffset, isWrite: true)] = state.ControlRegisters.Cr3; + } + + /// + /// Reads the general-purpose registers, EFLAGS and CR3 from the TSS at + /// directly into , and returns the segment/EIP/LDT-selector fields as a + /// for the caller to apply. + /// + public static TssSnapshot LoadState(State state, IMemory memory, uint tssBase) { + state.ControlRegisters.Cr3 = memory.UInt32[Lin(memory, tssBase + CrThreeOffset, isWrite: false)]; + state.Flags.FlagRegister = memory.UInt32[Lin(memory, tssBase + EflagsOffset, isWrite: false)]; + state.EAX = memory.UInt32[Lin(memory, tssBase + EaxOffset, isWrite: false)]; + state.ECX = memory.UInt32[Lin(memory, tssBase + EcxOffset, isWrite: false)]; + state.EDX = memory.UInt32[Lin(memory, tssBase + EdxOffset, isWrite: false)]; + state.EBX = memory.UInt32[Lin(memory, tssBase + EbxOffset, isWrite: false)]; + state.ESP = memory.UInt32[Lin(memory, tssBase + EspOffset, isWrite: false)]; + state.EBP = memory.UInt32[Lin(memory, tssBase + EbpOffset, isWrite: false)]; + state.ESI = memory.UInt32[Lin(memory, tssBase + EsiOffset, isWrite: false)]; + state.EDI = memory.UInt32[Lin(memory, tssBase + EdiOffset, isWrite: false)]; + return new TssSnapshot( + Eip: memory.UInt32[Lin(memory, tssBase + EipOffset, isWrite: false)], + Es: memory.UInt16[Lin(memory, tssBase + EsOffset, isWrite: false)], + Cs: memory.UInt16[Lin(memory, tssBase + CsOffset, isWrite: false)], + Ss: memory.UInt16[Lin(memory, tssBase + SsOffset, isWrite: false)], + Ds: memory.UInt16[Lin(memory, tssBase + DsOffset, isWrite: false)], + Fs: memory.UInt16[Lin(memory, tssBase + FsOffset, isWrite: false)], + Gs: memory.UInt16[Lin(memory, tssBase + GsOffset, isWrite: false)], + LdtSelector: memory.UInt16[Lin(memory, tssBase + LdtSelectorOffset, isWrite: false)]); + } + + /// + /// Translates a linear TSS field address through paging when it is enabled - TSS bases (like + /// GDT/LDT/IDT bases) are linear addresses, so field reads/writes must go through the page tables + /// once CR0.PG is set, exactly like an ordinary segmented memory access would. + /// + private static uint Lin(IMemory memory, uint linearAddress, bool isWrite) { + return memory.Mmu.TranslateLinearAddress(linearAddress, isWrite); + } +} diff --git a/src/Spice86.Core/Emulator/CPU/DescriptorTables/TaskSwitchOperations.cs b/src/Spice86.Core/Emulator/CPU/DescriptorTables/TaskSwitchOperations.cs new file mode 100644 index 0000000000..345322db4b --- /dev/null +++ b/src/Spice86.Core/Emulator/CPU/DescriptorTables/TaskSwitchOperations.cs @@ -0,0 +1,139 @@ +namespace Spice86.Core.Emulator.CPU.DescriptorTables; + +using Spice86.Core.Emulator.CPU.Exceptions; +using Spice86.Core.Emulator.CPU.Registers; +using Spice86.Core.Emulator.Memory; +using Spice86.Shared.Emulator.Memory; + +/// +/// Protected-mode hardware task switching: a far CALL whose target selector resolves to an available +/// 32-bit TSS descriptor in the GDT (rather than an ordinary code segment or call gate) performs a full +/// task switch instead of an ordinary call - saving every general/segment register, EFLAGS, EIP and CR3 +/// into the current task's TSS, then loading the same fields from the new task's TSS. The new task is +/// marked busy and linked back to the calling task via EFLAGS.NT and the new TSS's back-link field, so a +/// subsequent IRET with EFLAGS.NT=1 () resumes the calling task +/// instead of performing an ordinary interrupt return. An interrupt/exception vectoring through an IDT +/// task gate (see ) reuses this exact same +/// mechanism. Only the CALL/interrupt-triggered (nested) form of task +/// switching is implemented: the calling task's busy bit is never touched (only a JMP-triggered switch +/// clears it), and only - not a JMP or another CALL - resumes it. +/// JMP directly to a TSS selector and GDT/LDT task-gate descriptors (reached via a direct CALL/JMP rather +/// than an IDT vector) are not yet supported - matches the existing "JMP through a call gate" and +/// "32-bit call gates" gaps left by call-gate dispatch. +/// +public static class TaskSwitchOperations { + private const byte AvailableTss386Type = 0x9; + private const byte BusyBitMask = 0b0000_0010; + + /// + /// Attempts to decode as an available 32-bit TSS descriptor in the GDT + /// (TSS selectors are never resolved through the LDT). Returns false (no side effects) for a + /// null selector, an out-of-bounds selector, an LDT-referencing selector, or any descriptor that + /// isn't an available 32-bit TSS - callers should fall back to call-gate/direct-transfer handling. + /// + public static bool TryReadAvailableTss(State state, IMemory memory, ushort selector) { + if (state.CpuMode != CpuMode.Protected) { + return false; + } + SegmentSelector segmentSelector = new(selector); + if (segmentSelector.IsNull || segmentSelector.ReferencesLocalDescriptorTable) { + return false; + } + uint entryOffset = (uint)segmentSelector.Index * 8u; + if (entryOffset + 7u > state.Gdtr.Limit) { + return false; + } + byte typeByte = memory[memory.Mmu.TranslateLinearAddress(state.Gdtr.Base + entryOffset + 5u, isWrite: false)]; + if ((typeByte & 0b0001_0000) != 0) { + return false; // S bit set: an ordinary code/data segment, not a system descriptor. + } + return (typeByte & 0x0F) == AvailableTss386Type; + } + + /// + /// Performs a task switch via far CALL to the TSS selector already validated by + /// : saves the calling task's full state into its own TSS, loads the + /// new task's state, marks the new task busy, sets EFLAGS.NT and the new task's back-link to the + /// calling task, and returns the new task's entry address. This only implements the CALL-triggered + /// (nested) form of task switching - the calling task's own busy bit is deliberately left untouched + /// (it is only ever cleared by a JMP-triggered switch, which is not yet supported). + /// + public static SegmentedAddress SwitchToNewTask(State state, IMemory memory, ushort tssSelector, uint returnEip) { + if (!DescriptorTableReader.TryReadDescriptor(tssSelector, state.Gdtr.Base, state.Gdtr.Limit, + state.Ldtr.DescriptorCache.Base, state.Ldtr.DescriptorCache.Limit, address => memory[memory.Mmu.TranslateLinearAddress(address, isWrite: false)], out SegmentDescriptorCache newTssDescriptor)) { + throw new CpuGeneralProtectionFaultException($"TSS selector 0x{tssSelector:X4} is invalid", new SegmentSelector(tssSelector).ErrorCode); + } + if (!newTssDescriptor.Present) { + throw new CpuSegmentNotPresentException($"TSS selector 0x{tssSelector:X4} is not present", new SegmentSelector(tssSelector).ErrorCode); + } + + ushort oldTssSelector = state.Tr.Selector; + uint oldTssBase = state.Tr.DescriptorCache.Base; + TaskStateSegment.SaveState(state, memory, oldTssBase, returnEip); + + uint newTssBase = newTssDescriptor.Base; + TaskStateSegment.TssSnapshot snapshot = TaskStateSegment.LoadState(state, memory, newTssBase); + memory.UInt16[memory.Mmu.TranslateLinearAddress(newTssBase + TaskStateSegment.LinkOffset, isWrite: true)] = oldTssSelector; + + state.Tr.Selector = tssSelector; + state.Tr.DescriptorCache = newTssDescriptor; + SetBusyBit(memory, GdtDescriptorOffsetOf(state, tssSelector), busy: true); + state.Flags.FlagRegister |= Flags.NestedTask; + + ApplySnapshotSegments(state, memory, snapshot); + return new SegmentedAddress(state.CS, state.IP); + } + + /// + /// Performs a task switch back to the calling task via its TSS back-link selector, invoked instead + /// of an ordinary IRET whenever EFLAGS.NT is set on entry - the mirror image of + /// : saves the current (nested) task's state, clears its busy bit, then + /// loads the calling task's state (already marked busy from the original switch, left unchanged) and + /// resumes it exactly where it left off. + /// + public static SegmentedAddress SwitchBackViaBackLink(State state, IMemory memory) { + ushort nestedTssSelector = state.Tr.Selector; + uint nestedTssBase = state.Tr.DescriptorCache.Base; + ushort callerTssSelector = memory.UInt16[memory.Mmu.TranslateLinearAddress(nestedTssBase + TaskStateSegment.LinkOffset, isWrite: false)]; + + if (!DescriptorTableReader.TryReadDescriptor(callerTssSelector, state.Gdtr.Base, state.Gdtr.Limit, + state.Ldtr.DescriptorCache.Base, state.Ldtr.DescriptorCache.Limit, address => memory[memory.Mmu.TranslateLinearAddress(address, isWrite: false)], out SegmentDescriptorCache callerTssDescriptor)) { + throw new CpuGeneralProtectionFaultException($"Back-link TSS selector 0x{callerTssSelector:X4} is invalid", new SegmentSelector(callerTssSelector).ErrorCode); + } + + TaskStateSegment.SaveState(state, memory, nestedTssBase, state.IP); + SetBusyBit(memory, GdtDescriptorOffsetOf(state, nestedTssSelector), busy: false); + + uint callerTssBase = callerTssDescriptor.Base; + TaskStateSegment.TssSnapshot snapshot = TaskStateSegment.LoadState(state, memory, callerTssBase); + + state.Tr.Selector = callerTssSelector; + state.Tr.DescriptorCache = callerTssDescriptor; + + ApplySnapshotSegments(state, memory, snapshot); + return new SegmentedAddress(state.CS, state.IP); + } + + private static void ApplySnapshotSegments(State state, IMemory memory, TaskStateSegment.TssSnapshot snapshot) { + SegmentAndControlRegisterOperations.LoadLdtr(state, memory, snapshot.LdtSelector); + // CS must be loaded before SS: state.Cpl (used to validate the new stack segment) is derived + // from CS, so SS validation must see the new task's CPL, not whatever CPL was active before. + SegmentAndControlRegisterOperations.LoadSegmentRegister(state, memory, (uint)SegmentRegisterIndex.CsIndex, snapshot.Cs); + SegmentAndControlRegisterOperations.LoadSegmentRegister(state, memory, (uint)SegmentRegisterIndex.SsIndex, snapshot.Ss); + SegmentAndControlRegisterOperations.LoadSegmentRegister(state, memory, (uint)SegmentRegisterIndex.DsIndex, snapshot.Ds); + SegmentAndControlRegisterOperations.LoadSegmentRegister(state, memory, (uint)SegmentRegisterIndex.EsIndex, snapshot.Es); + SegmentAndControlRegisterOperations.LoadSegmentRegister(state, memory, (uint)SegmentRegisterIndex.FsIndex, snapshot.Fs); + SegmentAndControlRegisterOperations.LoadSegmentRegister(state, memory, (uint)SegmentRegisterIndex.GsIndex, snapshot.Gs); + state.IP = (ushort)snapshot.Eip; + } + + private static uint GdtDescriptorOffsetOf(State state, ushort selector) { + return state.Gdtr.Base + (uint)new SegmentSelector(selector).Index * 8u; + } + + private static void SetBusyBit(IMemory memory, uint descriptorOffset, bool busy) { + uint typeByteAddress = memory.Mmu.TranslateLinearAddress(descriptorOffset + 5u, isWrite: true); + byte typeByte = memory[typeByteAddress]; + memory[typeByteAddress] = busy ? (byte)(typeByte | BusyBitMask) : (byte)(typeByte & ~BusyBitMask); + } +} diff --git a/src/Spice86.Core/Emulator/CPU/Exceptions/CpuGeneralProtectionFaultException.cs b/src/Spice86.Core/Emulator/CPU/Exceptions/CpuGeneralProtectionFaultException.cs index 793f4daee5..9de6d3fbf3 100644 --- a/src/Spice86.Core/Emulator/CPU/Exceptions/CpuGeneralProtectionFaultException.cs +++ b/src/Spice86.Core/Emulator/CPU/Exceptions/CpuGeneralProtectionFaultException.cs @@ -15,8 +15,8 @@ public class CpuGeneralProtectionFaultException : CpuException { /// Initializes a new instance. /// /// The message describing the error. - /// Some exceptions may have an error code pushed on the stack. - public CpuGeneralProtectionFaultException(string message, ushort? errorCode = null) + /// The selector-related error code, or 0 for a violation not tied to a specific selector (#GP always carries an error code on real hardware). + public CpuGeneralProtectionFaultException(string message, ushort? errorCode = 0) : base(message, 0x0D, CpuExceptionType.Fault, "#GP", errorCode) { } } \ No newline at end of file diff --git a/src/Spice86.Core/Emulator/CPU/Exceptions/CpuPageFaultException.cs b/src/Spice86.Core/Emulator/CPU/Exceptions/CpuPageFaultException.cs new file mode 100644 index 0000000000..f99404b20d --- /dev/null +++ b/src/Spice86.Core/Emulator/CPU/Exceptions/CpuPageFaultException.cs @@ -0,0 +1,23 @@ +namespace Spice86.Core.Emulator.CPU.Exceptions; + +/// +/// A Page Fault (#PF, vector 14) is raised by the paging unit when a linear-to-physical translation +/// fails: the page directory or page table entry is not present, or the access violates the +/// entry's combined User/Supervisor or Read/Write protection. The faulting linear address is recorded +/// in CR2 by the paging unit before this exception is thrown. +/// +/// +/// Error code bit layout (pushed on the stack like any other exception with an error code): +/// bit 0 (P) is 0 for a not-present page, 1 for a protection violation on a present page; bit 1 (W/R) +/// is 1 when the fault was caused by a write; bit 2 (U/S) is 1 when the fault occurred at CPL 3. +/// +public class CpuPageFaultException : CpuException { + /// + /// Initializes a new instance. + /// + /// The message describing the error. + /// The page-fault error code (P/W/U bits). + public CpuPageFaultException(string message, ushort errorCode) + : base(message, 0x0E, CpuExceptionType.Fault, "#PF", errorCode) { + } +} diff --git a/src/Spice86.Core/Emulator/CPU/Exceptions/CpuSegmentNotPresentException.cs b/src/Spice86.Core/Emulator/CPU/Exceptions/CpuSegmentNotPresentException.cs new file mode 100644 index 0000000000..65da2d5d49 --- /dev/null +++ b/src/Spice86.Core/Emulator/CPU/Exceptions/CpuSegmentNotPresentException.cs @@ -0,0 +1,18 @@ +namespace Spice86.Core.Emulator.CPU.Exceptions; + +/// +/// A Segment Not Present fault (#NP, vector 11) is raised when a segment-load instruction (other than +/// loading SS, which raises instead) references a +/// descriptor whose present bit is clear. The saved instruction pointer points to the instruction +/// that caused the exception. +/// +public class CpuSegmentNotPresentException : CpuException { + /// + /// Initializes a new instance. + /// + /// The message describing the error. + /// The selector-related error code, or 0 for a violation not tied to a specific selector (#NP always carries an error code on real hardware). + public CpuSegmentNotPresentException(string message, ushort? errorCode = 0) + : base(message, 0x0B, CpuExceptionType.Fault, "#NP", errorCode) { + } +} diff --git a/src/Spice86.Core/Emulator/CPU/Exceptions/CpuStackSegmentFaultException.cs b/src/Spice86.Core/Emulator/CPU/Exceptions/CpuStackSegmentFaultException.cs index da5ed1f3e1..03b251b711 100644 --- a/src/Spice86.Core/Emulator/CPU/Exceptions/CpuStackSegmentFaultException.cs +++ b/src/Spice86.Core/Emulator/CPU/Exceptions/CpuStackSegmentFaultException.cs @@ -11,8 +11,8 @@ public class CpuStackSegmentFaultException : CpuException { /// Initializes a new instance. /// /// The message describing the error. - /// Some exceptions may have an error code pushed on the stack. - public CpuStackSegmentFaultException(string message, ushort? errorCode = null) + /// The selector-related error code, or 0 for a violation not tied to a specific selector (#SS always carries an error code on real hardware). + public CpuStackSegmentFaultException(string message, ushort? errorCode = 0) : base(message, 0x0C, CpuExceptionType.Fault, "#SS", errorCode) { } } diff --git a/src/Spice86.Core/Emulator/CPU/Flags.cs b/src/Spice86.Core/Emulator/CPU/Flags.cs index bb25637824..0ce8caaff1 100644 --- a/src/Spice86.Core/Emulator/CPU/Flags.cs +++ b/src/Spice86.Core/Emulator/CPU/Flags.cs @@ -14,9 +14,11 @@ public class Flags { [CpuModel.INTEL_8086] = new(bitsAlwaysOn: [1, 12, 13, 14, 15], bitsAlwaysOff: [3, 5]), // Since we dont handle IO privilege (12 / 13) and nested task (14), let's put them as always off [CpuModel.INTEL_80286] = new(bitsAlwaysOn: [1], bitsAlwaysOff: [3, 5, 12, 13, 14, 15]), - [CpuModel.INTEL_80386] = new(bitsAlwaysOn: [1, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31], bitsAlwaysOff: [3, 5, 12, 13, 14, 15]) + // IOPL (12-13) and NT (14) are real, software-visible bits on the 386 (privilege checks and + // task-switch nesting both depend on them) so they are no longer forced off here. + [CpuModel.INTEL_80386] = new(bitsAlwaysOn: [1, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31], bitsAlwaysOff: [3, 5, 15]) }.ToFrozenDictionary(); - + private record BitsOnOff { public BitsOnOff(List bitsAlwaysOn, List bitsAlwaysOff) { BitsAlwaysOn = BitMaskUtils.BitMaskFromBitList(bitsAlwaysOn); @@ -26,7 +28,7 @@ public BitsOnOff(List bitsAlwaysOn, List bitsAlwaysOff) { /// Or that into the register /// public uint BitsAlwaysOn { get; } - + /// /// And that into the register /// @@ -77,6 +79,24 @@ public BitsOnOff(List bitsAlwaysOn, List bitsAlwaysOff) { /// public const ushort Overflow = 0b00001000_00000000; //11 + /// + /// The I/O privilege level field bitmask (EFLAGS bits 12-13). A 2-bit field, not a single boolean + /// flag; use to read/write the numeric level. + /// + public const uint IoPrivilegeLevelMask = 0b0011_0000_0000_0000; //12-13 + + /// + /// The nested-task flag bitmask (EFLAGS bit 14). Set on entry to a task via a `CALL`/`INT` to a + /// task gate, so `IRET` knows to return via the back-link instead of a normal stack pop. + /// + public const uint NestedTask = 0x0000_4000; //14 + + /// + /// The virtual-8086 mode flag bitmask (EFLAGS bit 17). Only meaningful once protected mode is + /// enabled; toggled to enter/leave V86 mode via IRET or a task switch. + /// + public const uint Virtual8086Mode = 0x0002_0000; //17 + /// /// rflag mask to OR with flags, useful to compare with values emulated by DOSBox. /// @@ -123,6 +143,28 @@ public void SetFlag(ushort mask, bool value) { } } + /// + /// Gets the value of a particular flag outside the low 16 bits of the register (e.g. VM). + /// + /// The bitmask to apply on the flags register. + /// The value of a particular flag, as a boolean. + public bool GetFlag(uint mask) { + return (FlagRegister & mask) == mask; + } + + /// + /// Sets the value of a particular flag outside the low 16 bits of the register (e.g. VM). + /// + /// The bitmask to access a particular flag in the flags register. + /// The boolean value of the flag. + public void SetFlag(uint mask, bool value) { + if (value) { + FlagRegister |= mask; + } else { + FlagRegister &= ~mask; + } + } + /// /// Gets the 16-bit value of the flags register. /// diff --git a/src/Spice86.Core/Emulator/CPU/Registers/ControlRegisters.cs b/src/Spice86.Core/Emulator/CPU/Registers/ControlRegisters.cs new file mode 100644 index 0000000000..f847163519 --- /dev/null +++ b/src/Spice86.Core/Emulator/CPU/Registers/ControlRegisters.cs @@ -0,0 +1,55 @@ +namespace Spice86.Core.Emulator.CPU.Registers; + +/// +/// The 386 control registers (CR0, CR2, CR3, CR4). Only the bits meaningful without FPU emulation are +/// exposed as named accessors; the raw register values are still stored in full so that +/// MOV CRn, r32 round-trips every bit, including ones this emulator does not act upon. +/// +public class ControlRegisters { + /// CR0: machine status and mode control. + public uint Cr0 { get; set; } + + /// CR2: the linear address that caused the most recent page fault. + public uint Cr2 { get; set; } + + /// CR3: the physical base address of the page directory. + public uint Cr3 { get; set; } + + /// CR4: extended feature control. The 386 defines no CR4 bits; reserved for later CPU models. + public uint Cr4 { get; set; } + + /// Protection Enable bit (CR0 bit 0): true once the CPU has entered protected mode. + public bool ProtectionEnable { + get => (Cr0 & 0x1) != 0; + set => Cr0 = SetBit(Cr0, 0, value); + } + + /// Monitor Coprocessor bit (CR0 bit 1). Stored for round-tripping only; no FPU is emulated. + public bool MonitorCoprocessor { + get => (Cr0 & 0x2) != 0; + set => Cr0 = SetBit(Cr0, 1, value); + } + + /// Task Switched bit (CR0 bit 3): set by the CPU on every task switch, cleared by CLTS. + public bool TaskSwitched { + get => (Cr0 & 0x8) != 0; + set => Cr0 = SetBit(Cr0, 3, value); + } + + /// Extension Type bit (CR0 bit 4): reserved on the 386, present for round-tripping only. + public bool ExtensionType { + get => (Cr0 & 0x10) != 0; + set => Cr0 = SetBit(Cr0, 4, value); + } + + /// Paging Enable bit (CR0 bit 31): true when linear-to-physical paging translation is active. + public bool PagingEnable { + get => (Cr0 & 0x8000_0000) != 0; + set => Cr0 = SetBit(Cr0, 31, value); + } + + private static uint SetBit(uint register, int bitIndex, bool value) { + uint mask = 1u << bitIndex; + return value ? register | mask : register & ~mask; + } +} diff --git a/src/Spice86.Core/Emulator/CPU/Registers/DescriptorTableRegister.cs b/src/Spice86.Core/Emulator/CPU/Registers/DescriptorTableRegister.cs new file mode 100644 index 0000000000..571e9c0a53 --- /dev/null +++ b/src/Spice86.Core/Emulator/CPU/Registers/DescriptorTableRegister.cs @@ -0,0 +1,13 @@ +namespace Spice86.Core.Emulator.CPU.Registers; + +/// +/// A descriptor table register (GDTR or IDTR): a linear base address and a byte limit, loaded by +/// LGDT/LIDT and read back by SGDT/SIDT. +/// +public class DescriptorTableRegister { + /// The 32-bit linear base address of the table. + public uint Base { get; set; } + + /// The byte limit of the table (table size in bytes, minus one). + public ushort Limit { get; set; } +} diff --git a/src/Spice86.Core/Emulator/CPU/Registers/SegmentDescriptorCache.cs b/src/Spice86.Core/Emulator/CPU/Registers/SegmentDescriptorCache.cs new file mode 100644 index 0000000000..c1d3d7cf95 --- /dev/null +++ b/src/Spice86.Core/Emulator/CPU/Registers/SegmentDescriptorCache.cs @@ -0,0 +1,72 @@ +namespace Spice86.Core.Emulator.CPU.Registers; + +/// +/// The hidden descriptor cache a CPU loads into a segment register when a selector is loaded into +/// it. In protected mode this is decoded from a GDT/LDT descriptor at load time; in real/V86 mode it +/// is synthesized as Base = selector * 16, Limit = 0xFFFF. Memory accesses through the +/// segment use this cache directly, without re-reading the descriptor table on every access. +/// +public readonly record struct SegmentDescriptorCache { + /// + /// Initializes a new instance. + /// + /// The 32-bit linear base address of the segment. + /// The segment limit, already scaled by granularity to a byte-granular value. + /// The raw access-rights byte (present, DPL, S, type). + /// Whether the segment defaults to 32-bit operands/addressing (D/B bit). + /// Whether the limit is scaled in 4K pages (G bit) rather than bytes. + /// Whether the descriptor's present bit is set. + public SegmentDescriptorCache(uint @base, uint limit, byte accessRights, bool defaultBig, bool granularity4K, bool present) { + Base = @base; + Limit = limit; + AccessRights = accessRights; + DefaultBig = defaultBig; + Granularity4K = granularity4K; + Present = present; + } + + /// The 32-bit linear base address of the segment. + public uint Base { get; } + + /// The segment limit, already scaled by granularity to a byte-granular value. + public uint Limit { get; } + + /// The raw access-rights byte (present, DPL, S, type). + public byte AccessRights { get; } + + /// Whether the segment defaults to 32-bit operands/addressing (D/B bit). + public bool DefaultBig { get; } + + /// Whether the limit is scaled in 4K pages (G bit) rather than bytes. + public bool Granularity4K { get; } + + /// Whether the descriptor's present bit is set. + public bool Present { get; } + + /// The descriptor privilege level (access-rights byte bits 5-6). + public byte DescriptorPrivilegeLevel => (byte)((AccessRights >> 5) & 0b11); + + /// Whether the descriptor is a code-or-data segment (S bit set) rather than a system descriptor. + public bool IsCodeOrDataSegment => (AccessRights & 0b0001_0000) != 0; + + /// Whether a code-or-data descriptor describes executable (code) memory. + public bool IsCode => IsCodeOrDataSegment && (AccessRights & 0b0000_1000) != 0; + + /// Whether a code descriptor is conforming (executable at any CPL <= its DPL, without changing CPL). + public bool IsConforming => IsCode && (AccessRights & 0b0000_0100) != 0; + + /// Whether a data-or-code descriptor's writable/readable bit (access byte bit 1) is set. + public bool IsReadWriteBitSet => (AccessRights & 0b10) != 0; + + /// Creates the descriptor cache synthesized for a real-mode or V86-mode segment load. + /// The raw segment value being loaded. + public static SegmentDescriptorCache CreateRealMode(ushort selector) { + return new SegmentDescriptorCache( + @base: (uint)(selector << 4), + limit: 0xFFFF, + accessRights: 0b1001_0011, // present, DPL 0, code-or-data, read/write, accessed + defaultBig: false, + granularity4K: false, + present: true); + } +} diff --git a/src/Spice86.Core/Emulator/CPU/Registers/SegmentDescriptorCaches.cs b/src/Spice86.Core/Emulator/CPU/Registers/SegmentDescriptorCaches.cs new file mode 100644 index 0000000000..501d275249 --- /dev/null +++ b/src/Spice86.Core/Emulator/CPU/Registers/SegmentDescriptorCaches.cs @@ -0,0 +1,30 @@ +namespace Spice86.Core.Emulator.CPU.Registers; + +/// +/// Holds the hidden descriptor cache for each of the six segment registers, indexed by +/// . Initialized for real-mode operation with a null selector; +/// segment loads (real or protected mode) overwrite the relevant entry. +/// +public class SegmentDescriptorCaches { + private readonly SegmentDescriptorCache[] _caches; + + /// + /// Initializes a new instance with every segment register defaulting to a real-mode cache for + /// selector 0. + /// + public SegmentDescriptorCaches() { + _caches = new SegmentDescriptorCache[6]; + for (int index = 0; index < _caches.Length; index++) { + _caches[index] = SegmentDescriptorCache.CreateRealMode(0); + } + } + + /// + /// Gets or sets the descriptor cache for the given segment register. + /// + /// The segment register whose cache to access. + public SegmentDescriptorCache this[SegmentRegisterIndex segmentRegisterIndex] { + get => _caches[(int)segmentRegisterIndex]; + set => _caches[(int)segmentRegisterIndex] = value; + } +} diff --git a/src/Spice86.Core/Emulator/CPU/Registers/SegmentSelector.cs b/src/Spice86.Core/Emulator/CPU/Registers/SegmentSelector.cs new file mode 100644 index 0000000000..760421a2af --- /dev/null +++ b/src/Spice86.Core/Emulator/CPU/Registers/SegmentSelector.cs @@ -0,0 +1,49 @@ +namespace Spice86.Core.Emulator.CPU.Registers; + +/// +/// Decomposes a raw 16-bit segment selector value into its table index, table indicator (GDT/LDT), +/// and requested privilege level, as defined by the Intel segment selector layout. +/// +public readonly record struct SegmentSelector { + /// + /// Initializes a new instance from a raw selector value. + /// + /// The raw 16-bit selector value. + public SegmentSelector(ushort value) { + Value = value; + } + + /// + /// The raw 16-bit selector value. + /// + public ushort Value { get; } + + /// + /// The requested privilege level (bits 0-1). + /// + public byte RequestedPrivilegeLevel => (byte)(Value & 0b11); + + /// + /// Whether the selector references the LDT (bit 2 set) instead of the GDT. + /// + public bool ReferencesLocalDescriptorTable => (Value & 0b100) != 0; + + /// + /// The index of the descriptor within its table (bits 3-15). + /// + public int Index => Value >> 3; + + /// + /// Whether the selector is the null selector: index 0 and the GDT (TI=0). An LDT selector + /// with index 0 is a normal, usable selector (it references the first LDT entry) and is not null - + /// only a GDT index-0 selector is architecturally reserved as "null". + /// + public bool IsNull => Index == 0 && !ReferencesLocalDescriptorTable; + + /// + /// The value pushed as a selector-related exception error code: the RPL bits are NOT part of the + /// error-code layout (bit 0 EXT, bit 1 IDT, bit 2 TI, bits 3-15 index) and are always masked out, + /// regardless of the RPL the faulting selector itself carried. + /// + public ushort ErrorCode => (ushort)(Value & 0xFFFC); +} diff --git a/src/Spice86.Core/Emulator/CPU/Registers/SystemSegmentRegister.cs b/src/Spice86.Core/Emulator/CPU/Registers/SystemSegmentRegister.cs new file mode 100644 index 0000000000..77b3235633 --- /dev/null +++ b/src/Spice86.Core/Emulator/CPU/Registers/SystemSegmentRegister.cs @@ -0,0 +1,13 @@ +namespace Spice86.Core.Emulator.CPU.Registers; + +/// +/// A system segment register (LDTR or TR): a selector into the GDT plus the descriptor cache loaded +/// for it, loaded by LLDT/LTR and read back by SLDT/STR. +/// +public class SystemSegmentRegister { + /// The selector currently loaded into the register. + public ushort Selector { get; set; } + + /// The descriptor cache loaded for . + public SegmentDescriptorCache DescriptorCache { get; set; } +} diff --git a/src/Spice86.Core/Emulator/CPU/Stack.cs b/src/Spice86.Core/Emulator/CPU/Stack.cs index 17a1dee3c2..a0ff2b3315 100644 --- a/src/Spice86.Core/Emulator/CPU/Stack.cs +++ b/src/Spice86.Core/Emulator/CPU/Stack.cs @@ -1,6 +1,7 @@ namespace Spice86.Core.Emulator.CPU; using Spice86.Core.Emulator.CPU.Exceptions; +using Spice86.Core.Emulator.CPU.Registers; using Spice86.Core.Emulator.Memory; using Spice86.Core.Emulator.Memory.Mmu; using Spice86.Shared.Emulator.Memory; @@ -29,13 +30,11 @@ /// Lower Memory Addresses
/// /// -/// Why SP and not ESP: Spice86 targets real-mode DOS, where the stack address is always computed -/// as SS:SP with SP truncated to 16 bits. The 32-bit ESP register would only govern stack addressing -/// if the SS segment descriptor's B (Big) bit were set, which only occurs in protected-mode 32-bit -/// stack segments. Real-mode BIOS initialization leaves B=0, so SP is the authoritative pointer. -/// Note that the operand size (16-bit vs 32-bit values pushed or popped) is independent of this: -/// and correctly store 32-bit values while still advancing -/// the 16-bit SP, matching actual 386+ real-mode behaviour. +/// SP vs ESP: the address computation uses SS:ESP (32-bit) when the SS descriptor's D/B bit is +/// set, and SS:SP (16-bit, matching real mode) otherwise - is the single +/// point that decides this for every method below. Note that the operand size (16-bit vs 32-bit values +/// pushed or popped) is entirely independent of the address size: and +/// correctly store 32-bit values regardless of which stack address width is active. /// /// public class Stack { @@ -53,13 +52,44 @@ public Stack(IMemory memory, State state) { this._state = state; } + /// Whether SS's descriptor has the D/B bit set, making ESP (not SP) the authoritative stack address. + private bool StackAddressIs32Bit => _state.SegmentDescriptorCaches[SegmentRegisterIndex.SsIndex].DefaultBig; + + /// + /// The authoritative stack pointer: when SS is a 32-bit-default segment, + /// (zero-extended) otherwise. Every push/pop/peek/poke method below reads and + /// writes through this single property so the SP/ESP choice is made in exactly one place. + /// + private uint StackPointer { + get => StackAddressIs32Bit ? _state.ESP : _state.SP; + set { + if (StackAddressIs32Bit) { + _state.ESP = value; + } else { + _state.SP = (ushort)value; + } + } + } + + /// + /// Wraps an address computation to the current stack address width: full 32-bit range when SS is + /// 32-bit-default, or 16-bit (matching real hardware SP register wraparound) otherwise. + /// + private uint MaskAddress(uint value) => StackAddressIs32Bit ? value : (ushort)value; + + /// + /// Computes + , wrapped to the current stack + /// address width. may be negative. + /// + private uint OffsetStackPointer(int delta) => MaskAddress(unchecked((uint)((int)StackPointer + delta))); + /// /// Peeks a 8 bit value from the stack /// /// The offset from the stack top /// The value in memory. public byte Peek8(int index) { - ushort offset = (ushort)(_state.SP + index); + uint offset = OffsetStackPointer(index); return _memory.UInt8[_state.SS, offset, SegmentAccessKind.Stack]; } @@ -69,7 +99,7 @@ public byte Peek8(int index) { /// The offset from the stack top /// The value in memory. public ushort Peek16(int index) { - ushort offset = (ushort)(_state.SP + index); + uint offset = OffsetStackPointer(index); return _memory.UInt16[_state.SS, offset, SegmentAccessKind.Stack]; } @@ -79,7 +109,7 @@ public ushort Peek16(int index) { /// The offset from the stack top /// The value to store in memory. public void Poke16(int index, ushort value) { - ushort offset = (ushort)(_state.SP + index); + uint offset = OffsetStackPointer(index); _memory.UInt16[_state.SS, offset, SegmentAccessKind.Stack] = value; } @@ -88,8 +118,8 @@ public void Poke16(int index, ushort value) { /// /// The value retrieved from the stack, therefore read from memory public ushort Pop16() { - ushort res = _memory.UInt16[_state.SS, _state.SP, SegmentAccessKind.Stack]; - _state.SP = (ushort)(_state.SP + 2); + ushort res = _memory.UInt16[_state.SS, StackPointer, SegmentAccessKind.Stack]; + StackPointer = OffsetStackPointer(2); return res; } @@ -98,9 +128,9 @@ public ushort Pop16() { /// /// The value pushed onto the stack, therefore stored in memory. public void Push16(ushort value) { - ushort newSp = (ushort)(_state.SP - 2); + uint newSp = OffsetStackPointer(-2); _memory.UInt16[_state.SS, newSp, SegmentAccessKind.Stack] = value; - _state.SP = newSp; + StackPointer = newSp; } /// @@ -108,7 +138,7 @@ public void Push16(ushort value) { /// /// The offset from the stack top public uint Peek32(int index) { - ushort offset = (ushort)(_state.SP + index); + uint offset = OffsetStackPointer(index); return _memory.UInt32[_state.SS, offset, SegmentAccessKind.Stack]; } @@ -118,7 +148,7 @@ public uint Peek32(int index) { /// The offset from the stack top /// The value to store in memory. public void Poke32(int index, uint value) { - ushort offset = (ushort)(_state.SP + index); + uint offset = OffsetStackPointer(index); _memory.UInt32[_state.SS, offset, SegmentAccessKind.Stack] = value; } @@ -127,32 +157,48 @@ public void Poke32(int index, uint value) { /// /// The value popped from the stack. public uint Pop32() { - uint res = _memory.UInt32[_state.SS, _state.SP, SegmentAccessKind.Stack]; - _state.SP = (ushort)(_state.SP + 4); + uint res = _memory.UInt32[_state.SS, StackPointer, SegmentAccessKind.Stack]; + StackPointer = OffsetStackPointer(4); return res; } /// - /// Performs a 32-bit LEAVE using a 16-bit stack address. + /// Pops a 16-bit segment selector from a stack slot of the given width: reads only the low 16 bits + /// of the slot (matching real hardware's POP Sreg, which discards any upper bits in a 32-bit slot) + /// but advances the stack pointer by the full slot width. /// - public void Leave32() { - ushort framePointer = _state.BP; - uint basePointer = _memory.UInt32[_state.SS, framePointer, SegmentAccessKind.Stack]; - _state.SP = (ushort)(framePointer + 4); - _state.EBP = basePointer; + /// The slot width in bytes: 2 for a 16-bit operand-size POP, 4 for 32-bit. + public ushort PopSegmentSelector(int slotSizeBytes) { + ushort value = _memory.UInt16[_state.SS, StackPointer, SegmentAccessKind.Stack]; + StackPointer = OffsetStackPointer(slotSizeBytes); + return value; } /// - /// Performs a 16-bit LEAVE using a 16-bit stack address. - /// Reads the new BP from [SS:BP] before committing SP, so a #SS during the - /// pop leaves SP unchanged (matching real-80386 fault atomicity). - /// Only the low 16 bits of EBP are written; upper 16 bits of EBP are preserved. + /// Performs LEAVE: releases the current stack frame by setting the stack pointer to the frame + /// pointer's value, then popping the caller's saved frame pointer back off the stack. + /// Two independent axes control this instruction: + /// - The ADDRESS used to locate the saved frame pointer (and the resulting new stack pointer) + /// follows the stack's own address width (SS's D/B bit via ): + /// EBP/ESP when the stack is 32-bit-default, BP/SP otherwise - resolved fresh every call since SS + /// can differ between calls to the same code address. + /// - The VALUE popped back into the frame-pointer register, and the WIDTH of that register write, + /// follow the instruction's operand size (, safe to fix at parse + /// time since it comes from CS): a 16-bit-operand LEAVE only ever writes BP (leaving EBP's upper + /// half untouched), even when the stack itself is 32-bit - matching a plain POP's semantics. + /// The saved frame pointer is read before the stack pointer is committed, so a fault reading it + /// leaves the stack pointer unchanged (matching real-80386 fault atomicity). /// - public void Leave16() { - ushort framePointer = _state.BP; - ushort basePointer = _memory.UInt16[_state.SS, framePointer, SegmentAccessKind.Stack]; - _state.SP = (ushort)(framePointer + 2); - _state.BP = basePointer; + public void Leave(bool operandSize32) { + uint frameAddress = StackAddressIs32Bit ? _state.EBP : _state.BP; + uint poppedValue = ReadFrameValue(frameAddress, operandSize32); + int pointerSize = operandSize32 ? 4 : 2; + StackPointer = MaskAddress(frameAddress + (uint)pointerSize); + if (operandSize32) { + _state.EBP = poppedValue; + } else { + _state.BP = (ushort)poppedValue; + } } /// @@ -160,47 +206,50 @@ public void Leave16() { /// /// The value to store onto the stack. public void Push32(uint value) { - ushort newSp = (ushort)(_state.SP - 4); + uint newSp = OffsetStackPointer(-4); _memory.UInt32[_state.SS, newSp, SegmentAccessKind.Stack] = value; - _state.SP = newSp; + StackPointer = newSp; } /// /// Pre-validates that all slots for a multi-register push (PUSHA/PUSHAD) are accessible. - /// Checks each slot going downward from the current SP. Raises #SS if any slot crosses the segment limit. - /// No state is modified if the check fails. + /// Checks each slot going downward from the current stack pointer. Raises #SS if any slot crosses + /// the segment limit. No state is modified if the check fails. /// /// Size of each value in bytes (2 for 16-bit, 4 for 32-bit). /// Number of values to push. public void ValidateStackPushRange(ushort valueSizeBytes, ushort valueCount) { - ushort offset = _state.SP; + uint offset = StackPointer; for (ushort i = 0; i < valueCount; i++) { - offset = (ushort)(offset - valueSizeBytes); - _memory.Mmu.CheckAccess(_state.SS, offset, valueSizeBytes, SegmentAccessKind.Stack); + offset = MaskAddress(offset - valueSizeBytes); + _memory.Mmu.CheckAccess(_state.SS, offset, valueSizeBytes, SegmentAccessKind.Stack, isWrite: true); } } /// /// Pre-validates that all slots for a multi-register pop (POPA/POPAD) are accessible. - /// Checks each slot going upward from the current SP. Raises #SS if any slot crosses the segment limit. - /// No state is modified if the check fails. + /// Checks each slot going upward from the current stack pointer. Raises #SS if any slot crosses the + /// segment limit. No state is modified if the check fails. /// /// Size of each value in bytes (2 for 16-bit, 4 for 32-bit). /// Number of values to pop. public void ValidateStackPopRange(ushort valueSizeBytes, ushort valueCount) { - ushort offset = _state.SP; + uint offset = StackPointer; for (ushort i = 0; i < valueCount; i++) { - _memory.Mmu.CheckAccess(_state.SS, offset, valueSizeBytes, SegmentAccessKind.Stack); - offset = (ushort)(offset + valueSizeBytes); + _memory.Mmu.CheckAccess(_state.SS, offset, valueSizeBytes, SegmentAccessKind.Stack, isWrite: false); + offset = MaskAddress(offset + valueSizeBytes); } } /// /// Pushes all 8 general-purpose 16-bit registers (PUSHA order: AX, CX, DX, BX, SP, BP, SI, DI). + /// The range is validated up front, all eight slots are written, and the #SS (if any slot crossed + /// the segment limit) is raised only afterwards - matching real-80386 PUSHAD, which stores every + /// register before reporting the fault. /// public void PushAll16(ushort ax, ushort cx, ushort dx, ushort bx, ushort sp, ushort bp, ushort si, ushort di) { CpuStackSegmentFaultException? pendingFault = GetStackPushRangeFault(2, 8); - ushort offset = _state.SP; + ushort offset = (ushort)StackPointer; offset = (ushort)(offset - 2); _memory.WriteUInt16Segmented(_state.SS, offset, ax); offset = (ushort)(offset - 2); _memory.WriteUInt16Segmented(_state.SS, offset, cx); offset = (ushort)(offset - 2); _memory.WriteUInt16Segmented(_state.SS, offset, dx); @@ -209,18 +258,21 @@ public void PushAll16(ushort ax, ushort cx, ushort dx, ushort bx, ushort sp, ush offset = (ushort)(offset - 2); _memory.WriteUInt16Segmented(_state.SS, offset, bp); offset = (ushort)(offset - 2); _memory.WriteUInt16Segmented(_state.SS, offset, si); offset = (ushort)(offset - 2); _memory.WriteUInt16Segmented(_state.SS, offset, di); - if (pendingFault != null) { + if (pendingFault is not null) { throw pendingFault; } - _state.SP = offset; + StackPointer = offset; } /// /// Pushes all 8 general-purpose 32-bit registers (PUSHAD order: EAX, ECX, EDX, EBX, ESP, EBP, ESI, EDI). + /// The range is validated up front, all eight slots are written, and the #SS (if any slot crossed + /// the segment limit) is raised only afterwards - matching real-80386 PUSHAD, which stores every + /// register before reporting the fault. /// public void PushAll32(uint eax, uint ecx, uint edx, uint ebx, uint esp, uint ebp, uint esi, uint edi) { CpuStackSegmentFaultException? pendingFault = GetStackPushRangeFault(4, 8); - ushort offset = _state.SP; + ushort offset = (ushort)StackPointer; offset = (ushort)(offset - 4); _memory.WriteUInt32Segmented(_state.SS, offset, eax); offset = (ushort)(offset - 4); _memory.WriteUInt32Segmented(_state.SS, offset, ecx); offset = (ushort)(offset - 4); _memory.WriteUInt32Segmented(_state.SS, offset, edx); @@ -229,18 +281,26 @@ public void PushAll32(uint eax, uint ecx, uint edx, uint ebx, uint esp, uint ebp offset = (ushort)(offset - 4); _memory.WriteUInt32Segmented(_state.SS, offset, ebp); offset = (ushort)(offset - 4); _memory.WriteUInt32Segmented(_state.SS, offset, esi); offset = (ushort)(offset - 4); _memory.WriteUInt32Segmented(_state.SS, offset, edi); - if (pendingFault != null) { + if (pendingFault is not null) { throw pendingFault; } - _state.SP = offset; + StackPointer = offset; } + /// + /// Walks the push slots going downward from the current stack pointer and captures the first #SS + /// any slot raises, without throwing. Returns null when the whole range is accessible so the caller + /// can complete its writes before re-raising the captured fault (deferred-fault PUSHAD semantics). + /// + /// Size of each value in bytes (2 for 16-bit, 4 for 32-bit). + /// Number of values to push. + /// The first stack-segment fault encountered, or null if all slots are valid. private CpuStackSegmentFaultException? GetStackPushRangeFault(ushort valueSizeBytes, ushort valueCount) { - ushort offset = _state.SP; - for (ushort index = 0; index < valueCount; index++) { - offset = (ushort)(offset - valueSizeBytes); + uint offset = StackPointer; + for (ushort i = 0; i < valueCount; i++) { + offset = MaskAddress(offset - valueSizeBytes); try { - _memory.Mmu.CheckAccess(_state.SS, offset, valueSizeBytes, SegmentAccessKind.Stack); + _memory.Mmu.CheckAccess(_state.SS, offset, valueSizeBytes, SegmentAccessKind.Stack, isWrite: true); } catch (CpuStackSegmentFaultException exception) { return exception; } @@ -251,49 +311,49 @@ public void PushAll32(uint eax, uint ecx, uint edx, uint ebx, uint esp, uint ebp /// /// Pops all 8 general-purpose 16-bit registers (POPA order: DI, SI, BP, skip SP, BX, DX, CX, AX). /// Each slot is read individually; if a slot raises #SS, earlier register assignments persist - /// while SP is left at its original value (matches 80386 partial-pop fault semantics). + /// while the stack pointer is left at its original value (matches 80386 partial-pop fault semantics). /// public void PopAll16() { - ushort offset = _state.SP; - _state.DI = _memory.UInt16[_state.SS, offset, SegmentAccessKind.Stack]; offset = (ushort)(offset + 2); - _state.SI = _memory.UInt16[_state.SS, offset, SegmentAccessKind.Stack]; offset = (ushort)(offset + 2); - _state.BP = _memory.UInt16[_state.SS, offset, SegmentAccessKind.Stack]; offset = (ushort)(offset + 2); - _memory.Mmu.CheckAccess(_state.SS, offset, 2, SegmentAccessKind.Stack); offset = (ushort)(offset + 2); // skip SP slot - _state.BX = _memory.UInt16[_state.SS, offset, SegmentAccessKind.Stack]; offset = (ushort)(offset + 2); - _state.DX = _memory.UInt16[_state.SS, offset, SegmentAccessKind.Stack]; offset = (ushort)(offset + 2); - _state.CX = _memory.UInt16[_state.SS, offset, SegmentAccessKind.Stack]; offset = (ushort)(offset + 2); - _state.AX = _memory.UInt16[_state.SS, offset, SegmentAccessKind.Stack]; offset = (ushort)(offset + 2); - _state.SP = offset; + uint offset = StackPointer; + _state.DI = _memory.UInt16[_state.SS, offset, SegmentAccessKind.Stack]; offset = MaskAddress(offset + 2); + _state.SI = _memory.UInt16[_state.SS, offset, SegmentAccessKind.Stack]; offset = MaskAddress(offset + 2); + _state.BP = _memory.UInt16[_state.SS, offset, SegmentAccessKind.Stack]; offset = MaskAddress(offset + 2); + _memory.Mmu.CheckAccess(_state.SS, offset, 2, SegmentAccessKind.Stack, isWrite: false); offset = MaskAddress(offset + 2); // skip SP slot + _state.BX = _memory.UInt16[_state.SS, offset, SegmentAccessKind.Stack]; offset = MaskAddress(offset + 2); + _state.DX = _memory.UInt16[_state.SS, offset, SegmentAccessKind.Stack]; offset = MaskAddress(offset + 2); + _state.CX = _memory.UInt16[_state.SS, offset, SegmentAccessKind.Stack]; offset = MaskAddress(offset + 2); + _state.AX = _memory.UInt16[_state.SS, offset, SegmentAccessKind.Stack]; offset = MaskAddress(offset + 2); + StackPointer = offset; } /// /// Pops all 8 general-purpose 32-bit registers (POPAD order: EDI, ESI, EBP, skip ESP, EBX, EDX, ECX, EAX). /// Each slot is read individually; if a slot raises #SS, earlier register assignments persist - /// while SP is left at its original value (matches 80386 partial-pop fault semantics). - /// The ESP slot's upper 16 bits are preserved while the lower 16 bits come from the final SP. + /// while the stack pointer is left at its original value (matches 80386 partial-pop fault semantics). + /// The ESP slot is advanced past without being popped into the register, but its upper 16 bits are + /// folded back into ESP (matching real hardware: POPAD never changes the high word of ESP, only the + /// low word advances past the 8 slots). /// public void PopAll32() { - ushort originalSp = _state.SP; - ushort offset = originalSp; - _state.EDI = _memory.UInt32[_state.SS, offset, SegmentAccessKind.Stack]; offset = (ushort)(offset + 4); - _state.ESI = _memory.UInt32[_state.SS, offset, SegmentAccessKind.Stack]; offset = (ushort)(offset + 4); - _state.EBP = _memory.UInt32[_state.SS, offset, SegmentAccessKind.Stack]; offset = (ushort)(offset + 4); - uint espSlot = _memory.UInt32[_state.SS, offset, SegmentAccessKind.Stack]; offset = (ushort)(offset + 4); - _state.EBX = _memory.UInt32[_state.SS, offset, SegmentAccessKind.Stack]; offset = (ushort)(offset + 4); - _state.EDX = _memory.UInt32[_state.SS, offset, SegmentAccessKind.Stack]; offset = (ushort)(offset + 4); - _state.ECX = _memory.UInt32[_state.SS, offset, SegmentAccessKind.Stack]; offset = (ushort)(offset + 4); - _state.EAX = _memory.UInt32[_state.SS, offset, SegmentAccessKind.Stack]; offset = (ushort)(offset + 4); - // ESP: preserve high word from the slot, low word is the new SP + uint offset = StackPointer; + _state.EDI = _memory.UInt32[_state.SS, offset, SegmentAccessKind.Stack]; offset = MaskAddress(offset + 4); + _state.ESI = _memory.UInt32[_state.SS, offset, SegmentAccessKind.Stack]; offset = MaskAddress(offset + 4); + _state.EBP = _memory.UInt32[_state.SS, offset, SegmentAccessKind.Stack]; offset = MaskAddress(offset + 4); + uint espSlot = _memory.UInt32[_state.SS, offset, SegmentAccessKind.Stack]; offset = MaskAddress(offset + 4); + _state.EBX = _memory.UInt32[_state.SS, offset, SegmentAccessKind.Stack]; offset = MaskAddress(offset + 4); + _state.EDX = _memory.UInt32[_state.SS, offset, SegmentAccessKind.Stack]; offset = MaskAddress(offset + 4); + _state.ECX = _memory.UInt32[_state.SS, offset, SegmentAccessKind.Stack]; offset = MaskAddress(offset + 4); + _state.EAX = _memory.UInt32[_state.SS, offset, SegmentAccessKind.Stack]; offset = MaskAddress(offset + 4); _state.ESP = (espSlot & 0xFFFF0000u) | offset; } - + /// /// Peeks a SegmentedAddress value from the stack /// /// The offset from the stack top /// The value in memory. public SegmentedAddress PeekSegmentedAddress(int index) { - ushort offset = (ushort)(_state.SP + index); + uint offset = OffsetStackPointer(index); return _memory.SegmentedAddress16[_state.SS, offset, SegmentAccessKind.Stack]; } @@ -303,10 +363,10 @@ public SegmentedAddress PeekSegmentedAddress(int index) { /// The offset from the stack top /// The value to store in memory. public void PokeSegmentedAddress(int index, SegmentedAddress value) { - ushort offset = (ushort)(_state.SP + index); + uint offset = OffsetStackPointer(index); ValidateStackAccess(offset, 4); - _memory.WriteUInt16Segmented(_state.SS, offset, value.Offset); - _memory.WriteUInt16Segmented(_state.SS, (ushort)(offset + 2), value.Segment); + _memory.UInt16[_state.SS, offset, SegmentAccessKind.Stack] = value.Offset; + _memory.UInt16[_state.SS, MaskAddress(offset + 2), SegmentAccessKind.Stack] = value.Segment; } /// @@ -314,19 +374,19 @@ public void PokeSegmentedAddress(int index, SegmentedAddress value) { /// /// The value retrieved from the stack, therefore read from memory public SegmentedAddress PopSegmentedAddress() { - SegmentedAddress res = _memory.SegmentedAddress16[_state.SS, _state.SP, SegmentAccessKind.Stack]; - _state.SP = (ushort)(_state.SP + 4); + SegmentedAddress res = _memory.SegmentedAddress16[_state.SS, StackPointer, SegmentAccessKind.Stack]; + StackPointer = OffsetStackPointer(4); return res; } - + /// /// Pops a SegmentedAddress32 value from the stack. /// The indexer performs two separate 4-byte MMU checks matching hardware's per-pop semantics. /// /// The value retrieved from the stack, therefore read from memory public SegmentedAddress32 PopSegmentedAddress32() { - SegmentedAddress32 res = _memory.SegmentedAddress32[_state.SS, _state.SP, SegmentAccessKind.Stack]; - _state.SP = (ushort)(_state.SP + 8); + SegmentedAddress32 res = _memory.SegmentedAddress32[_state.SS, StackPointer, SegmentAccessKind.Stack]; + StackPointer = OffsetStackPointer(8); return res; } @@ -343,11 +403,11 @@ public SegmentedAddress PopInterruptPointer32() { /// /// The value pushed onto the stack, therefore stored in memory. public void PushSegmentedAddress(SegmentedAddress value) { - ushort newSp = (ushort)(_state.SP - 4); + uint newSp = OffsetStackPointer(-4); ValidateStackAccess(newSp, 4); - _memory.WriteUInt16Segmented(_state.SS, newSp, value.Offset); - _memory.WriteUInt16Segmented(_state.SS, (ushort)(newSp + 2), value.Segment); - _state.SP = newSp; + _memory.UInt16[_state.SS, newSp, SegmentAccessKind.Stack] = value.Offset; + _memory.UInt16[_state.SS, MaskAddress(newSp + 2), SegmentAccessKind.Stack] = value.Segment; + StackPointer = newSp; } /// @@ -355,16 +415,16 @@ public void PushSegmentedAddress(SegmentedAddress value) { /// /// The 32-bit segmented address to push. public void PushFarPointer32(SegmentedAddress32 value) { - ushort newSp = (ushort)(_state.SP - 8); + uint newSp = OffsetStackPointer(-8); ValidateStackAccess(newSp, 8); - _memory.WriteUInt32Segmented(_state.SS, newSp, value.Offset); - _memory.WriteUInt16Segmented(_state.SS, (ushort)(newSp + 4), value.Segment); - _memory.WriteUInt16Segmented(_state.SS, (ushort)(newSp + 6), 0); - _state.SP = newSp; + _memory.UInt32[_state.SS, newSp, SegmentAccessKind.Stack] = value.Offset; + _memory.UInt16[_state.SS, MaskAddress(newSp + 4), SegmentAccessKind.Stack] = value.Segment; + _memory.UInt16[_state.SS, MaskAddress(newSp + 6), SegmentAccessKind.Stack] = 0; + StackPointer = newSp; } - private void ValidateStackAccess(ushort offset, uint accessSizeBytes) { - _memory.Mmu.CheckAccess(_state.SS, offset, accessSizeBytes, SegmentAccessKind.Stack); + private void ValidateStackAccess(uint offset, uint accessSizeBytes) { + _memory.Mmu.CheckAccess(_state.SS, offset, accessSizeBytes, SegmentAccessKind.Stack, isWrite: true); } /// @@ -372,9 +432,96 @@ private void ValidateStackAccess(ushort offset, uint accessSizeBytes) { /// /// The number of bytes to pop. The Stack Pointer Register will be incremented by this value public void Discard(int numberOfBytesToPop) { - _state.SP = (ushort)(numberOfBytesToPop + _state.SP); + StackPointer = OffsetStackPointer(numberOfBytesToPop); } + /// + /// ENTER: creates a nested stack frame. Pushes the current frame pointer, then - for a nesting + /// level above 0 - copies -1 additional frame pointers from the enclosing + /// frames followed by the new frame pointer itself, before allocating + /// bytes of dynamic storage. + /// Two independent axes control this instruction, and must not be conflated: + /// - The stack's own address width (SS's D/B bit, via ) governs + /// how the frame-pointer CHAIN-WALK addresses are computed/wrapped (BP-based 16-bit addressing + /// vs EBP-based 32-bit addressing), and how the new frame pointer value itself is formed: when + /// the stack is 16-bit, the eventual BP writeback only ever touches BP's 16 bits on real hardware, + /// so the untouched upper half of EBP must be folded back into the pushed/stored frame-pointer + /// value everywhere it is used (chain copies and the register writeback alike) - resolved fresh + /// every call since SS can differ between calls to the same code address. + /// - The instruction's operand size (, safe to fix at parse time + /// since it comes from CS) governs only the WIDTH of the data pushed/copied on the stack (2 vs 4 + /// bytes) - independent of the stack's address width. + /// The stack pointer and frame-pointer register are committed only after the storage-allocation + /// validation succeeds, so a fault leaves both unchanged (matching real hardware's atomic fault semantics). + /// + public void Enter(ushort storageSize, byte level, bool operandSize32) { + level = (byte)(level & 0x1F); + int pointerSize = operandSize32 ? 4 : 2; + + uint oldBaseValue = operandSize32 ? _state.EBP : _state.BP; + uint newSp = OffsetStackPointer(-pointerSize); + WriteFrameValue(newSp, oldBaseValue, operandSize32); + + // The frame-pointer register writeback width follows the stack's own address width: in real + // (16-bit-default) mode ENTER writes the narrow 16-bit BP, zeroing EBP's upper half (matching + // real hardware, where only BP's 16 bits are affected); in 32-bit mode it writes the full EBP. + // The value stored on the stack (and used for chain copies) is always the new stack address. + uint newFrameAddress = newSp; + + uint sp = newSp; + if (level > 0) { + uint chainAddress = StackAddressIs32Bit ? _state.EBP : _state.BP; + for (int i = 1; i < level; i++) { + chainAddress = MaskAddress(chainAddress - (uint)pointerSize); + sp = OffsetStackPointerFrom(sp, -pointerSize); + WriteFrameValue(sp, ReadFrameValue(chainAddress, operandSize32), operandSize32); + } + sp = OffsetStackPointerFrom(sp, -pointerSize); + WriteFrameValue(sp, newFrameAddress, operandSize32); + } + + // ENTER reserves storageSize bytes of dynamic storage without writing to it, but real hardware + // still validates that a write at the FINAL stack pointer (after this reservation) would + // succeed - raising the same #PF/#GP/#SS a later access there would, even though no data is + // actually stored at that address by ENTER itself. + uint finalSp = OffsetStackPointerFrom(sp, -storageSize); + _memory.Mmu.CheckAccess(_state.SS, finalSp, 1, SegmentAccessKind.Stack, isWrite: true); + _memory.Mmu.TranslateAddress(_state.SS, finalSp, isWrite: true); + + StackPointer = finalSp; + if (StackAddressIs32Bit) { + _state.EBP = newFrameAddress; + } else { + _state.BP = (ushort)newFrameAddress; + if (operandSize32) { + // 16-bit stack, 32-bit operand size: zero the upper half of EBP. + _state.EBP = _state.BP; + } else { + // 16-bit stack, 16-bit operand size: preserve the upper half of EBP. + _state.EBP = (_state.EBP & 0xFFFF0000u) | _state.BP; + } + } + } + + /// + /// Computes a stack pointer offset from an explicit base value (rather than the live + /// ), wrapped to the current stack address width. + /// + private uint OffsetStackPointerFrom(uint baseValue, int delta) => MaskAddress(unchecked((uint)((int)baseValue + delta))); + + private uint ReadFrameValue(uint offset, bool operandSize32) { + return operandSize32 ? _memory.UInt32[_state.SS, offset, SegmentAccessKind.Stack] : _memory.UInt16[_state.SS, offset, SegmentAccessKind.Stack]; + } + + private void WriteFrameValue(uint offset, uint value, bool operandSize32) { + if (operandSize32) { + _memory.UInt32[_state.SS, offset, SegmentAccessKind.Stack] = value; + } else { + _memory.UInt16[_state.SS, offset, SegmentAccessKind.Stack] = (ushort)value; + } + } + + /// /// Sets the flag on the interrupt stack, which is at SS:SP+4
/// The interrupt stack is a special stack used to store the state of the processor when an interrupt occurs.
@@ -384,7 +531,7 @@ public void Discard(int numberOfBytesToPop) { /// A boolean that determines whether the bits specified by the flagMask should be set (if true) or cleared (if false). public void SetFlagOnInterruptStack(int flagMask, bool flagValue) { int value = Peek16(4); - + if (flagValue) { value |= flagMask; } else { diff --git a/src/Spice86.Core/Emulator/CPU/State.cs b/src/Spice86.Core/Emulator/CPU/State.cs index 864d57f16b..cb0bdf336e 100644 --- a/src/Spice86.Core/Emulator/CPU/State.cs +++ b/src/Spice86.Core/Emulator/CPU/State.cs @@ -69,17 +69,17 @@ public class State(CpuModel cpuModel) { /// Gets or sets the Base Register High Byte ///
public byte BH { get => GeneralRegisters.UInt8High[(uint)RegisterIndex.BxIndex]; set => GeneralRegisters.UInt8High[(uint)RegisterIndex.BxIndex] = value; } - + /// /// Gets or sets the Base Register Low Byte /// public byte BL { get => GeneralRegisters.UInt8Low[(uint)RegisterIndex.BxIndex]; set => GeneralRegisters.UInt8Low[(uint)RegisterIndex.BxIndex] = value; } - + /// /// Gets or sets the Base Register First Word /// public ushort BX { get => GeneralRegisters.UInt16[(uint)RegisterIndex.BxIndex]; set => GeneralRegisters.UInt16[(uint)RegisterIndex.BxIndex] = value; } - + /// /// Gets or sets the Extended Base general purpose register /// @@ -122,7 +122,7 @@ public class State(CpuModel cpuModel) { /// Gets or sets the word value of the Data general purpose register. /// public ushort DX { get => GeneralRegisters.UInt16[(uint)RegisterIndex.DxIndex]; set => GeneralRegisters.UInt16[(uint)RegisterIndex.DxIndex] = value; } - + /// /// Extended Data general purpose register. /// @@ -135,7 +135,7 @@ public class State(CpuModel cpuModel) { /// Gets or sets the word value of the Destination Index general purpose register. /// public ushort DI { get => GeneralRegisters.UInt16[(uint)RegisterIndex.DiIndex]; set => GeneralRegisters.UInt16[(uint)RegisterIndex.DiIndex] = value; } - + /// /// Extended Destination Index general purpose register. /// @@ -214,9 +214,19 @@ public class State(CpuModel cpuModel) { public ushort SS { get => SegmentRegisters.UInt16[(uint)SegmentRegisterIndex.SsIndex]; set => SegmentRegisters.UInt16[(uint)SegmentRegisterIndex.SsIndex] = value; } /// - /// Gets or sets the Instruction Pointer segment register. + /// Gets or sets the low 16 bits of the instruction pointer (a view over , mirroring + /// the AX/EAX relationship): the authoritative value for real mode and 16-bit-default protected-mode + /// code segments. /// - public ushort IP { get; set; } + public ushort IP { get => (ushort)EIP; set => EIP = (EIP & 0xFFFF_0000) | value; } + + /// + /// Gets or sets the full 32-bit instruction pointer. Authoritative once a 32-bit-default code + /// segment (CS descriptor D/B bit set) is executing; real mode and 16-bit segments never set bits + /// 16-31, so alone is equivalent for them. + /// + public uint EIP { get; set; } + /// /// Contains the flags of the CPU. This is the flags register. @@ -253,22 +263,22 @@ public class State(CpuModel cpuModel) { /// /// public bool TrapFlag { get => Flags.GetFlag(Flags.Trap); set => Flags.SetFlag(Flags.Trap, value); } - + /// /// Gets or sets the sign flag. Set equal to high-order bit of result (0 is positive, 1 if negative). /// public bool SignFlag { get => Flags.GetFlag(Flags.Sign); set => Flags.SetFlag(Flags.Sign, value); } - + /// /// Gets or sets the value of the Zero Flag. Set if result is zero; cleared otherwise. /// public bool ZeroFlag { get => Flags.GetFlag(Flags.Zero); set => Flags.SetFlag(Flags.Zero, value); } - + /// /// Gets or sets the value of the Auxiliary Flag. Set if there is a carry from bit 3 to bit 4 of the result; cleared otherwise.
///
public bool AuxiliaryFlag { get => Flags.GetFlag(Flags.Auxiliary); set => Flags.SetFlag(Flags.Auxiliary, value); } - + /// /// Gets or sets the value of the Parity Flag.
Set if low-order eight bits of result contain an even number of 1 bits; cleared otherwise. ///
@@ -325,7 +335,7 @@ public SegmentedAddress IpSegmentedAddress { /// /// The physical address of the stack in memory /// - public uint StackPhysicalAddress => MemoryUtils.ToPhysicalAddress(SS, SP); + public uint StackPhysicalAddress => MemoryUtils.ToPhysicalAddress(SS, SP); /// /// The CPU registers @@ -338,6 +348,68 @@ public SegmentedAddress IpSegmentedAddress { /// public SegmentRegisters SegmentRegisters { get; } = new(); + /// + /// The hidden descriptor cache (base, limit, access rights) loaded for each segment register. + /// + public SegmentDescriptorCaches SegmentDescriptorCaches { get; } = new(); + + /// + /// The 386 control registers (CR0-CR4). + /// + public ControlRegisters ControlRegisters { get; } = new(); + + /// + /// The Global Descriptor Table register, loaded by LGDT and read back by SGDT. + /// + public DescriptorTableRegister Gdtr { get; } = new(); + + /// + /// The Interrupt Descriptor Table register, loaded by LIDT and read back by SIDT. + /// + public DescriptorTableRegister Idtr { get; } = new(); + + /// + /// The Local Descriptor Table register, loaded by LLDT and read back by SLDT. + /// + public SystemSegmentRegister Ldtr { get; } = new(); + + /// + /// The Task Register, loaded by LTR and read back by STR. + /// + public SystemSegmentRegister Tr { get; } = new(); + + /// + /// The addressing/execution mode the CPU is currently operating in, derived from + /// and the EFLAGS VM bit. + /// + public CpuMode CpuMode { + get { + if (!ControlRegisters.ProtectionEnable) { + return CpuMode.Real; + } + return Flags.GetFlag(Flags.Virtual8086Mode) ? CpuMode.Virtual8086 : CpuMode.Protected; + } + } + + /// + /// The Current Privilege Level: 0 outside protected mode, the CS selector's RPL in protected mode + /// (kept in sync with CPL by every control transfer), or 3 in Virtual-8086 mode. + /// + public byte Cpl => CpuMode switch { + CpuMode.Real => 0, + CpuMode.Virtual8086 => 3, + _ => new SegmentSelector(CS).RequestedPrivilegeLevel + }; + + /// + /// Gets or sets the I/O Privilege Level (EFLAGS bits 12-13): the minimum CPL allowed to execute + /// `IN`/`OUT`/`CLI`/`STI` (and, in V86 mode, the level always required to be 3 for those to succeed). + /// + public byte IoPrivilegeLevel { + get => (byte)((Flags.FlagRegister & Flags.IoPrivilegeLevelMask) >> 12); + set => Flags.FlagRegister = (Flags.FlagRegister & ~Flags.IoPrivilegeLevelMask) | (((uint)value & 0b11) << 12); + } + /// /// Gets or sets a value indicating whether the CPU is running. /// diff --git a/src/Spice86.Core/Emulator/InterruptHandlers/Dos/Xms/ExtendedMemoryManager.cs b/src/Spice86.Core/Emulator/InterruptHandlers/Dos/Xms/ExtendedMemoryManager.cs index 3e4698a0ca..466a84bce0 100644 --- a/src/Spice86.Core/Emulator/InterruptHandlers/Dos/Xms/ExtendedMemoryManager.cs +++ b/src/Spice86.Core/Emulator/InterruptHandlers/Dos/Xms/ExtendedMemoryManager.cs @@ -170,11 +170,6 @@ public sealed class ExtendedMemoryManager : IVirtualDevice { public const ushort XmsMemorySize = (ushort)((16384 * 1024 - A20Gate.StartOfHighMemoryArea) / 1024); - /// - /// XMS plain old memory. - /// - public Ram XmsRam { get; private set; } = new(XmsMemorySize * 1024); - /// /// DOS Device Driver Name. /// @@ -262,7 +257,7 @@ public ExtendedMemoryManager(IMemory memory, State state, A20Gate a20Gate, // Initialize XMS memory as a single free block if (TryGetFreeHandle(out ushort? handle)) { _xmsBlocksLinkedList.AddLast(new XmsBlock(handle.Value, offset: 0, - XmsRam.Size, free: true)); + XmsMemorySize * 1024u, free: true)); } _canChangeA20Line = !a20Gate.IsEnabled; } @@ -298,6 +293,13 @@ public ExtendedMemoryManager(IMemory memory, State state, A20Gate a20Gate, ///
public IReadOnlyList BlocksSnapshot => _xmsBlocksLinkedList.ToList(); + /// + /// Reads a slice of bytes from the shared memory bus at a block-relative offset. + /// + /// Offset of the block from . + /// Number of bytes to read. + public IList GetSlice(uint blockOffset, int length) => _memory.GetSlice((int)(XmsBaseAddress + blockOffset), length); + /// /// Gets a snapshot of allocated handles and their lock counts. /// @@ -1196,7 +1198,7 @@ public void MoveExtendedMemoryBlock() { _state.BL = (byte)XmsErrorCodes.XmsInvalidLength; return; } - srcBytes = XmsRam.GetSlice((int)(srcBlock.Value.Offset + move.SourceOffset), (int)move.Length); + srcBytes = _memory.GetSlice((int)(XmsBaseAddress + srcBlock.Value.Offset + move.SourceOffset), (int)move.Length); } // Determine destination @@ -1228,7 +1230,7 @@ public void MoveExtendedMemoryBlock() { _state.BL = (byte)XmsErrorCodes.XmsInvalidLength; return; } - dstBytes = XmsRam.GetSlice((int)(dstBlock.Value.Offset + move.DestOffset), (int)move.Length); + dstBytes = _memory.GetSlice((int)(XmsBaseAddress + dstBlock.Value.Offset + move.DestOffset), (int)move.Length); } // Check for overlap if both source and destination are in the same XMS block diff --git a/src/Spice86.Core/Emulator/LoadableFile/ExecutableFileLoader.cs b/src/Spice86.Core/Emulator/LoadableFile/ExecutableFileLoader.cs index 7666f3e30a..90b8145022 100644 --- a/src/Spice86.Core/Emulator/LoadableFile/ExecutableFileLoader.cs +++ b/src/Spice86.Core/Emulator/LoadableFile/ExecutableFileLoader.cs @@ -2,6 +2,7 @@ namespace Spice86.Core.Emulator.LoadableFile; using Spice86.Core.Emulator.CPU; +using Spice86.Core.Emulator.CPU.Registers; using Spice86.Core.Emulator.Memory; using Spice86.Shared.Interfaces; using Spice86.Shared.Utils; @@ -66,6 +67,9 @@ protected byte[] ReadFile(string file) { protected void SetEntryPoint(ushort cs, ushort ip) { _state.CS = cs; _state.IP = ip; + // Real hardware always keeps CS's hidden descriptor cache in sync with its raw value; + // loaders set CS directly rather than through a segment-load instruction, so refresh it here. + _state.SegmentDescriptorCaches[SegmentRegisterIndex.CsIndex] = SegmentDescriptorCache.CreateRealMode(cs); if (_loggerService.IsEnabled(LogLevel.Trace)) { _loggerService.LogTrace("Program entry point is {ProgramEntry}", ConvertUtils.ToSegmentedAddressRepresentation(cs, ip)); } diff --git a/src/Spice86.Core/Emulator/Mcp/EmulatorMcpTools.cs b/src/Spice86.Core/Emulator/Mcp/EmulatorMcpTools.cs index 9658cd4590..afc56462e0 100644 --- a/src/Spice86.Core/Emulator/Mcp/EmulatorMcpTools.cs +++ b/src/Spice86.Core/Emulator/Mcp/EmulatorMcpTools.cs @@ -1757,7 +1757,7 @@ public CallToolResult ReadXmsMemory(int handle, uint offset, int length) { throw new InvalidOperationException("Read would exceed block boundary"); } - IList data = _services.XmsManager.XmsRam.GetSlice((int)(xmsBlock.Value.Offset + offset), length); + IList data = _services.XmsManager.GetSlice(xmsBlock.Value.Offset + offset, length); byte[] dataArray = new byte[data.Count]; data.CopyTo(dataArray, 0); @@ -1802,7 +1802,7 @@ public CallToolResult SearchXmsMemory(int handle, [StringSyntax("Hexadecimal")] if (_services.XmsManager == null) { throw new InvalidOperationException("XMS is not enabled"); } - IList data = _services.XmsManager.XmsRam.GetSlice((int)(block.Offset + startOffset), searchLength); + IList data = _services.XmsManager.GetSlice(block.Offset + startOffset, searchLength); uint[] matches = SearchArray(data, needle, startOffset, limit); return new { Handle = handle, diff --git a/src/Spice86.Core/Emulator/Memory/A20Gate.cs b/src/Spice86.Core/Emulator/Memory/A20Gate.cs index 9b21f78324..3bb3634c54 100644 --- a/src/Spice86.Core/Emulator/Memory/A20Gate.cs +++ b/src/Spice86.Core/Emulator/Memory/A20Gate.cs @@ -45,7 +45,7 @@ public A20Gate(bool enabled = true) { /// The memory address that is to be accessed. /// The transformed address if the 20th address line is silenced. The same address if it isn't. [Pure] - public int TransformAddress(int address) => (int) (address & AddressMask); + public int TransformAddress(int address) => (int)(address & AddressMask); /// /// Calculates the new memory address with the 20th address line silenced.
@@ -57,14 +57,18 @@ public A20Gate(bool enabled = true) { public uint TransformAddress(uint address) => (address & AddressMask); /// - /// The value for the when is false + /// The value for the when is false: + /// clears bit 20 (0x100000) specifically, matching real 80286+ A20 gate hardware - which gates + /// exactly one address line, not a whole range - while leaving every other bit unaffected. /// - public const uint DisabledAddressMask = 0xFFFFF; + public const uint DisabledAddressMask = ~0x100000u; /// - /// The value for the when is true + /// The value for the when is true: no + /// masking at all, matching real hardware where an enabled A20 line simply lets bit 20 (and every + /// other address bit) propagate normally, with no artificial ceiling on the address space. /// - public const uint EnabledAddressMask = 0x1FFFFF; + public const uint EnabledAddressMask = 0xFFFFFFFF; /// /// The address mask used over memory accesses. diff --git a/src/Spice86.Core/Emulator/Memory/Indexer/MemoryIndexer.cs b/src/Spice86.Core/Emulator/Memory/Indexer/MemoryIndexer.cs index 2eb13a5b99..b8767da8d0 100644 --- a/src/Spice86.Core/Emulator/Memory/Indexer/MemoryIndexer.cs +++ b/src/Spice86.Core/Emulator/Memory/Indexer/MemoryIndexer.cs @@ -55,11 +55,11 @@ protected MemoryIndexer(IMmu mmu, uint accessSize) { /// The semantic access kind. public virtual T this[ushort segment, uint offset, SegmentAccessKind accessKind] { get { - Mmu.CheckAccess(segment, offset, _accessSize, accessKind); + Mmu.CheckAccess(segment, offset, _accessSize, accessKind, isWrite: false); return ReadSegmented(segment, offset); } set { - Mmu.CheckAccess(segment, offset, _accessSize, accessKind); + Mmu.CheckAccess(segment, offset, _accessSize, accessKind, isWrite: true); WriteSegmented(segment, offset, value); } } diff --git a/src/Spice86.Core/Emulator/Memory/Indexer/SegmentedAddress32Indexer.cs b/src/Spice86.Core/Emulator/Memory/Indexer/SegmentedAddress32Indexer.cs index 4df6975c39..fe642291d8 100644 --- a/src/Spice86.Core/Emulator/Memory/Indexer/SegmentedAddress32Indexer.cs +++ b/src/Spice86.Core/Emulator/Memory/Indexer/SegmentedAddress32Indexer.cs @@ -47,15 +47,15 @@ public override SegmentedAddress32 this[uint address] { /// public override SegmentedAddress32 this[ushort segment, uint offset, SegmentAccessKind accessKind] { get { - Mmu.CheckAccess(segment, offset, 4, accessKind); + Mmu.CheckAccess(segment, offset, 4, accessKind, isWrite: false); // Cast to ushort: models SP register wrapping between the two hardware pops. // Each pop checks its own 4-byte span at the wrapped 16-bit offset. - Mmu.CheckAccess(segment, (ushort)(offset + 4), 4, accessKind); + Mmu.CheckAccess(segment, (ushort)(offset + 4), 4, accessKind, isWrite: false); return ReadSegmented(segment, offset); } set { - Mmu.CheckAccess(segment, offset, 4, accessKind); - Mmu.CheckAccess(segment, (ushort)(offset + 4), 4, accessKind); + Mmu.CheckAccess(segment, offset, 4, accessKind, isWrite: true); + Mmu.CheckAccess(segment, (ushort)(offset + 4), 4, accessKind, isWrite: true); WriteSegmented(segment, offset, value); } } @@ -72,7 +72,7 @@ internal override void WriteSegmented(ushort segment, uint offset, SegmentedAddr _uInt32Indexer.WriteSegmented(segment, offset, value.Offset); _uInt16Indexer.WriteSegmented(segment, offset + 4u, value.Segment); } - + /// public override int Count => _uInt16Indexer.Count / 3; } \ No newline at end of file diff --git a/src/Spice86.Core/Emulator/Memory/Indexer/UInt16BigEndianIndexer.cs b/src/Spice86.Core/Emulator/Memory/Indexer/UInt16BigEndianIndexer.cs index 4ddf473779..8aba1b4706 100644 --- a/src/Spice86.Core/Emulator/Memory/Indexer/UInt16BigEndianIndexer.cs +++ b/src/Spice86.Core/Emulator/Memory/Indexer/UInt16BigEndianIndexer.cs @@ -30,19 +30,19 @@ public override ushort this[uint address] { /// internal override ushort ReadSegmented(ushort segment, uint offset) { - uint address1 = Mmu.TranslateAddress(segment, offset); - uint address2 = Mmu.TranslateAddress(segment, offset + 1); + uint address1 = Mmu.TranslateAddress(segment, offset, isWrite: false); + uint address2 = Mmu.TranslateAddress(segment, offset + 1, isWrite: false); return (ushort)(_byteReaderWriter[address2] | _byteReaderWriter[address1] << 8); } /// internal override void WriteSegmented(ushort segment, uint offset, ushort value) { - uint address1 = Mmu.TranslateAddress(segment, offset); - uint address2 = Mmu.TranslateAddress(segment, offset + 1); + uint address1 = Mmu.TranslateAddress(segment, offset, isWrite: true); + uint address2 = Mmu.TranslateAddress(segment, offset + 1, isWrite: true); _byteReaderWriter[address1] = (byte)(value >> 8); _byteReaderWriter[address2] = (byte)value; } - + /// public override int Count => _byteReaderWriter.Length / 2; } \ No newline at end of file diff --git a/src/Spice86.Core/Emulator/Memory/Indexer/UInt16Indexer.cs b/src/Spice86.Core/Emulator/Memory/Indexer/UInt16Indexer.cs index 1a8485aeb8..7bc06e3531 100644 --- a/src/Spice86.Core/Emulator/Memory/Indexer/UInt16Indexer.cs +++ b/src/Spice86.Core/Emulator/Memory/Indexer/UInt16Indexer.cs @@ -30,19 +30,19 @@ public override ushort this[uint address] { /// internal override ushort ReadSegmented(ushort segment, uint offset) { - uint address1 = Mmu.TranslateAddress(segment, offset); - uint address2 = Mmu.TranslateAddress(segment, offset + 1); + uint address1 = Mmu.TranslateAddress(segment, offset, isWrite: false); + uint address2 = Mmu.TranslateAddress(segment, offset + 1, isWrite: false); return (ushort)(_byteReaderWriter[address1] | _byteReaderWriter[address2] << 8); } /// internal override void WriteSegmented(ushort segment, uint offset, ushort value) { - uint address1 = Mmu.TranslateAddress(segment, offset); - uint address2 = Mmu.TranslateAddress(segment, offset + 1); + uint address1 = Mmu.TranslateAddress(segment, offset, isWrite: true); + uint address2 = Mmu.TranslateAddress(segment, offset + 1, isWrite: true); _byteReaderWriter[address1] = (byte)value; _byteReaderWriter[address2] = (byte)(value >> 8); } - + /// public override int Count => _byteReaderWriter.Length / 2; } \ No newline at end of file diff --git a/src/Spice86.Core/Emulator/Memory/Indexer/UInt32Indexer.cs b/src/Spice86.Core/Emulator/Memory/Indexer/UInt32Indexer.cs index 74c0517f7d..92dbb99248 100644 --- a/src/Spice86.Core/Emulator/Memory/Indexer/UInt32Indexer.cs +++ b/src/Spice86.Core/Emulator/Memory/Indexer/UInt32Indexer.cs @@ -32,26 +32,26 @@ public override uint this[uint address] { /// internal override uint ReadSegmented(ushort segment, uint offset) { - uint address1 = Mmu.TranslateAddress(segment, offset); - uint address2 = Mmu.TranslateAddress(segment, offset + 1); - uint address3 = Mmu.TranslateAddress(segment, offset + 2); - uint address4 = Mmu.TranslateAddress(segment, offset + 3); + uint address1 = Mmu.TranslateAddress(segment, offset, isWrite: false); + uint address2 = Mmu.TranslateAddress(segment, offset + 1, isWrite: false); + uint address3 = Mmu.TranslateAddress(segment, offset + 2, isWrite: false); + uint address4 = Mmu.TranslateAddress(segment, offset + 3, isWrite: false); return (uint)(_byteReaderWriter[address1] | _byteReaderWriter[address2] << 8 | _byteReaderWriter[address3] << 16 | _byteReaderWriter[address4] << 24); } /// internal override void WriteSegmented(ushort segment, uint offset, uint value) { - uint address1 = Mmu.TranslateAddress(segment, offset); - uint address2 = Mmu.TranslateAddress(segment, offset + 1); - uint address3 = Mmu.TranslateAddress(segment, offset + 2); - uint address4 = Mmu.TranslateAddress(segment, offset + 3); + uint address1 = Mmu.TranslateAddress(segment, offset, isWrite: true); + uint address2 = Mmu.TranslateAddress(segment, offset + 1, isWrite: true); + uint address3 = Mmu.TranslateAddress(segment, offset + 2, isWrite: true); + uint address4 = Mmu.TranslateAddress(segment, offset + 3, isWrite: true); _byteReaderWriter[address1] = (byte)value; _byteReaderWriter[address2] = (byte)(value >> 8); _byteReaderWriter[address3] = (byte)(value >> 16); _byteReaderWriter[address4] = (byte)(value >> 24); } - + /// public override int Count => _byteReaderWriter.Length / 4; } \ No newline at end of file diff --git a/src/Spice86.Core/Emulator/Memory/Indexer/UInt8Indexer.cs b/src/Spice86.Core/Emulator/Memory/Indexer/UInt8Indexer.cs index c0275d20b1..a551883cdf 100644 --- a/src/Spice86.Core/Emulator/Memory/Indexer/UInt8Indexer.cs +++ b/src/Spice86.Core/Emulator/Memory/Indexer/UInt8Indexer.cs @@ -26,12 +26,12 @@ public override byte this[uint address] { /// internal override byte ReadSegmented(ushort segment, uint offset) { - return _byteReaderWriter[Mmu.TranslateAddress(segment, offset)]; + return _byteReaderWriter[Mmu.TranslateAddress(segment, offset, isWrite: false)]; } /// internal override void WriteSegmented(ushort segment, uint offset, byte value) { - _byteReaderWriter[Mmu.TranslateAddress(segment, offset)] = value; + _byteReaderWriter[Mmu.TranslateAddress(segment, offset, isWrite: true)] = value; } /// diff --git a/src/Spice86.Core/Emulator/Memory/Memory.cs b/src/Spice86.Core/Emulator/Memory/Memory.cs index 2d9c6ce50c..9e57fd3e51 100644 --- a/src/Spice86.Core/Emulator/Memory/Memory.cs +++ b/src/Spice86.Core/Emulator/Memory/Memory.cs @@ -80,16 +80,16 @@ public void SneakilyWrite(uint address, byte value) { /// public void WriteUInt16Segmented(ushort segment, ushort offset, ushort value) { - this[Mmu.TranslateAddress(segment, offset)] = (byte)value; - this[Mmu.TranslateAddress(segment, (uint)offset + 1u)] = (byte)(value >> 8); + this[Mmu.TranslateAddress(segment, offset, isWrite: true)] = (byte)value; + this[Mmu.TranslateAddress(segment, (uint)offset + 1u, isWrite: true)] = (byte)(value >> 8); } /// public void WriteUInt32Segmented(ushort segment, ushort offset, uint value) { - this[Mmu.TranslateAddress(segment, offset)] = (byte)value; - this[Mmu.TranslateAddress(segment, (uint)offset + 1u)] = (byte)(value >> 8); - this[Mmu.TranslateAddress(segment, (uint)offset + 2u)] = (byte)(value >> 16); - this[Mmu.TranslateAddress(segment, (uint)offset + 3u)] = (byte)(value >> 24); + this[Mmu.TranslateAddress(segment, offset, isWrite: true)] = (byte)value; + this[Mmu.TranslateAddress(segment, (uint)offset + 1u, isWrite: true)] = (byte)(value >> 8); + this[Mmu.TranslateAddress(segment, (uint)offset + 2u, isWrite: true)] = (byte)(value >> 16); + this[Mmu.TranslateAddress(segment, (uint)offset + 3u, isWrite: true)] = (byte)(value >> 24); } /// diff --git a/src/Spice86.Core/Emulator/Memory/Mmu/CpuMmu.cs b/src/Spice86.Core/Emulator/Memory/Mmu/CpuMmu.cs new file mode 100644 index 0000000000..7aae272da9 --- /dev/null +++ b/src/Spice86.Core/Emulator/Memory/Mmu/CpuMmu.cs @@ -0,0 +1,55 @@ +namespace Spice86.Core.Emulator.Memory.Mmu; + +using Spice86.Core.Emulator.CPU; + +/// +/// Resolves segmented memory accesses to the real-mode or descriptor-cache-based MMU. CS is always +/// resolved through its descriptor cache, regardless of the live : real hardware +/// keeps fetching through the CS cache across a CR0.PE transition until the mandatory far jump reloads +/// it, so dispatching CS by the live mode would pick the wrong translation at the exact instant +/// CR0.PE changes. Every other segment register dispatches by as usual, because +/// plenty of code outside instruction execution (BIOS/VGA/DOS setup) writes ES/DS/etc. directly +/// without ever populating their descriptor cache, and real mode never needs one (base is always +/// selector * 16). +/// +public sealed class CpuMmu : IMmu { + private readonly State _state; + private readonly IMmu _realModeMmu; + private readonly IMmu _cachedSegmentMmu; + + /// + /// Initializes a new instance. + /// + /// The CPU state, used to read the current CS value and . + /// The MMU used for non-CS accesses while not in protected mode. + /// The descriptor-cache-based MMU used for CS and for protected-mode accesses. + public CpuMmu(State state, IMmu realModeMmu, IMmu cachedSegmentMmu) { + _state = state; + _realModeMmu = realModeMmu; + _cachedSegmentMmu = cachedSegmentMmu; + } + + private IMmu Resolve(ushort segment) { + if (segment == _state.CS) { + return _cachedSegmentMmu; + } + return _state.CpuMode == CpuMode.Protected ? _cachedSegmentMmu : _realModeMmu; + } + + /// + public void CheckAccess(ushort segment, uint offset, uint length, SegmentAccessKind accessKind, bool isWrite) { + Resolve(segment).CheckAccess(segment, offset, length, accessKind, isWrite); + } + + /// + public uint TranslateAddress(ushort segment, uint offset, bool isWrite) { + return Resolve(segment).TranslateAddress(segment, offset, isWrite); + } + + /// + /// A no-op: paging is applied by the outer that wraps this MMU, not here. + /// + public uint TranslateLinearAddress(uint linearAddress, bool isWrite) { + return linearAddress; + } +} diff --git a/src/Spice86.Core/Emulator/Memory/Mmu/CpuMmuFactory.cs b/src/Spice86.Core/Emulator/Memory/Mmu/CpuMmuFactory.cs new file mode 100644 index 0000000000..54f502a998 --- /dev/null +++ b/src/Spice86.Core/Emulator/Memory/Mmu/CpuMmuFactory.cs @@ -0,0 +1,28 @@ +namespace Spice86.Core.Emulator.Memory.Mmu; + +using Spice86.Core.Emulator.CPU; + +/// +/// Creates the MMU for a CPU model: a plain real-mode MMU for pre-386 models (no descriptor cache +/// concept at all), or a descriptor-cache-based for the 386, which resolves both +/// real- and protected-mode accesses through the segment register's cached descriptor. +/// +public static class CpuMmuFactory { + /// + /// Creates the MMU configured for a CPU model. + /// + /// The configured CPU model. + /// The CPU state, needed to read segment registers and their descriptor caches. + /// The raw memory device backing GDT/LDT reads. + public static IMmu Create(CpuModel cpuModel, State state, IMemoryDevice ram) { + if (cpuModel != CpuModel.INTEL_80386) { + return RealModeMmuFactory.FromCpuModel(cpuModel); + } + + IMmu realModeMmu = RealModeMmuFactory.FromCpuModel(cpuModel); + PagingUnit pagingUnit = new(state, ram); + IMmu cachedSegmentMmu = new ProtectedModeMmu386(state, ram, pagingUnit); + IMmu cpuMmu = new CpuMmu(state, realModeMmu, cachedSegmentMmu); + return new PagingMmu(state, cpuMmu, pagingUnit); + } +} diff --git a/src/Spice86.Core/Emulator/Memory/Mmu/IMmu.cs b/src/Spice86.Core/Emulator/Memory/Mmu/IMmu.cs index bd250f6eaa..870fd79a3d 100644 --- a/src/Spice86.Core/Emulator/Memory/Mmu/IMmu.cs +++ b/src/Spice86.Core/Emulator/Memory/Mmu/IMmu.cs @@ -12,13 +12,27 @@ public interface IMmu { /// The effective offset before any truncation. /// The access length in bytes. /// The semantic access kind. - void CheckAccess(ushort segment, uint offset, uint length, SegmentAccessKind accessKind); + /// Whether the access is a write; a write to a non-writable data segment raises #GP. + void CheckAccess(ushort segment, uint offset, uint length, SegmentAccessKind accessKind, bool isWrite); /// /// Translates a segmented byte lane to a physical address. /// /// The segment selector or real-mode segment value. /// The byte-lane offset. + /// Whether this lane is being written rather than read; used by paging to set the PTE Dirty bit and to enforce the Read/Write protection bit. /// The translated physical address. - uint TranslateAddress(ushort segment, uint offset); + uint TranslateAddress(ushort segment, uint offset, bool isWrite); + + /// + /// Translates an already-computed linear address (not a segment:offset pair) to a physical address: + /// used for GDT/LDT/IDT/TSS accesses, whose base addresses are linear rather than segment-relative. + /// Applies paging when it is enabled; a no-op otherwise. Every implementation + /// other than returns unchanged, since + /// paging is applied only by the outermost wrapper in the MMU chain. + /// + /// The linear address to translate. + /// Whether this address is being written rather than read; used by paging to set the PTE Dirty bit and to enforce the Read/Write protection bit. + /// The translated physical address. + uint TranslateLinearAddress(uint linearAddress, bool isWrite); } \ No newline at end of file diff --git a/src/Spice86.Core/Emulator/Memory/Mmu/PagingMmu.cs b/src/Spice86.Core/Emulator/Memory/Mmu/PagingMmu.cs new file mode 100644 index 0000000000..0b7dcf62bf --- /dev/null +++ b/src/Spice86.Core/Emulator/Memory/Mmu/PagingMmu.cs @@ -0,0 +1,44 @@ +namespace Spice86.Core.Emulator.Memory.Mmu; + +using Spice86.Core.Emulator.CPU; + +/// +/// Adds a paging translation stage after segment translation: when +/// is set, the linear address produced by the +/// wrapped MMU's is further translated to a physical address via +/// . Segment-level limit checks () are delegated +/// unchanged, since paging does not affect segment limits. +/// +public sealed class PagingMmu : IMmu { + private readonly State _state; + private readonly IMmu _inner; + private readonly PagingUnit _pagingUnit; + + /// + /// Initializes a new instance. + /// + /// The CPU state, used to read . + /// The segment-translation MMU whose output is treated as the linear address. + /// The page-directory/page-table walker used once paging is enabled. + public PagingMmu(State state, IMmu inner, PagingUnit pagingUnit) { + _state = state; + _inner = inner; + _pagingUnit = pagingUnit; + } + + /// + public void CheckAccess(ushort segment, uint offset, uint length, SegmentAccessKind accessKind, bool isWrite) { + _inner.CheckAccess(segment, offset, length, accessKind, isWrite); + } + + /// + public uint TranslateAddress(ushort segment, uint offset, bool isWrite) { + uint linearAddress = _inner.TranslateAddress(segment, offset, isWrite); + return _state.ControlRegisters.PagingEnable ? _pagingUnit.Translate(linearAddress, isWrite) : linearAddress; + } + + /// + public uint TranslateLinearAddress(uint linearAddress, bool isWrite) { + return _state.ControlRegisters.PagingEnable ? _pagingUnit.Translate(linearAddress, isWrite) : linearAddress; + } +} diff --git a/src/Spice86.Core/Emulator/Memory/Mmu/PagingUnit.cs b/src/Spice86.Core/Emulator/Memory/Mmu/PagingUnit.cs new file mode 100644 index 0000000000..4f52eda5fd --- /dev/null +++ b/src/Spice86.Core/Emulator/Memory/Mmu/PagingUnit.cs @@ -0,0 +1,123 @@ +namespace Spice86.Core.Emulator.Memory.Mmu; + +using Spice86.Core.Emulator.CPU; +using Spice86.Core.Emulator.CPU.Exceptions; + +/// +/// CR3-rooted two-level page-directory/page-table walk (32-bit paging, 4KB pages), translating a +/// linear address to a physical address when is +/// set. Enforces the Present bit and the combined User/Supervisor and Read/Write protection of the +/// page-directory and page-table entry against the current CPL and access kind, matching the 80386's +/// documented combining rules: an access is user-accessible only if BOTH entries are User, and (since +/// this emulator does not implement CR0.WP) only a user-mode write is checked against the Read/Write +/// bit - supervisor writes are always permitted. +/// +public sealed class PagingUnit { + private const uint PresentBit = 0x1; + private const uint WriteBit = 0x2; + private const uint UserSupervisorBit = 0x4; + private const uint AccessedBit = 0x20; + private const uint DirtyBit = 0x40; + private const uint TableAddressMask = 0xFFFF_F000; + private const uint PageOffsetMask = 0xFFF; + + private readonly State _state; + private readonly IMemoryDevice _ram; + + /// + /// Initializes a new instance. + /// + /// The CPU state, used to read CR3, CR2 (written on fault), and CPL. + /// The raw memory device backing page-directory/page-table reads. + public PagingUnit(State state, IMemoryDevice ram) { + _state = state; + _ram = ram; + } + + /// + /// Translates a linear address to a physical address, walking the page directory and page table + /// rooted at CR3. Throws (and sets CR2) on any not-present or + /// protection violation. Sets the Accessed bit on both entries, and the Dirty bit on the PTE when + /// is set, matching the 80386's documented behavior: these bits are only + /// set once the FULL two-level walk succeeds - a fault at either level leaves every entry's + /// Accessed/Dirty bits untouched, even if an earlier level was valid. + /// + /// The linear address to translate. + /// Whether the access is a write, used for the Dirty bit and the Read/Write protection check. + public uint Translate(uint linearAddress, bool isWrite) { + uint pageDirectoryIndex = linearAddress >> 22; + uint pageTableIndex = (linearAddress >> 12) & 0x3FF; + uint pageOffset = linearAddress & PageOffsetMask; + + uint pageDirectoryEntryAddress = (_state.ControlRegisters.Cr3 & TableAddressMask) + pageDirectoryIndex * 4; + uint pageDirectoryEntry = ReadUInt32(pageDirectoryEntryAddress); + EnsurePresent(pageDirectoryEntry, linearAddress, isWrite); + + uint pageTableEntryAddress = (pageDirectoryEntry & TableAddressMask) + pageTableIndex * 4; + uint pageTableEntry = ReadUInt32(pageTableEntryAddress); + EnsurePresent(pageTableEntry, linearAddress, isWrite); + + EnsureProtection(pageDirectoryEntry, pageTableEntry, linearAddress, isWrite); + + MarkAccessed(pageDirectoryEntryAddress, pageDirectoryEntry); + MarkAccessed(pageTableEntryAddress, pageTableEntry); + if (isWrite && (pageTableEntry & DirtyBit) == 0) { + WriteUInt32(pageTableEntryAddress, pageTableEntry | DirtyBit); + } + + return (pageTableEntry & TableAddressMask) + pageOffset; + } + + private void MarkAccessed(uint entryAddress, uint entry) { + if ((entry & AccessedBit) == 0) { + WriteUInt32(entryAddress, entry | AccessedBit); + } + } + + private void EnsurePresent(uint entry, uint linearAddress, bool isWrite) { + if ((entry & PresentBit) == 0) { + throw CreatePageFault(linearAddress, protectionViolation: false, isWrite); + } + } + + private void EnsureProtection(uint pageDirectoryEntry, uint pageTableEntry, uint linearAddress, bool isWrite) { + if (_state.Cpl != 3) { + return; // supervisor accesses ignore U/S and R/W entirely (CR0.WP is not implemented). + } + bool userAccessible = (pageDirectoryEntry & UserSupervisorBit) != 0 && (pageTableEntry & UserSupervisorBit) != 0; + if (!userAccessible) { + throw CreatePageFault(linearAddress, protectionViolation: true, isWrite); + } + bool writable = (pageDirectoryEntry & WriteBit) != 0 && (pageTableEntry & WriteBit) != 0; + if (isWrite && !writable) { + throw CreatePageFault(linearAddress, protectionViolation: true, isWrite); + } + } + + private uint ReadUInt32(uint address) { + return (uint)_ram.Read(address) + | ((uint)_ram.Read(address + 1) << 8) + | ((uint)_ram.Read(address + 2) << 16) + | ((uint)_ram.Read(address + 3) << 24); + } + + private void WriteUInt32(uint address, uint value) { + _ram.Write(address, (byte)value); + _ram.Write(address + 1, (byte)(value >> 8)); + _ram.Write(address + 2, (byte)(value >> 16)); + _ram.Write(address + 3, (byte)(value >> 24)); + } + + private CpuPageFaultException CreatePageFault(uint linearAddress, bool protectionViolation, bool isWrite) { + _state.ControlRegisters.Cr2 = linearAddress; + bool userMode = _state.Cpl == 3; + ushort errorCode = (ushort)((protectionViolation ? 1u : 0u) | (isWrite ? 0b10u : 0u) | (userMode ? 0b100u : 0u)); + if (Environment.GetEnvironmentVariable("SPICE86_TRACE_PAGING") is not null) { + System.IO.Directory.CreateDirectory("tmp"); + System.IO.File.AppendAllText("tmp/paging_trace.txt", + $"PF linear=0x{linearAddress:X8} errorCode=0b{Convert.ToString(errorCode, 2).PadLeft(3, '0')} protectionViolation={protectionViolation} isWrite={isWrite} cpl={_state.Cpl}\n"); + } + string reason = protectionViolation ? "protection violation" : "not present"; + return new CpuPageFaultException($"Page fault at linear address 0x{linearAddress:X8} ({reason})", errorCode); + } +} diff --git a/src/Spice86.Core/Emulator/Memory/Mmu/ProtectedModeMmu386.cs b/src/Spice86.Core/Emulator/Memory/Mmu/ProtectedModeMmu386.cs new file mode 100644 index 0000000000..4866725cbf --- /dev/null +++ b/src/Spice86.Core/Emulator/Memory/Mmu/ProtectedModeMmu386.cs @@ -0,0 +1,94 @@ +namespace Spice86.Core.Emulator.Memory.Mmu; + +using Spice86.Core.Emulator.CPU; +using Spice86.Core.Emulator.CPU.DescriptorTables; +using Spice86.Core.Emulator.CPU.Exceptions; +using Spice86.Core.Emulator.CPU.Registers; + +/// +/// Protected-mode MMU for 386-class CPUs. Translates and validates segmented accesses using the +/// descriptor cache already loaded into the segment register the caller is using, matching real +/// hardware (which caches base/limit/access-rights at segment-load time rather than re-reading the +/// GDT/LDT on every access). +/// +/// +/// callers only pass the raw selector currently held by a segment register, not +/// which register it came from. This MMU recovers the register identity by matching the raw value +/// against the CPU's live segment register contents and uses that register's cache; if two registers +/// coincidentally hold the same selector, using either cache is harmless because both were decoded +/// from the same descriptor unless it was edited between the two loads (an edge case out of scope +/// here). If no register currently holds a matching selector — e.g. a stale/literal value used right +/// after CR0.PE flips before the mandatory far jump reloads CS — the descriptor is decoded directly +/// from the live GDT/LDT as a fallback. +/// +public sealed class ProtectedModeMmu386 : IMmu { + private static readonly SegmentRegisterIndex[] AllSegmentRegisterIndices = [ + SegmentRegisterIndex.EsIndex, SegmentRegisterIndex.CsIndex, SegmentRegisterIndex.SsIndex, + SegmentRegisterIndex.DsIndex, SegmentRegisterIndex.FsIndex, SegmentRegisterIndex.GsIndex + ]; + + private readonly State _state; + private readonly IMemoryDevice _ram; + private readonly PagingUnit _pagingUnit; + + /// + /// Initializes a new instance. + /// + /// The CPU state, used to read segment registers, their descriptor caches, and the GDTR/LDTR. + /// The raw memory device backing linear/physical address reads (no MMU translation applied). + /// The page-directory/page-table walker, shared with the outer so both agree on Accessed/Dirty-bit state. + public ProtectedModeMmu386(State state, IMemoryDevice ram, PagingUnit pagingUnit) { + _state = state; + _ram = ram; + _pagingUnit = pagingUnit; + } + + /// + public void CheckAccess(ushort segment, uint offset, uint length, SegmentAccessKind accessKind, bool isWrite) { + SegmentDescriptorCache descriptorCache = ResolveDescriptorCache(segment); + if (!descriptorCache.Present) { + throw new CpuGeneralProtectionFaultException($"Segment 0x{segment:X4} is not present"); + } + if (isWrite && descriptorCache.IsCodeOrDataSegment && !descriptorCache.IsCode && !descriptorCache.IsReadWriteBitSet) { + throw new CpuGeneralProtectionFaultException($"Segment 0x{segment:X4} is not writable"); + } + if (offset <= descriptorCache.Limit && length - 1u <= descriptorCache.Limit - offset) { + return; + } + + string message = $"Segment access 0x{offset:X8}+{length}B exceeds segment limit 0x{descriptorCache.Limit:X8}"; + if (accessKind == SegmentAccessKind.Stack) { + throw new CpuStackSegmentFaultException(message); + } + throw new CpuGeneralProtectionFaultException(message); + } + + /// + public uint TranslateAddress(ushort segment, uint offset, bool isWrite) { + SegmentDescriptorCache descriptorCache = ResolveDescriptorCache(segment); + return descriptorCache.Base + offset; + } + + /// + /// A no-op: paging is applied by the outer that wraps this MMU, not here. + /// + public uint TranslateLinearAddress(uint linearAddress, bool isWrite) { + return linearAddress; + } + + private SegmentDescriptorCache ResolveDescriptorCache(ushort segment) { + foreach (SegmentRegisterIndex index in AllSegmentRegisterIndices) { + if (_state.SegmentRegisters.UInt16[(uint)index] == segment) { + return _state.SegmentDescriptorCaches[index]; + } + } + return DescriptorTableReader.ReadDescriptor(segment, + _state.Gdtr.Base, _state.Gdtr.Limit, + _state.Ldtr.DescriptorCache.Base, _state.Ldtr.DescriptorCache.Limit, + address => _ram.Read(TranslateLinearForFallback(address))); + } + + private uint TranslateLinearForFallback(uint linearAddress) { + return _state.ControlRegisters.PagingEnable ? _pagingUnit.Translate(linearAddress, isWrite: false) : linearAddress; + } +} diff --git a/src/Spice86.Core/Emulator/Memory/Mmu/RealModeMmu386.cs b/src/Spice86.Core/Emulator/Memory/Mmu/RealModeMmu386.cs index a15d8dc605..2c49414730 100644 --- a/src/Spice86.Core/Emulator/Memory/Mmu/RealModeMmu386.cs +++ b/src/Spice86.Core/Emulator/Memory/Mmu/RealModeMmu386.cs @@ -12,7 +12,7 @@ public sealed class RealModeMmu386 : IMmu { private const uint SegmentLimit = 0xFFFFu; /// - public void CheckAccess(ushort segment, uint offset, uint length, SegmentAccessKind accessKind) { + public void CheckAccess(ushort segment, uint offset, uint length, SegmentAccessKind accessKind, bool isWrite) { if (IsValidAccess(offset, length)) { return; } @@ -29,7 +29,14 @@ private static bool IsValidAccess(uint offset, uint length) { } /// - public uint TranslateAddress(ushort segment, uint offset) { + public uint TranslateAddress(ushort segment, uint offset, bool isWrite) { return MemoryUtils.ToPhysicalAddress(segment, (ushort)offset); } + + /// + /// A no-op: paging requires protected mode and is applied by the outer , not here. + /// + public uint TranslateLinearAddress(uint linearAddress, bool isWrite) { + return linearAddress; + } } diff --git a/src/Spice86.Core/Emulator/Memory/Mmu/RealModeMmu8086.cs b/src/Spice86.Core/Emulator/Memory/Mmu/RealModeMmu8086.cs index f5c5ab323d..94c8451baa 100644 --- a/src/Spice86.Core/Emulator/Memory/Mmu/RealModeMmu8086.cs +++ b/src/Spice86.Core/Emulator/Memory/Mmu/RealModeMmu8086.cs @@ -8,12 +8,17 @@ namespace Spice86.Core.Emulator.Memory.Mmu; ///
public sealed class RealModeMmu8086 : IMmu { /// - public void CheckAccess(ushort segment, uint offset, uint length, SegmentAccessKind accessKind) { + public void CheckAccess(ushort segment, uint offset, uint length, SegmentAccessKind accessKind, bool isWrite) { // 8086 wraps within segment — all accesses are valid. } /// - public uint TranslateAddress(ushort segment, uint offset) { + public uint TranslateAddress(ushort segment, uint offset, bool isWrite) { return MemoryUtils.ToPhysicalAddress(segment, (ushort)offset); } + + /// The 8086 has no paging concept: returns unchanged. + public uint TranslateLinearAddress(uint linearAddress, bool isWrite) { + return linearAddress; + } } diff --git a/src/Spice86.Core/Emulator/OperatingSystem/Dos.cs b/src/Spice86.Core/Emulator/OperatingSystem/Dos.cs index 1868f1ea5d..dcee7d6a43 100644 --- a/src/Spice86.Core/Emulator/OperatingSystem/Dos.cs +++ b/src/Spice86.Core/Emulator/OperatingSystem/Dos.cs @@ -185,18 +185,6 @@ public sealed class Dos : IDriveStatusProvider, IDiscSwapper, IDriveMountService /// The I/O port dispatcher for accessing hardware ports. /// The logger service implementation. /// Floppy I/O timing service used by absolute floppy image reads and writes. - /// DOS runtime options projected from the command-line configuration. - /// The emulator memory. - /// Provides current call flow handler to peek call stack. - /// The CPU stack. - /// The CPU state. - /// The BIOS keyboard buffer structure in emulated memory. - /// The BIOS interrupt handler that writes/reads the BIOS Keyboard Buffer. - /// The memory mapped BIOS values and settings. - /// The high-level VGA functions. - /// The DOS environment variables. - /// The I/O port dispatcher for accessing hardware ports. - /// The logger service implementation. /// The sound channel creator, used to stream CD audio when an image is mounted. /// Notifier that surfaces per-drive read/write activity to the UI. /// Optional XMS manager to expose through DOS. diff --git a/src/Spice86.Core/Emulator/ReverseEngineer/CSharpOverrideHelper.cs b/src/Spice86.Core/Emulator/ReverseEngineer/CSharpOverrideHelper.cs index ad2b72241e..cc15dedda3 100644 --- a/src/Spice86.Core/Emulator/ReverseEngineer/CSharpOverrideHelper.cs +++ b/src/Spice86.Core/Emulator/ReverseEngineer/CSharpOverrideHelper.cs @@ -4,6 +4,8 @@ using Spice86.Core.Emulator.CPU; using Spice86.Core.Emulator.CPU.CfgCpu.ParsedInstruction; +using Spice86.Core.Emulator.CPU.DescriptorTables; +using Spice86.Core.Emulator.CPU.Registers; using Spice86.Core.Emulator.Devices.ExternalInput; using Spice86.Core.Emulator.Devices.Timer; using Spice86.Core.Emulator.Function; @@ -120,6 +122,134 @@ public class CSharpOverrideHelper { /// public State State => Machine.CpuState; + /// + /// Loads a raw selector value into a segment register and refreshes its descriptor cache. Mirrors + /// + /// so generated/hand-written overrides behave identically to interpreted execution. + /// + public void LoadSegmentRegister(uint segmentRegisterIndex, ushort selector) { + SegmentAndControlRegisterOperations.LoadSegmentRegister(State, Memory, segmentRegisterIndex, selector); + } + + /// + /// Validates a direct (non-gate) far JMP/CALL code-segment transfer target. Called by generated code + /// before loading CS for a same-partition far jump, so the compiled override enforces the same + /// DPL/RPL rules as the interpreter (). + /// + public void ValidateFarCodeSegmentTransfer(ushort selector) { + PrivilegeChecks.ValidateFarCodeSegmentTransfer(State, Memory, selector); + } + + /// LGDT: loads GDTR from a 6-byte memory pointer (2-byte limit, 4-byte base). + public void LoadGdtr(ushort segment, uint offset) { + SegmentAndControlRegisterOperations.LoadGdtr(State, Memory, segment, offset); + } + + /// SGDT: stores GDTR to a 6-byte memory pointer (2-byte limit, 4-byte base). + public void StoreGdtr(ushort segment, uint offset) { + SegmentAndControlRegisterOperations.StoreGdtr(State, Memory, segment, offset); + } + + /// LIDT: loads IDTR from a 6-byte memory pointer (2-byte limit, 4-byte base). + public void LoadIdtr(ushort segment, uint offset) { + SegmentAndControlRegisterOperations.LoadIdtr(State, Memory, segment, offset); + } + + /// SIDT: stores IDTR to a 6-byte memory pointer (2-byte limit, 4-byte base). + public void StoreIdtr(ushort segment, uint offset) { + SegmentAndControlRegisterOperations.StoreIdtr(State, Memory, segment, offset); + } + + /// MOV r32, CRn: reads CR0/CR2/CR3/CR4. + public uint ReadControlRegister(uint crNumber) { + return SegmentAndControlRegisterOperations.ReadControlRegister(State, crNumber); + } + + /// MOV CRn, r32: writes CR0/CR2/CR3/CR4. + public void WriteControlRegister(uint crNumber, uint value) { + SegmentAndControlRegisterOperations.WriteControlRegister(State, crNumber, value); + } + + /// SMSW: reads the low 16 bits of CR0. + public ushort ReadMachineStatusWord() { + return SegmentAndControlRegisterOperations.ReadMachineStatusWord(State); + } + + /// LMSW: writes the low 4 bits of CR0 (PE, MP, EM, TS). + public void LoadMachineStatusWord(ushort value) { + SegmentAndControlRegisterOperations.LoadMachineStatusWord(State, value); + } + + /// CLTS: clears CR0.TS. + public void Clts() { + SegmentAndControlRegisterOperations.Clts(State); + } + + /// Throws #GP if CPL/IOPL do not permit `IN`/`OUT`/`CLI`/`STI`. + public void EnsureIoPrivilege() { + PrivilegeChecks.EnsureIoPrivilege(State); + } + + /// LLDT: loads LDTR from a GDT selector. + public void LoadLdtr(ushort selector) { + SegmentAndControlRegisterOperations.LoadLdtr(State, Memory, selector); + } + + /// SLDT: reads the current LDTR selector. + public ushort StoreLdtr() { + return SegmentAndControlRegisterOperations.StoreLdtr(State); + } + + /// LTR: loads the Task Register from a GDT selector. + public void LoadTr(ushort selector) { + SegmentAndControlRegisterOperations.LoadTr(State, Memory, selector); + } + + /// STR: reads the current Task Register selector. + public ushort StoreTr() { + return SegmentAndControlRegisterOperations.StoreTr(State); + } + + /// ARPL: returns the r/m operand with its RPL raised to the register operand's RPL if lower. + public ushort AdjustRequestedPrivilegeLevel(ushort rmSelector, ushort regSelector) { + return SegmentAndControlRegisterOperations.AdjustRequestedPrivilegeLevel(rmSelector, regSelector); + } + + /// ARPL: whether the r/m operand's RPL was raised (sets ZF). + public bool WasPrivilegeLevelAdjusted(ushort rmSelector, ushort regSelector) { + return SegmentAndControlRegisterOperations.WasPrivilegeLevelAdjusted(rmSelector, regSelector); + } + + /// LAR: whether a selector resolves to a present descriptor (sets ZF). + public bool IsSelectorValidForLar(ushort selector) { + return SegmentAndControlRegisterOperations.IsSelectorValidForLar(State, Memory, selector); + } + + /// LAR: loads the packed access-rights doubleword for a selector. + public uint LoadAccessRights(ushort selector) { + return SegmentAndControlRegisterOperations.LoadAccessRights(State, Memory, selector); + } + + /// LSL: whether a selector resolves to a present segment descriptor (sets ZF). + public bool IsSelectorValidForLsl(ushort selector) { + return SegmentAndControlRegisterOperations.IsSelectorValidForLsl(State, Memory, selector); + } + + /// LSL: loads the granularity-scaled limit for a selector. + public uint LoadSegmentLimit(ushort selector) { + return SegmentAndControlRegisterOperations.LoadSegmentLimit(State, Memory, selector); + } + + /// VERR: whether a selector is a present, readable data or code segment. + public bool VerifyReadable(ushort selector) { + return SegmentAndControlRegisterOperations.VerifyReadable(State, Memory, selector); + } + + /// VERW: whether a selector is a present, writable data segment. + public bool VerifyWritable(ushort selector) { + return SegmentAndControlRegisterOperations.VerifyWritable(State, Memory, selector); + } + /// /// Arithmetic-logic unit for 8 bit operations /// @@ -365,6 +495,15 @@ public class CSharpOverrideHelper { /// public IDictionary FunctionInformations { get; private set; } + /// + /// Functions registered by their protected-mode flat linear address (see ), + /// kept separate from because a linear address has no + /// to key the latter dictionary by, and because + /// a call target's linear address must be resolved live through the current GDT/LDT (a selector's base + /// can be repointed by a descriptor edit after registration) rather than cached once at registration time. + /// + public IDictionary LinearFunctionInformations { get; } = new Dictionary(); + /// /// Gets or sets the /// @@ -455,6 +594,42 @@ public void DefineFunction(ushort segment, FunctionInformations[address] = (new(address, functionName, overrideFunc)); } + /// + /// Registers a function at a protected-mode flat linear address rather than a segment:offset pair, so + /// the override keeps firing even if a later descriptor edit repoints the selector that currently maps + /// to that address (see ). The + /// stored is a synthetic placeholder (segment 0, offset truncated from the linear address) for display + /// purposes only - lookup is always keyed by the raw linear address, never by that placeholder. + /// + /// The flat linear address the function starts at. + /// The function to register. + /// Whether to fail if a function is already defined at the specified address. Default is true. + /// The name of the function. If null, the name of the provided function will be parsed using the GhidraSymbolsDumper utility. + /// Thrown when is null and the name of the provided function cannot be parsed. + public void DefineFunction(uint linearAddress, Func overrideFunc, bool failOnExisting = true, string? name = null) { + if (failOnExisting && LinearFunctionInformations.TryGetValue(linearAddress, out FunctionInformation? existing)) { + throw new UnrecoverableException( + $"There is already a function overriden at linear address 0x{linearAddress:X8} named {existing.Name}. Please check your mappings for duplicates."); + } + + string functionName; + if (name != null) { + functionName = name; + } else { + string methodName = overrideFunc.Method.Name; + FunctionInformation? parsedFunctionInformation = GhidraSymbolsExporter.NameToFunctionInformation(_loggerService, methodName); + if (parsedFunctionInformation == null) { + throw new UnrecoverableException("Cannot parse " + methodName + + " into a spice86 function name as format is not correct."); + } + + functionName = parsedFunctionInformation.Name; + } + + SegmentedAddress placeholderAddress = new(0, unchecked((ushort)linearAddress)); + LinearFunctionInformations[linearAddress] = new FunctionInformation(placeholderAddress, functionName, overrideFunc); + } + /// /// Gets the function information for the function at the specified address. /// @@ -490,7 +665,12 @@ public void DefineFunction(ushort segment, /// The that will mutate CS and IP when invoked. public Action FarJump(ushort cs, ushort ip) { return () => { - State.CS = cs; + if (ProtectedModeCallGateDispatcher.TryReadCallGate(State, Memory, cs, out RawGateDescriptor gate)) { + ProtectedModeCallGateDispatcher.DispatchJump(State, Memory, gate, cs); + return; + } + PrivilegeChecks.ValidateFarCodeSegmentTransfer(State, Memory, cs); + LoadSegmentRegister((uint)SegmentRegisterIndex.CsIndex, cs); State.IP = ip; }; } @@ -500,11 +680,25 @@ public Action FarJump(ushort cs, ushort ip) { /// /// returns an that performs a far return instruction when invoked. public Action FarRet(ushort numberOfBytesToPop = 0) { - return () => ReturnOperationsHelper.FarRet16(numberOfBytesToPop); + return () => { + if (State.CpuMode == CpuMode.Protected) { + ProtectedModeInterruptDispatcher.FarReturn16(State, Memory, Stack, numberOfBytesToPop); + } else { + ReturnOperationsHelper.FarRet16(numberOfBytesToPop); + LoadSegmentRegister((uint)SegmentRegisterIndex.CsIndex, State.CS); + } + }; } public Action FarRet32(ushort numberOfBytesToPop = 0) { - return () => ReturnOperationsHelper.FarRet32(numberOfBytesToPop); + return () => { + if (State.CpuMode == CpuMode.Protected) { + ProtectedModeInterruptDispatcher.FarReturn32(State, Memory, Stack, numberOfBytesToPop); + return; + } + ReturnOperationsHelper.FarRet32(numberOfBytesToPop); + LoadSegmentRegister((uint)SegmentRegisterIndex.CsIndex, State.CS); + }; } /// @@ -512,7 +706,27 @@ public Action FarRet32(ushort numberOfBytesToPop = 0) { /// /// returns an that performs an interrupt return when invoked. public Action InterruptRet() { - return () => ReturnOperationsHelper.InterruptRet(); + return () => { + if (State.CpuMode == CpuMode.Protected) { + ProtectedModeInterruptDispatcher.InterruptReturn16(State, Memory, Stack); + } else { + ReturnOperationsHelper.InterruptRet(); + } + }; + } + + /// + /// Returns an that performs a 32-bit IRETD instruction when invoked. + /// + /// returns an that performs a 32-bit interrupt return when invoked. + public Action InterruptRet32() { + return () => { + if (State.CpuMode == CpuMode.Protected) { + ProtectedModeInterruptDispatcher.InterruptReturn32(State, Memory, Stack); + } else { + ReturnOperationsHelper.InterruptRet32(); + } + }; } /// @@ -574,6 +788,32 @@ public void NearCall32(ushort expectedReturnCs, ushort expectedReturnIp, FuncThe CS value of the callee, executed in for the duration of the call. /// The function to call. public void FarCall(ushort expectedReturnCs, ushort expectedReturnIp, ushort targetCs, Func function) { + SegmentedAddress expectedReturn = new(expectedReturnCs, expectedReturnIp); + if (TaskSwitchOperations.TryReadAvailableTss(State, Memory, targetCs)) { + SegmentedAddress taskTarget = TaskSwitchOperations.SwitchToNewTask(State, Memory, targetCs, expectedReturnIp); + Func? taskFunction = SearchFunctionOverride(taskTarget); + if (taskFunction is null) { + throw FailAsUntested($"Could not find an override at address {taskTarget}"); + } + ExecuteCallEnsuringSameStack(expectedReturnCs, expectedReturnIp, taskTarget.Segment, taskFunction, () => { + Action taskReturnAction = taskFunction.Invoke(0); + taskReturnAction.Invoke(); + }); + return; + } + if (ProtectedModeCallGateDispatcher.TryReadCallGate(State, Memory, targetCs, out RawGateDescriptor gate)) { + SegmentedAddress gateTarget = ProtectedModeCallGateDispatcher.Dispatch(State, Memory, Stack, gate, targetCs, expectedReturn); + Func? gateFunction = SearchFunctionOverride(gateTarget); + if (gateFunction is null) { + throw FailAsUntested($"Could not find an override at address {gateTarget}"); + } + ExecuteCallEnsuringSameStack(expectedReturnCs, expectedReturnIp, gateTarget.Segment, gateFunction, () => { + Action gateReturnAction = gateFunction.Invoke(0); + gateReturnAction.Invoke(); + }); + return; + } + PrivilegeChecks.ValidateFarCodeSegmentTransfer(State, Memory, targetCs); ExecuteCallEnsuringSameStack(expectedReturnCs, expectedReturnIp, targetCs, function, () => { Stack.Push16(expectedReturnCs); Stack.Push16(expectedReturnIp); @@ -592,6 +832,10 @@ public void FarCall(ushort expectedReturnCs, ushort expectedReturnIp, ushort tar /// The CS value of the callee, executed in for the duration of the call. /// The function to call. public void FarCall32(ushort expectedReturnCs, ushort expectedReturnIp, ushort targetCs, Func function) { + if (ProtectedModeCallGateDispatcher.TryReadCallGate(State, Memory, targetCs, out RawGateDescriptor gate)) { + throw FailAsUntested($"32-bit far call through call gate 0x{targetCs:X4} is not yet supported"); + } + PrivilegeChecks.ValidateFarCodeSegmentTransfer(State, Memory, targetCs); ExecuteCallEnsuringSameStack(expectedReturnCs, expectedReturnIp, targetCs, function, () => { Stack.PushFarPointer32(new SegmentedAddress32(expectedReturnCs, expectedReturnIp)); Action returnAction = function.Invoke(0); @@ -633,6 +877,24 @@ public void InterruptCall(ushort expectedReturnCs, ushort expectedReturnIp, usho /// The vector number to call for the interrupt. /// If the interrupt vector number is not recognized. public void InterruptCall(ushort expectedReturnCs, ushort expectedReturnIp, byte vectorNumber) { + if (State.CpuMode is CpuMode.Protected or CpuMode.Virtual8086) { + // Dispatch already pushes the return frame and sets CS:IP to the gate's target, so the + // action below (unlike the real-mode overload) has nothing left to push. The expected return + // address is passed explicitly (not read from State.IP, which generated code does not keep + // continuously in sync). + SegmentedAddress expectedReturn = new(expectedReturnCs, expectedReturnIp); + SegmentedAddress protectedModeTarget = ProtectedModeInterruptDispatcher.Dispatch( + State, Memory, Stack, vectorNumber, checkGateDpl: true, errorCode: null, expectedReturn); + Func? protectedModeFunction = SearchFunctionOverride(protectedModeTarget); + if (protectedModeFunction is null) { + throw FailAsUntested($"Could not find an override at address {protectedModeTarget}"); + } + ExecuteCallEnsuringSameStack(expectedReturnCs, expectedReturnIp, protectedModeTarget.Segment, protectedModeFunction, () => { + Action returnAction = protectedModeFunction.Invoke(0); + returnAction.Invoke(); + }); + return; + } SegmentedAddress target = Machine.InterruptVectorTable[vectorNumber]; Func? function = SearchFunctionOverride(target); if (function is null) { @@ -654,34 +916,71 @@ public void EnterCpuFaultHandler(ushort faultingInstructionCs, ushort faultingIn Stack.Push16(State.Flags.FlagRegister16); Stack.PushSegmentedAddress(new SegmentedAddress(faultingInstructionCs, faultingInstructionIp)); InterruptFlag = false; - CS = target.Segment; + LoadSegmentRegister((uint)SegmentRegisterIndex.CsIndex, target.Segment); IP = target.Offset; } + /// + /// Resolves the fault-handler target for a CPU exception caught in generated code and performs the + /// full entry sequence: protected mode AND Virtual-8086 mode both walk the IDT via + /// (respecting DPL escalation, stack switch, and + /// error-code push); only real mode uses the real-mode IVT via . + /// Mirrors + /// . + /// + public SegmentedAddress ResolveCpuFaultTarget(byte vectorNumber, ushort? errorCode, ushort faultingCs, ushort faultingIp) { + if (State.CpuMode is CpuMode.Protected or CpuMode.Virtual8086) { + return ProtectedModeInterruptDispatcher.Dispatch( + State, Memory, Stack, vectorNumber, checkGateDpl: false, errorCode, new SegmentedAddress(faultingCs, faultingIp)); + } + SegmentedAddress target = Machine.InterruptVectorTable[vectorNumber]; + EnterCpuFaultHandler(faultingCs, faultingIp, target); + return target; + } + /// /// Returns the C# function override, or null if not found. /// /// The where the function is defined. /// The C# function override, or null if not found. + /// + /// Returns the C# function override, or null if not found. Checks + /// (keyed by segment:offset) first, then, in protected mode only, resolves 's + /// segment live through the current GDT/LDT to a flat linear address and checks + /// - so a linear-address override keeps firing even if the + /// selector currently mapping to that address changed since registration. + /// + /// The where the function is defined. + /// The C# function override, or null if not found. public Func? SearchFunctionOverride(SegmentedAddress target) { - if (!FunctionInformations.TryGetValue(target, - out FunctionInformation? functionInformation)) { - return null; + if (FunctionInformations.TryGetValue(target, out FunctionInformation? functionInformation)) { + return functionInformation.FunctionOverride; } - return functionInformation.FunctionOverride; + if (State.CpuMode != CpuMode.Protected) { + return null; + } + if (!DescriptorTableReader.TryReadDescriptor(target.Segment, State.Gdtr.Base, State.Gdtr.Limit, + State.Ldtr.DescriptorCache.Base, State.Ldtr.DescriptorCache.Limit, address => Memory[Memory.Mmu.TranslateLinearAddress(address, isWrite: false)], out SegmentDescriptorCache descriptor)) { + return null; + } + uint linearAddress = descriptor.Base + target.Offset; + return LinearFunctionInformations.TryGetValue(linearAddress, out FunctionInformation? linearFunctionInformation) + ? linearFunctionInformation.FunctionOverride + : null; } private void ExecuteCallEnsuringSameStack(ushort expectedReturnCs, ushort expectedReturnIp, ushort targetCs, Func function, Action action) { uint expectedStackAddress = State.StackPhysicalAddress; // CS is set to the callee's segment for the duration of the call: this is the segment the emulated - // CPU runs in while inside the callee, so reads of CS (e.g. a "push CS" idiom) observe the right value. - // For near calls targetCs equals expectedReturnCs so CS is unchanged. IP is set to the expected return - // offset as a fallback for the post-call validation below; generated code never reads State.IP mid-call + // CPU runs in while inside the callee, so reads of CS (e.g. a "push CS" idiom, or a protected-mode + // CPL/privilege check) observe the right value and descriptor cache. For near calls targetCs equals + // expectedReturnCs so this is a same-selector no-op reload. IP is set to the expected return offset + // as a fallback for the post-call validation below; generated code never reads State.IP mid-call // (CheckExternalEvents takes the offset explicitly), and the callee's return action overwrites CS:IP // with the actual return address before the loop runs. - State.CS = targetCs; + LoadSegmentRegister((uint)SegmentRegisterIndex.CsIndex, targetCs); State.IP = expectedReturnIp; ExecuteCall(function, action); ushort actualReturnCs = State.CS; @@ -748,6 +1047,22 @@ public void OverrideInstruction(ushort segment, ushort offset, Func rena EmulatorBreakpointsManager.ToggleBreakPoint(breakPoint, true); } + /// + /// Overrides the machine code at the specified flat linear address, for protected-mode code whose + /// selector's base may not be known/stable at registration time. See + /// for the segment:offset overload. + /// + /// The flat linear address of the instruction to override. + /// An action that provides the new implementation to use for the instruction. + public void OverrideInstruction(uint linearAddress, Func renamedOverride) { + AddressBreakPoint breakPoint = new( + BreakPointType.CPU_EXECUTION_ADDRESS, + linearAddress, + _ => renamedOverride.Invoke().Invoke() + , false); + EmulatorBreakpointsManager.ToggleBreakPoint(breakPoint, true); + } + /// /// Executes the specified action on top of the instruction at the specified segment and offset. /// @@ -765,6 +1080,22 @@ public void DoOnTopOfInstruction(ushort segment, ushort offset, Action action) { EmulatorBreakpointsManager.ToggleBreakPoint(breakPoint, true); } + /// + /// Executes the specified action on top of the instruction at the specified flat linear address, for + /// protected-mode code. See for the + /// segment:offset overload. + /// + /// The flat linear address of the instruction to execute the action on. + /// The action to execute on top of the instruction. + public void DoOnTopOfInstruction(uint linearAddress, Action action) { + AddressBreakPoint breakPoint = new( + BreakPointType.CPU_EXECUTION_ADDRESS, + linearAddress, + _ => action.Invoke() + , false); + EmulatorBreakpointsManager.ToggleBreakPoint(breakPoint, true); + } + /// /// Executes the specified action when the byte at the specified segment and offset is written to. /// @@ -938,7 +1269,12 @@ public void SetProvidedInterruptHandlersAsOverridden() { /// Halt the program. /// /// An that exits the program. - public Action Hlt() => Exit; + public Action Hlt() { + return () => { + PrivilegeChecks.EnsureCpl0(State, "HLT"); + Exit(); + }; + } /// /// Exit the program. diff --git a/src/Spice86.Core/Emulator/ReverseEngineer/CfgCodeGeneration/CSharpAstEmitter.cs b/src/Spice86.Core/Emulator/ReverseEngineer/CfgCodeGeneration/CSharpAstEmitter.cs index 4c3f3bfd77..5f370ca938 100644 --- a/src/Spice86.Core/Emulator/ReverseEngineer/CfgCodeGeneration/CSharpAstEmitter.cs +++ b/src/Spice86.Core/Emulator/ReverseEngineer/CfgCodeGeneration/CSharpAstEmitter.cs @@ -140,8 +140,17 @@ public EmittedCode VisitJumpFarNode(JumpFarNode node) { ushort? segment = TryGetConstantWord(node.TargetAddress.Segment); ushort? offset = TryGetConstantWord(node.TargetAddress.Offset); if (segment is not null && offset is not null) { - return Transfer.Emit(Context.ResolveEdge(node.Instruction, InstructionSuccessorType.Normal, - new SegmentedAddress(segment.Value, offset.Value)), CurrentMethod); + EmittedCode transfer = EmittedCode.Concat( + EmittedCode.Line($"ValidateFarCodeSegmentTransfer(0x{segment.Value:X4});"), + EmittedCode.Line($"LoadSegmentRegister(0x{(uint)SegmentRegisterIndex.CsIndex:X8}u, 0x{segment.Value:X4});")); + // A far jump observed to always fault during discovery (e.g. a privilege violation) has no + // Normal successor at all - only the CpuFaultWrapper's surrounding try/catch, which every real + // execution actually takes, has anywhere to go. There is genuinely no continuation to lower. + if (Context.GetSuccessorEdges(node.Instruction, InstructionSuccessorType.Normal).Count == 0) { + return transfer; + } + return EmittedCode.Concat(transfer, Transfer.Emit(Context.ResolveEdge(node.Instruction, InstructionSuccessorType.Normal, + new SegmentedAddress(segment.Value, offset.Value)), CurrentMethod)); } return BuildFarRuntimeDispatch(node.Instruction, Expr(node.TargetAddress.Segment), Expr(node.TargetAddress.Offset), "jump", @@ -149,7 +158,10 @@ public EmittedCode VisitJumpFarNode(JumpFarNode node) { // throw as the next sibling (unlike the near switch, whose `break` exits past the construct to // the next node). An empty matched branch would fall through the remaining checks into that // throw, so the adjacency-fallthrough optimization cannot apply here. - edge => Transfer.Emit(edge, CurrentMethod, forceSameMethodGoto: true).AsStatements()); + edge => EmittedCode.Concat( + EmittedCode.Line($"ValidateFarCodeSegmentTransfer(0x{edge.Target.Address.Segment:X4});"), + EmittedCode.Line($"LoadSegmentRegister(0x{(uint)SegmentRegisterIndex.CsIndex:X8}u, 0x{edge.Target.Address.Segment:X4});"), + Transfer.Emit(edge, CurrentMethod, forceSameMethodGoto: true)).AsStatements()); } public EmittedCode VisitCallNearNode(CallNearNode node) { @@ -176,8 +188,24 @@ public EmittedCode VisitCallFarNode(CallFarNode node) { } SegmentedAddress targetAddress = new SegmentedAddress(targetSegment.Value, targetOffset.Value); - ResolvedCfgEdge targetEdge = Context.ResolveEdge(node.Instruction, InstructionSuccessorType.Normal, targetAddress); - return Transfer.EmitCallHelperAndContinuation(helperName, node.Instruction, Transfer.FunctionExpression(targetEdge), CurrentMethod, farCallTargetCs: targetSegment.Value); + if (Context.TryResolveEdge(node.Instruction, InstructionSuccessorType.Normal, targetAddress) is ResolvedCfgEdge targetEdge) { + return Transfer.EmitCallHelperAndContinuation(helperName, node.Instruction, Transfer.FunctionExpression(targetEdge), CurrentMethod, farCallTargetCs: targetSegment.Value); + } + + // No direct-target edge was ever observed for this literal operand selector: during discovery + // this call always redirected through a call gate (or another selector-driven indirection) to a + // different real target, so the CFG edge lives there instead. There is nothing to statically + // resolve here - FarCall/FarCall32 perform the gate detection and dynamic dispatch themselves + // (mirroring InterruptCall), so the "static" function argument is unreachable by construction. + // The literal selector is emitted as a raw hex constant (not via GetSegmentVariable): a gate + // selector is never itself a segment code ever executes in, so it has no registered csN field. + CallContinuation gateContinuation = Context.ResolveCallContinuation(node.Instruction); + string neverCalledFunction = $"_ => throw FailAsUntested(\"Far call at {node.Instruction.Address} unexpectedly resolved to a direct target\")"; + SegmentedAddress gateExpectedReturn = gateContinuation.ExpectedReturnAddress; + string callLine = $"{helperName}({Context.GetSegmentVariable(gateExpectedReturn.Segment)}, 0x{gateExpectedReturn.Offset:X4}, 0x{targetSegment.Value:X4}, {neverCalledFunction});"; + return EmittedCode.Concat( + EmittedCode.Line(callLine), + Transfer.EmitPostCallContinuation(node.Instruction, gateContinuation, CurrentMethod)); } public EmittedCode VisitInterruptCallNode(InterruptCallNode node) { @@ -444,7 +472,8 @@ public EmittedCode VisitThrowNode(ThrowNode node) => public EmittedCode VisitReturnNearNode(ReturnNearNode node) => LowerReturn(NearRetExpression(node)); public EmittedCode VisitReturnFarNode(ReturnFarNode node) => LowerReturn(FarRetExpression(node)); - public EmittedCode VisitReturnInterruptNode(ReturnInterruptNode node) => LowerReturn("InterruptRet()"); + public EmittedCode VisitReturnInterruptNode(ReturnInterruptNode node) => + LowerReturn(node.OperandSize == BitWidth.DWORD_32 ? "InterruptRet32()" : "InterruptRet()"); private static EmittedCode LowerReturn(string returnActionExpression) => EmittedCode.Diverging($"return {returnActionExpression};"); diff --git a/src/Spice86.Core/Emulator/ReverseEngineer/CfgCodeGeneration/CpuFaultWrapper.cs b/src/Spice86.Core/Emulator/ReverseEngineer/CfgCodeGeneration/CpuFaultWrapper.cs index 4b35a08b12..e52048df95 100644 --- a/src/Spice86.Core/Emulator/ReverseEngineer/CfgCodeGeneration/CpuFaultWrapper.cs +++ b/src/Spice86.Core/Emulator/ReverseEngineer/CfgCodeGeneration/CpuFaultWrapper.cs @@ -27,14 +27,12 @@ public EmittedCode Wrap(CfgInstruction instruction, EmittedCode body, MethodPlan } List catchBody = [ - new LineStatement("SegmentedAddress cpuFaultTarget = Machine.InterruptVectorTable[cpuException.InterruptVector];") + new LineStatement($"SegmentedAddress cpuFaultTarget = ResolveCpuFaultTarget(cpuException.InterruptVector, cpuException.ErrorCode, {context.GetSegmentVariable(instruction.Address.Segment)}, 0x{instruction.Address.Offset:X4});") ]; foreach (ResolvedCfgEdge edge in faultEdges) { catchBody.Add(new BlockStatement( - $"if (cpuFaultTarget == new SegmentedAddress({context.GetSegmentVariable(edge.Target.Address.Segment)}, 0x{edge.Target.Address.Offset:X4}))", [ - new LineStatement($"EnterCpuFaultHandler({context.GetSegmentVariable(instruction.Address.Segment)}, 0x{instruction.Address.Offset:X4}, cpuFaultTarget);"), - .. transferEmitter.Emit(edge, method).AsStatements() - ])); + $"if (cpuFaultTarget == new SegmentedAddress({context.GetSegmentVariable(edge.Target.Address.Segment)}, 0x{edge.Target.Address.Offset:X4}))", + transferEmitter.Emit(edge, method).AsStatements())); } catchBody.Add(new LineStatement($"throw FailAsUntested($\"Unknown CPU fault target {{cpuFaultTarget}} at {instruction.Address}\");", Diverges: true)); diff --git a/src/Spice86.Shared/Emulator/Memory/CfgCodeAddress.cs b/src/Spice86.Shared/Emulator/Memory/CfgCodeAddress.cs new file mode 100644 index 0000000000..8c9deda6b9 --- /dev/null +++ b/src/Spice86.Shared/Emulator/Memory/CfgCodeAddress.cs @@ -0,0 +1,59 @@ +namespace Spice86.Shared.Emulator.Memory; + +/// +/// A CFG code address: either a real/V86-mode (selector:offset, +/// Linear = segment*16+offset) or a protected-mode flat linear address. Exists so protected-mode C# +/// override registration can be keyed by the address code actually executes at, which survives a +/// descriptor edit that repoints a selector's base - unlike , whose +/// is always segment*16+offset and has no notion of a +/// GDT/LDT-resolved base. Equality and ordering are defined purely by : two instances +/// referring to the same linear address are equal regardless of whether one was constructed from a +/// and the other from a raw linear value. +/// +public readonly record struct CfgCodeAddress : IComparable { + private readonly SegmentedAddress? _segmentedAddress; + + /// + /// Constructs a real/V86-mode address from a segment:offset pair. + /// + public CfgCodeAddress(SegmentedAddress segmentedAddress) { + _segmentedAddress = segmentedAddress; + Linear = segmentedAddress.Linear; + } + + /// + /// Constructs a protected-mode flat linear address with no associated segment:offset pair. + /// + public CfgCodeAddress(uint linearAddress) { + _segmentedAddress = null; + Linear = linearAddress; + } + + /// + /// The segment:offset pair this address was constructed from, or null if it was constructed + /// from a raw linear address. + /// + public SegmentedAddress? SegmentedAddress => _segmentedAddress; + + /// + /// The flat linear address, used for equality, ordering, and hashing. + /// + public uint Linear { get; } + + /// + public bool Equals(CfgCodeAddress other) => Linear == other.Linear; + + /// + public override int GetHashCode() => Linear.GetHashCode(); + + /// + public int CompareTo(CfgCodeAddress other) => Linear.CompareTo(other.Linear); + + /// + /// Implicitly wraps a as a . + /// + public static implicit operator CfgCodeAddress(SegmentedAddress address) => new(address); + + /// + public override string ToString() => _segmentedAddress?.ToString() ?? $"0x{Linear:X8}"; +} diff --git a/src/Spice86/Spice86DependencyInjection.cs b/src/Spice86/Spice86DependencyInjection.cs index e80e1dffea..2202faef8b 100644 --- a/src/Spice86/Spice86DependencyInjection.cs +++ b/src/Spice86/Spice86DependencyInjection.cs @@ -190,7 +190,7 @@ internal Spice86DependencyInjection(Configuration configuration, MainWindow? mai loggerService.LogInformation("IO port dispatcher created..."); } - Ram ram = new(A20Gate.EndOfHighMemoryArea); + Ram ram = new((uint)configuration.RamSizeKb * 1024); if (loggerService.IsEnabled(LogLevel.Information)) { loggerService.LogInformation("RAM created..."); @@ -202,7 +202,7 @@ internal Spice86DependencyInjection(Configuration configuration, MainWindow? mai loggerService.LogInformation("A20 gate created..."); } - IMmu mmu = RealModeMmuFactory.FromCpuModel(configuration.CpuModel); + IMmu mmu = CpuMmuFactory.Create(configuration.CpuModel, state, ram); Memory memory = new(memoryReadWriteBreakpoints, ram, a20Gate, diff --git a/src/Spice86/ViewModels/DataModels/XmsBlockBinaryDocument.cs b/src/Spice86/ViewModels/DataModels/XmsBlockBinaryDocument.cs index 7412725b6c..dbcfb5dd00 100644 --- a/src/Spice86/ViewModels/DataModels/XmsBlockBinaryDocument.cs +++ b/src/Spice86/ViewModels/DataModels/XmsBlockBinaryDocument.cs @@ -2,16 +2,16 @@ namespace Spice86.ViewModels.DataModels; using AvaloniaHex.Document; -using Spice86.Core.Emulator.Memory; +using Spice86.Core.Emulator.InterruptHandlers.Dos.Xms; /// public sealed class XmsBlockBinaryDocument : IBinaryDocument { - private readonly Ram _xmsRam; + private readonly ExtendedMemoryManager _xms; private readonly uint _blockOffset; private readonly uint _blockLength; - public XmsBlockBinaryDocument(Ram xmsRam, uint blockOffset, uint blockLength) { - _xmsRam = xmsRam; + public XmsBlockBinaryDocument(ExtendedMemoryManager xms, uint blockOffset, uint blockLength) { + _xms = xms; _blockOffset = blockOffset; _blockLength = blockLength; IsReadOnly = true; @@ -53,7 +53,7 @@ public void ReadBytes(ulong offset, Span buffer) { return; } int readableLength = (int)Math.Min((ulong)buffer.Length, _blockLength - startOffset); - IList blockSlice = _xmsRam.GetSlice((int)(_blockOffset + startOffset), readableLength); + IList blockSlice = _xms.GetSlice(_blockOffset + startOffset, readableLength); for (int index = 0; index < readableLength; index++) { buffer[index] = blockSlice[index]; } diff --git a/src/Spice86/ViewModels/XmsViewModel.cs b/src/Spice86/ViewModels/XmsViewModel.cs index 011de3fe83..35e5f0de2b 100644 --- a/src/Spice86/ViewModels/XmsViewModel.cs +++ b/src/Spice86/ViewModels/XmsViewModel.cs @@ -289,7 +289,7 @@ private void RefreshSelectedBlockDocument() { return; } - SelectedBlockDocument = new XmsBlockBinaryDocument(_xms.XmsRam, SelectedBlock.Offset, SelectedBlock.Length); + SelectedBlockDocument = new XmsBlockBinaryDocument(_xms, SelectedBlock.Offset, SelectedBlock.Length); string handleText; if (SelectedBlock.IsFree) { handleText = "Free"; diff --git a/tests/Spice86.Tests/Bios/SystemBiosInt15HandlerTests.cs b/tests/Spice86.Tests/Bios/SystemBiosInt15HandlerTests.cs new file mode 100644 index 0000000000..362b701ad7 --- /dev/null +++ b/tests/Spice86.Tests/Bios/SystemBiosInt15HandlerTests.cs @@ -0,0 +1,54 @@ +namespace Spice86.Tests.Bios; + +using FluentAssertions; + +using Spice86.Shared.Interfaces; +using Spice86.Tests.Utility; + +using Xunit; + +/// +/// Integration tests for BIOS INT 15h, AH=87h (Copy Extended Memory), run as real assembly code +/// through the emulation stack. +/// +public class SystemBiosInt15HandlerTests +{ + enum TestResult : byte + { + Success = 0x00, + Failure = 0xFF + } + + /// + /// + /// always read/wrote through the shared Memory bus directly (no private XMS/EMS-style + /// array), so it transparently gained the ability to address the full unified extended-memory + /// pool once Ram was grown past the old ~1.06MB conventional+HMA ceiling. This round-trips a + /// marker word through a linear address 3MB in, well beyond that old ceiling. + /// + [Fact] + public void BiosInt15h_87h_ShouldCopyAcrossFullUnifiedMemoryRange() + { + string resourcePath = Path.Join(AppContext.BaseDirectory, "Resources", "BiosInt15Tests", "bios_int15h_87h.com"); + string cDrive = Path.GetDirectoryName(resourcePath) ?? AppContext.BaseDirectory; + + using Spice86Creator creator = new Spice86Creator( + binName: resourcePath, + installInterruptVectors: true, + cDrive: cDrive + ); + using Spice86DependencyInjection spice86DependencyInjection = creator.Create(); + + TestIoPortHandler testHandler = new( + spice86DependencyInjection.Machine.CpuState, + NSubstitute.Substitute.For(), + spice86DependencyInjection.Machine.IoPortDispatcher + ); + spice86DependencyInjection.ProgramExecutor.Run(); + + testHandler.Results.Should().Contain((byte)TestResult.Success, + "copying a word to and from a linear address 3MB in should succeed"); + testHandler.Results.Should().NotContain((byte)TestResult.Failure); + testHandler.Details.Should().Contain(0x02, "both copy directions should have completed"); + } +} diff --git a/tests/Spice86.Tests/CfgCodeAddressTest.cs b/tests/Spice86.Tests/CfgCodeAddressTest.cs new file mode 100644 index 0000000000..5323eaa64f --- /dev/null +++ b/tests/Spice86.Tests/CfgCodeAddressTest.cs @@ -0,0 +1,36 @@ +namespace Spice86.Tests; + +using FluentAssertions; + +using Spice86.Shared.Emulator.Memory; + +using Xunit; + +public class CfgCodeAddressTest { + [Fact] + public void SegmentedAndLinearConstruction_WithSameLinearValue_AreEqual() { + CfgCodeAddress fromSegmented = new SegmentedAddress(0x1000, 0x0050); + CfgCodeAddress fromLinear = new(0x1000u * 16 + 0x50); + + fromSegmented.Should().Be(fromLinear); + fromSegmented.GetHashCode().Should().Be(fromLinear.GetHashCode()); + fromSegmented.CompareTo(fromLinear).Should().Be(0); + } + + [Fact] + public void DifferentLinearValues_AreNotEqual() { + CfgCodeAddress a = new(0x1000u); + CfgCodeAddress b = new(0x2000u); + + a.Should().NotBe(b); + a.CompareTo(b).Should().BeLessThan(0); + } + + [Fact] + public void LinearOnlyAddress_HasNoSegmentedAddress() { + CfgCodeAddress linearOnly = new(0x1000u); + + linearOnly.SegmentedAddress.Should().BeNull(); + linearOnly.Linear.Should().Be(0x1000u); + } +} diff --git a/tests/Spice86.Tests/Dos/DriveAbstractionTests.cs b/tests/Spice86.Tests/Dos/DriveAbstractionTests.cs index 1066f19890..f57e1ff365 100644 --- a/tests/Spice86.Tests/Dos/DriveAbstractionTests.cs +++ b/tests/Spice86.Tests/Dos/DriveAbstractionTests.cs @@ -12,7 +12,6 @@ namespace Spice86.Tests.Dos; /// /// Tests for drive abstraction strategy (host-backed vs memory-backed). -/// Phase 2 architecture: foundational for Z: memory drive and AUTOEXEC.BAT generation. /// public class DriveAbstractionTests { diff --git a/tests/Spice86.Tests/Dos/Xms/Xms32BitUnitTests.cs b/tests/Spice86.Tests/Dos/Xms/Xms32BitUnitTests.cs index 0704bd5c1e..671120d438 100644 --- a/tests/Spice86.Tests/Dos/Xms/Xms32BitUnitTests.cs +++ b/tests/Spice86.Tests/Dos/Xms/Xms32BitUnitTests.cs @@ -6,6 +6,7 @@ using NSubstitute; +using Spice86.Core.CLI; using Spice86.Core.Emulator.CPU; using Spice86.Core.Emulator.InterruptHandlers.Common.Callback; using Spice86.Core.Emulator.InterruptHandlers.Common.MemoryWriter; @@ -35,7 +36,7 @@ public Xms32BitFunctionsTests() { // Setup memory and state _state = new State(CpuModel.INTEL_80286); _a20Gate = new A20Gate(false); - _memory = new Memory(new(), new Ram(A20Gate.EndOfHighMemoryArea), _a20Gate, new RealModeMmu386(), false); + _memory = new Memory(new(), new Ram(Configuration.RamSizeDefaultKb * 1024), _a20Gate, new RealModeMmu386(), false); _loggerService = Substitute.For(); _callbackHandler = new CallbackHandler(_state, _loggerService); _dosTables = new DosTables(_memory); diff --git a/tests/Spice86.Tests/Dos/Xms/XmsUnitTests.cs b/tests/Spice86.Tests/Dos/Xms/XmsUnitTests.cs index 2d1bcf3ec3..3a313fb61b 100644 --- a/tests/Spice86.Tests/Dos/Xms/XmsUnitTests.cs +++ b/tests/Spice86.Tests/Dos/Xms/XmsUnitTests.cs @@ -6,6 +6,7 @@ using NSubstitute; +using Spice86.Core.CLI; using Spice86.Core.Emulator.CPU; using Spice86.Core.Emulator.InterruptHandlers.Common.Callback; using Spice86.Core.Emulator.InterruptHandlers.Common.MemoryWriter; @@ -37,7 +38,7 @@ public XmsUnitTests() { // Setup memory and state _state = new State(CpuModel.INTEL_80286); _a20Gate = new A20Gate(false); - _memory = new Memory(new(), new Ram(A20Gate.EndOfHighMemoryArea), _a20Gate, new RealModeMmu386(), false); + _memory = new Memory(new(), new Ram(Configuration.RamSizeDefaultKb * 1024), _a20Gate, new RealModeMmu386(), false); _loggerService = Substitute.For(); _callbackHandler = new CallbackHandler(_state, _loggerService); _dosTables = new DosTables(_memory); @@ -139,7 +140,7 @@ public void A20AlreadyEnabledAtStartup_ShouldPreventDisabling() { // Arrange - Create a new XMS manager with A20 already enabled State state = new State(CpuModel.INTEL_80286); A20Gate a20Gate = new A20Gate(true); // A20 is ALREADY enabled at startup - Memory memory = new Memory(new(), new Ram(A20Gate.EndOfHighMemoryArea), a20Gate, new RealModeMmu386(), false); + Memory memory = new Memory(new(), new Ram(Configuration.RamSizeDefaultKb * 1024), a20Gate, new RealModeMmu386(), false); ILogger loggerService = Substitute.For(); CallbackHandler callbackHandler = new CallbackHandler(state, loggerService); DosTables dosTables = new DosTables(memory); @@ -354,9 +355,11 @@ public void MoveExtendedMemoryBlock_ShouldMoveData() { _xms.RunMultiplex(); uint destAddress = MemoryUtils.To32BitAddress(_state.DX, _state.BX); - // Verify data was copied - _xms.XmsRam.Read(destAddress - A20Gate.StartOfHighMemoryArea).Should().Be(0x42, "First byte should be copied"); - _xms.XmsRam.Read(destAddress - A20Gate.StartOfHighMemoryArea + 1).Should().Be(0x43, "Second byte should be copied"); + // XMS block addresses are above 1MB (bit 20 set) - A20 must be enabled to read them + // through the shared, A20-gated memory bus. + _a20Gate.IsEnabled = true; + _memory.UInt8[destAddress].Should().Be(0x42, "First byte should be copied"); + _memory.UInt8[destAddress + 1].Should().Be(0x43, "Second byte should be copied"); } [Fact] @@ -629,9 +632,11 @@ public void MoveExtendedMemoryBlock_XmsToConventional_ShouldSucceed() { _xms.RunMultiplex(); uint srcAddress = MemoryUtils.To32BitAddress(_state.DX, _state.BX); - // Write test pattern to XMS memory - _xms.XmsRam.Write(srcAddress - A20Gate.StartOfHighMemoryArea, 0x77); - _xms.XmsRam.Write(srcAddress - A20Gate.StartOfHighMemoryArea + 1, 0x88); + // Write test pattern to XMS memory - block addresses are above 1MB (bit 20 set), so A20 + // must be enabled to reach them through the shared, A20-gated memory bus. + _a20Gate.IsEnabled = true; + _memory.UInt8[srcAddress] = 0x77; + _memory.UInt8[srcAddress + 1] = 0x88; // Create move structure uint moveStructAddr = 0x2000; @@ -675,9 +680,11 @@ public void MoveExtendedMemoryBlock_XmsToXms_ShouldSucceed() { _xms.RunMultiplex(); uint srcAddress = MemoryUtils.To32BitAddress(_state.DX, _state.BX); - // Write test pattern to source XMS memory - _xms.XmsRam.Write(srcAddress - A20Gate.StartOfHighMemoryArea, 0x12); - _xms.XmsRam.Write(srcAddress - A20Gate.StartOfHighMemoryArea + 1, 0x34); + // Write test pattern to source XMS memory - block addresses are above 1MB (bit 20 set), so + // A20 must be enabled to reach them through the shared, A20-gated memory bus. + _a20Gate.IsEnabled = true; + _memory.UInt8[srcAddress] = 0x12; + _memory.UInt8[srcAddress + 1] = 0x34; // Create move structure uint moveStructAddr = 0x2000; @@ -706,8 +713,8 @@ public void MoveExtendedMemoryBlock_XmsToXms_ShouldSucceed() { uint destAddress = MemoryUtils.To32BitAddress(_state.DX, _state.BX); // Verify data was copied - _xms.XmsRam.Read(destAddress - A20Gate.StartOfHighMemoryArea).Should().Be(0x12, "First byte should be copied"); - _xms.XmsRam.Read(destAddress - A20Gate.StartOfHighMemoryArea + 1).Should().Be(0x34, "Second byte should be copied"); + _memory.UInt8[destAddress].Should().Be(0x12, "First byte should be copied"); + _memory.UInt8[destAddress + 1].Should().Be(0x34, "Second byte should be copied"); } [Fact] diff --git a/tests/Spice86.Tests/GeneratedCodeMachineTest.cs b/tests/Spice86.Tests/GeneratedCodeMachineTest.cs index 3800b5342a..8db1203f0b 100644 --- a/tests/Spice86.Tests/GeneratedCodeMachineTest.cs +++ b/tests/Spice86.Tests/GeneratedCodeMachineTest.cs @@ -12,6 +12,7 @@ namespace Spice86.Tests; using Spice86.Core.Emulator.CPU; using Spice86.Core.Emulator.Function; using Spice86.Core.Emulator.IOPorts; +using Spice86.Core.Emulator.Memory; using Spice86.Core.Emulator.ReverseEngineer.CfgCodeGeneration; using Spice86.Core.Emulator.ReverseEngineer.CfgCodeGeneration.Model; using Spice86.Core.Emulator.ReverseEngineer.ControlFlowGraph; @@ -503,4 +504,156 @@ public void Test386ButNotProtectedModeGeneratedOverrideCompilesAndReachesPostFin postHandler.PostValues[^1].Should().Be(0xFF); }); } + + [Fact] + public void ProtectedModeInstructionsGeneratedOverrideCompilesAndMatchesMachineTestOracle() { + GeneratedCodeRunOptions options = new() { + MaxCycles = 1000, + CpuModel = CpuModel.INTEL_80386, + EnableSpeculativeCfgExploration = false + }; + + new GeneratedCodeMachineTestRunner().TestGeneratedCode("protectedmode_instructions", [], options, machine => { + IMemory memory = machine.Memory; + memory.UInt16[0, 0x0700].Should().Be(0x18); + memory.UInt16[0, 0x0702].Should().Be(0x18); + memory.UInt8[0, 0x0704].Should().Be(1); + memory.UInt8[0, 0x0705].Should().Be(0); + memory.UInt8[0, 0x0706].Should().Be(1); + memory.UInt8[0, 0x0707].Should().Be(0); + memory.UInt16[0, 0x0708].Should().Be(0x9200); + memory.UInt16[0, 0x070A].Should().Be(0xFFFF); + memory.UInt16[0, 0x070C].Should().Be(0x000B); + memory.UInt8[0, 0x070E].Should().Be(1); + memory.UInt16[0, 0x0710].Should().Be(0x0001); + memory.UInt16[0, 0x0712].Should().Be(0x0009); + memory.UInt16[0, 0x0714].Should().Be(0x0001); + }); + } + + [Fact] + public void ProtectedModePrivilegeHappyPathGeneratedOverrideCompilesAndMatchesMachineTestOracle() { + GeneratedCodeRunOptions options = new() { + MaxCycles = 1000, + CpuModel = CpuModel.INTEL_80386, + EnableSpeculativeCfgExploration = false + }; + + new GeneratedCodeMachineTestRunner().TestGeneratedCode("protectedmode_privilege", [], options, machine => { + machine.CpuState.ControlRegisters.ProtectionEnable.Should().BeFalse(); + machine.CpuState.CS.Should().Be(0xF000); + machine.CpuState.InterruptFlag.Should().BeTrue(); + }); + } + + [Fact] + public void ProtectedModeIdtDispatchGeneratedOverrideCompilesAndMatchesMachineTestOracle() { + GeneratedCodeRunOptions options = new() { + MaxCycles = 1000, + CpuModel = CpuModel.INTEL_80386, + EnableSpeculativeCfgExploration = false + }; + + new GeneratedCodeMachineTestRunner().TestGeneratedCode("protectedmode_idt", [], options, machine => { + machine.CpuState.ControlRegisters.ProtectionEnable.Should().BeFalse(); + machine.CpuState.CS.Should().Be(0xF000); + machine.Memory.ReadRam(3)[2].Should().Be(0x99); + machine.Memory.ReadRam(4)[3].Should().Be(0x77); + }); + } + + [Fact] + public void ProtectedModeFarTransferPrivilegeViolationGeneratedOverrideCompilesAndMatchesMachineTestOracle() { + GeneratedCodeRunOptions options = new() { + MaxCycles = 1000, + CpuModel = CpuModel.INTEL_80386, + EnableSpeculativeCfgExploration = false + }; + + new GeneratedCodeMachineTestRunner().TestGeneratedCode("protectedmode_far_privilege", [], options, machine => { + machine.Memory.ReadRam(5)[4].Should().Be(0xDD); + }); + } + + [Fact] + public void ProtectedModeCallGateGeneratedOverrideCompilesAndMatchesMachineTestOracle() { + GeneratedCodeRunOptions options = new() { + MaxCycles = 1000, + CpuModel = CpuModel.INTEL_80386, + EnableSpeculativeCfgExploration = false + }; + + new GeneratedCodeMachineTestRunner().TestGeneratedCode("protectedmode_callgate", [], options, machine => { + machine.CpuState.ControlRegisters.ProtectionEnable.Should().BeFalse(); + machine.CpuState.CS.Should().Be(0xF000); + machine.Memory.ReadRam(7)[6].Should().Be(0xAB); + machine.Memory.ReadRam(8)[7].Should().Be(0xCD); + machine.Memory.ReadRam(9)[8].Should().Be(0xEF); + }); + } + + [Fact] + public void ProtectedModePagingGeneratedOverrideCompilesAndMatchesMachineTestOracle() { + GeneratedCodeRunOptions options = new() { + MaxCycles = 1000, + CpuModel = CpuModel.INTEL_80386, + EnableSpeculativeCfgExploration = false + }; + + new GeneratedCodeMachineTestRunner().TestGeneratedCode("protectedmode_paging", [], options, machine => { + machine.CpuState.ControlRegisters.PagingEnable.Should().BeTrue(); + machine.Memory.ReadRam(0xF0011)[0xF0010].Should().Be(0xAA); + machine.Memory.ReadRam(0xF0021)[0xF0020].Should().Be(0xCC); + machine.CpuState.ControlRegisters.Cr2.Should().Be(0xF1000u); + }); + } + + [Fact] + public void ProtectedModeTaskSwitchGeneratedOverrideCompilesAndMatchesMachineTestOracle() { + GeneratedCodeRunOptions options = new() { + MaxCycles = 1000, + CpuModel = CpuModel.INTEL_80386, + EnableSpeculativeCfgExploration = false + }; + + new GeneratedCodeMachineTestRunner().TestGeneratedCode("protectedmode_taskswitch", [], options, machine => { + machine.Memory.ReadRam(0xF0031)[0xF0030].Should().Be(0x11); + machine.Memory.ReadRam(0xF0032)[0xF0031].Should().Be(0x22); + machine.Memory.ReadRam(0xF0033)[0xF0032].Should().Be(0x33); + machine.CpuState.EAX.Should().Be(0x11111111u); + machine.CpuState.Tr.Selector.Should().Be(0x18); + }); + } + + [Fact] + public void ProtectedModeV86GeneratedOverrideCompilesAndMatchesMachineTestOracle() { + GeneratedCodeRunOptions options = new() { + MaxCycles = 1000, + CpuModel = CpuModel.INTEL_80386, + EnableSpeculativeCfgExploration = false + }; + + new GeneratedCodeMachineTestRunner().TestGeneratedCode("protectedmode_v86", [], options, machine => { + machine.Memory.ReadRam(0xF0031)[0xF0030].Should().Be(0x11); + machine.Memory.ReadRam(0xF0035)[0xF0034].Should().Be(0x44); + machine.Memory.ReadRam(0xF0037)[0xF0036].Should().Be(0x55); + }); + } + + [Fact] + public void ProtectedModeTaskGateGeneratedOverrideCompilesAndMatchesMachineTestOracle() { + GeneratedCodeRunOptions options = new() { + MaxCycles = 1000, + CpuModel = CpuModel.INTEL_80386, + EnableSpeculativeCfgExploration = false + }; + + new GeneratedCodeMachineTestRunner().TestGeneratedCode("protectedmode_taskgate", [], options, machine => { + machine.Memory.ReadRam(0xF0031)[0xF0030].Should().Be(0x11); + machine.Memory.ReadRam(0xF0032)[0xF0031].Should().Be(0x22); + machine.Memory.ReadRam(0xF0033)[0xF0032].Should().Be(0x33); + machine.CpuState.EAX.Should().Be(0x11111111u); + machine.CpuState.Tr.Selector.Should().Be(0x18); + }); + } } \ No newline at end of file diff --git a/tests/Spice86.Tests/GeneratedCodeMachineTestRunner.cs b/tests/Spice86.Tests/GeneratedCodeMachineTestRunner.cs index c7d64b2149..00cedf884f 100644 --- a/tests/Spice86.Tests/GeneratedCodeMachineTestRunner.cs +++ b/tests/Spice86.Tests/GeneratedCodeMachineTestRunner.cs @@ -32,7 +32,7 @@ public void TestGeneratedCode(string binName, byte[] expected, GeneratedCodeRunO using Spice86Creator creator = new(binName: binName, maxCycles: options.MaxCycles, enablePit: options.EnablePit, installInterruptVectors: options.InstallInterruptVectors, failOnUnhandledPort: options.FailOnUnhandledPort, enableA20Gate: options.EnableA20Gate, jitMode: JitMode.InterpretedOnly, overrideSupplier: compiledOverride.Supplier, - enableSpeculativeCfgExploration: options.EnableSpeculativeCfgExploration); + enableSpeculativeCfgExploration: options.EnableSpeculativeCfgExploration, cpuModel: options.CpuModel); using Spice86DependencyInjection spice86DependencyInjection = creator.Create(); options.ConfigureMachine?.Invoke(spice86DependencyInjection.Machine); spice86DependencyInjection.FunctionCatalogue.FunctionInformations.Values @@ -81,7 +81,7 @@ private static CfgPartitionedProgram GenerateProgram(string binName, GeneratedCo using Spice86Creator creator = new(binName: binName, maxCycles: options.MaxCycles, enablePit: options.EnablePit, installInterruptVectors: options.InstallInterruptVectors, failOnUnhandledPort: options.FailOnUnhandledPort, enableA20Gate: options.EnableA20Gate, jitMode: JitMode.InterpretedOnly, - enableSpeculativeCfgExploration: options.EnableSpeculativeCfgExploration); + enableSpeculativeCfgExploration: options.EnableSpeculativeCfgExploration, cpuModel: options.CpuModel); using Spice86DependencyInjection spice86DependencyInjection = creator.Create(); options.ConfigureMachine?.Invoke(spice86DependencyInjection.Machine); spice86DependencyInjection.ProgramExecutor.Run(); diff --git a/tests/Spice86.Tests/GeneratedCodeRunOptions.cs b/tests/Spice86.Tests/GeneratedCodeRunOptions.cs index 58ae2a3704..54ceab1e57 100644 --- a/tests/Spice86.Tests/GeneratedCodeRunOptions.cs +++ b/tests/Spice86.Tests/GeneratedCodeRunOptions.cs @@ -1,5 +1,6 @@ namespace Spice86.Tests; +using Spice86.Core.Emulator.CPU; using Spice86.Core.Emulator.VM; internal sealed class GeneratedCodeRunOptions { @@ -9,6 +10,7 @@ internal sealed class GeneratedCodeRunOptions { public bool InstallInterruptVectors { get; init; } public bool FailOnUnhandledPort { get; init; } public bool EnableSpeculativeCfgExploration { get; init; } = true; + public CpuModel CpuModel { get; init; } = CpuModel.ZET_86; /// /// Optional hook invoked on the freshly created machine before the program runs, for both the discovery /// run and the generated-code run. Used to install custom I/O port handlers (e.g. the test386 POST port). diff --git a/tests/Spice86.Tests/LinearAddressOverrideTest.cs b/tests/Spice86.Tests/LinearAddressOverrideTest.cs new file mode 100644 index 0000000000..4dadbb89c5 --- /dev/null +++ b/tests/Spice86.Tests/LinearAddressOverrideTest.cs @@ -0,0 +1,87 @@ +using Spice86.Core.CLI; + +namespace Spice86.Tests; + +using FluentAssertions; + +using Microsoft.Extensions.Logging; + +using NSubstitute; + +using Spice86.Core.Emulator.Function; +using Spice86.Core.Emulator.ReverseEngineer; +using Spice86.Core.Emulator.VM; +using Spice86.Shared.Emulator.Memory; + +using System.Collections.Generic; + +using Xunit; + +/// +/// Verifies linear-address override registration +/// ( / +/// ): an override registered by flat linear +/// address keeps firing after a GDT descriptor edit repoints a DIFFERENT selector's base to alias the +/// same linear address. +/// +public class LinearAddressOverrideTest { + private readonly ILogger _loggerServiceMock = Substitute.For(); + + [Fact] + public void LinearOverride_SurvivesDescriptorEditRepointingADifferentSelector() { + using Spice86Creator creator = new Spice86Creator(binName: "jump2"); + using Spice86DependencyInjection res = creator.Create(); + Machine machine = res.Machine; + + const uint gdtBase = 0x600; + const ushort selectorA = 0x08; + const ushort selectorB = 0x10; + const ushort offset = 0x0050; + const uint linearAddress = 0x1000 + offset; + + machine.CpuState.ControlRegisters.Cr0 = 1; // PE=1, enter protected mode + machine.CpuState.Gdtr.Base = gdtBase; + machine.CpuState.Gdtr.Limit = 0x17; // 3 entries (null, A, B) + WriteCodeDescriptor(machine, gdtBase, selectorA, @base: 0x1000); + WriteCodeDescriptor(machine, gdtBase, selectorB, @base: 0x2000); + + LinearOverrideProbe probe = new(new Dictionary(), machine, _loggerServiceMock, new Configuration { HttpApiPort = 0 }); + probe.DefineFunction(linearAddress, probe.TargetFunction, name: "TargetFunction"); + + probe.SearchFunctionOverride(new SegmentedAddress(selectorA, offset)).Should().NotBeNull(); + probe.SearchFunctionOverride(new SegmentedAddress(selectorB, offset)).Should().BeNull(); + + // Repoint selector B's base to alias the same linear address as selector A, as if a running + // protected-mode program edited its own GDT. + WriteCodeDescriptor(machine, gdtBase, selectorB, @base: 0x1000); + + Func? foundViaB = probe.SearchFunctionOverride(new SegmentedAddress(selectorB, offset)); + foundViaB.Should().NotBeNull(); + foundViaB!(0); + probe.TargetFunctionCalled.Should().Be(1); + } + + private static void WriteCodeDescriptor(Machine machine, uint gdtBase, ushort selector, uint @base) { + uint entryOffset = gdtBase + (uint)(selector >> 3) * 8u; + machine.Memory.UInt16[entryOffset] = 0xFFFF; // limit_low + machine.Memory.UInt16[entryOffset + 2] = (ushort)@base; // base_low + machine.Memory[entryOffset + 4] = (byte)(@base >> 16); // base_mid + machine.Memory[entryOffset + 5] = 0x9A; // present, DPL0, code, executable, readable + machine.Memory[entryOffset + 6] = 0x00; // no granularity/big flags + machine.Memory[entryOffset + 7] = (byte)(@base >> 24); // base_high + } +} + +class LinearOverrideProbe : CSharpOverrideHelper { + public int TargetFunctionCalled { get; private set; } + + public LinearOverrideProbe(IDictionary functionInformations, + Machine machine, ILogger loggerService, Configuration configuration) + : base(functionInformations, machine, loggerService, configuration) { + } + + public Action TargetFunction(int loadOffset) { + TargetFunctionCalled++; + return NearRet(); + } +} diff --git a/tests/Spice86.Tests/MachineTest.cs b/tests/Spice86.Tests/MachineTest.cs index 2d4eaab918..9d3277c32a 100755 --- a/tests/Spice86.Tests/MachineTest.cs +++ b/tests/Spice86.Tests/MachineTest.cs @@ -1001,9 +1001,269 @@ public void Test386ButNotProtectedMode(JitMode jitMode) { CompareCfgBlocksJsonWithExpected(binName, machine); } + [Fact] + public void TestProtectedModeEntry() { + //Arrange + using Spice86Creator creator = new Spice86Creator( + binName: "protectedmode_entry", cpuModel: CpuModel.INTEL_80386, maxCycles: 1000, + enableSpeculativeCfgExploration: false); + using Spice86DependencyInjection spice86DependencyInjection = creator.Create(); + Machine machine = spice86DependencyInjection.Machine; + + //Act + spice86DependencyInjection.ProgramExecutor.Run(); + + //Assert + State state = machine.CpuState; + // The marker byte was written to physical address 0 through a protected-mode flat data selector. + Assert.Equal(0x42, machine.Memory.ReadRam(1)[0]); + // Execution returned to real mode and reloaded CS as a real-mode segment before halting. + Assert.False(state.ControlRegisters.ProtectionEnable); + Assert.Equal(0xF000, state.CS); + } + + [Fact] + public void TestUnifiedExtendedMemoryPoolIsAddressable() { + //Arrange + using Spice86Creator creator = new Spice86Creator( + binName: "protectedmode_unified_pool", cpuModel: CpuModel.INTEL_80386, maxCycles: 1000, + enableSpeculativeCfgExploration: false, enableA20Gate: true); + using Spice86DependencyInjection spice86DependencyInjection = creator.Create(); + Machine machine = spice86DependencyInjection.Machine; + + //Act + spice86DependencyInjection.ProgramExecutor.Run(); + + //Assert + State state = machine.CpuState; + // The marker byte was written to physical address 0x500000, inside the unified pool. + Assert.Equal(0x42, machine.Memory.ReadRam(0x500001)[0x500000]); + Assert.False(state.ControlRegisters.ProtectionEnable); + Assert.Equal(0xF000, state.CS); + } + + [Fact] + public void TestProtectedModeInstructions() { + //Arrange + using Spice86Creator creator = new Spice86Creator( + binName: "protectedmode_instructions", cpuModel: CpuModel.INTEL_80386, maxCycles: 1000, + enableSpeculativeCfgExploration: false); + using Spice86DependencyInjection spice86DependencyInjection = creator.Create(); + Machine machine = spice86DependencyInjection.Machine; + + //Act + spice86DependencyInjection.ProgramExecutor.Run(); + + //Assert + State state = machine.CpuState; + IMemory memory = machine.Memory; + Assert.False(state.ControlRegisters.ProtectionEnable); + Assert.Equal(0xF000, state.CS); + + Assert.Equal(0x18, memory.UInt16[0, 0x0700]); // SLDT: LDTR selector loaded via LLDT + Assert.Equal(0x18, memory.UInt16[0, 0x0702]); // STR: TR selector loaded via LTR + Assert.Equal(1, memory.UInt8[0, 0x0704]); // VERR(0x10): flat data segment is readable + Assert.Equal(0, memory.UInt8[0, 0x0705]); // VERR(0x28): non-readable code segment + Assert.Equal(1, memory.UInt8[0, 0x0706]); // VERW(0x10): flat data segment is writable + Assert.Equal(0, memory.UInt8[0, 0x0707]); // VERW(0x20): read-only data segment + Assert.Equal(0x9200, memory.UInt16[0, 0x0708]); // LAR(0x10): packed access-rights byte 0x92 + Assert.Equal(0xFFFF, memory.UInt16[0, 0x070A]); // LSL(0x10): byte-granular 0xFFFF limit + Assert.Equal(0x000B, memory.UInt16[0, 0x070C]); // ARPL: RPL raised from 0 to 3 + Assert.Equal(1, memory.UInt8[0, 0x070E]); // ARPL: ZF set because the RPL was adjusted + Assert.Equal(0x0001, memory.UInt16[0, 0x0710]); // SMSW right after entering protected mode: PE=1, TS=0 + Assert.Equal(0x0009, memory.UInt16[0, 0x0712]); // SMSW after LMSW: PE=1, TS=1 + Assert.Equal(0x0001, memory.UInt16[0, 0x0714]); // SMSW after CLTS: TS cleared back to 0 + } + + [Fact] + public void TestProtectedModePrivilegeHappyPath() { + //Arrange + using Spice86Creator creator = new Spice86Creator( + binName: "protectedmode_privilege", cpuModel: CpuModel.INTEL_80386, maxCycles: 1000, + enableSpeculativeCfgExploration: false); + using Spice86DependencyInjection spice86DependencyInjection = creator.Create(); + Machine machine = spice86DependencyInjection.Machine; + + //Act + spice86DependencyInjection.ProgramExecutor.Run(); + + //Assert + State state = machine.CpuState; + Assert.False(state.ControlRegisters.ProtectionEnable); + Assert.Equal(0xF000, state.CS); + Assert.True(state.InterruptFlag); // STI ran after CLI, so IF ends up set + } + + [Fact] + public void TestProtectedModeIdtDispatch() { + //Arrange + using Spice86Creator creator = new Spice86Creator( + binName: "protectedmode_idt", cpuModel: CpuModel.INTEL_80386, maxCycles: 1000, + enableSpeculativeCfgExploration: false); + using Spice86DependencyInjection spice86DependencyInjection = creator.Create(); + Machine machine = spice86DependencyInjection.Machine; + + //Act + spice86DependencyInjection.ProgramExecutor.Run(); + + //Assert + State state = machine.CpuState; + Assert.False(state.ControlRegisters.ProtectionEnable); + Assert.Equal(0xF000, state.CS); + Assert.Equal(0x99, machine.Memory.ReadRam(3)[2]); // the IDT-dispatched handler ran + Assert.Equal(0x77, machine.Memory.ReadRam(4)[3]); // IRET resumed at the correct return address + } + + [Fact] + public void TestProtectedModeFarTransferPrivilegeViolation() { + //Arrange + using Spice86Creator creator = new Spice86Creator( + binName: "protectedmode_far_privilege", cpuModel: CpuModel.INTEL_80386, maxCycles: 1000, + enableSpeculativeCfgExploration: false); + using Spice86DependencyInjection spice86DependencyInjection = creator.Create(); + Machine machine = spice86DependencyInjection.Machine; + + //Act + spice86DependencyInjection.ProgramExecutor.Run(); + + //Assert + Assert.Equal(0xDD, machine.Memory.ReadRam(5)[4]); // the #GP handler ran instead of the faulting jump succeeding + } + + [Fact] + public void TestProtectedModeCallGate() { + //Arrange + using Spice86Creator creator = new Spice86Creator( + binName: "protectedmode_callgate", cpuModel: CpuModel.INTEL_80386, maxCycles: 1000, + enableSpeculativeCfgExploration: false); + using Spice86DependencyInjection spice86DependencyInjection = creator.Create(); + Machine machine = spice86DependencyInjection.Machine; + + //Act + spice86DependencyInjection.ProgramExecutor.Run(); + + //Assert + State state = machine.CpuState; + Assert.False(state.ControlRegisters.ProtectionEnable); + Assert.Equal(0xF000, state.CS); + Assert.Equal(0xAB, machine.Memory.ReadRam(7)[6]); // reached ring 3 via the bootstrap RETF + Assert.Equal(0xCD, machine.Memory.ReadRam(8)[7]); // reached ring 0 via the call gate + Assert.Equal(0xEF, machine.Memory.ReadRam(9)[8]); // resumed at the correct ring-3 address after the call + } + + [Fact] + public void TestProtectedModePaging() { + //Arrange + using Spice86Creator creator = new Spice86Creator( + binName: "protectedmode_paging", cpuModel: CpuModel.INTEL_80386, maxCycles: 1000, + enableSpeculativeCfgExploration: false); + using Spice86DependencyInjection spice86DependencyInjection = creator.Create(); + Machine machine = spice86DependencyInjection.Machine; + + //Act + spice86DependencyInjection.ProgramExecutor.Run(); + + //Assert + State state = machine.CpuState; + Assert.True(state.ControlRegisters.PagingEnable); + Assert.Equal(0xAA, machine.Memory.ReadRam(0xF0011)[0xF0010]); // normal access through the identity mapping + Assert.Equal(0xCC, machine.Memory.ReadRam(0xF0021)[0xF0020]); // the #PF handler ran + Assert.Equal(0xF1000u, state.ControlRegisters.Cr2); // CR2 holds the faulting linear address + } + + [Fact] + public void TestProtectedModeTaskSwitch() { + //Arrange + using Spice86Creator creator = new Spice86Creator( + binName: "protectedmode_taskswitch", cpuModel: CpuModel.INTEL_80386, maxCycles: 1000, + enableSpeculativeCfgExploration: false); + using Spice86DependencyInjection spice86DependencyInjection = creator.Create(); + Machine machine = spice86DependencyInjection.Machine; + + //Act + spice86DependencyInjection.ProgramExecutor.Run(); + + //Assert + State state = machine.CpuState; + Assert.Equal(0x11, machine.Memory.ReadRam(0xF0031)[0xF0030]); // task A ran before the switch + Assert.Equal(0x22, machine.Memory.ReadRam(0xF0032)[0xF0031]); // task B ran after the switch + Assert.Equal(0x33, machine.Memory.ReadRam(0xF0033)[0xF0032]); // task A resumed after IRET switched back + Assert.Equal(0x11111111u, state.EAX); // EAX survived the round trip through TSS A's save/restore + Assert.Equal(0x18, state.Tr.Selector); // TR is back on task A after the switch-back + byte tssATypeByte = machine.Memory.ReadRam(0x61E)[0x61D]; + byte tssBTypeByte = machine.Memory.ReadRam(0x626)[0x625]; + Assert.Equal(0x89, tssATypeByte); // task A's own busy bit is never touched (CALL-triggered switches don't clear it) + Assert.Equal(0x89, tssBTypeByte); // task B's descriptor is available again (its busy bit was cleared on return) + } + + [Fact] + public void TestProtectedModeV86() { + //Arrange + using Spice86Creator creator = new Spice86Creator( + binName: "protectedmode_v86", cpuModel: CpuModel.INTEL_80386, maxCycles: 1000, + enableSpeculativeCfgExploration: false); + using Spice86DependencyInjection spice86DependencyInjection = creator.Create(); + Machine machine = spice86DependencyInjection.Machine; + + //Act + spice86DependencyInjection.ProgramExecutor.Run(); + + //Assert + Assert.Equal(0x11, machine.Memory.ReadRam(0xF0031)[0xF0030]); // task A ran before the task switch + Assert.Equal(0x44, machine.Memory.ReadRam(0xF0035)[0xF0034]); // V86 code ran before faulting + Assert.Equal(0x55, machine.Memory.ReadRam(0xF0037)[0xF0036]); // the reflected #GP reached the ring-0 IDT handler + } + + [Theory] + [MemberData(nameof(JitModes))] + public void Test386ProtectedMode(JitMode jitMode) { + string binName = "test386_pmode"; + using Spice86Creator creator = new Spice86Creator( + binName: binName, cpuModel: CpuModel.INTEL_80386, + enablePit: false, maxCycles: long.MaxValue, + failOnUnhandledPort: true, jitMode: jitMode); + using Spice86DependencyInjection spice86DependencyInjection = creator.Create(); + Machine machine = spice86DependencyInjection.Machine; + using LoggerService loggerService = new(); + Test386ButNotProtectedModeHandler debugPortsHandler = new(machine.CpuState, loggerService, machine.IoPortDispatcher); + + spice86DependencyInjection.ProgramExecutor.Run(); + + List expectedPostCheckpoints = [ + 0, 1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, + 20, 21, 22, 23, 24, 25, 26, 27, 28, 224, 238, 255 + ]; + Assert.Equal(expectedPostCheckpoints, debugPortsHandler.PostValues); + } + + [Fact] + public void TestProtectedModeTaskGate() { + //Arrange + using Spice86Creator creator = new Spice86Creator( + binName: "protectedmode_taskgate", cpuModel: CpuModel.INTEL_80386, maxCycles: 1000, + enableSpeculativeCfgExploration: false); + using Spice86DependencyInjection spice86DependencyInjection = creator.Create(); + Machine machine = spice86DependencyInjection.Machine; + + //Act + spice86DependencyInjection.ProgramExecutor.Run(); + + //Assert + State state = machine.CpuState; + Assert.Equal(0x11, machine.Memory.ReadRam(0xF0031)[0xF0030]); // task A ran before INT 0x40 + Assert.Equal(0x22, machine.Memory.ReadRam(0xF0032)[0xF0031]); // task B ran after the task-gate switch + Assert.Equal(0x33, machine.Memory.ReadRam(0xF0033)[0xF0032]); // task A resumed after IRET switched back + Assert.Equal(0x11111111u, state.EAX); // EAX survived the round trip through TSS A's save/restore + Assert.Equal(0x18, state.Tr.Selector); // TR is back on task A after the switch-back + } + private class Test386ButNotProtectedModeHandler : DefaultIOPortHandler { private const int PostPort = 0x999; private const int AsciiOutPort = 0x998; + // test386.asm's printChar routine does `out OUT_PORT, al` with the 8-bit-immediate OUT opcode, + // which NASM truncates OUT_PORT (0x998) down to its low byte (0x98) - the truncated port must + // be wired to the same ASCII buffer or protected-mode-only debug output crashes as unhandled. + private const int TruncatedAsciiOutPort = 0x98; public List PostValues { get; } = new(); public string AsciiError { get; private set; } = ""; @@ -1012,16 +1272,16 @@ public Test386ButNotProtectedModeHandler(State state, ILogger loggerService, IOPortDispatcher ioPortDispatcher) : base(state, true, loggerService) { ioPortDispatcher.AddIOPortHandler(PostPort, this); ioPortDispatcher.AddIOPortHandler(AsciiOutPort, this); + ioPortDispatcher.AddIOPortHandler(TruncatedAsciiOutPort, this); } public override void WriteByte(ushort port, byte value) { - if (port == AsciiOutPort) { + if (port == AsciiOutPort || port == TruncatedAsciiOutPort) { AsciiError += Encoding.ASCII.GetString(new byte[] { value }); } else if (port == PostPort) { if (PostValues.Contains(value)) { throw new UnhandledOperationException(_state, $"POST value {value} already sent. Is test looping?"); } - PostValues.Add(value); } } diff --git a/tests/Spice86.Tests/McpIntegrationContext.cs b/tests/Spice86.Tests/McpIntegrationContext.cs index fd6f06c8e3..6f56b7bd4c 100644 --- a/tests/Spice86.Tests/McpIntegrationContext.cs +++ b/tests/Spice86.Tests/McpIntegrationContext.cs @@ -1,5 +1,6 @@ namespace Spice86.Tests; +using Spice86.Core.CLI; using Spice86.Core.Emulator.Devices.Sound; using Spice86.Core.Emulator.Devices.Sound.Blaster; using Spice86.Core.Emulator.InterruptHandlers.Dos.Ems; @@ -93,7 +94,8 @@ public static async Task CreateAsync( bool enableEms, bool initializeDos, SbType sbType, - OplMode oplMode) { + OplMode oplMode, + int ramSizeKb = Configuration.RamSizeDefaultKb) { Spice86Creator creator = new( testProgramName, enablePit: false, @@ -101,7 +103,8 @@ public static async Task CreateAsync( enableXms: enableXms, enableEms: enableEms, sbType: sbType, - oplMode: oplMode); + oplMode: oplMode, + ramSizeKb: ramSizeKb); Spice86DependencyInjection spice86 = creator.Create(); EmulatorMcpServices services = spice86.McpServices; diff --git a/tests/Spice86.Tests/McpServerToolStateTests.cs b/tests/Spice86.Tests/McpServerToolStateTests.cs index 5346dd2039..e4881e397b 100644 --- a/tests/Spice86.Tests/McpServerToolStateTests.cs +++ b/tests/Spice86.Tests/McpServerToolStateTests.cs @@ -966,10 +966,14 @@ private static int PrepareXmsBlockWithPattern(McpIntegrationContext context) { return 0; } - xmsManager.XmsRam.Write(block.Value.Offset + 0, 0xDE); - xmsManager.XmsRam.Write(block.Value.Offset + 1, 0xAD); - xmsManager.XmsRam.Write(block.Value.Offset + 2, 0xBE); - xmsManager.XmsRam.Write(block.Value.Offset + 3, 0xEF); + uint blockAddress = ExtendedMemoryManager.XmsBaseAddress + block.Value.Offset; + // XMS block addresses are above 1MB (bit 20 set) - A20 must be enabled to reach them + // through the shared, A20-gated memory bus. + context.Services.Memory.A20Gate.IsEnabled = true; + context.Services.Memory.UInt8[blockAddress + 0] = 0xDE; + context.Services.Memory.UInt8[blockAddress + 1] = 0xAD; + context.Services.Memory.UInt8[blockAddress + 2] = 0xBE; + context.Services.Memory.UInt8[blockAddress + 3] = 0xEF; return handle; } @@ -1410,8 +1414,12 @@ public async Task ReadDisassembly_ShouldDisassembleMemoryAtAddressAsync() { [Fact] public async Task ReadDisassembly_ShouldTruncateNearMemoryBoundaryAsync() { - // Arrange - await using McpIntegrationContext context = await McpIntegrationContext.CreateAsync(TestProgramName); + // Arrange - a RAM size matching the old conventional+HMA-only ceiling, so the fixed + // 16-bit segment:offset boundary below (the tool's own address-space limit) sits right at + // the edge of memory again, reproducing the truncation scenario deterministically. + await using McpIntegrationContext context = await McpIntegrationContext.CreateAsync( + TestProgramName, enableXms: false, enableEms: false, initializeDos: false, + sbType: SbType.None, oplMode: OplMode.None, ramSizeKb: 1087); await context.InitializeAsync(); // Act — disassemble at the very end of addressable memory (FFFF:FFFF physical = 0x10FFEF) diff --git a/tests/Spice86.Tests/PagingUnitTests.cs b/tests/Spice86.Tests/PagingUnitTests.cs new file mode 100644 index 0000000000..85b01eb5ba --- /dev/null +++ b/tests/Spice86.Tests/PagingUnitTests.cs @@ -0,0 +1,212 @@ +namespace Spice86.Tests; + +using FluentAssertions; + +using Spice86.Core.Emulator.CPU; +using Spice86.Core.Emulator.CPU.Exceptions; +using Spice86.Core.Emulator.Memory; +using Spice86.Core.Emulator.Memory.Mmu; + +using Xunit; + +public class PagingUnitTests { + private const uint PageDirectoryBase = 0x1000; + private const uint PageTableBase = 0x2000; + private const uint PageBase = 0x3000; + private const uint WriteBit = 0x2; + private const uint AccessedBit = 0x20; + private const uint DirtyBit = 0x40; + + private static (State state, Ram ram, PagingUnit unit) CreateIdentityMappedSetup(bool userAccessible) { + State state = new(CpuModel.INTEL_80386); + Ram ram = new(0x10000); + state.ControlRegisters.Cr3 = PageDirectoryBase; + + uint userSupervisorBit = userAccessible ? 0b100u : 0u; + WriteUInt32(ram, PageDirectoryBase, PageTableBase | 0b1u | userSupervisorBit); // present + WriteUInt32(ram, PageTableBase, PageBase | 0b1u | userSupervisorBit); // present + + PagingUnit unit = new(state, ram); + return (state, ram, unit); + } + + private static void WriteUInt32(Ram ram, uint address, uint value) { + ram.Write(address, (byte)value); + ram.Write(address + 1, (byte)(value >> 8)); + ram.Write(address + 2, (byte)(value >> 16)); + ram.Write(address + 3, (byte)(value >> 24)); + } + + [Fact] + public void Translate_PresentEntries_ReturnsPhysicalAddressWithOffset() { + (State state, _, PagingUnit unit) = CreateIdentityMappedSetup(userAccessible: true); + state.ControlRegisters.Cr0 = 1; // PE=1 + state.CS = 0; // CPL 0 + + uint physicalAddress = unit.Translate(0x0234, isWrite: false); + + physicalAddress.Should().Be(PageBase + 0x234); + } + + [Fact] + public void Translate_PageDirectoryEntryNotPresent_ThrowsPageFaultWithNotPresentErrorCode() { + State state = new(CpuModel.INTEL_80386); + Ram ram = new(0x10000); + state.ControlRegisters.Cr3 = PageDirectoryBase; + // Page directory entry left at 0 (not present). + PagingUnit unit = new(state, ram); + + Action act = () => unit.Translate(0x0234, isWrite: false); + + act.Should().Throw() + .Which.ErrorCode.Should().Be(0); + state.ControlRegisters.Cr2.Should().Be(0x0234u); + } + + [Fact] + public void Translate_PageTableEntryNotPresent_ThrowsPageFaultWithNotPresentErrorCode() { + State state = new(CpuModel.INTEL_80386); + Ram ram = new(0x10000); + state.ControlRegisters.Cr3 = PageDirectoryBase; + WriteUInt32(ram, PageDirectoryBase, PageTableBase | 0b1u | 0b100u); // present, user-accessible + // Page table entry left at 0 (not present). + PagingUnit unit = new(state, ram); + + Action act = () => unit.Translate(0x0234, isWrite: false); + + act.Should().Throw() + .Which.ErrorCode.Should().Be(0); + } + + [Fact] + public void Translate_PageTableEntryNotPresentOnWrite_ThrowsPageFaultWithWriteBitSet() { + State state = new(CpuModel.INTEL_80386); + Ram ram = new(0x10000); + state.ControlRegisters.Cr3 = PageDirectoryBase; + WriteUInt32(ram, PageDirectoryBase, PageTableBase | 0b1u | 0b100u); // present, user-accessible + // Page table entry left at 0 (not present). + PagingUnit unit = new(state, ram); + + Action act = () => unit.Translate(0x0234, isWrite: true); + + act.Should().Throw() + .Which.ErrorCode.Should().Be(0b010); // P=0 (not present), W/R=1 (write) + } + + [Fact] + public void Translate_SupervisorOnlyPageAccessedFromUserMode_ThrowsPageFaultWithProtectionAndUserBits() { + (State state, _, PagingUnit unit) = CreateIdentityMappedSetup(userAccessible: false); + state.ControlRegisters.Cr0 = 1; // PE=1 + state.CS = 3; // CPL 3 + + Action act = () => unit.Translate(0x0234, isWrite: false); + + act.Should().Throw() + .Which.ErrorCode.Should().Be(0b101); // P=1 (protection violation), U/S=1 (user mode) + } + + [Fact] + public void Translate_SupervisorOnlyPageAccessedFromSupervisorMode_Succeeds() { + (State state, _, PagingUnit unit) = CreateIdentityMappedSetup(userAccessible: false); + state.ControlRegisters.Cr0 = 1; // PE=1 + state.CS = 0; // CPL 0 + + uint physicalAddress = unit.Translate(0x0234, isWrite: false); + + physicalAddress.Should().Be(PageBase + 0x234); + } + + [Fact] + public void Translate_UserAccessiblePageAccessedFromUserMode_Succeeds() { + (State state, _, PagingUnit unit) = CreateIdentityMappedSetup(userAccessible: true); + state.ControlRegisters.Cr0 = 1; // PE=1 + state.CS = 3; // CPL 3 + + uint physicalAddress = unit.Translate(0x0234, isWrite: false); + + physicalAddress.Should().Be(PageBase + 0x234); + } + + [Fact] + public void Translate_SuccessfulAccess_SetsAccessedBitOnBothEntriesButNotDirty() { + (State state, Ram ram, PagingUnit unit) = CreateIdentityMappedSetup(userAccessible: true); + state.ControlRegisters.Cr0 = 1; // PE=1 + state.CS = 0; // CPL 0 + + unit.Translate(0x0234, isWrite: false); + + (ReadUInt32(ram, PageDirectoryBase) & AccessedBit).Should().NotBe(0u); + (ReadUInt32(ram, PageTableBase) & AccessedBit).Should().NotBe(0u); + (ReadUInt32(ram, PageTableBase) & DirtyBit).Should().Be(0u); + } + + [Fact] + public void Translate_WriteAccess_SetsDirtyBitOnPageTableEntryOnly() { + (State state, Ram ram, PagingUnit unit) = CreateIdentityMappedSetup(userAccessible: true); + state.ControlRegisters.Cr0 = 1; // PE=1 + state.CS = 0; // CPL 0 + WriteUInt32(ram, PageDirectoryBase, ReadUInt32(ram, PageDirectoryBase) | WriteBit); + WriteUInt32(ram, PageTableBase, ReadUInt32(ram, PageTableBase) | WriteBit); + + unit.Translate(0x0234, isWrite: true); + + (ReadUInt32(ram, PageTableBase) & DirtyBit).Should().NotBe(0u); + (ReadUInt32(ram, PageDirectoryBase) & DirtyBit).Should().Be(0u); + } + + [Fact] + public void Translate_FaultingAccess_LeavesAccessedAndDirtyBitsUntouchedOnBothEntries() { + (State state, Ram ram, PagingUnit unit) = CreateIdentityMappedSetup(userAccessible: false); + state.ControlRegisters.Cr0 = 1; // PE=1 + state.CS = 3; // CPL 3 - not user-accessible, so this access must fault + + Action act = () => unit.Translate(0x0234, isWrite: false); + + act.Should().Throw(); + (ReadUInt32(ram, PageDirectoryBase) & AccessedBit).Should().Be(0u); + (ReadUInt32(ram, PageTableBase) & AccessedBit).Should().Be(0u); + } + + [Fact] + public void Translate_UserModeWriteToReadOnlyPage_ThrowsPageFaultWithWriteAndUserBits() { + (State state, _, PagingUnit unit) = CreateIdentityMappedSetup(userAccessible: true); + state.ControlRegisters.Cr0 = 1; // PE=1 + state.CS = 3; // CPL 3 - both entries are user-accessible but neither is writable + + Action act = () => unit.Translate(0x0234, isWrite: true); + + act.Should().Throw() + .Which.ErrorCode.Should().Be(0b111); // P=1 (protection violation), W/R=1 (write), U/S=1 (user mode) + } + + [Fact] + public void Translate_SupervisorModeWriteToReadOnlyPage_SucceedsRegardlessOfWriteBit() { + (State state, _, PagingUnit unit) = CreateIdentityMappedSetup(userAccessible: true); + state.ControlRegisters.Cr0 = 1; // PE=1 + state.CS = 0; // CPL 0 - supervisor writes ignore the Read/Write bit (CR0.WP is not implemented) + + uint physicalAddress = unit.Translate(0x0234, isWrite: true); + + physicalAddress.Should().Be(PageBase + 0x234); + } + + [Fact] + public void Translate_UserModeWriteRequiresBothEntriesWritable() { + (State state, Ram ram, PagingUnit unit) = CreateIdentityMappedSetup(userAccessible: true); + state.ControlRegisters.Cr0 = 1; // PE=1 + state.CS = 3; // CPL 3 + WriteUInt32(ram, PageDirectoryBase, ReadUInt32(ram, PageDirectoryBase) | WriteBit); // PDE writable, PTE still read-only + + Action act = () => unit.Translate(0x0234, isWrite: true); + + act.Should().Throw() + .Which.ErrorCode.Should().Be(0b111); + } + + private static uint ReadUInt32(Ram ram, uint address) { + return (uint)ram.Read(address) + | ((uint)ram.Read(address + 1) << 8) + | ((uint)ram.Read(address + 2) << 16) + | ((uint)ram.Read(address + 3) << 24); + } +} diff --git a/tests/Spice86.Tests/PrivilegeChecksTests.cs b/tests/Spice86.Tests/PrivilegeChecksTests.cs new file mode 100644 index 0000000000..69e8b8c8b3 --- /dev/null +++ b/tests/Spice86.Tests/PrivilegeChecksTests.cs @@ -0,0 +1,188 @@ +namespace Spice86.Tests; + +using FluentAssertions; + +using Spice86.Core.Emulator.CPU; +using Spice86.Core.Emulator.CPU.DescriptorTables; +using Spice86.Core.Emulator.CPU.Exceptions; +using Spice86.Core.Emulator.CPU.Registers; + +using Xunit; + +public class PrivilegeChecksTests { + private static State CreateProtectedModeState(byte cpl, byte iopl = 0, bool virtual8086 = false) { + State state = new(CpuModel.INTEL_80386); + state.ControlRegisters.Cr0 = 1; // PE=1 + state.Flags.SetFlag(Flags.Virtual8086Mode, virtual8086); + state.CS = (ushort)((1 << 3) | cpl); + state.IoPrivilegeLevel = iopl; + return state; + } + + [Fact] + public void EnsureIoPrivilege_RealMode_NeverThrows() { + State state = new(CpuModel.INTEL_80386); + state.CS = 0x0003; // would be CPL 3 if interpreted in protected mode + state.IoPrivilegeLevel = 0; + + Action act = () => PrivilegeChecks.EnsureIoPrivilege(state); + + act.Should().NotThrow(); + } + + [Fact] + public void EnsureIoPrivilege_ProtectedMode_CplBelowOrEqualIopl_DoesNotThrow() { + State state = CreateProtectedModeState(cpl: 0, iopl: 0); + + Action act = () => PrivilegeChecks.EnsureIoPrivilege(state); + + act.Should().NotThrow(); + } + + [Fact] + public void EnsureIoPrivilege_ProtectedMode_CplAboveIopl_ThrowsGeneralProtectionFault() { + State state = CreateProtectedModeState(cpl: 3, iopl: 0); + + Action act = () => PrivilegeChecks.EnsureIoPrivilege(state); + + act.Should().Throw(); + } + + [Fact] + public void EnsureIoPrivilege_ProtectedMode_CplEqualsIopl_DoesNotThrow() { + State state = CreateProtectedModeState(cpl: 3, iopl: 3); + + Action act = () => PrivilegeChecks.EnsureIoPrivilege(state); + + act.Should().NotThrow(); + } + + [Fact] + public void EnsureIoPrivilege_Virtual8086Mode_IoplThree_DoesNotThrow() { + State state = CreateProtectedModeState(cpl: 0, iopl: 3, virtual8086: true); + + Action act = () => PrivilegeChecks.EnsureIoPrivilege(state); + + act.Should().NotThrow(); + } + + [Fact] + public void EnsureIoPrivilege_Virtual8086Mode_IoplBelowThree_Throws() { + State state = CreateProtectedModeState(cpl: 0, iopl: 2, virtual8086: true); + + Action act = () => PrivilegeChecks.EnsureIoPrivilege(state); + + act.Should().Throw(); + } + + [Fact] + public void ValidateDataSegmentLoad_RealMode_NeverThrowsEvenForBadDescriptor() { + State state = new(CpuModel.INTEL_80386); + SegmentDescriptorCache notPresent = new(0, 0xFFFF, accessRights: 0x00, defaultBig: false, granularity4K: false, present: false); + + Action act = () => PrivilegeChecks.ValidateDataSegmentLoad(state, SegmentRegisterIndex.DsIndex, 0x0008, notPresent); + + act.Should().NotThrow(); + } + + [Fact] + public void ValidateDataSegmentLoad_CsIndex_NeverValidated() { + State state = CreateProtectedModeState(cpl: 0); + SegmentDescriptorCache notPresent = new(0, 0xFFFF, accessRights: 0x00, defaultBig: false, granularity4K: false, present: false); + + Action act = () => PrivilegeChecks.ValidateDataSegmentLoad(state, SegmentRegisterIndex.CsIndex, 0x0008, notPresent); + + act.Should().NotThrow(); + } + + [Fact] + public void ValidateDataSegmentLoad_PresentWritableDataAtSufficientDpl_DoesNotThrow() { + State state = CreateProtectedModeState(cpl: 0); + SegmentDescriptorCache data = new(0, 0xFFFF, accessRights: 0x92, defaultBig: false, granularity4K: false, present: true); + + Action act = () => PrivilegeChecks.ValidateDataSegmentLoad(state, SegmentRegisterIndex.DsIndex, 0x0008, data); + + act.Should().NotThrow(); + } + + [Fact] + public void ValidateDataSegmentLoad_NotPresent_ThrowsSegmentNotPresent() { + State state = CreateProtectedModeState(cpl: 0); + SegmentDescriptorCache data = new(0, 0xFFFF, accessRights: 0x92, defaultBig: false, granularity4K: false, present: false); + + Action act = () => PrivilegeChecks.ValidateDataSegmentLoad(state, SegmentRegisterIndex.DsIndex, 0x0008, data); + + act.Should().Throw(); + } + + [Fact] + public void ValidateDataSegmentLoad_MaxOfCplAndRplExceedsDpl_ThrowsGeneralProtectionFault() { + State state = CreateProtectedModeState(cpl: 3); + SegmentDescriptorCache dpl0Data = new(0, 0xFFFF, accessRights: 0x92, defaultBig: false, granularity4K: false, present: true); + + Action act = () => PrivilegeChecks.ValidateDataSegmentLoad(state, SegmentRegisterIndex.DsIndex, 0x0008, dpl0Data); + + act.Should().Throw(); + } + + [Fact] + public void ValidateDataSegmentLoad_NonReadableCodeSegment_ThrowsGeneralProtectionFault() { + State state = CreateProtectedModeState(cpl: 0); + SegmentDescriptorCache nonReadableCode = new(0, 0xFFFF, accessRights: 0x98, defaultBig: false, granularity4K: false, present: true); + + Action act = () => PrivilegeChecks.ValidateDataSegmentLoad(state, SegmentRegisterIndex.DsIndex, 0x0008, nonReadableCode); + + act.Should().Throw(); + } + + [Fact] + public void ValidateDataSegmentLoad_ConformingCodeWithLowerDplThanCpl_DoesNotThrow() { + State state = CreateProtectedModeState(cpl: 3); + SegmentDescriptorCache conformingReadableCode = new(0, 0xFFFF, accessRights: 0x9E, defaultBig: false, granularity4K: false, present: true); + + Action act = () => PrivilegeChecks.ValidateDataSegmentLoad(state, SegmentRegisterIndex.DsIndex, 0x0008, conformingReadableCode); + + act.Should().NotThrow(); + } + + [Fact] + public void ValidateDataSegmentLoad_Ss_WritableDataMatchingCpl_DoesNotThrow() { + State state = CreateProtectedModeState(cpl: 0); + SegmentDescriptorCache data = new(0, 0xFFFF, accessRights: 0x92, defaultBig: false, granularity4K: false, present: true); + + Action act = () => PrivilegeChecks.ValidateDataSegmentLoad(state, SegmentRegisterIndex.SsIndex, 0x0008, data); + + act.Should().NotThrow(); + } + + [Fact] + public void ValidateDataSegmentLoad_Ss_NotPresent_ThrowsStackSegmentFault() { + State state = CreateProtectedModeState(cpl: 0); + SegmentDescriptorCache data = new(0, 0xFFFF, accessRights: 0x92, defaultBig: false, granularity4K: false, present: false); + + Action act = () => PrivilegeChecks.ValidateDataSegmentLoad(state, SegmentRegisterIndex.SsIndex, 0x0008, data); + + act.Should().Throw(); + } + + [Fact] + public void ValidateDataSegmentLoad_Ss_RplNotEqualCpl_ThrowsGeneralProtectionFault() { + State state = CreateProtectedModeState(cpl: 0); + SegmentDescriptorCache data = new(0, 0xFFFF, accessRights: 0x92, defaultBig: false, granularity4K: false, present: true); + + // Selector RPL (low 2 bits) is 3, which does not match CPL 0. + Action act = () => PrivilegeChecks.ValidateDataSegmentLoad(state, SegmentRegisterIndex.SsIndex, 0x000B, data); + + act.Should().Throw(); + } + + [Fact] + public void ValidateDataSegmentLoad_Ss_NonWritableData_ThrowsGeneralProtectionFault() { + State state = CreateProtectedModeState(cpl: 0); + SegmentDescriptorCache readOnlyData = new(0, 0xFFFF, accessRights: 0x90, defaultBig: false, granularity4K: false, present: true); + + Action act = () => PrivilegeChecks.ValidateDataSegmentLoad(state, SegmentRegisterIndex.SsIndex, 0x0008, readOnlyData); + + act.Should().Throw(); + } +} diff --git a/tests/Spice86.Tests/RealModeMmuTest.cs b/tests/Spice86.Tests/RealModeMmuTest.cs index bc1fc01ab1..afc6867908 100644 --- a/tests/Spice86.Tests/RealModeMmuTest.cs +++ b/tests/Spice86.Tests/RealModeMmuTest.cs @@ -12,7 +12,7 @@ public void StrictDataByteAtSegmentLimitShouldSucceed() { IMmu mmu = new RealModeMmu386(); // Act & Assert - mmu.CheckAccess(0, 0xFFFF, 1, SegmentAccessKind.Data); + mmu.CheckAccess(0, 0xFFFF, 1, SegmentAccessKind.Data, isWrite: false); } [Fact] @@ -21,7 +21,7 @@ public void StrictDataWordAtSegmentLimitShouldRaiseGeneralProtectionFault() { IMmu mmu = new RealModeMmu386(); // Act & Assert - Assert.Throws(() => mmu.CheckAccess(0, 0xFFFF, 2, SegmentAccessKind.Data)); + Assert.Throws(() => mmu.CheckAccess(0, 0xFFFF, 2, SegmentAccessKind.Data, isWrite: false)); } [Fact] @@ -30,7 +30,7 @@ public void StrictStackWordAtSegmentLimitShouldRaiseStackSegmentFault() { IMmu mmu = new RealModeMmu386(); // Act & Assert - Assert.Throws(() => mmu.CheckAccess(0, 0xFFFF, 2, SegmentAccessKind.Stack)); + Assert.Throws(() => mmu.CheckAccess(0, 0xFFFF, 2, SegmentAccessKind.Stack, isWrite: true)); } [Theory] @@ -41,7 +41,7 @@ public void StrictDataDwordOutsideSegmentShouldRaiseGeneralProtectionFault(uint IMmu mmu = new RealModeMmu386(); // Act & Assert - Assert.Throws(() => mmu.CheckAccess(0, offset, 4, SegmentAccessKind.Data)); + Assert.Throws(() => mmu.CheckAccess(0, offset, 4, SegmentAccessKind.Data, isWrite: false)); } [Fact] @@ -50,8 +50,8 @@ public void WrapPolicyShouldTranslateOffsetPastSegmentLimitLikeOffsetZero() { IMmu mmu = new RealModeMmu8086(); // Act - uint wrappedAddress = mmu.TranslateAddress(0x1234, 0x10000); - uint zeroAddress = mmu.TranslateAddress(0x1234, 0); + uint wrappedAddress = mmu.TranslateAddress(0x1234, 0x10000, isWrite: false); + uint zeroAddress = mmu.TranslateAddress(0x1234, 0, isWrite: false); // Assert Assert.Equal(zeroAddress, wrappedAddress); @@ -63,7 +63,7 @@ public void WrapPolicyShouldAllowCrossBoundaryAccess() { IMmu mmu = new RealModeMmu8086(); // Act & Assert - mmu.CheckAccess(0, 0xFFFF, 4, SegmentAccessKind.Data); + mmu.CheckAccess(0, 0xFFFF, 4, SegmentAccessKind.Data, isWrite: false); } [Fact] @@ -72,7 +72,7 @@ public void StrictCodeNextIpWithinSegmentLimitShouldSucceed() { IMmu mmu = new RealModeMmu386(); // Act & Assert — 1-byte instruction at 0xFFFE: next IP = 0xFFFF, within segment - mmu.CheckAccess(0, 0xFFFF, 1, SegmentAccessKind.Data); + mmu.CheckAccess(0, 0xFFFF, 1, SegmentAccessKind.Data, isWrite: false); } [Fact] @@ -81,7 +81,7 @@ public void StrictCodeNextIpPastSegmentLimitShouldRaiseGeneralProtectionFault() IMmu mmu = new RealModeMmu386(); // Act & Assert — 1-byte instruction at 0xFFFF: next IP = 0x10000, overflows segment - Assert.Throws(() => mmu.CheckAccess(0, 0x10000u, 1, SegmentAccessKind.Data)); + Assert.Throws(() => mmu.CheckAccess(0, 0x10000u, 1, SegmentAccessKind.Data, isWrite: false)); } [Fact] @@ -90,6 +90,6 @@ public void WrapPolicyShouldAllowCodeCrossBoundaryAccess() { IMmu mmu = new RealModeMmu8086(); // Act & Assert — 2-byte instruction at 0xFFFF: next IP = 0x10001, wraps on 8086 - mmu.CheckAccess(0, 0x10001u, 1, SegmentAccessKind.Data); + mmu.CheckAccess(0, 0x10001u, 1, SegmentAccessKind.Data, isWrite: false); } } \ No newline at end of file diff --git a/tests/Spice86.Tests/Resources/BiosInt15Tests/bios_int15h_87h.asm b/tests/Spice86.Tests/Resources/BiosInt15Tests/bios_int15h_87h.asm new file mode 100644 index 0000000000..cbdbcaeef0 --- /dev/null +++ b/tests/Spice86.Tests/Resources/BiosInt15Tests/bios_int15h_87h.asm @@ -0,0 +1,90 @@ +; Build: nasm -f bin bios_int15h_87h.asm -o bios_int15h_87h.com +; +; Regression test for BIOS INT 15h, AH=87h (COPY EXTENDED MEMORY - +; SystemBiosInt15Handler.CopyExtendedMemory, a reimplementation of SeaBIOS's handle_1587). This +; function always read/wrote through the shared Memory bus directly (no private XMS/EMS-style +; array), so it transparently gained the ability to address the full unified extended-memory pool +; once the XMS/EMS unification plan's Phase 1 grew Ram beyond the old ~1.06MB conventional+HMA +; ceiling. This test proves that by round-tripping a marker word through a linear address well +; beyond that old ceiling (3MB), safely inside the new default 16MB pool. +cpu 386 +use16 +org 100h + +result_port equ 0999h +details_port equ 0998h +success equ 00h +failure equ 0FFh +far_address equ 0300000h ; 3MB - unreachable before Ram was grown past ~0x110000 + +start: + mov ax, cs + mov es, ax + + mov word [srcBuffer], 1234h + + ; Zero the 48-byte GDT structure once; only the handle/address fields change per call. + mov di, gdt + mov cx, 30h + xor al, al + cld + rep stosb + + ; --- Copy conventional (srcBuffer) -> extended (far_address) --- + mov word [gdt+14h], 0 ; SourceHandle = 0 (conventional memory) + xor eax, eax + mov ax, cs + shl eax, 16 + mov dword [gdt+16h], eax + mov word [gdt+16h], srcBuffer ; SourceOffsetOrAddress = CS:srcBuffer + + mov word [gdt+1Ah], 1 ; DestinationHandle != 0 (extended memory) + mov dword [gdt+1Ch], far_address + + mov cx, 1 ; 1 word = 2 bytes + mov si, gdt + mov ah, 87h + int 15h + jc failed + mov al, 1 + mov dx, details_port + out dx, al + + ; --- Copy extended (far_address) -> conventional (verifyBuffer) --- + mov word [gdt+14h], 1 ; SourceHandle != 0 (extended memory) + mov dword [gdt+16h], far_address + + mov word [gdt+1Ah], 0 ; DestinationHandle = 0 (conventional memory) + xor eax, eax + mov ax, cs + shl eax, 16 + mov dword [gdt+1Ch], eax + mov word [gdt+1Ch], verifyBuffer ; DestinationOffsetOrAddress = CS:verifyBuffer + + mov cx, 1 + mov si, gdt + mov ah, 87h + int 15h + jc failed + mov al, 2 + mov dx, details_port + out dx, al + + cmp word [verifyBuffer], 1234h + jne failed + + mov al, success + jmp write_result + +failed: + mov al, failure + +write_result: + mov dx, result_port + out dx, al + hlt + +align 4 +gdt: times 30h db 0 +srcBuffer: dw 0 +verifyBuffer: dw 0 diff --git a/tests/Spice86.Tests/Resources/BiosInt15Tests/bios_int15h_87h.com b/tests/Spice86.Tests/Resources/BiosInt15Tests/bios_int15h_87h.com new file mode 100644 index 0000000000..b01e65a6a4 Binary files /dev/null and b/tests/Spice86.Tests/Resources/BiosInt15Tests/bios_int15h_87h.com differ diff --git a/tests/Spice86.Tests/Resources/cpuTests/asmsrc/test386.asm/src/configuration.asm b/tests/Spice86.Tests/Resources/cpuTests/asmsrc/test386.asm/src/configuration.asm index 63fb7ae78d..b643c778f9 100644 --- a/tests/Spice86.Tests/Resources/cpuTests/asmsrc/test386.asm/src/configuration.asm +++ b/tests/Spice86.Tests/Resources/cpuTests/asmsrc/test386.asm/src/configuration.asm @@ -38,6 +38,13 @@ TEST_UNDEF equ 0 ; Enable PMODE tests TEST_PMODE equ 0 +; Skip the unverified arithmetic/logic/BCD diagnostic-print tests that run after POST EE. +; They have no pass/fail assertions of their own (meant for manual comparison against a +; reference file) and print a very large amount of ASCII output. Enabling this jumps straight +; from POST EE to POST FF and halts. +; Possible values: 1=skip straight to POST FF, 0=run the unverified tests (upstream default) +SKIP_UNVERIFIED_TESTS equ 0 + ; The CPU family option is used only when POST E0 is enabled. ; Possible values: 3=80386 CPU_FAMILY equ 3 diff --git a/tests/Spice86.Tests/Resources/cpuTests/asmsrc/test386.asm/src/test386.asm b/tests/Spice86.Tests/Resources/cpuTests/asmsrc/test386.asm/src/test386.asm index ce7789ba10..84bb6911ff 100644 --- a/tests/Spice86.Tests/Resources/cpuTests/asmsrc/test386.asm/src/test386.asm +++ b/tests/Spice86.Tests/Resources/cpuTests/asmsrc/test386.asm/src/test386.asm @@ -1365,7 +1365,11 @@ arithLogicTests: ; Now run a series of unverified tests for arithmetical and logical opcodes ; Manually verify by comparing the tests output with a reference file ; + %if SKIP_UNVERIFIED_TESTS + jmp postFF + %else jmp bcdTests + %endif bcdTests: testBCD daa, 0x12340503, PS_AF, PS_CF | PS_PF | PS_ZF | PS_SF | PS_AF diff --git a/tests/Spice86.Tests/Resources/cpuTests/protectedmode_callgate.bin b/tests/Spice86.Tests/Resources/cpuTests/protectedmode_callgate.bin new file mode 100644 index 0000000000..b42fe47076 Binary files /dev/null and b/tests/Spice86.Tests/Resources/cpuTests/protectedmode_callgate.bin differ diff --git a/tests/Spice86.Tests/Resources/cpuTests/protectedmode_entry.bin b/tests/Spice86.Tests/Resources/cpuTests/protectedmode_entry.bin new file mode 100644 index 0000000000..8dc244901b Binary files /dev/null and b/tests/Spice86.Tests/Resources/cpuTests/protectedmode_entry.bin differ diff --git a/tests/Spice86.Tests/Resources/cpuTests/protectedmode_far_privilege.bin b/tests/Spice86.Tests/Resources/cpuTests/protectedmode_far_privilege.bin new file mode 100644 index 0000000000..d1cc0e1ca4 Binary files /dev/null and b/tests/Spice86.Tests/Resources/cpuTests/protectedmode_far_privilege.bin differ diff --git a/tests/Spice86.Tests/Resources/cpuTests/protectedmode_idt.bin b/tests/Spice86.Tests/Resources/cpuTests/protectedmode_idt.bin new file mode 100644 index 0000000000..2cb04321c7 Binary files /dev/null and b/tests/Spice86.Tests/Resources/cpuTests/protectedmode_idt.bin differ diff --git a/tests/Spice86.Tests/Resources/cpuTests/protectedmode_instructions.bin b/tests/Spice86.Tests/Resources/cpuTests/protectedmode_instructions.bin new file mode 100644 index 0000000000..2271b8eec5 Binary files /dev/null and b/tests/Spice86.Tests/Resources/cpuTests/protectedmode_instructions.bin differ diff --git a/tests/Spice86.Tests/Resources/cpuTests/protectedmode_paging.bin b/tests/Spice86.Tests/Resources/cpuTests/protectedmode_paging.bin new file mode 100644 index 0000000000..e731c9924d Binary files /dev/null and b/tests/Spice86.Tests/Resources/cpuTests/protectedmode_paging.bin differ diff --git a/tests/Spice86.Tests/Resources/cpuTests/protectedmode_privilege.bin b/tests/Spice86.Tests/Resources/cpuTests/protectedmode_privilege.bin new file mode 100644 index 0000000000..abfd11fe8a Binary files /dev/null and b/tests/Spice86.Tests/Resources/cpuTests/protectedmode_privilege.bin differ diff --git a/tests/Spice86.Tests/Resources/cpuTests/protectedmode_taskgate.bin b/tests/Spice86.Tests/Resources/cpuTests/protectedmode_taskgate.bin new file mode 100644 index 0000000000..58707ecdd9 Binary files /dev/null and b/tests/Spice86.Tests/Resources/cpuTests/protectedmode_taskgate.bin differ diff --git a/tests/Spice86.Tests/Resources/cpuTests/protectedmode_taskswitch.bin b/tests/Spice86.Tests/Resources/cpuTests/protectedmode_taskswitch.bin new file mode 100644 index 0000000000..5a06a49978 Binary files /dev/null and b/tests/Spice86.Tests/Resources/cpuTests/protectedmode_taskswitch.bin differ diff --git a/tests/Spice86.Tests/Resources/cpuTests/protectedmode_unified_pool.asm b/tests/Spice86.Tests/Resources/cpuTests/protectedmode_unified_pool.asm new file mode 100644 index 0000000000..90aea84f39 --- /dev/null +++ b/tests/Spice86.Tests/Resources/cpuTests/protectedmode_unified_pool.asm @@ -0,0 +1,74 @@ +; Build: nasm -f bin protectedmode_unified_pool.asm -o protectedmode_unified_pool.bin +; +; Phase 1 of the XMS/EMS unified-memory-backing-store plan: proves a flat protected-mode +; descriptor can directly address physical memory well above the old ~1.06MB conventional+HMA +; ceiling (0x500000 = 5MB in, comfortably inside the new unified extended-memory pool but +; unreachable with the old Ram size) with no XMS/EMS API involved at all. +; +; Mirrors protectedmode_entry.bin's proven structure: the GDT is built at runtime via plain +; memory writes to a fixed low-memory scratch address (0x600), not embedded as static data +; relative to this file's own (irrelevant) load offset. The code selector (0x08) stays a +; 16-bit segment based at 0xF0000 (matching where this BIOS-style image is loaded), so no +; operand-size prefixes are needed for the transition jumps - only the data selector (0x10) +; is a true 32-bit flat (base 0, 4GB limit) descriptor, needed to reach 0x500000. +cpu 386 +org 0 + +start: + mov sp, 0x1000 + + ; Descriptor 1 (selector 0x08): 16-bit code, base=0xF0000 (matches this image's load + ; address), limit=0xFFFF, byte-granular - so EIP can keep using this file's own small, + ; org-0-relative offsets after the far jump. + mov word [0x0608], 0xFFFF + mov word [0x060A], 0x0000 + mov byte [0x060C], 0x0F + mov byte [0x060D], 0x9A + mov byte [0x060E], 0x00 + mov byte [0x060F], 0x00 + + ; Descriptor 2 (selector 0x10): true flat 32-bit data, base=0, limit=0xFFFFF with 4K + ; granularity (~4GB reach) - needed to address 0x500000. + mov word [0x0610], 0xFFFF + mov word [0x0612], 0x0000 + mov byte [0x0614], 0x00 + mov byte [0x0615], 0x92 + mov byte [0x0616], 0xCF + mov byte [0x0617], 0x00 + + ; GDT pseudo-descriptor at 0x0620: limit (3 entries * 8 - 1), base = 0x000600. + mov word [0x0620], 0x0017 + mov dword [0x0622], 0x00000600 + + lgdt [0x0620] + + mov eax, cr0 + or eax, 1 + mov cr0, eax + + jmp 0x08:protected_entry + +protected_entry: + mov ax, 0x10 + mov ds, ax + + mov byte [dword 0x500000], 0x42 + + mov eax, cr0 + and eax, 0xFFFFFFFE + mov cr0, eax + + jmp 0xF000:real_mode_entry + +real_mode_entry: + mov ax, 0xF000 + mov ds, ax + mov es, ax + mov ss, ax + mov sp, 0xFFF0 + hlt + +times 0xFFF0 - ($-$$) db 0 + jmp 0xF000:start + +times 0x10000 - ($-$$) db 0 diff --git a/tests/Spice86.Tests/Resources/cpuTests/protectedmode_unified_pool.bin b/tests/Spice86.Tests/Resources/cpuTests/protectedmode_unified_pool.bin new file mode 100644 index 0000000000..3e6a6b2c6d Binary files /dev/null and b/tests/Spice86.Tests/Resources/cpuTests/protectedmode_unified_pool.bin differ diff --git a/tests/Spice86.Tests/Resources/cpuTests/protectedmode_v86.bin b/tests/Spice86.Tests/Resources/cpuTests/protectedmode_v86.bin new file mode 100644 index 0000000000..73f19f1a97 Binary files /dev/null and b/tests/Spice86.Tests/Resources/cpuTests/protectedmode_v86.bin differ diff --git a/tests/Spice86.Tests/Resources/cpuTests/test386_pmode.bin b/tests/Spice86.Tests/Resources/cpuTests/test386_pmode.bin new file mode 100644 index 0000000000..7e3f57e097 Binary files /dev/null and b/tests/Spice86.Tests/Resources/cpuTests/test386_pmode.bin differ diff --git a/tests/Spice86.Tests/Spice86Creator.cs b/tests/Spice86.Tests/Spice86Creator.cs index 2785f32ab7..25e5190f5c 100644 --- a/tests/Spice86.Tests/Spice86Creator.cs +++ b/tests/Spice86.Tests/Spice86Creator.cs @@ -29,7 +29,8 @@ public Spice86Creator(string binName, bool enablePit = false, string? exeArgs = null, long? instructionTimeScale = null, JitMode jitMode = JitMode.InterpretedOnly, bool failOnInvalidOpcode = false, string? recordedDataDirectory = null, bool reloadCfgGraph = false, - bool enableSpeculativeCfgExploration = true) { + bool enableSpeculativeCfgExploration = true, CpuModel cpuModel = CpuModel.ZET_86, + bool cpuHeavyLog = false, int ramSizeKb = Configuration.RamSizeDefaultKb) { string executablePath = Path.IsPathRooted(binName) ? binName : $"Resources/cpuTests/{binName}.bin"; if (overrideSupplierClassName != null && overrideSupplier != null) { throw new ArgumentException("Provide either an override supplier instance or an override supplier class name, not both."); @@ -84,6 +85,9 @@ public Spice86Creator(string binName, bool enablePit = false, FailOnInvalidOpcode = failOnInvalidOpcode, ReloadCfgGraph = reloadCfgGraph, EnableSpeculativeCfgExploration = enableSpeculativeCfgExploration, + CpuModel = cpuModel, + CpuHeavyLog = cpuHeavyLog, + RamSizeKb = ramSizeKb, }; _maxCycles = maxCycles;