Skip to content

dos_emu ​

Tool tools/emu/dos_emu (library, Emulators). Call functions inside a 16-bit DOS or Windows 3.x program (NE or MZ) under Unicorn.

A library: other tools and your own scripts import it. Source on GitHub.

Its own help ​

This is the help text in the tool itself, as ./gameframe tool dos_emu prints it.

text
Call functions inside a 16-bit DOS or Windows 3.x program (NE or MZ) under Unicorn.

The 16-bit counterpart of pe_emu.py: run one original routine with inputs you choose and read
its exact result (ground truth for a reimplementation), without DOS, DOSBox or the game.

    from dos_emu import DosEmu, Long, FarPtr
    emu = DosEmu('game.exe')                       # NE: protected mode, Ghidra's selectors
    emu.call('RNG_SEED', Long(1234))               # an exported name, '1000:000f', or (sel, off)
    print(emu.call('1000:0028', 100))              # AX; call_long() returns DX:AX
    buf = emu.bytes_in(b'abc')                     # a FarPtr into emulated memory
    emu.call('CHECKSUM', buf, 3)
    screen = emu.new_segment()                     # a whole 64 KB segment at offset 0
    emu.run_range(emu.entry, '1000:ca59')          # a running program: entry to a chosen point,
    emu.run_to_any(['1000:cb00', '1000:cb40'])     # then on to whichever comes first (its index)

NE programs (Borland Pascal 7 DPMI16, Windows 3.x) run in 16-bit protected mode. Segment N gets
selector 0x1000 + 8 * (N - 1), the same numbers Ghidra's NE loader uses, so an address from the
Ghidra export (`1000:0028`, `FUN_1000_0028`) is the address here. Relocations are applied:
selectors, far pointers and offsets to the program's own segments; imports (by ordinal or name)
become stubs. DS is the auto data segment; SS:SP come from the NE header (the auto data
segment when the header says so, as in Windows programs; Borland Pascal 7 DPMI16 programs
have a stack segment of their own).

MZ programs (real mode) load at segment 0x1000, as in Ghidra, with the PSP at 0x0FF0 and DS = ES
= PSP as DOS leaves them; SS:SP come from the header.

Arguments: an int is one word; `Long(v)` is a 32-bit value (high word pushed first); a `FarPtr`
or a (segment, offset) tuple is a far pointer (segment pushed first). The default convention is
`pascal`: arguments pushed left to right and removed by the callee, as Borland Pascal and the
Windows API do. Pass conv='cdecl' for C code (pushed right to left). Routines are called far
(a near routine returns with a plain `ret`: call it through a far caller).

Imports and interrupts: `stubs={'KERNEL.3': fn}` (module.ordinal or module.NAME) and
`interrupts={0x21: fn}` give Python handlers; each gets the emulator, reads and sets registers
with `emu.reg('ax')` / `emu.set(ax=...)`, and an import handler returns the number of argument
bytes to remove (a Pascal callee's `retf N`). An import or interrupt with no handler stops the
run with its name and the caller's address, so you know exactly what to stub next.

What it does not do: run DOS, load overlays or DLLs, emulate DPMI services, timers, VGA or sound
hardware, or the x87 (Borland's floating-point emulator fixups are left as they are). Use DOSBox-X
(scripts/dosbox-run.sh) when you need the whole program running. Needs `unicorn`.

Named by ​

Skills: re-inventory, re-observe, re-emulate, re-verify.

Code: Apache 2.0. Content: CC0 1.0. No game material: bring your own copy.