Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: DOS LOAD AND/OR EXEC and DOS process management #871

Draft
wants to merge 8 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 169 additions & 1 deletion src/Spice86.Core/Emulator/InterruptHandlers/Dos/DosInt21Handler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,16 @@
using Spice86.Core.Emulator.Errors;
using Spice86.Core.Emulator.InterruptHandlers;
using Spice86.Core.Emulator.InterruptHandlers.Input.Keyboard;
using Spice86.Core.Emulator.LoadableFile.Dos;
using Spice86.Core.Emulator.LoadableFile.Dos.Exe;
using Spice86.Core.Emulator.Memory;
using Spice86.Core.Emulator.Memory.ReaderWriter;
using Spice86.Core.Emulator.OperatingSystem;
using Spice86.Core.Emulator.OperatingSystem.Devices;
using Spice86.Core.Emulator.OperatingSystem.Enums;
using Spice86.Core.Emulator.OperatingSystem.Structures;
using Spice86.Shared.Emulator.Errors;
using Spice86.Shared.Emulator.Memory;
using Spice86.Shared.Interfaces;
using Spice86.Shared.Utils;

Expand All @@ -25,7 +30,7 @@
/// </summary>
public class DosInt21Handler : InterruptHandler {
private readonly Encoding _cp850CharSet;

private readonly DosMemoryManager _dosMemoryManager;
private readonly InterruptVectorTable _interruptVectorTable;
private bool _isCtrlCFlag;
Expand Down Expand Up @@ -83,6 +88,7 @@
AddAction(0x2F, GetDiskTransferAddress);
AddAction(0x30, GetDosVersion);
AddAction(0x33, GetSetControlBreak);
AddAction(0x34, GetInDosFlag);
AddAction(0x35, GetInterruptVector);
AddAction(0x36, GetFreeDiskSpace);
AddAction(0x38, () => SetCountryCode(true));
Expand All @@ -103,6 +109,7 @@
AddAction(0x48, () => AllocateMemoryBlock(true));
AddAction(0x49, () => FreeMemoryBlock(true));
AddAction(0x4A, () => ModifyMemoryBlock(true));
AddAction(0x4B, () => LoadAndOrExecuteProgram(true));
AddAction(0x4C, QuitWithExitCode);
AddAction(0x4E, () => FindFirstMatchingFile(true));
AddAction(0x4F, () => FindNextMatchingFile(true));
Expand Down Expand Up @@ -669,6 +676,13 @@
}
}

/// <summary>
/// Returns the current value of the IN DOS flag in the BX register.
/// </summary>
public void GetInDosFlag() {
//State.ES
}

/// <summary>
/// Returns the current MS-DOS time in CH (hour), CL (minute), DH (second), and DL (millisecond) from the host's DateTime.Now.
/// </summary>
Expand Down Expand Up @@ -707,6 +721,67 @@
State.BX = 0;
}
}

/// <summary>
/// Load and or execute a program.
/// AL = 0: Load and execute. <br/>
/// AL = 1: Load only. <br/>
/// AL = 2: Load overlay. <br/>
/// DS:DX: ASCIIZ program name with extension. <br/>
/// ES:BX: EXEC Parameter block. <br/>
/// <returns>
/// CF is cleared on success. <br/>
/// CF is set on error.
/// TODO: This needs the DOS Swappable Area, and a lot of other DOS globals (current drive, current folder, ...)
/// </returns>
/// </summary>
/// <param name="calledFromVm">Whether the code was called by the emulator.</param>
public void LoadAndOrExecuteProgram(bool calledFromVm) {
bool success = false;
byte typeOfLoadByte = State.AL;
if (!Enum.IsDefined(typeof(DosExecOperation), typeOfLoadByte)) {
SetCarryFlag(false, calledFromVm);
return;
}
DosExecParameterBlock dosExecParameterBlock = new DosExecParameterBlock(Memory, MemoryUtils.ToPhysicalAddress(State.ES, State.BX));

Check warning

Code scanning / CodeQL

Useless assignment to local variable Warning

This assignment to
dosExecParameterBlock
is useless, since its value is never read.
DosExecOperation dosExecOperation = (DosExecOperation)State.AL;
string programName = GetZeroTerminatedStringAtDsDx();
string? fullHostPath = _dosFileManager.TryGetFullHostPathFromDos(programName);

if(string.IsNullOrWhiteSpace(fullHostPath) || !File.Exists(fullHostPath)) {
SetCarryFlag(false, calledFromVm);
return;
}

if (LoggerService.IsEnabled(LogEventLevel.Verbose)) {
LoggerService.Verbose("LOAD AND/OR EXECUTE PROGRAM {DosExecOperation}, {ProgramName}", dosExecOperation, programName);
}

bool isComFile = string.Equals(Path.GetExtension(programName).ToLowerInvariant(), ".com", StringComparison.OrdinalIgnoreCase);

switch (dosExecOperation) {
case DosExecOperation.LoadAndExecute:
if (isComFile) {
LoadAndExecComFile(fullHostPath, "", 0x1000);
} else {
LoadAndExecExeFile(fullHostPath, "", 0x1000);
}
success = true;
break;
case DosExecOperation.LoadOnly:
// Not implemented
success = false;
break;
case DosExecOperation.LoadOverlay:
// Not implemented
success = false;
break;
default:
SetCarryFlag(false, calledFromVm);
return;
}
SetCarryFlag(success, calledFromVm);
}

/// <summary>
/// Moves a file using a DOS handle. AL specifies the origin of the move, BX the file handle, CX:DX the offset.
Expand Down Expand Up @@ -1069,4 +1144,97 @@
State.DX = (ushort)(value >> 16);
}
}

private const ushort ComOffset = 0x100;

internal void LoadAndExecExeFile(string hostFile, string? arguments, ushort startSegment) {
byte[] exe = File.ReadAllBytes(hostFile);
if (LoggerService.IsEnabled(LogEventLevel.Debug)) {
LoggerService.Debug("Exe size: {ExeSize}", exe.Length);
}
ExeFile exeFile = new ExeFile(new ByteArrayReaderWriter(exe));
if (!exeFile.IsValid) {
if (LoggerService.IsEnabled(LogEventLevel.Error)) {
LoggerService.Error("Invalid EXE file {File}", hostFile);
}
throw new UnrecoverableException($"Invalid EXE file {hostFile}");
}
if (LoggerService.IsEnabled(LogEventLevel.Verbose)) {
LoggerService.Verbose("Read header: {ReadHeader}", exeFile);
}

LoadExeFileInMemory(exeFile, startSegment);
ushort pspSegment = (ushort)(startSegment - 0x10);
SetupCpuForExe(exeFile, startSegment, pspSegment);
new PspGenerator(Memory, _dos.EnvironmentVariables, _dosMemoryManager, _dosFileManager).GeneratePsp(pspSegment, arguments);
if (LoggerService.IsEnabled(LogEventLevel.Debug)) {
LoggerService.Debug("Initial CPU State: {CpuState}", State);
}
}

/// <summary>
/// Loads the program image and applies any necessary relocations to it.
/// </summary>
/// <param name="exeFile">The EXE file to load.</param>
/// <param name="startSegment">The starting segment for the program.</param>
private void LoadExeFileInMemory(ExeFile exeFile, ushort startSegment) {
uint physicalStartAddress = MemoryUtils.ToPhysicalAddress(startSegment, 0);
Memory.LoadData(physicalStartAddress, exeFile.ProgramImage);
foreach (SegmentedAddress address in exeFile.RelocationTable) {
// Read value from memory, add the start segment offset and write back
uint addressToEdit = MemoryUtils.ToPhysicalAddress(address.Segment, address.Offset) + physicalStartAddress;
Memory.UInt16[addressToEdit] += startSegment;
}
}

/// <summary>
/// Sets up the CPU to execute the loaded program.
/// </summary>
/// <param name="exeFile">The EXE file that was loaded.</param>
/// <param name="startSegment">The starting segment address of the program.</param>
/// <param name="pspSegment">The segment address of the program's PSP (Program Segment Prefix).</param>
private void SetupCpuForExe(ExeFile exeFile, ushort startSegment, ushort pspSegment) {
// MS-DOS uses the values in the file header to set the SP and SS registers and
// adjusts the initial value of the SS register by adding the start-segment
// address to it.
State.SS = (ushort)(exeFile.InitSS + startSegment);
State.SP = exeFile.InitSP;

// Make DS and ES point to the PSP
State.DS = pspSegment;
State.ES = pspSegment;

State.InterruptFlag = true;

// Finally, MS-DOS reads the initial CS and IP values from the program's file
// header, adjusts the CS register value by adding the start-segment address to
// it, and transfers control to the program at the adjusted address.
SetEntryPoint((ushort)(exeFile.InitCS + startSegment), exeFile.InitIP);
}

/// <summary>
/// Sets the entry point of the loaded file to the specified segment and offset values.
/// </summary>
/// <param name="cs">The segment value of the entry point.</param>
/// <param name="ip">The offset value of the entry point.</param>
private void SetEntryPoint(ushort cs, ushort ip) {
State.CS = cs;
State.IP = ip;
if (LoggerService.IsEnabled(LogEventLevel.Verbose)) {
LoggerService.Verbose("Program entry point is {ProgramEntry}", ConvertUtils.ToSegmentedAddressRepresentation(cs, ip));
}
}

internal void LoadAndExecComFile(string hostFile, string? arguments, ushort startSegment) {
new PspGenerator(Memory, _dos.EnvironmentVariables, _dosMemoryManager, _dosFileManager).GeneratePsp(startSegment, arguments);
byte[] com = File.ReadAllBytes(hostFile);
uint physicalStartAddress = MemoryUtils.ToPhysicalAddress(startSegment, ComOffset);
Memory.LoadData(physicalStartAddress, com);

// Make DS and ES point to the PSP
State.DS = startSegment;
State.ES = startSegment;
SetEntryPoint(startSegment, ComOffset);
State.InterruptFlag = true;
}
}
46 changes: 32 additions & 14 deletions src/Spice86.Core/Emulator/LoadableFile/Bios/BiosLoader.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
namespace Spice86.Core.Emulator.LoadableFile.Bios;

using Spice86.Core.Emulator.CPU;
using Spice86.Shared.Interfaces;
using Serilog.Events;

using Spice86.Core.Emulator.LoadableFile;
using Spice86.Core.Emulator.CPU;
using Spice86.Core.Emulator.Memory;
using Spice86.Shared.Interfaces;
using Spice86.Shared.Utils;

/// <summary>
/// Loader for BIOS files.<br/>
/// Bios entry point is at physical address 0xFFFF0 (F000:FFF0).
/// </summary>
public class BiosLoader : ExecutableFileLoader {

public class BiosLoader {
private readonly string _hostFileName;
private readonly State _state;
private readonly IMemory _memory;
private readonly ILoggerService _loggerService;

/// <summary>
/// Offset of the BIOS code within the segment.
/// </summary>
Expand All @@ -26,28 +30,42 @@ public class BiosLoader : ExecutableFileLoader {
/// <summary>
/// Indicates whether DOS initialization is needed for the loaded file (always false for BIOS).
/// </summary>
public override bool DosInitializationNeeded => false;
public bool DosInitializationNeeded => false;

/// <summary>
/// Initializes a new instance of the <see cref="BiosLoader"/> class
/// </summary>
/// <param name="memory">The memory bus.</param>
/// <param name="state">The CPU state.</param>
/// <param name="hostFileName">The absolute host path to the BIOS file.</param>
/// <param name="loggerService">The logger service to log messages to.</param>
public BiosLoader(IMemory memory, State state, ILoggerService loggerService) : base(memory, state, loggerService) {
public BiosLoader(IMemory memory, State state, string hostFileName, ILoggerService loggerService) {
_hostFileName = hostFileName;
_state = state;
_memory = memory;
_loggerService = loggerService;
}

/// <summary>
/// Sets the entry point of the loaded file to the specified segment and offset values.
/// </summary>
/// <param name="cs">The segment value of the entry point.</param>
/// <param name="ip">The offset value of the entry point.</param>
private void SetEntryPoint(ushort cs, ushort ip) {
_state.CS = cs;
_state.IP = ip;
if (_loggerService.IsEnabled(LogEventLevel.Verbose)) {
_loggerService.Verbose("Program entry point is {ProgramEntry}", ConvertUtils.ToSegmentedAddressRepresentation(cs, ip));
}
}

/// <summary>
/// Loads the specified BIOS <paramref name="file"/> into memory and sets the entry point to the BIOS address.
/// Loads the specified BIOS file into memory and sets the entry point to the BIOS address.
/// </summary>
/// <param name="file">The path to the BIOS file to load.</param>
/// <param name="arguments">Ignored for BIOS files.</param>
/// <returns>The loaded BIOS file as a byte array.</returns>
public override byte[] LoadFile(string file, string? arguments) {
byte[] bios = ReadFile(file);
public void LoadHostFile() {
byte[] bios = File.ReadAllBytes(_hostFileName);
uint physicalStartAddress = MemoryUtils.ToPhysicalAddress(CodeSegment, 0);
_memory.LoadData(physicalStartAddress, bios);
SetEntryPoint(CodeSegment, CodeOffset);
return bios;
}
}
58 changes: 0 additions & 58 deletions src/Spice86.Core/Emulator/LoadableFile/Dos/Com/ComLoader.cs

This file was deleted.

25 changes: 0 additions & 25 deletions src/Spice86.Core/Emulator/LoadableFile/Dos/DosFileLoader.cs

This file was deleted.

2 changes: 1 addition & 1 deletion src/Spice86.Core/Emulator/LoadableFile/Dos/Exe/ExeFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
using Spice86.Shared.Emulator.Memory;

/// <summary>
/// Representation of an EXE file as a MemoryBasedDataStructure
/// Representation of an EXE file as it is stored on disk, loaded into memory.
/// </summary>
public class ExeFile : MemoryBasedDataStructure {
/// <summary>
Expand Down
Loading
Loading