# 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](https://github.com/tanepiper/game-frame/tree/main/tools/emu/dos_emu).

## 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](/skills/re-inventory), [re-observe](/skills/re-observe), [re-emulate](/skills/re-emulate), [re-verify](/skills/re-verify).
