modrm11.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. #!/usr/bin/env python3
  2. """modrm11.py -- check the mod=11 half of the 16-bit ModR/M table.
  3. Establishes the register identities for ModRM mod=11 by asking GNU `as`
  4. (.code16) to ENCODE the register moves, FCML to decode what came out, and
  5. requiring all of that to agree with both the hard-coded byte sequences below
  6. and the table recorded in Runtime.mod. See probe/modrm11.s for why the
  7. assembler is the right oracle here, and why the obvious qemu probe for this
  8. half cannot work.
  9. The table
  10. ---------
  11. ModRM is mod:b7b6 reg:b5b4b3 r/m:b2b1b0. In mod=11 both fields name a
  12. register, in the same order, with the same list:
  13. code 000 001 010 011 100 101 110 111
  14. reg AX CX DX BX SP BP SI DI
  15. r/m AX CX DX BX SP BP SI DI
  16. Which of the two operands a field names depends on the opcode, and this is
  17. the part that has to be right or the whole table comes out shifted:
  18. 88 /r MOV r/m8, r8 89 /r MOV r/m16, r16 reg is the SOURCE
  19. 8A /r MOV r8, r/m8 8B /r MOV r16, r/m16 reg is the TARGET
  20. So for `movw %bx, %si` (AT&T: source BX, destination SI) the reg field is
  21. BX and the r/m field is SI -- and SI lands in the low three bits as 110.
  22. The memorable version of the r/m column drops AX off the front and invents a
  23. duplicate BX at the end, which shifts every code down by one cell. That is
  24. how "MovSiBx" came to be written 89 DC, which is MOV SP,BX. The shifted
  25. version gets 100 right by luck (SP is at 100 either way), so the error hides
  26. until you check a register below it, and every wrong byte still decodes
  27. cleanly -- a structural check and a disassembly golden both accept it. That
  28. is the entire reason this file exists.
  29. Non-vacuity
  30. -----------
  31. Groups 1-3 of the .s compare that file against the assembler, which on its
  32. own cannot fail for a reason other than "someone edited the claims": edit
  33. the .s and the assembler faithfully re-encodes the new claim. Two things
  34. stop that. Every instruction's bytes are also hard-coded in EXPECT below, so
  35. editing the .s to contradict the table fails even though the encoder agrees.
  36. And group 4 of the .s is four anchors nobody types by hand -- ADD SP,8 /
  37. ADD SI,2 / MOV BP,SP / MOV SP,BP -- whose cells are asserted against the
  38. table directly, so a table shifted by one cell cannot satisfy them. Both
  39. failure modes are exercised by probe/README.md's non-vacuity runs.
  40. Usage: modrm11.py [-v] # -v prints the table
  41. """
  42. import os
  43. import re
  44. import subprocess
  45. import sys
  46. HERE = os.path.dirname(os.path.abspath(__file__))
  47. sys.path.insert(0, HERE)
  48. sys.path.insert(0, os.path.dirname(HERE)) # disasm16.py lives one level up
  49. import disasm16 # noqa: E402
  50. SRC = os.path.join(HERE, "modrm11.s")
  51. # The word-form register list, used for BOTH the reg field and the r/m field
  52. # in mod=11. Index is the code.
  53. REG = ["Ax", "Cx", "Dx", "Bx", "Sp", "Bp", "Si", "Di"]
  54. # The 8-bit list, which differs from REG at index 4 only: AH, not SP.
  55. REG8 = ["Al", "Cl", "Dl", "Bl", "Ah", "Ch", "Dh", "Bh"]
  56. # Group boundaries in the .s, as (first, count).
  57. G_RM, G_REG, G_BYTE, G_ANCHOR = 0, 8, 16, 22
  58. # The bytes every instruction in the .s must assemble to, in order. These are
  59. # the point of the file: they are asserted independently of the .s text, so
  60. # the .s cannot be edited into agreement with a wrong table.
  61. EXPECT = [
  62. "89 D9", "89 DA", "89 DB", "89 DC", "89 DD", "89 DE", "89 DF", "89 DB",
  63. "89 C7", "89 CF", "89 D7", "89 DF", "89 E7", "89 EF", "89 F7", "89 FF",
  64. "88 C6", "88 F0", "88 C2", "88 D0", "88 C3", "88 D8",
  65. "83 C4 08", "83 C6 02", "8B EC", "8B E5",
  66. ]
  67. # The anchors, each with the decode its .s comment claims and the table cells
  68. # it pins. (index into REG, register). The opcode's reg field carries the
  69. # operation (/0 = ADD) for the first two, so only their r/m fields say
  70. # anything about the register list; the two MOV anchors pin both fields.
  71. ANCHORS = [
  72. ("ADD SP, 8", "add sp,8h", [(4, "Sp")]),
  73. ("ADD SI, 2", "add si,2h", [(6, "Si")]),
  74. ("MOV BP, SP", "mov bp,sp", [(5, "Bp"), (4, "Sp")]),
  75. ("MOV SP, BP", "mov sp,bp", [(4, "Sp"), (5, "Bp")]),
  76. ]
  77. RE_INSN = re.compile(r"^\s*([a-z]+)\s+([^,]+),\s*([^#;]+?)\s*(?:#.*|;.*)?$")
  78. def build():
  79. """assemble and objcopy, returning the .text bytes"""
  80. # scratch beside the tree, not in /tmp; HERE is shell/tests/probe
  81. tmp = os.path.normpath(os.path.join(HERE, "..", "..", "..", "tmp"))
  82. os.makedirs(tmp, exist_ok=True)
  83. o = os.path.join(tmp, "modrm11.o")
  84. b = os.path.join(tmp, "modrm11.bin")
  85. subprocess.run(["as", "--32", "-o", o, SRC], check=True)
  86. subprocess.run(["objcopy", "-O", "binary", "-j", ".text", o, b], check=True)
  87. with open(b, "rb") as f:
  88. return f.read()
  89. def source_instructions():
  90. """the (mnemonic, src, dst) list the .s file asks for, in order.
  91. The four anchor instructions are literal .byte directives, so they do
  92. not appear here; they are checked from EXPECT and ANCHORS instead.
  93. """
  94. out = []
  95. with open(SRC) as f:
  96. for line in f:
  97. m = RE_INSN.match(line)
  98. if m and m.group(1) in ("movw", "movb"):
  99. # strip the AT&T sigils: FCML prints Intel order, without them
  100. out.append((m.group(1),
  101. m.group(2).strip().lstrip("%"),
  102. m.group(3).strip().lstrip("%")))
  103. return out
  104. def decode_all(text, problems):
  105. """linear sweep, returning [(hexbytes, inteltext)] per instruction"""
  106. got = []
  107. off = 0
  108. while off < len(text):
  109. t, n = disasm16.decode(text[off:], off)
  110. if n == 0:
  111. problems.append("decode error at +%d (%s)"
  112. % (off, text[off:off + 6].hex(" ").upper()))
  113. break
  114. got.append((text[off:off + n].hex(" ").upper(), (t or "?").lower()))
  115. off += n
  116. return got
  117. def main(argv):
  118. verbose = "-v" in argv
  119. problems = []
  120. want = source_instructions()
  121. if not want:
  122. print("FAIL: no instructions parsed out of %s" % SRC)
  123. return 1
  124. if len(want) != len(EXPECT) - len(ANCHORS):
  125. print("FAIL: %s asks for %d mnemonic lines; EXPECT has %d entries "
  126. "less %d anchors" % (SRC, len(want), len(EXPECT), len(ANCHORS)))
  127. return 1
  128. text = build()
  129. got = decode_all(text, problems)
  130. if len(got) != len(EXPECT):
  131. problems.append("assembled %d instructions, expected %d"
  132. % (len(got), len(EXPECT)))
  133. if problems:
  134. print("FAIL: %d problem(s)" % len(problems))
  135. for p in problems:
  136. print(" - %s" % p)
  137. return 1
  138. def field(i, shift):
  139. """the 3-bit ModRM field of instruction i"""
  140. return (int(got[i][0].split()[1], 16) >> shift) & 7
  141. # --- the bytes must be the ones recorded here, not merely self-consistent
  142. for i, exp in enumerate(EXPECT):
  143. if got[i][0] != exp:
  144. # instructions at or past G_ANCHOR are literal .byte directives and
  145. # so have no source line to name them by
  146. label = (" ".join(want[i]) if i < G_ANCHOR
  147. else "anchor %d" % (i - G_ANCHOR))
  148. problems.append("instruction %d (%s): assembled %s, expected %s"
  149. % (i, label, got[i][0], exp))
  150. # --- every mnemonic line must decode to what the .s asked for ---------
  151. for i in range(G_ANCHOR):
  152. mne, src, dst = want[i]
  153. want_txt = "mov %s,%s" % (dst.lower(), src.lower())
  154. if got[i][1] != want_txt:
  155. problems.append("instruction %d: asked for AT&T `%s %s, %s`, "
  156. "as+fcml gave Intel %r"
  157. % (i, mne, src, dst, got[i][1]))
  158. # --- group 1: the r/m column, low three bits, naming the DESTINATION --
  159. for i in range(G_RM, G_RM + 8):
  160. mne, src, dst = want[i]
  161. c = field(i, 0)
  162. if REG[c].lower() != dst.lower():
  163. problems.append("r/m %d: %s has ModRM r/m=%d, table says %s but "
  164. "the .s asked for %s"
  165. % (i - G_RM, got[i][0], c, REG[c], dst))
  166. # --- group 2: the reg column, bits 5..3, naming the SOURCE -----------
  167. for i in range(G_REG, G_REG + 8):
  168. mne, src, dst = want[i]
  169. c = field(i, 3)
  170. if REG[c].lower() != src.lower():
  171. problems.append("reg %d: %s has ModRM reg=%d, table says %s but "
  172. "the .s asked for %s"
  173. % (i - G_REG, got[i][0], c, REG[c], src))
  174. # --- group 3: the 8-bit list, and the direction the opcode implies ---
  175. for i in range(G_BYTE, G_BYTE + 6):
  176. mne, src, dst = want[i]
  177. op = int(got[i][0].split()[0], 16)
  178. if op not in (0x88, 0x8A):
  179. problems.append("byte move %d: opcode %02X, expected 88 or 8A"
  180. % (i - G_BYTE, op))
  181. continue
  182. # 88 is MOV r/m8,r8 (r/m is the destination); 8A is MOV r8,r/m8
  183. # (r/m is the source). The ModRM byte is identical either way, so
  184. # only the opcode says which way the data goes.
  185. rmf = field(i, 0)
  186. rmf_reg = dst if op == 0x88 else src
  187. if REG8[rmf].lower() != rmf_reg.lower():
  188. problems.append("byte move %d: %s has r/m=%d, table says %s but "
  189. "the .s asked for %s"
  190. % (i - G_BYTE, got[i][0], rmf, REG8[rmf],
  191. rmf_reg))
  192. # --- group 4: the anchors, against the table directly ----------------
  193. # The anchors are literal .byte directives, so there is no source line to
  194. # cross-check: their bytes come from EXPECT and their decode comes from
  195. # here, and both are asserted. What they add is a table cell that this
  196. # file's author did not choose.
  197. for k, (label, want_txt, cells) in enumerate(ANCHORS):
  198. i = G_ANCHOR + k
  199. ghex, gtext = got[i]
  200. if gtext != want_txt:
  201. problems.append("anchor %s (%s): FCML decodes %r, the .s comment "
  202. "claims %r" % (label, ghex, gtext, want_txt))
  203. for c, reg in cells:
  204. if REG[c].lower() != reg.lower():
  205. problems.append("anchor %s (%s): ModRM code %d must be %s, "
  206. "table says %s"
  207. % (label, ghex, c, reg, REG[c]))
  208. if not any(REG[c].lower() == reg.lower() for c, reg in cells):
  209. problems.append("anchor %s (%s) pins no live cell -- the anchor "
  210. "has stopped testing anything" % (label, ghex))
  211. if verbose:
  212. print("mod=11 r/m field. 89 is MOV r/m,r, so reg is pinned to BX and")
  213. print("the low three bits ARE the r/m code, naming the destination.")
  214. for i in range(G_RM, G_RM + 8):
  215. print(" r/m %03d asked %-4s %s code %d table %-4s %s"
  216. % (i - G_RM, want[i][2], got[i][0], field(i, 0),
  217. REG[field(i, 0)],
  218. "ok" if REG[field(i, 0)].lower() == want[i][2].lower()
  219. else "MISMATCH"))
  220. print("mod=11 reg field. rm is pinned to DI, bits 5..3 are the reg")
  221. print("code, and for 89 that is the source.")
  222. for i in range(G_REG, G_REG + 8):
  223. print(" reg %03d asked %-4s %s code %d table %-4s %s"
  224. % (i - G_REG, want[i][1], got[i][0], field(i, 3),
  225. REG[field(i, 3)],
  226. "ok" if REG[field(i, 3)].lower() == want[i][1].lower()
  227. else "MISMATCH"))
  228. print("8-bit mod=11: same shape, list differs at 100 (AH not SP),")
  229. print("and the opcode alone says which way the data goes.")
  230. for i in range(G_BYTE, G_BYTE + 6):
  231. print(" asked %-4s -> %-4s %s r/m %d = %s"
  232. % (want[i][1], want[i][2], got[i][0], field(i, 0),
  233. REG8[field(i, 0)]))
  234. print("anchors: encodings nobody types by hand, asserted against the")
  235. print("table. A table shifted by one cell cannot satisfy these.")
  236. for k, (label, _, cells) in enumerate(ANCHORS):
  237. i = G_ANCHOR + k
  238. print(" %-10s %-9s %-12s pins %s"
  239. % (label, got[i][0], got[i][1],
  240. ", ".join("%d=%s" % (c, reg) for c, reg in cells)))
  241. print("mod=11 table: %d r/m codes, %d reg codes, %d byte moves and %d "
  242. "anchors all agree -- as encoding, FCML decode, the hard-coded"
  243. % (8, 8, 6, len(ANCHORS)))
  244. print(" EXPECT bytes, and the table in Runtime.mod")
  245. if problems:
  246. print("FAIL: %d problem(s)" % len(problems))
  247. for p in problems:
  248. print(" - %s" % p)
  249. return 1
  250. print("PASS: mod=11 register identities match the table in Runtime.mod")
  251. return 0
  252. if __name__ == "__main__":
  253. sys.exit(main(sys.argv))