Add interrupts module: INT 3 (CC breakpoint) vs INT 21h (DOS services)
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
This commit is contained in:
Executable
+71
@@ -0,0 +1,71 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user