run_modrm19.py 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. #!/usr/bin/env python3
  2. """run_modrm19.py -- re-run the mod=00/01/10 ModR/M probe and check the result.
  3. modrm19.s derives the 16-bit effective-address table by EXECUTION: it stores a
  4. marker through each candidate encoding on a real 8086 under qemu, then scans
  5. memory for the word and reports the offset it landed at. This script builds
  6. the bootable image, runs it, and requires the measured offsets to equal the
  7. table computed from first principles here -- which is the point, because the
  8. computation in EXPECTED is written from the register arithmetic (BX+SI and so
  9. on) and not from a remembered table.
  10. usage: run_modrm19.py [-v] [--keep]
  11. Requires qemu-system-i386, as and objcopy. This is not part of run_all.sh:
  12. it needs a hand-built floppy image and a 60-second qemu timeout, and its
  13. result is already recorded in the table in Runtime.mod, which
  14. tests/probe/modrm11.py checks on every run. Re-run this when you doubt the
  15. memory forms.
  16. mod=11 is not measured here -- it names a register, not an address. See
  17. modrm11.s for why, and modrm11.py for how that half is established.
  18. """
  19. import os
  20. import shutil
  21. import subprocess
  22. import sys
  23. HERE = os.path.dirname(os.path.abspath(__file__))
  24. SRC = os.path.join(HERE, "modrm19.s")
  25. # scratch beside the tree (TP3-comp/tmp), not in /tmp
  26. WORK = os.path.normpath(os.path.join(HERE, "..", "..", "..", "tmp", "modrm19"))
  27. # The probe's register setup, from modrm19.s. Distinct values, so the offset
  28. # a marker lands at identifies the effective address by arithmetic alone.
  29. BX, DI, SI, BP = 0x1000, 0x2000, 0x0030, 0x0040
  30. DISP8, DISP16 = 0x44, 0x1234
  31. # The effective address for (mod, rm), computed from the arithmetic. This is
  32. # the prediction; the probe's output is the measurement, and the two are
  33. # compared. mod=00 rm=110 is the direct disp16 form, so the address is the
  34. # displacement itself.
  35. def expected(mod, rm):
  36. base = {0: BX + SI, 1: BX + DI, 2: BP + SI, 3: BP + DI,
  37. 4: SI, 5: DI, 7: BX}
  38. if mod == 0:
  39. return DISP16 if rm == 6 else base[rm]
  40. disp = DISP8 if mod == 1 else DISP16
  41. if rm == 6: # [BP]+disp
  42. return BP + disp
  43. return base[rm] + disp
  44. # The one cell execution cannot answer. mod=10 rm=001 is BX+DI+disp16 =
  45. # 0x4234, and the probe's scan window stops at 0x3600, so the marker is
  46. # written but never seen. Recorded as a gap, not as a result; the cell is
  47. # covered statically by the recipes in Runtime.mod.
  48. GAP = {(2, 1)}
  49. def build():
  50. subprocess.run(["as", "--32", "-o", WORK + ".o", SRC], check=True)
  51. subprocess.run(["objcopy", "-O", "binary", "-j", ".text",
  52. WORK + ".o", WORK + ".bin"], check=True)
  53. with open(WORK + ".bin", "rb") as f:
  54. code = f.read()
  55. # A boot sector is 512 bytes: EB 3C at 0, code at 0x3E, 55 AA at 0x1FE.
  56. if 0x3E + len(code) > 512:
  57. raise SystemExit("probe code is %d bytes, does not fit after the "
  58. "0x3E header" % len(code))
  59. img = bytearray(512)
  60. img[0:2] = b"\xeb\x3c"
  61. img[0x3E:0x3E + len(code)] = code
  62. img[0x1FE:0x200] = b"\x55\xaa"
  63. with open(WORK + ".img", "wb") as f:
  64. f.write(bytes(img))
  65. return len(code)
  66. def run():
  67. """boot the image under qemu, return the captured serial bytes"""
  68. ser = WORK + ".ser"
  69. if os.path.exists(ser):
  70. os.remove(ser)
  71. # The guest does all its work in the first few milliseconds and then halts;
  72. # qemu keeps running, so the timeout is what ends it. rc=124 is the normal
  73. # outcome. Any other non-zero rc is a real failure.
  74. rc = subprocess.run(["timeout", "10", "qemu-system-i386",
  75. "-drive", "file=%s.img,format=raw,if=floppy" % WORK,
  76. "-serial", "file:" + ser,
  77. "-display", "none", "-no-reboot"],
  78. stdout=subprocess.DEVNULL,
  79. stderr=subprocess.DEVNULL).returncode
  80. if rc not in (0, 124):
  81. raise SystemExit("qemu exited %d; the probe did not run to the end"
  82. % rc)
  83. with open(ser, "rb") as f:
  84. return f.read()
  85. def main(argv):
  86. verbose = "-v" in argv
  87. for tool in ("as", "objcopy", "qemu-system-i386"):
  88. if not shutil.which(tool):
  89. print("SKIP: %s not installed" % tool)
  90. return 0
  91. os.makedirs(os.path.dirname(WORK), exist_ok=True)
  92. n = build()
  93. data = run()
  94. # 24 groups of "lo hi 0x20", then a blank line. The probe writes 0A 0A
  95. # but qemu can lose the last byte when it tears down, so require the tail
  96. # to be newline(s) and not insist on both.
  97. if len(data) < 24 * 3 + 1 or set(data[24 * 3:]) - {0x0A}:
  98. print("FAIL: serial capture is %d bytes and does not end in the "
  99. "expected blank line" % len(data))
  100. return 1
  101. got = [int.from_bytes(data[i:i + 2], "little")
  102. for i in range(0, 24 * 3, 3)]
  103. if any(data[i + 2] != 0x20 for i in range(0, 24 * 3, 3)):
  104. print("FAIL: a group separator is not 0x20")
  105. return 1
  106. bad = []
  107. for mod in range(3):
  108. for rm in range(8):
  109. g = got[mod * 8 + rm]
  110. if (mod, rm) in GAP:
  111. if g != 0xFFFF:
  112. bad.append("mod=%d rm=%d: expected the known gap "
  113. "(0xFFFF, outside the scan window), got %04X"
  114. % (mod, rm, g))
  115. continue
  116. w = expected(mod, rm)
  117. if g != w:
  118. bad.append("mod=%d rm=%d: measured %04X, arithmetic says %04X"
  119. % (mod, rm, g, w))
  120. if verbose:
  121. for mod in range(3):
  122. row = []
  123. for rm in range(8):
  124. g = got[mod * 8 + rm]
  125. row.append(" gap " if (mod, rm) in GAP else "%04X" % g)
  126. print("mod=%02d : %s" % (mod, " ".join(row)))
  127. print("mod=00/01/10: %d of 24 cells measured by execution on a real 8086"
  128. % (24 - len(GAP)))
  129. print(" %d cell is a documented gap, not a result"
  130. % len(GAP))
  131. if bad:
  132. print("FAIL: %d problem(s)" % len(bad))
  133. for b in bad:
  134. print(" - %s" % b)
  135. return 1
  136. print("PASS: measured effective addresses match the arithmetic in "
  137. "Runtime.mod")
  138. return 0
  139. if __name__ == "__main__":
  140. sys.exit(main(sys.argv))