#!/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()