rt_exec.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. #!/usr/bin/env python3
  2. """Execute the assembled 8086 runtime on a real CPU emulator and check it.
  3. The runtime (shell/Runtime.mod) is assembled by RtProbe, which prints its
  4. bytes as hex; this script loads them at offset 0 of a flat 64K segment - the
  5. layout a .COM gets - calls each entry with a known argument and compares the
  6. bytes it sends to INT 21h with what it expects.
  7. That is the whole point of the exercise: the library is hand-assembled 8086,
  8. so "it built" says nothing. This says it *runs*.
  9. """
  10. import re
  11. import subprocess
  12. import sys
  13. from unicorn import Uc, UC_ARCH_X86, UC_MODE_16
  14. from unicorn.x86_const import (
  15. UC_X86_REG_AX, UC_X86_REG_DX, UC_X86_REG_SP, UC_X86_REG_IP,
  16. UC_X86_REG_CS, UC_X86_REG_DS, UC_X86_REG_ES, UC_X86_REG_SS,
  17. )
  18. import unicorn
  19. HERE = __file__.rsplit("/", 1)[0]
  20. PROBE = HERE + "/rtprobe"
  21. HDR = 0x200 # where the fake program header sits
  22. DATA_BASE = 0x300
  23. DATA_END = 0x320
  24. STACK = 0xF000
  25. SENTINEL = 0xBEEF # "return address" that tells us an entry came back
  26. def load_runtime():
  27. """Run RtProbe, parse its hex dump, return (bytes, {entry: offset})."""
  28. out = subprocess.run([PROBE], capture_output=True, text=True, check=True).stdout
  29. size = int(re.search(r"^(\d+) bytes", out, re.M).group(1))
  30. entries = {n: int(v) for v, n in re.findall(r"entry \d+ = (\d+)\s+\((\w+)\)", out)}
  31. blob = bytearray()
  32. for line in out.splitlines():
  33. m = re.match(r"^[0-9A-F]{8} ((?:[0-9A-F]{2} )+)$", line)
  34. if m:
  35. blob += bytes.fromhex(m.group(1).replace(" ", ""))
  36. assert len(blob) == size, f"parsed {len(blob)} bytes, header says {size}"
  37. return bytes(blob), entries
  38. class Machine:
  39. def __init__(self, blob):
  40. self.blob = blob
  41. self.out = bytearray()
  42. self.input = bytearray()
  43. self.uc = Uc(UC_ARCH_X86, UC_MODE_16)
  44. self.uc.mem_map(0, 0x110000)
  45. self.uc.mem_write(0, blob)
  46. # a program header word block: flag, code end, data base, data end
  47. self.uc.mem_write(HDR, b"\x01\x00\x34\x02\x00\x03\x20\x03\x00\x00")
  48. self.uc.mem_write(DATA_BASE, b"\xAA" * (DATA_END - DATA_BASE)) # poison
  49. for r in (UC_X86_REG_CS, UC_X86_REG_DS, UC_X86_REG_ES, UC_X86_REG_SS):
  50. self.uc.reg_write(r, 0)
  51. self.uc.hook_add(unicorn.UC_HOOK_INTR, self._intr)
  52. def _intr(self, mu, intno, _):
  53. if intno != 0x21:
  54. return
  55. ah = (mu.reg_read(UC_X86_REG_AX) >> 8) & 0xFF
  56. if ah == 0x02: # display character
  57. self.out.append(mu.reg_read(UC_X86_REG_DX) & 0xFF)
  58. elif ah == 0x09: # display $-string
  59. a = mu.reg_read(UC_X86_REG_DX)
  60. while True:
  61. b = mu.mem_read(a, 1)[0]
  62. if b == ord("$"):
  63. break
  64. self.out.append(b)
  65. a += 1
  66. elif ah == 0x4C: # exit
  67. mu.emu_stop()
  68. elif ah == 0x08: # read char, no echo
  69. if self.input:
  70. mu.reg_write(UC_X86_REG_AX, (mu.reg_read(UC_X86_REG_AX) & 0xFF00) | self.input.pop(0))
  71. else:
  72. mu.reg_write(UC_X86_REG_AX, (mu.reg_read(UC_X86_REG_AX) & 0xFF00) | 0x1A)
  73. def call(self, entry, args=(), ax=0):
  74. """Call an entry with `args` pushed (caller-cleaned, like the compiler
  75. does). Returns whatever it wrote to stdout."""
  76. self.out.clear()
  77. uc = self.uc
  78. sp = STACK - 4 * len(args) - 2
  79. words = [SENTINEL] + list(args)
  80. uc.mem_write(sp, b"".join(w.to_bytes(2, "little") for w in words))
  81. uc.reg_write(UC_X86_REG_SP, sp)
  82. uc.reg_write(UC_X86_REG_AX, ax)
  83. uc.reg_write(UC_X86_REG_IP, entry)
  84. # end = SENTINEL: the entry stops by RETurning to it. Using 0 as the
  85. # end address would stop instantly for the entry that lives at 0.
  86. uc.emu_start(entry, SENTINEL, timeout=2_000_000, count=200000)
  87. if uc.reg_read(UC_X86_REG_IP) != SENTINEL:
  88. raise AssertionError(
  89. f"entry {entry} did not return (IP={uc.reg_read(UC_X86_REG_IP):#06x})")
  90. return bytes(self.out)
  91. def main():
  92. blob, ent = load_runtime()
  93. m = Machine(blob)
  94. fails = []
  95. def check(name, got, want):
  96. if got == want:
  97. print(f" ok {name}: {got!r}")
  98. else:
  99. print(f" FAIL {name}: got {got!r} want {want!r}")
  100. fails.append(name)
  101. # TU_InitMem must clear [data base, data end) and leave DS alone
  102. m.call(ent["initmem"], ax=HDR)
  103. cleared = m.uc.mem_read(DATA_BASE, DATA_END - DATA_BASE)
  104. check("initmem zeroes globals", cleared, b"\x00" * (DATA_END - DATA_BASE))
  105. # TU_WrInt: signed 16-bit decimal
  106. for v, want in [(0, b"0"), (1, b"1"), (7, b"7"), (10, b"10"), (999, b"999"),
  107. (12345, b"12345"), (32767, b"32767"),
  108. (-1, b"-1"), (-32768, b"-32768"), (-999, b"-999")]:
  109. check(f"wrint({v})", m.call(ent["wrint"], (v & 0xFFFF,)), want)
  110. check("wrchar('A')", m.call(ent["wrchar"], (ord("A"),)), b"A")
  111. check("wrchar('!')", m.call(ent["wrchar"], (ord("!"),)), b"!")
  112. check("wrbool(0)", m.call(ent["wrbool"], (0,)), b"FALSE")
  113. check("wrbool(1)", m.call(ent["wrbool"], (1,)), b"TRUE")
  114. check("wrbool(2)", m.call(ent["wrbool"], (2,)), b"TRUE")
  115. check("wrln", m.call(ent["wrln"]), b"\r\n")
  116. check("stackchk returns", m.call(ent["stackchk"]), b"")
  117. # a sequence, the way a program actually calls these
  118. m.out.clear()
  119. m.call(ent["wrint"], (42,))
  120. m.call(ent["wrchar"], (ord(" "),))
  121. m.call(ent["wrbool"], (1,))
  122. m.call(ent["wrln"])
  123. check("writeln(42) writeln TRUE", bytes(m.out), b"42 TRUE\r\n")
  124. # TU_RdInt / RdChar / RdBool / RdLn against supplied input
  125. store = 0x400
  126. for text, want in [(b" 42abc", 42), (b"-17 x", -17), (b"+5", 5),
  127. (b"0", 0), (b" 007", 7), (b"1234", 1234)]:
  128. m.input = bytearray(text)
  129. m.uc.mem_write(store, b"\xEE\xEE")
  130. m.call(ent["rdint"], (store,))
  131. got = int.from_bytes(m.uc.mem_read(store, 2), "little", signed=True)
  132. check(f"rdint({text!r})", got, want)
  133. # the delimiter must be left for the following rdln
  134. rest = bytes(m.input)
  135. m.out.clear()
  136. m.call(ent["rdln"])
  137. check(f"rdln eats {rest!r}", bytes(m.out), b"")
  138. for text, want in [(b"Q", ord("Q")), (b"7", ord("7"))]:
  139. m.input = bytearray(text)
  140. m.uc.mem_write(store, b"\xEE\xEE")
  141. m.call(ent["rdchar"], (store,))
  142. check(f"rdchar({text!r})", int.from_bytes(m.uc.mem_read(store, 2), "little"), want)
  143. for text, want in [(b"T", 1), (b"y", 1), (b"1", 1), (b"F", 0), (b"n", 0), (b"0", 0)]:
  144. m.input = bytearray(text)
  145. m.uc.mem_write(store, b"\xEE\xEE")
  146. m.call(ent["rdbool"], (store,))
  147. check(f"rdbool({text!r})", int.from_bytes(m.uc.mem_read(store, 2), "little"), want)
  148. # end of input must terminate the read loops rather than spin
  149. m.input = bytearray()
  150. m.uc.mem_write(store, b"\xEE\xEE")
  151. m.call(ent["rdint"], (store,))
  152. check("rdint at EOF", int.from_bytes(m.uc.mem_read(store, 2), "little"), 0)
  153. m.call(ent["rdln"])
  154. print()
  155. if fails:
  156. print(f"RUNTIME: {len(fails)} FAILURE(S): {', '.join(fails)}")
  157. return 1
  158. print("RUNTIME: all checks passed")
  159. return 0
  160. if __name__ == "__main__":
  161. sys.exit(main())