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.0 KiB
Python
Executable File
72 lines
3.0 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Mini software debugger on Unicorn (16-bit x86 core).
|
|
|
|
Puts a breakpoint on the first INC CX of bp_target.asm twice: once patched with the
|
|
one-byte CC, once with the two-byte CD 03. A real debugger saves ONE byte (because the
|
|
breakpoint opcode is one byte) and, on a trap, backs IP up by ONE. The experiment shows
|
|
why that only works for CC.
|
|
|
|
Unicorn is a modern x86 core in real mode, not an 8086: it is used here only for
|
|
instruction-decoding and control-flow facts (opcode lengths, return addresses).
|
|
"""
|
|
import subprocess, sys, pathlib
|
|
from unicorn import Uc, UcError, UC_ARCH_X86, UC_MODE_16, UC_HOOK_INTR
|
|
from unicorn.x86_const import UC_X86_REG_IP, UC_X86_REG_CX, UC_X86_REG_EFLAGS, UC_X86_REG_CS
|
|
|
|
HERE = pathlib.Path(__file__).resolve().parent
|
|
ASM = HERE.parent / "asm" / "bp_target.asm"
|
|
BASE = 0x7C00
|
|
|
|
def assemble():
|
|
out = HERE.parent / "build" / "bp_target.bin"
|
|
out.parent.mkdir(exist_ok=True)
|
|
subprocess.run(["nasm", "-f", "bin", str(ASM), "-o", str(out)], check=True)
|
|
return out.read_bytes()
|
|
|
|
def run(code, patch, label):
|
|
SITE, DONE = BASE + 2, BASE + 5 # offsets fixed by the listing (xor cx,cx = 2 bytes)
|
|
uc = Uc(UC_ARCH_X86, UC_MODE_16)
|
|
uc.mem_map(0, 0x100000)
|
|
uc.mem_write(BASE, code)
|
|
saved = bytes(uc.mem_read(SITE, 1)) # a debugger saves exactly ONE byte: CC is one byte
|
|
uc.mem_write(SITE, patch)
|
|
uc.reg_write(UC_X86_REG_CS, 0)
|
|
uc.reg_write(UC_X86_REG_EFLAGS, 0x202)
|
|
events = []
|
|
def on_int(uc, intno, _):
|
|
ip = uc.reg_read(UC_X86_REG_IP) # return address (first byte after the trapping instruction)
|
|
events.append((intno, ip))
|
|
if intno == 3:
|
|
bp = ip - 1 # "back up one byte" -- only right for CC
|
|
if bp == SITE:
|
|
uc.mem_write(SITE, saved) # restore the original byte, re-run it
|
|
uc.reg_write(UC_X86_REG_IP, bp)
|
|
uc.hook_add(UC_HOOK_INTR, on_int)
|
|
err = None
|
|
try:
|
|
uc.emu_start(BASE, DONE, count=50)
|
|
except UcError as e:
|
|
err = str(e)
|
|
cx = uc.reg_read(UC_X86_REG_CX)
|
|
ip = uc.reg_read(UC_X86_REG_IP)
|
|
print("%-8s patch=%-6s trap: vector=%d return IP=%04X (site=%04X, +%d) " % (
|
|
label, patch.hex().upper(), events[0][0], events[0][1], SITE, events[0][1] - SITE) +
|
|
"debugger restores IP=%04X" % (events[0][1] - 1))
|
|
print(" after resume: CX=%d (expected 3), final IP=%04X%s" % (cx, ip, (" [" + err + "]") if err else ""))
|
|
return cx, err
|
|
|
|
def main():
|
|
code = assemble()
|
|
print("clean run, no breakpoint:")
|
|
uc = Uc(UC_ARCH_X86, UC_MODE_16); uc.mem_map(0, 0x100000); uc.mem_write(BASE, code)
|
|
uc.reg_write(UC_X86_REG_CS, 0); uc.emu_start(BASE, BASE + 5, count=50)
|
|
print(" CX=%d\n" % uc.reg_read(UC_X86_REG_CX))
|
|
cx1, e1 = run(code, b"\xCC", "CC")
|
|
cx2, e2 = run(code, b"\xCD\x03", "CD 03")
|
|
ok = (cx1 == 3 and e1 is None and cx2 != 3)
|
|
print("\nRESULT:", "PASS" if ok else "UNEXPECTED")
|
|
sys.exit(0 if ok else 1)
|
|
|
|
if __name__ == "__main__":
|
|
main()
|