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;