Spice86 is a tool to execute, reverse engineer and rewrite real mode DOS programs for which source code is not available.
Releases are available on Nuget.
Pre-releases are also available on the Release page
NOTE: This is a port, and a continuation from the original Java Spice86.
It requires .NET 10 and runs on Windows, macOS, and Linux.
Rewriting a program from only the binary is a hard task.
Spice86 is a tool that helps you do so with a methodic divide and conquer approach.
General process:
- You start by emulating the program in the Spice86 emulator.
- As the program runs, Spice86 builds a Control Flow Graph (CFG) of the executed code.
- At the end of each run, the emulator dumps some runtime data (memory dump, execution flow and the CFG).
- From that CFG, Spice86 directly generates a self-contained, runnable C# project that overrides the original assembly with equivalent C# code.
- You then gradually rewrite the generated C# functions, replacing the mechanical translation with readable, intentful code (a task an LLM can help with).
- This is helpful because:
- Small sequences of assembly can be statically analyzed and are generally easy to translate to a higher level language.
- You work all the time with a fully working version of the program so it is relatively easy to catch mistakes early.
- Rewriting code function by function allows you to discover the intent of the author.
Spice86 also dumps Ghidra-compatible symbols, so you can optionally load the memory dump into Ghidra via the spice86-ghidra-plugin for deeper static analysis. This is no longer required to produce C# overrides; it is just an extra analysis aid.
| Command | Description |
|---|---|
Spice86 -e file.exe |
Run the specified executable |
Spice86 -e file.com |
Run a COM file |
Spice86 -e file.bat |
Run a DOS batch file |
Spice86 -e file.bin |
Run a BIOS file |
| Setting | Description |
|---|---|
| Environment Variable | SPICE86_DUMPS_FOLDER - Set this to control the base directory where data is dumped |
| Command Line | Use --RecordedDataDirectory to specify the base dump location |
| Default Location | Current directory if neither of the above is specified |
Note: Regardless of the base directory setting, dumps are always placed in a subdirectory named with the program's SHA-256 hash. This ensures that multiple executables from the same game (e.g., SETUP.EXE, GAME.EXE) have isolated dump folders.
The emulator dumps the following files:
- spice86dumpMemoryDump.bin: Snapshot of the real mode address space
- spice86dumpExecutionFlow.json: Contains function addresses, labels, and executed instructions
- spice86dumpGhidraSymbols.txt: Ghidra-compatible symbols (function and label addresses), for optional import into Ghidra
- spice86dumpCfgBlocks.json: The explored CFG as basic blocks with execution-context metadata
- spice86dumpCfgPartitions.json: The recovered function/helper partitions and inter-partition transfers, derived from the block graph
- spice86dumpCfgGeneratedOverrides.cs: C# overrides generated from the explored CFG
- GeneratedProject/: A self-contained, runnable C# override project (see Generated C# override project)
When there is already data in the specified location, the emulator will load it first and complement it.
CFG graph reload: The CFG instruction graph is also dumped (spice86dumpCfgReload.json) and, by default, reloaded from the recorded data directory at startup so previously explored program structure is preserved across runs. Disable with --ReloadCfgGraph false. It is a no-op if the file is absent.
Code overrides: When code overrides are active (--UseCodeOverride true with an --OverrideSupplierClassName), the CFG and execution flow dumps (spice86dumpExecutionFlow.json, spice86dumpCfgBlocks.json, spice86dumpCfgPartitions.json, spice86dumpCfgReload.json) are not written. Overrides replace the emulated asm, so the recorded graph would jump between overrides and no longer reflect the program.
For detailed debugging, you can enable CPU heavy logging which records every executed instruction to a file.
| Setting | Description |
|---|---|
--CpuHeavyLog |
Enables CPU heavy logging (default: false) |
--CpuHeavyLogDumpFile |
Custom path for the log file (optional). If set, will enable CpuHeavyLog as well. Default Location: {DumpDirectory}/cpu_heavy.log |
--CpuHeavyLogExpressions |
Named expressions appended to every CPU heavy log line. Repeat the option for each expression to append (for example: --CpuHeavyLogExpressions "life=AX+1" --CpuHeavyLogExpressions "myvalue=word ptr DS:[0x456]"). Only active when --CpuHeavyLog is true. |
Log Format: Each line contains: SegmentedAddress InstructionString
Example:
017D:0000 mov AX,0xDD1D
017D:0003 call near 0xE4AD
017D:E4AD mov SI,0x0080
Usage:
# Enable with default location
Spice86 -e program.exe --CpuHeavyLog
# Use custom log file path
Spice86 -e program.exe --CpuHeavyLog --CpuHeavyLogDumpFile "C:\logs\cpu.log"
# Append named expressions to each log line (example)
Spice86 -e program.exe --CpuHeavyLog \
--CpuHeavyLogExpressions "life=AX+1" \
--CpuHeavyLogExpressions "myvalue=word ptr DS:[0x456]" --Debug (Default: false) Starts the program paused and pauses once again when stopping.
--Cycles (Default: null) Target CPU cycles per ms, for the rare speed sensitive game. 3000 by default. Overrides Instructions per second option below if used.
--Xms (Default: true) Enables 15 MB of XMS memory.
--Ems (Default: true) Enables EMS memory. EMS adds 8 MB of memory accessible to DOS programs through the EMM Page Frame.
--A20Gate (Default: false) Disables the 20th address line to support programs relying on the rollover of memory addresses above the HMA (slightly above 1 MB).
-m, --Mt32RomsPath Zip file or directory containing the MT-32 ROM files
-c, --CDrive Path to C drive, default is exe parent
-r, --RecordedDataDirectory Directory to dump data to when not specified otherwise. If blank dumps to SPICE86_DUMPS_FOLDER, and if not defined dumps to a sub directory named with the program SHA 256 signature
-e, --Exe Required. Path to executable
-a, --ExeArgs List of parameters to give to the emulated program
-x, --ExpectedChecksum Hexadecimal string representing the expected SHA256 checksum of the emulated program
-f, --FailOnUnhandledPort (Default: false) If true, will fail when encountering an unhandled IO port. Useful to check for unimplemented hardware. false by default.
-g, --GdbPort GDB port. If 0, GDB server will be disabled. Default is 10000.
-o, --OverrideSupplierClassName Name of a class that will generate the initial function information. See documentation for more information.
-p, --ProgramEntryPointSegment (Default: 4096) Segment where to load the program. DOS PSP and MCB will be created before it.
-u, --UseCodeOverride (Default: true) <true or false> if false it will use the names provided by overrideSupplierClassName but not the code
-i, --InstructionTimeScale <number of instructions that have to be executed by the emulator to consider a second passed> if blank will use time based timer.
--ClockJitterSeed <CLOCKJITTERSEED> Optional integer seed enabling small deterministic clock jitter (±0.01 ms). Omit to disable.
--ClockStartTime <CLOCKSTARTTIME> Optional UTC start date/time for the emulated clock (parseable by DateTime.Parse, e.g. 1993-06-01T00:00:00Z). When omitted defaults to the current UTC time.
-t, --TimeMultiplier (Default: 1) <time multiplier> if >1 will go faster, if <1 will go slower.
-h, --HeadlessMode [Mode] (Default: false) Headless mode. The mode 'Minimal' does not use any UI components, 'Avalonia' uses the full UI and consumes a bit more memory.
-l, --VerboseLogs (Default: false) Enable verbose level logs
-w, --WarningLogs (Default: false) Enable warning level logs
-s, --SilencedLogs (Default: false) Disable all logs
-i, --InitializeDOS (Default: true) Install DOS interrupt vectors or not.
--CpuHeavyLog (Default: false) Enable CPU heavy logging. Logs every executed instruction to a file. Warning: significant performance impact.
--CpuHeavyLogDumpFile Custom file path for CPU heavy log output. If not specified, defaults to {DumpDirectory}/cpu_heavy.log
--CpuHeavyLogExpressions (Default: none) Named expressions appended to every CPU heavy log line. Repeat the option for each expression to append (for example: --CpuHeavyLogExpressions "life=AX+1"). Only active when --CpuHeavyLog is true.
--ReloadCfgGraph (Default: true) Reload the previously dumped CFG instruction graph (spice86dumpCfgReload.json) at startup so explored program structure is preserved across runs. No-op if the file is absent.
--AsmRenderingStyle Style of the ASM rendering. Spice86 or DosBox.
--StructureFile Path to a C header file that describes the structures in the application. Works best with exports from IDA or Ghidra
--mcp-http-port (Default: 8081) Port for the MCP HTTP server
--RenderingMode (Default: Async) Selects the VGA rendering mode. Sync fires VGA events on the emulation thread for determinism; The default mode is for performance.
--JitMode (Default: InterpretedThenCompiled) Controls how the JIT compiler handles instruction execution delegates.
--AllowIvtAddress0 (Default: false) Controls whether an INT instruction whose IVT entry is 0:0 is treated as valid.
--version Display version information.
- SbType: Sound Blaster card type. Values: None, SB1, SB2, SBPro1, SBPro2, Sb16, GameBlaster, AdlibGold.
- SbIrq: Sound Blaster IRQ line. Default is 7. Common values: 5, 7, 9, 10.
- SbDma: Sound Blaster 8-bit DMA channel. Default is 1. Common values: 0, 1, 3.
- SbHdma: Sound Blaster 16-bit high DMA channel. Default is 5. Common values: 5, 6, 7.
- SbBase: Sound Blaster base I/O address (hex). Default is 0x220. Common values: 0x220, 0x240, 0x260, 0x280.
- OplMode: OPL synthesis mode. Values: None, Opl2, DualOpl2, Opl3, Opl3Gold. Default is Opl3.
- SbMixer: Enable Sound Blaster mixer control of OPL voices. Default is true.
Spice86 speaks the GDB remote protocol:
- it supports most of the commands you need to debug.
- it also provides custom GDB commands to do dynamic analysis.
The GDB server is always started along with the program to execute unless option is set to 0. Default port is 10000.
If you want to pause before starting execution to setup breakpoints and so on, use the --Debug option.
Here is how to connect from GDB command line client and how to set the architecture:
(gdb) target remote localhost:10000
(gdb) set architecture i8086
You can add breakpoints, step, view memory and so on.
Example with a breakpoint on VGA VRAM writes:
(gdb) watch *0xA0000
Viewing assembly:
(gdb) layout asm
Removing a breakpoint:
(gdb) remove 1
Searching for a sequence of bytes in memory (start address 0, length F0000, ascii bytes of 'Spice86' string):
(gdb) find /b 0x0, 0xF0000, 0x53, 0x70, 0x69, 0x63, 0x65, 0x38, 0x36
GDB does not support x86 real mode segmented addressing, so pointers need to refer to the actual physical address in memory. VRAM at address A000:0000 would be 0xA0000 in GDB.
Similarly, The $pc variable in GDB will be exposed by Spice86 as the physical address pointed by CS:IP.
The list of custom commands can be displayed like this:
(gdb) monitor help
(gdb) monitor dumpall
Dumps everything described below in one shot. Files are created in the dump folder as explained here Several files are produced:
- spice86dumpMemoryDump.bin: Snapshot of the real mode address space. Contains the instructions that are actually loaded and executed. They may differ from the exe you are running because DOS programs can rewrite some of their instructions / load additional modules in memory.
- spice86dumpExecutionFlow.json: Contains information about the run such as addresses of the functions, the labels, and the instructions that have been executed. Also consumed by the optional spice86-ghidra-plugin.
- spice86dumpCfgBlocks.json and spice86dumpCfgPartitions.json: the explored CFG as basic blocks, and the function/helper partitions plus transfers derived from those blocks.
- spice86dumpCfgGeneratedOverrides.cs and GeneratedProject/: the C# overrides and runnable project generated from the explored CFG (see Generated C# override project).
Break after x emulated CPU Cycles:
(gdb) monitor breakCycles 1000
Break at the end of the emulated program:
(gdb) monitor breakStop
For a pleasing and productive experience with GDB, the seerGDB client is highly recommended.
At best, use the configuration file spice86.seer provided in the doc directory (here): run Seer with seergdb --project spice86.seer.
If you use a different port for gdb, adjust spice86.seer correspondingly.
Also, while in Seer, set Settings/Configuration/Assembly/Disassembly Mode to “Length”, otherwise the Assembly View won't work.
| Component | Support Level |
|---|---|
| CPU | 8086/8088 fully implemented and tested |
| 80186 (BOUND instruction missing) | |
| 80286 (protected mode not implemented) | |
| 80386 (partial support, protected mode not implemented) | |
| Only 16-bit instructions fully supported | |
| Most 32-bit instructions implemented but not fully tested | |
| No FPU except detection instructions | |
| Memory | 1MB address space with segmented addressing |
| A20 Gate support | |
| EMS 3.2 implemented | |
| XMS 4.0 is implemented | |
| HMA is implemented | |
| No paging support | |
| Graphics | Text modes, VGA, EGA, and CGA implemented |
| EGA and CGA modes are best effort (you may find bugs) | |
| VESA VBE 1.2 is supported | |
| DOS | Largely complete DOS and INT 21h implementation (DOS 5.0) |
| Input | Keyboard and mouse supported |
| No joystick support | |
| CD-ROM | MSCDEX and CDDA support is implemented, including CD images |
| Floppy | Floppy disk emulation is implemented, including floppy images and booting on a floppy image |
| Sound Type | Support Level | Notes |
|---|---|---|
| PC Speaker | ✅ Full | Implemented |
| Adlib/SB OPL | ✅ Full | Ported from DOSBox Staging |
| SoundBlaster | ✅ Full | Ported from DOSBox Staging |
| Adlib Gold | ✅ Full | Ported from DOSBox Staging |
| MT-32 | Not available on macOS | |
| Gravis Ultrasound | ❌ None | Not implemented yet |
| General MIDI | ✅ Full | Supported |
| Feature | Details |
|---|---|
| C Drive | Configurable with --CDrive, defaults to current folder |
| Program Arguments | Pass up to 127 chars with --ExeArgs |
| Time Handling | Real elapsed time (adjustable with --TimeMultiplier) or instruction-based timing with --InstructionTimeScale |
| Screen Refresh | 30 FPS and on VGA retrace wait detection |
| Structure Viewer | Requires C header file (--StructureFile) to display memory structures |
Concrete example with Cryo Dune here.
First run your program and make sure everything works fine in Spice86. If you encounter issues it could be due to unimplemented hardware / DOS / BIOS features.
When Spice86 exits, it dumps its data (memory dump, execution flow, CFG) and generates a C# override project in the dump folder (current folder, or the folder specified by the SPICE86_DUMPS_FOLDER env variable / --RecordedDataDirectory).
The generated project under GeneratedProject/ is immediately runnable and already overrides the explored assembly with equivalent C#. From there you rewrite the generated functions, function by function, to discover and document the program's intent. This is well suited to LLM assistance.
See Generated C# override project for how to run it and Overriding emulated code with C# code for the override API.
Optionally, you can also open the memory dump in Ghidra with the spice86-ghidra-plugin for additional static analysis (see Ghidra plugin).
On every dump (when not already running with overrides), Spice86 turns the explored CFG into a ready-to-run C# project, so you do not need any external tool to start rewriting the program in C#.
Two artifacts are produced in the dump folder:
- spice86dumpCfgGeneratedOverrides.cs: the generated overrides on their own (a
CfgGeneratedOverrideSupplierplus aCfgGeneratedOverrides : CSharpOverrideHelpermapping every discovered function viaDefineFunction). - GeneratedProject/: a self-contained, runnable project wrapping those overrides:
Spice86.Generated.csprojreferences the Spice86 GUI project, so the build always matches the running emulator.Program.csforwards all your arguments to Spice86 and defaults in--OverrideSupplierClassName,--UseCodeOverride true, and the program's--ExpectedChecksum(so the overrides cannot run against a different binary).CfgGeneratedOverrides.cscontains the generated overrides.
Run it like a normal .NET project, passing the path to your executable:
cd <dump folder>/GeneratedProject
dotnet run -- -e /path/to/PROGRAM.EXEThen progressively replace the mechanically generated function bodies with your own readable C# implementations.
Note: while running with overrides active (
--UseCodeOverride true), the CFG and execution-flow dumps are not rewritten, so regenerating from scratch should be done from a plain (non-override) run.
You can provide your own C# code to override the program original assembly code. This is exactly what the generated project does for you automatically; the section below explains the API so you (or an LLM) can write or refine overrides.
Spice86 can take in input an instance of Spice86.Core.Emulator.Function.IOverrideSupplier that builds a mapping between the memory address of functions and their C# overrides.
For a complete example you can check the source code of Cryogenic.
Here is a simple example of how it would look like:
namespace My.Program;
// This class is responsible for providing the overrides to spice86.
// There is only one per program you reimplement.
public class MyProgramOverrideSupplier : IOverrideSupplier {
public IDictionary<SegmentedAddress, FunctionInformation> GenerateFunctionInformations(
ILoggerService loggerService, Configuration configuration, ushort programStartSegment, Machine machine) {
return new MyOverrides(new Dictionary<SegmentedAddress, FunctionInformation>(),
machine, loggerService, configuration).FunctionInformations;
}
public override string ToString() {
return "Overrides My program exe. class is " + GetType().FullName;
}
}
// This class contains the actual overrides. As the project grows, you will probably need to split the reverse engineered code in several classes.
public class MyOverrides : CSharpOverrideHelper {
private readonly MyOverridesGlobalsOnDs globalsOnDs;
public MyOverrides(IDictionary<SegmentedAddress, FunctionInformation> functionInformations,
Machine machine, ILoggerService loggerService, Configuration configuration)
: base(functionInformations, machine, loggerService, configuration) {
globalsOnDs = new MyOverridesGlobalsOnDs(Memory, State.SegmentRegisters);
// IncDialogueCount47A8 will get executed instead of the assembly code when a call to 017D:A1E8 is performed.
// The override method receives the load offset (segment where the program was loaded) as parameter.
DefineFunction(0x017D, 0xA1E8, IncDialogueCount47A8_017D_A1E8_0C0B8);
DefineFunction(0x017D, 0x0100, AddOneToAX_017D_0100_01FD0);
}
public Action IncDialogueCount47A8_017D_A1E8_0C0B8(int loadOffset) {
// Accessing the memory via accessors
globalsOnDs.SetDialogueCount47A8(globalsOnDs.GetDialogueCount47A8() + 1);
// Depends on the actual return instruction performed by the function, needed to be called from the emulated code as
// some programs like to mess with the stack ...
return NearRet();
}
public Action AddOneToAX_017D_0100_01FD0(int loadOffset) {
// Assembly for this would be
// INC AX
// RETN
// Note that you can access the whole emulator to change the state in the overrides.
State.AX++;
return NearRet();
}
}
// Memory accesses can be encapsulated into classes like this to give names to addresses and make the code shorter.
public class GlobalsOnDs : MemoryBasedDataStructureWithDsBaseAddress {
public GlobalsOnDs(IByteReaderWriter memory, SegmentRegisters segmentRegisters) : base(memory, segmentRegisters) {
}
// Getters and Setters for address 0x1DD:0x2/0x1DD2.
// Was accessed via the following registers: DS
public int Get01DD_0002_Word16() {
return UInt16[0x2];
}
public void Set01DD_0002_Word16(ushort value) {
UInt16[0x2] = value;
}
// Getters and Setters for address 0x1138:0x0/0x11380.
public int Get1138_0000_Word16() {
return UInt16[0x0];
}
}The override method name encodes the address (Name_<segment>_<offset>_<linear>) so it can be parsed back into a FunctionInformation; pass an explicit name argument to DefineFunction if you prefer not to follow that convention.
Remember: You must tell Spice86 to use your C# code overrides with the command line argument "--UseCodeOverride true" when debugging your project, along with the mandatory path to your DOS program passed with the "-e" / "--Exe" argument.
Spice86 comes with a built-in debugger. that can be used to debug the emulated program. It is a simple debugger that allows you to inspect the memory, the disassembly, the registers, and the stack.
The structure viewer allows you to inspect the memory in a structured way. It is useful to inspect the memory as a structure, like the DOS PSP, the DOS MCB, the VGA registers, etc.
First you need a C header file that describes the structures in the application. You can generate one with Ghidra or IDA. Then you can load it with the --StructureFile commandline argument.
This will enable the "Structure view" button in the Memory tab of the debugger.
There you enter a segment:offset address and choose the structure you want to view. The structure will be displayed in a tree view and the memory in a hex view.
The display updates whenever the application is paused, so you can step through the program and see how the structure changes. Exporting a new C header file from Ghidra or IDA will also update the structure viewer with the new information real-time.
You can also enter the Structure view by selecting a range of bytes in the Memory tab and right-clicking on it.
Spice86 includes a built-in HTTP server for quick runtime inspection and memory access.
- The HTTP server is disabled by default. Enable it by specifying a port with
--HttpApiPort.
Available endpoints:
| Method | Route | Description |
|---|---|---|
GET |
/api |
API metadata and endpoint list |
GET |
/api/status |
Current emulator status (pause state, CPU state, CS:IP, cycles, memory size) |
GET |
/api/memory/{address}/byte |
Read one byte at address |
PUT |
/api/memory/{address}/byte |
Write one byte at address |
GET |
/api/memory/{address}/range/{length} |
Read a memory range |
It is possible to provide a C: Drive for emulated DOS functions with the option --CDrive. Default is executed program folder. For some games you may need to set the C drive to the parent folder.
You can pass arguments (max 127 chars!) to the emulated program with the option --ExeArgs. Default is empty.
The emulated Timer hardware of the PC (Intel 8259) supports measuring time from either:
- The real elapsed time. Speed can be altered with parameter --TimeMultiplier.
- The number of instructions the emulated CPU executed. This is the behaviour that is activated with parameter --InstructionTimeScale and is forced when in GDB mode so that you can debug with peace of mind without the timer triggering.
Compatibility list available here.
- Install the .NET 10 SDK (once)
- clone the repo
- run this where Spice86.sln is located:
dotnet build Spice86 -e <path to executable>or use this where Spice86.csproj is located:
dotnet run -e <path to executable>if you don't want to manually build and run separately, use the helper script at the root of the repository which builds and runs the app, passing all arguments through:
./run.sh -e <path to executable>Importing the dump into Ghidra is optional. Spice86 already generates a runnable C# override project on its own (see Generated C# override project); the Ghidra plugin is only useful if you additionally want to explore the dump inside Ghidra (static analysis, labeling, or producing a --StructureFile C header for the structure viewer).
This uses Ghidra and Java 17.
Before using it, define an environnement variable named SPICE86_DUMPS_FOLDER pointing to a folder where the Spice86 dumps are located. They are generated on exit.
General procedure, in order:
1.Ghidra's own script 'ImportSymbolScript.py' (input used is "spice86dumpGhidraSymbols.txt")
2.Ghidra's Auto-Analyze (only enable 'Dissasemble Entry Points')
3.Now, you can use the plugin.
Remember: if Ghidra displays SUBROUTINES, use the 'f' key to convert them into functions.
Also, if you have any weird behaviour, make sure you have Java 17 and ONLY Java 17. That's how Ghidra likes it.
Doc here
MCP server documentation is available in doc/mcp.md.
Cryo dune:
Betrayal at Krondor:
The SoundBlaster and Adlib Gold implementations are fully ported from dosbox-staging, replacing the previous one which was modified from the Aeon emulator. This includes PCM and OPL sound quality improvements, far greater emulation accuracy, SB/OPL compatibility, mixer thread logic, audio events, audio hardware delays, and a complete audio re-architecture.
The NukedOpl3 port to C# was done by codeEngine. It is bit-accurate. Thanks a lot!
The DOS implementation is heavily inspired by the clean code from FreeDOS, and DOSBox Staging.
The implementations of MSCDEX, CDDA, Floppy emulation, CD images support, CD drive emulation all used DOSBox Staging as a model (even if the architecture is different), escpecially for conformance about expected behavior (ie. IOCTL).
The BIOS implementation draws heavily from SeaBIOS, IBM PC BIOS reconstructionns, and sometimes DOSBox Staging.
EMS was written with the help of the specs. XMS was written with the help of the specs, but a lot had to be rewritten by looking at the real HIMEM driver, and by reproducing the XMS tests from Microsoft.
Additionally, the project no longer relies on PortAudio. Instead, it uses a fully cross-platform C# port of the SDL2 audio APIs. We only depend on WASAPI (Windows), ALSA (Linux), or CoreAudio (macOS).
This project uses JetBrains Rider licenses, thanks to JetBrains' Open Source Community Support.
The UI is powered by Avalonia UI.





