NASM sources, DOSBox/FreeDOS Debug/Unicorn harness, captured output and 31 assertions backing the ankurm.com article "INT 3 vs INT 21h in 8086 Assembly". Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01KqJyCidz3ZgRyHABv2GVJh
72 lines
3.2 KiB
Python
Executable File
72 lines
3.2 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Assemble the .COM demos with NASM and run them inside DOSBox (headless).
|
|
|
|
DOSBox provides a real MS-DOS-compatible INT 21h implementation (and its own INT 3
|
|
default vector), so this is where the INT 21h and INT 3 behaviour is actually executed.
|
|
"""
|
|
import os, pathlib, re, shutil, subprocess, sys, tempfile
|
|
|
|
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
|
ASM, BUILD, OUT = ROOT / "asm", ROOT / "build", ROOT / "output"
|
|
BUILD.mkdir(exist_ok=True); OUT.mkdir(exist_ok=True)
|
|
|
|
PROGRAMS = { # source -> DOS 8.3 name
|
|
"hello21": "HELLO21.COM", "fn_tour": "TOUR.COM", "int3_handler": "I3H.COM",
|
|
"int3_default": "I3D.COM", "dbg_cc": "DBGCC.COM", "dbg_cd03": "DBGCD.COM",
|
|
}
|
|
|
|
def nasm(src, out, listing=None):
|
|
cmd = ["nasm", "-f", "bin", str(ASM / (src + ".asm")), "-o", str(out)]
|
|
if listing:
|
|
cmd += ["-l", str(listing)]
|
|
subprocess.run(cmd, check=True, cwd=ASM)
|
|
|
|
def clean(text):
|
|
text = text.replace("\r\n", "\n").replace("\r", "\n")
|
|
return "\n".join(l.rstrip() for l in text.split("\n")).rstrip("\n") + "\n"
|
|
|
|
def main():
|
|
# 1. opcode listing (assembled, never executed)
|
|
nasm("opcodes", BUILD / "opcodes.bin", OUT / "01-opcodes.lst")
|
|
lst = (OUT / "01-opcodes.lst").read_text()
|
|
lst = "\n".join(l for l in lst.split("\n") if l.strip())
|
|
(OUT / "01-opcodes.lst").write_text(lst + "\n")
|
|
|
|
work = pathlib.Path(tempfile.mkdtemp(prefix="dosbox_"))
|
|
for src, dos in PROGRAMS.items():
|
|
nasm(src, work / dos)
|
|
have_debug = (BUILD / "DEBUG.COM").exists()
|
|
if have_debug:
|
|
shutil.copy(BUILD / "DEBUG.COM", work / "DEBUG.COM")
|
|
(work / "IN.TXT").write_bytes(b"ZAnkur\r\n") # stdin for fn_tour: AH=01h reads Z, AH=0Ah reads Ankur
|
|
# DEBUG script: list code, run to the breakpoint, show registers, single-step once, show registers, run on, quit
|
|
(work / "DC.TXT").write_bytes(b"u 100 L9\r\ng\r\nr\r\nt\r\nr\r\ng\r\nq\r\n")
|
|
|
|
steps = [
|
|
("HELLO21.COM > O1.TXT", None),
|
|
("if errorlevel 7 echo ERRORLEVEL is at least 7 >> O1.TXT", None),
|
|
("TOUR.COM < IN.TXT > O2.TXT", None),
|
|
("I3H.COM > O3.TXT", None),
|
|
("I3D.COM > O4.TXT", None),
|
|
]
|
|
if have_debug:
|
|
steps += [("DEBUG DBGCC.COM < DC.TXT > O5.TXT", None), ("DEBUG DBGCD.COM < DC.TXT > O6.TXT", None)]
|
|
conf = "[sdl]\nfullscreen=false\n[autoexec]\nmount c %s\nc:\n%s\nexit\n" % (work, "\n".join(s for s, _ in steps))
|
|
(work / "run.conf").write_text(conf)
|
|
env = dict(os.environ, SDL_VIDEODRIVER="dummy", SDL_AUDIODRIVER="dummy")
|
|
subprocess.run(["dosbox", "-conf", str(work / "run.conf"), "-noconsole"], env=env, timeout=180,
|
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
|
|
names = {"O1.TXT": "02-hello21.txt", "O2.TXT": "03-fn-tour.txt", "O3.TXT": "04-int3-handler.txt",
|
|
"O4.TXT": "05-int3-default.txt", "O5.TXT": "06-debug-cc.txt", "O6.TXT": "07-debug-cd03.txt"}
|
|
for src, dst in names.items():
|
|
p = work / src
|
|
if p.exists():
|
|
(OUT / dst).write_text(clean(p.read_text(errors="replace")))
|
|
else:
|
|
print("missing", src, "(DEBUG.COM not fetched? run scripts/fetch-debug.sh)", file=sys.stderr)
|
|
shutil.rmtree(work, ignore_errors=True)
|
|
|
|
if __name__ == "__main__":
|
|
main()
|