audit_helpers.py 55 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068
  1. #!/usr/bin/env python3
  2. """audit_helpers.py -- check that Runtime.mod's one-liner emitter procedures
  3. emit the instruction their name claims.
  4. Why this exists
  5. ---------------
  6. Runtime.mod hand-assembles 8086 by writing raw bytes. Most of it is wrapped
  7. up as one-line helpers:
  8. PROCEDURE MovSiBx ; BEGIN B (89H) ; B (0DCH) END MovSiBx ;
  9. and those names are the only documentation of what the bytes mean. That is
  10. exactly the setup for a silent, expensive mistake: write the right opcode and
  11. the wrong ModRM, and nothing complains. It happened twice here, in the same
  12. direction both times:
  13. MovSiBx emitted 89 DC -- which is MOV SP,BX, not MOV SI,BX
  14. CmpSiBx emitted 39 DC -- ditto, CMP SP,BX
  15. The comment next to CmpSiBx even spelled the ModRM out as "11 011 100" and
  16. still got it wrong, because 11 011 100 in mod=11 means rm=100=SP; SI is
  17. rm=100 only in mod=00, where it means [SI]. Both bugs shipped together into
  18. EmitWrInt, which then stored decimal digits through an SI register it had
  19. never initialised. The structural checks in check_runtime.py could not see
  20. any of this: the bytes decoded cleanly, the sweep stayed in sync, the branch
  21. targets were all on boundaries and none of the entry prologues moved. Only a
  22. *name* versus a *decode* comparison finds it, because only that knows what
  23. the author was trying to say.
  24. So: for every helper whose body is a literal byte list, disassemble those
  25. bytes with FCML and require the decode to match the name. The name grammar
  26. is deliberately narrow and mechanical:
  27. <Op><Dest><Src> e.g. MovSiBx, StDiAx, CmpSpBx, MovDlSi
  28. <Op><Reg> e.g. XorAxAx, NegAx, NotDx, IncSi, DecSi
  29. with Op in {Mov, Lea, St, Ld, Cmp, Add, Sub, Xor, And, Or, Not, Inc, Dec,
  30. Push, Pop, Int, Jmp, Jne, Jge, Jnl, Jle, Jl, Je, Jae, Jbe, Jb, Ja, JaE...}
  31. and the register/operand words below. A helper whose name does not parse is
  32. reported as UNPARSED rather than silently skipped -- a name the grammar does
  33. not understand is a name we are not checking, and that must be visible.
  34. This is a source-level check, so it needs no re-baselining: it is a function
  35. of the current source, and it gets stricter as more names are added to the
  36. grammar. It also runs without building anything.
  37. Usage: audit_helpers.py [MODULE.mod] # default ../Runtime.mod
  38. audit_helpers.py -v # print every helper, passing or not
  39. """
  40. import os
  41. import re
  42. import sys
  43. HERE = os.path.dirname(os.path.abspath(__file__))
  44. SHELL = os.path.dirname(HERE)
  45. sys.path.insert(0, HERE)
  46. import disasm16 # noqa: E402 (path set above)
  47. # An emitter may be declared with or without a parameter list, so the
  48. # parentheses are optional. This is not cosmetic: the version that required
  49. # `PROCEDURE Name ;` matched ZERO of Compiler.mod's 22 emitters, because they
  50. # are all declared `PROCEDURE EmName () ;`. So the audit reported "everything
  51. # agrees" for a module where it had examined nothing - a check that cannot fail
  52. # is not a check.
  53. RE_PROC_HEAD = re.compile(
  54. r"^PROCEDURE\s+(\w+)\s*(?:\(\s*\))?\s*;", re.MULTILINE)
  55. RE_ANY_PROC = re.compile(r"^PROCEDURE\s+(\w+)", re.MULTILINE)
  56. # --- what the one-line grammar deliberately does NOT reach ----------------
  57. #
  58. # The name audit checks procedures whose body is a list of CONSTANT bytes, so
  59. # that the bytes can be disassembled and compared against the name. Three
  60. # kinds of emitter cannot be checked that way, and all three are listed here
  61. # with their reason. A name in this table is a documented exclusion; a name
  62. # that is merely absent is a finding, and the coverage check below is what
  63. # tells the two apart.
  64. #
  65. # 1. PARAMETERISED operands -- a byte that is a parameter, not a literal.
  66. # `SubAl (v)` emits 2C v, which disassembles to "sub al, 0" whatever v is,
  67. # so the name can be checked only by also trusting the source order. The
  68. # grammar could learn this (the pattern Int([0-9A-Fa-f]+) already handles
  69. # the analogous case for INT); it does not yet, and until it does these are
  70. # unchecked BY THE AUDIT, not verified.
  71. # 2. COMPUTED or DISPATCHED bytes -- a ModRM chosen by a comparison, a
  72. # rel16 with a patch slot, a form that depends on which kind of variable it
  73. # is. There is no single byte sequence to disassemble.
  74. # 3. DATA, not code -- the B(...) calls here emit the D_ block, so running
  75. # them through a disassembler yields "add [bx+si],al" and a decode error.
  76. # tests/check_runtime.py pins these bytes against runtime.golden, so they
  77. # are pinned; only the name-to-opcode correspondence does not apply.
  78. #
  79. # Named per module because the same name can be a one-liner in one and not the
  80. # other. The Vx family in Runtime.mod is the reason a name is worth listing
  81. # separately: those four ARE audited, by audit_vx_helpers, because their shape
  82. # is fixed even though their immediate is a data offset.
  83. PARAM = "parameterised operand - the byte is an argument, not a literal"
  84. COMP = "computed or dispatched bytes - no single sequence to disassemble"
  85. DATA = "emits DATA (the D_ block), not instructions"
  86. NON_ONE_LINE = {
  87. "Runtime.mod": {
  88. # Real (MBF) entry helpers whose operands are data/field offsets or
  89. # immediates; each is a fixed sequence, pinned by rt_exec's MBF cases.
  90. "MovAxImm": COMP, "Jmp8": COMP, "AndDhImm": COMP,
  91. "StDiOffBx": COMP, "StDiOffDx": COMP,
  92. # The MBF core's parameterised moves and data-byte helpers. Each is
  93. # a fixed instruction/sequence; real_add/real_sub's 16 rt_exec cases
  94. # pin the behaviour (alignment, add/sub, normalisation, sign).
  95. "AdcBxImm": COMP, "AdcDxImm": COMP, "AddAhImm": COMP,
  96. "AndDataImm": COMP, "Call": COMP, "CmpClImm": COMP,
  97. "LdAlData": COMP, "LdClData": COMP, "LdCxDiOff": COMP,
  98. "LdCxSiOff": COMP, "LdDiDiOff": COMP, "LdDxSiOff": COMP,
  99. "LdSiDiOff": COMP, "MovClImm": COMP, "OrDhImm": COMP,
  100. "OrDiImm": COMP, "StDataCl": COMP, "StDataDi": COMP, "StDataImm": COMP,
  101. "SubClImm": COMP, "TestDhImm": COMP, "TestDataImm": COMP,
  102. "XorDhData": COMP, "XorDiData": COMP, "XorDataDh": COMP,
  103. "XorDataImm": COMP, "PushBp4": COMP,
  104. # remult/testexp helpers. The multiplication is pinned by rt_exec's
  105. # ten real_mul cases, including zero, cancellation and signs.
  106. "AdcBxData": COMP, "AdcDxData": COMP, "AddAhData": COMP,
  107. "AddAlImm": COMP, "AndChImm": COMP, "JccNear": COMP,
  108. "JmpNear": COMP, "MovDiData": COMP, "MovSiImm": COMP,
  109. "StDataAx": COMP, "StDataBx": COMP, "StDataCh": COMP,
  110. "StDataCx": COMP, "StDataDx": COMP, "StDataSi": COMP,
  111. # rediv's memory-operand compares/subtracts and result loads. The
  112. # displacement is a parameter, so the bytes cannot be disassembled as
  113. # a fixed sequence; rt_exec's fourteen real_div cases pin them.
  114. "CmpDxData": COMP, "CmpBxData": COMP, "CmpAhData": COMP,
  115. "MovCxData": COMP, "MovBxData": COMP, "MovDxData": COMP,
  116. "SbbBxData": COMP, "SbbDxData": COMP,
  117. "SubAhData": COMP, "StDataAl": COMP,
  118. "MovBxImm": PARAM, "MovCxImm": PARAM, "MovDxImm": PARAM,
  119. "MovDl": PARAM, "MovAlD": PARAM, "MovAh": PARAM,
  120. "MovDhImm": PARAM, "MovChImm": PARAM, "OrChImm": PARAM,
  121. "TestChImm": PARAM, "StSiImm": PARAM,
  122. # the fmtreal/ascreal data-offset and parameterised stores. Each is a
  123. # fixed sequence with a parameter/address, so there is no single set of
  124. # literal bytes to disassemble; rt_exec's exact-output cases pin them.
  125. "MovBxDataAddr": COMP, "MovSiData": COMP, "StDataByte": COMP,
  126. "MovBxByte": COMP, "MovBxOff1Byte": COMP, "TestDiOffImm": COMP,
  127. "XorDiOffImm": COMP, "LdBxDiOff": COMP, "LdDxDiOff": COMP,
  128. "LeaDiBxData": COMP, "MovAxData": COMP,
  129. "AddAxImm": COMP, "AddChImm": COMP, "AddClImm": COMP,
  130. "AndBlImm": COMP, "AndClImm": COMP, "CmpAxImm": COMP,
  131. "CmpBxByte": COMP, "CmpDxImm": COMP, "CmpSiData": COMP,
  132. "DecData": COMP, "AndAhImm": COMP, "AndAxImm": COMP,
  133. "CmpAhImm": COMP, "TestAhImm": COMP, "TestAxImm": COMP,
  134. "SubAl": PARAM, "CmpAl": PARAM, "AddDl": PARAM, "J8": COMP,
  135. "Jcc": COMP, "CmpCxV": PARAM,
  136. "B": DATA, "C8": DATA,
  137. # File control-block operations: a multi-instruction routine with
  138. # labels and raw DOS sequences, not a one-line ModRM helper. Its
  139. # bytes are pinned by rt_exec's file_assign case and the runtime
  140. # golden; the name grammar cannot read a whole routine.
  141. "EmitFileOps": COMP,
  142. "EmitWrCharW": COMP, "EmitWrBoolW": COMP, "EmitWrStrW": COMP,
  143. "EmitMemCopy": COMP,
  144. "EmitRealToInt": COMP,
  145. "EmitMsdos": COMP,
  146. "AddDiImm": COMP, "AddBxImm": COMP,
  147. },
  148. "Compiler.mod": {
  149. "FileIo": COMP, # typed-file INT 21h sequence; see EmFileIo
  150. "InlineCh": COMP, # emits a string blob, not one instruction
  151. "InlineCrLf": COMP, # emits a string blob, not one instruction
  152. "PushRealAddr": COMP, "PushRealSlot": COMP,
  153. "AsciiZLiteral": COMP, "XorByteGlob": COMP,
  154. "CopyRealTo": COMP,
  155. "AddSp": PARAM, "SubSp": PARAM, "Setcc": PARAM,
  156. "MovChImm": PARAM,
  157. "Call": COMP, "Jcc": COMP, "JmpNear": COMP,
  158. "BpDisp": COMP, "LoadVar": COMP, "StoreVar": COMP,
  159. "PushVarAddr": COMP, "MovAxi": COMP, "CmpAxi": COMP,
  160. "MovCxi": COMP, "LeaDi": COMP,
  161. # PushStrAddr is PushVarAddr's sibling one level up: it pushes the
  162. # ADDRESS of a whole-string operand, and a string LITERAL has no storage
  163. # to take the address of, so it emits the literal INLINE - a jump over
  164. # it, then <len><chars> - and pushes the address of those bytes. The
  165. # emitted sequence therefore depends on the operand: a variable is a
  166. # single EmPushVarAddr, a literal is two bytes of jump plus the string
  167. # itself. That is COMP, not PARAM (whose immediate varies but whose
  168. # opcode does not). Listed here rather than left out: an emitter the
  169. # name grammar cannot describe has to be written down, or the report is
  170. # green and the helper unchecked.
  171. "PushStrAddr": COMP,
  172. # SetBlob is PushStrAddr's sibling for sets: a set cannot fold to a
  173. # word, so its bytes travel inline - a JMP over them, then the bytes -
  174. # and the address of those bytes is the value. Like PushStrAddr the
  175. # emitted sequence depends on the operand (its length), so there is no
  176. # single sequence to disassemble; the LAYOUT it produces is pinned by
  177. # t61_setconst's .out instead.
  178. "SetBlob": COMP,
  179. # StrBlob is the string-literal blob on its own (factored out of
  180. # PushStrAddr so a string value parameter can copy from it); PushStrSlot
  181. # reserves a slot and copies a string value into it. Both emit data
  182. # behind a jump / a data-dependent byte loop, so there is no single
  183. # sequence to disassemble. The layouts they produce are pinned by the
  184. # string and string-parameter fixtures instead.
  185. "StrBlob": COMP,
  186. "PushStrSlot": COMP,
  187. # The CRT sequences (INT 10h) are one parameterised helper: several
  188. # instructions each, with no single name grammar. Listed, not left
  189. # implicit.
  190. "CrtSeq": COMP,
  191. # A VAR parameter's slot holds an ADDRESS. These three read and write
  192. # one level down: LoadVarInd fetches the pointer into BX and then the
  193. # byte/word at [BX]; StoreVarIndPrep parks the pointer in DI across the
  194. # right-hand side and StoreInd stores through it. The sequence depends
  195. # on the width (one byte or a word), so there is no single opcode to
  196. # name; t73_varparam's execution pins both widths, the local/global
  197. # address forms, passing one on, and READ through one.
  198. "LoadVarInd": COMP,
  199. "StoreVarIndPrep": COMP,
  200. "StoreInd": COMP,
  201. # The nested-procedure static-link accessors. Each chooses a byte or
  202. # word load/store, or a LEA, through [BX+disp] after walking the static
  203. # chain; the sequence depends on the operand width, so the name grammar
  204. # cannot describe it. t115_nestedlocal (outer scalar read), t116_nestedstr
  205. # (outer string write/read) and CALC's cell-entry path pin them.
  206. "LdBxBx": COMP, "LoadVarBx": COMP, "StoreVarBx": COMP,
  207. "LeaDiBx": COMP, "LoadVarIndSym": COMP, "PushSymAddr": COMP,
  208. # The WITH/2-D field helpers. AddDiImm adds a record field's fixed
  209. # offset to an address already in DI; LdDiGlob and StDiGlob reload and
  210. # park that address in a data slot across a WITH body. Each is a fixed
  211. # 3-4 byte sequence whose operand is a data/field offset, and t77_with
  212. # pins the behaviour (2-D element, constant element, field offsets).
  213. "AddDiImm": COMP, "AddBxImm": COMP,
  214. "LdDiGlob": COMP,
  215. "StDiGlob": COMP,
  216. # Set-field helpers: store one constructor byte through DI
  217. # (StDiImm), and test one element's bit in a DI-addressed set
  218. # (LdAlDiOff + AndAlImm). t78_setfield pins both.
  219. "StDiImm": COMP,
  220. "LdAlDiOff": COMP,
  221. "AndAlImm": COMP,
  222. # A real copy through DI: one byte of the six.
  223. "StDiAlOff": COMP,
  224. # LoadAtom and Assignment are parser/codegen entry points, not Em*
  225. # helpers, but they emit inline IN/OUT sequences for Port[...] and
  226. # width-dependent loads/stores. Their sequences are pinned by the
  227. # Port fixture and the ordinary load/store fixtures, not by a
  228. # one-instruction name grammar.
  229. "LoadAtom": COMP, "Assignment": COMP,
  230. # UpCase's byte tests and the 'a'..'z' fold.
  231. "CmpAlImm": COMP,
  232. "SubAlImm": COMP,
  233. # The run-time set bit test (`e in set'): shift/mask/index helpers.
  234. "MovClAl": COMP, "AndClImm": COMP,
  235. "MovBxi": COMP, "LeaBx": COMP, "MovBxDi": COMP,
  236. "AddBxAx": COMP, "LdAlBx": COMP, "MovDlImm": COMP,
  237. "AndAlDl": COMP,
  238. },
  239. }
  240. # The Vx family is NOT here: those four are audited by audit_vx_helpers,
  241. # because their shape is fixed even though their immediate is a data offset.
  242. # They are named explicitly rather than filtered out of VX_KINDS so that this
  243. # table stands on its own and a name added to VX_KINDS later cannot silently
  244. # become unchecked - it would instead be reported, which is the point.
  245. _NONLINE_VX = ("LdBxVx", "MovBxVx", "MovDxVx", "StVxBx")
  246. def scan_emitters(src):
  247. """Yield (name, text) for every PROCEDURE, however it is written.
  248. The INVENTORY side of the coverage check, and deliberately as dumb as it
  249. can be: a name at the start of a line, and everything up to the next one.
  250. It does not check the parameter list, does not look for BEGIN, does not
  251. care where comments are, and cannot be defeated by the same mistake twice.
  252. The reason it exists at all is that the other list - the one the audit
  253. actually reads - is produced by find_helpers, which is a real parser and
  254. therefore fails on real inputs (a parameter list it did not expect, a
  255. comment in the wrong place). Comparing a parser against itself finds
  256. nothing; comparing it against a scan that cannot parse anything finds
  257. exactly the cases where the parser is the thing that is wrong.
  258. """
  259. heads = list(RE_ANY_PROC.finditer(src))
  260. for k, h in enumerate(heads):
  261. stop = heads[k + 1].start() if k + 1 < len(heads) else len(src)
  262. yield h.group(1), src[h.end():stop]
  263. def find_helpers(src):
  264. """Yield (name, body) for every PROCEDURE, in source order.
  265. A documented emitter must be auditable, so text between the header and
  266. BEGIN - which is where the explanatory comment belongs, and the only place
  267. it can be read next to the code it describes - is allowed through. A
  268. regex that permitted it had nested quantifiers and took exponential time
  269. on these files, so this splits on procedure HEADERS instead and takes the
  270. text up to the next header. That is linear, and it also cannot read one
  271. procedure's bytes as another's.
  272. The header regex is still strict about the parameter list (empty only),
  273. because that is what distinguishes an emitter from a real routine.
  274. """
  275. heads = list(RE_PROC_HEAD.finditer(src))
  276. for k, h in enumerate(heads):
  277. stop = heads[k + 1].start() if k + 1 < len(heads) else len(src)
  278. chunk = src[h.end():stop]
  279. m = re.search(r"\bBEGIN\b(.*)\bEND\s+%s\s*;" % re.escape(h.group(1)),
  280. chunk, re.DOTALL)
  281. if not m:
  282. continue
  283. yield h.group(1), m.group(1)
  284. # The H suffix is optional: the source mixes B (8AH) and B (0) for the same
  285. # kind of literal, and a byte written without H used to be silently dropped
  286. # from the audit, which made three helpers look like truncated prefixes.
  287. #
  288. # BOTH B (...) and Ebyte (...) are accepted. Runtime.mod spells a byte B(v)
  289. # and Compiler.mod spells the same thing Ebyte(v); the Em prefix is on the
  290. # *procedure* names there, not on the byte call, so the byte regex has to cover
  291. # both spellings or Compiler.mod's 22 emitters are silently skipped - which is
  292. # what happened, and how EmXchgAxCx stayed wrong for its whole life with
  293. # correct byte counts and a green compile matrix.
  294. RE_BYTE = re.compile(r"(?:\bB|\bEbyte)\s*\(\s*([0-9A-Fa-f]+)H?\s*\)")
  295. # Both modules emit bytes, so both must be swept. Compiler.mod is where the
  296. # procedure-skip jump, the FOR test ordering and EmXchgAxCx (93h = XCHG BX,AX
  297. # where the name says XCHG AX,CX) all live: bugs that break every two-variable
  298. # arithmetic and comparison in every program, invisible because the audit
  299. # looked at Runtime.mod and found nothing wrong there.
  300. MODULES = ["Runtime.mod", "Compiler.mod", "CompilerEmit.mod"]
  301. # --- name grammar -----------------------------------------------------
  302. #
  303. # An ordered table of (name regex, mnemonic, operand specs, allowed opcodes).
  304. #
  305. # Why the opcode column exists
  306. # ----------------------------
  307. # "MovDlSi" and "MovBpSp" are ambiguous from the name alone: SI and BP are
  308. # both in the register list and in the memory-base list. What settles it is
  309. # the opcode, and the rule is the asymmetry that makes 16-bit hand-assembly
  310. # so error-prone:
  311. #
  312. # 88 /r MOV r/m8, r8 reg is the SOURCE (store)
  313. # 89 /r MOV r/m16, r16 reg is the SOURCE (store)
  314. # 8A /r MOV r8, r/m8 reg is the DESTINATION (load)
  315. # 8B /r MOV r16, r/m16 reg is the DESTINATION (load)
  316. #
  317. # A byte move (8A/88) to or from a bare base register can only be a memory
  318. # access, because "mov dl, si" is not an instruction. A word move (8B/89)
  319. # could be either, so for word moves the register reading is tried first.
  320. # Encoding that rule in the table, rather than in pattern order alone, is what
  321. # stops a helper being "verified" against the wrong reading of its own name.
  322. #
  323. # Operand spec tokens:
  324. #
  325. # "r:Name" a bare register spelled Name (Ax, Al, Dx, Si, Ds, ...)
  326. # "i:N" the immediate N; the name spells it in DECIMAL
  327. # "ih:N" like i: but the name spells it in HEX (only Int, whose
  328. # vector 21h FCML prints as "21h" and which reads as decimal
  329. # 33 if you do not notice)
  330. # "m:Base" memory at [Base], with no displacement
  331. # "m:Base+D" memory at [Base + D]
  332. #
  333. # "Arg" is the emitter's alias for BP: on entry BP points at the return
  334. # address, so a word argument starts at [BP+2]. Naming it "Arg" rather than
  335. # "Bp" is what stops these being misread as [BP] accesses, and it is also
  336. # how the grammar knows to expect the +2.
  337. #
  338. # "Arg" is the frame in which the entry did NOT push BP, and it is the only
  339. # one where the argument is two bytes up: an entry that pushes BP first has
  340. # moved it to +4, which is a different name and a different byte
  341. # (MovAlArg2 / MovAlArg4, CmpArg2W0 / CmpArg4W0). A name that says only
  342. # "Arg" always means +2, with no exceptions -- the alternatives to spelling
  343. # the displacement out were a Modula-2 parameter, which this audit cannot
  344. # see, and a comment, which nothing checks.
  345. #
  346. # A name matching NO pattern is reported UNPARSED, which counts as a failure
  347. # on purpose: a helper the audit cannot read is a helper nobody is checking.
  348. REGS = ["Ax", "Bx", "Cx", "Dx", "Si", "Di", "Bp", "Sp",
  349. "Al", "Bl", "Cl", "Dl", "Ah", "Bh", "Ch", "Dh"]
  350. # Segment registers: the 8086 can only PUSH/POP them, never MOV to or from
  351. # one, so they need names of their own.
  352. SEGS = ["Ds", "Es", "Cs", "Ss"]
  353. JCCS = ["Je", "Jne", "Jz", "Jnz", "Jge", "Jnl", "Jle", "Jl", "Ja", "Jae",
  354. "Jb", "Jbe", "Jg", "Jns", "Js", "Jo", "Jno", "Jp", "Jnp", "Jcxz",
  355. "Jecxz", "Jrcxz", "Loop", "Loope", "Loopne"]
  356. _ALT = "|".join(REGS)
  357. _ALT16 = "Ax|Bx|Cx|Dx|Si|Di|Bp|Sp"
  358. _BASE = "Si|Di|Bx|Bp|Sp|Arg|Data"
  359. _STORE = (0x88, 0x89) # reg is the source
  360. _LOAD = (0x8A, 0x8B) # reg is the destination
  361. PATTERNS = [
  362. # --- no-operand and fixed-operand forms -------------------------
  363. (re.compile(r"^RetR?$"), "ret", [], None),
  364. (re.compile(r"^LeaveR?$"), "leave", [], None),
  365. (re.compile(r"^Int([0-9A-Fa-f]+)$"), "int", ["ih:%(1)s"], {0xCD}),
  366. (re.compile(r"^(Push|Pop)(%s)$" % "|".join(SEGS)),
  367. None, ["r:%(2)s"], None),
  368. (re.compile(r"^(Push|Pop)(%s)$" % _ALT), None, ["r:%(2)s"], None),
  369. # --- jumps: the target is a fixup, so only the mnemonic is checked
  370. (re.compile(r"^(%s)(8|16)?$" % "|".join(JCCS)), None, [], None),
  371. (re.compile(r"^Jmp(%s)$" % _ALT), "jmp", ["r:%(1)s"], {0xFF}),
  372. # --- MUL names one operand; 16-bit DIV names two because it always
  373. # divides DX:AX, so the decode has an AX the name has no room for
  374. (re.compile(r"^Mul(%s)$" % _ALT), "mul", ["r:%(1)s"], {0xF7}),
  375. (re.compile(r"^Div(%s)$" % _ALT), "div", ["r:Ax", "r:%(1)s"], {0xF7}),
  376. (re.compile(r"^MovAxDs$"), "mov", ["r:Ax", "r:Ds"], {0x8C}),
  377. # --- store: name is St<base><reg>, decode puts the register last --
  378. (re.compile(r"^St(%s)(%s)$" % (_BASE, _ALT)),
  379. "mov", ["m:%(1)s", "r:%(2)s"], _STORE),
  380. # --- load through a bare base register: Ld<reg><base> ------------
  381. # The mirror of the St pattern above, and spelled Ld rather than Mov on
  382. # purpose. `8A 07` and `8A C3` are one byte apart and do opposite
  383. # things: LdAlBx reads the byte AT the pointer in BX, MovAlBl takes the
  384. # low byte OF BX. Naming the first "MovAlBx" put the two one letter
  385. # apart, and getch's pushback path used the wrong one - it read memory
  386. # 010Ah, the address of the character, instead of the character. So the
  387. # grammar is part of the safety: Ld* may only be spelled for a memory
  388. # source, which is what distinguishes it from Mov*<reg><lowreg>.
  389. (re.compile(r"^Ld(%s)(%s)$" % (_ALT, _BASE)),
  390. "mov", ["r:%(1)s", "m:%(2)s"], (0x8A, 0x8B)),
  391. # --- XCHG is symmetric, so the name's operand order carries no
  392. # information and must not be checked positionally. 87 /r is
  393. # XCHG r/m16, r16, so FCML always prints the r/m operand first:
  394. # 87 C7 - reg=AX, rm=DI - decodes as "xchg di,ax" whichever way the
  395. # author thought about it. What the audit is really for here is the
  396. # PAIR: XchgAxDi must not come out as XCHG AX,CX. So the two
  397. # registers are compared as a set (see the "rx:" handling below).
  398. (re.compile(r"^Xchg(%s)(%s)$" % (_ALT, _ALT)),
  399. "xchg", ["rx:%(1)s|%(2)s"], {0x87, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96,
  400. 0x97}),
  401. # 8-bit XCHG, 86 /r. The MBF intreal swaps DL and DH to move the low byte
  402. # of a 16-bit value into the exponent position.
  403. (re.compile(r"^Xchg(Al|Cl|Dl|Bl|Ah|Ch|Dh|Bh)(Al|Cl|Dl|Bl|Ah|Ch|Dh|Bh)$"),
  404. "xchg", ["rx:%(1)s|%(2)s"], {0x86}),
  405. # --- the accumulator-implicit XCHGs, 91h..97h, join the /r form -----
  406. # One byte, no ModRM, both registers fixed by the opcode. They are in
  407. # the same opcode set because the same `rx:` set comparison decides them,
  408. # and that comparison is exactly what catches 93h (XCHG BX,AX) under the
  409. # name XchgAxCx - the fault that broke every two-variable arithmetic
  410. # operation in every program. 90h is deliberately absent: it is NOP, and
  411. # FCML decodes it as "nop", not as an XCHG AX,AX.
  412. #
  413. # --- MOV r8, imm8 : B0+reg, and the AH-specific B4 form -----------
  414. # MOV AH,imm8 is B4 imm8, which is not B0+reg. The name says which
  415. # register, so the opcode is checked against it: B4 must be AH and B0+4
  416. # must not be. (The runtime's wrchar used to load the character with the
  417. # wrong register, so the store landed in AL-adjacent memory.)
  418. # B4 imm8 is MOV AH,imm8 and is NOT B0+reg, so AH and AL are listed
  419. # separately with their own opcodes rather than sharing one pattern that
  420. # would accept B4 under the name MovAl0.
  421. # --- shifts: by one, and by CL. The set membership test uses both:
  422. # D1 E8 (SHR AX,1) three times, and D2 E2 (SHL DL,CL) for the mask.
  423. # Shifts and rotates by one, byte and word. The MBF core shifts a 40-bit
  424. # mantissa with SHR/RCR and normalises with SHL/RCL.
  425. (re.compile(r"^(Shl|Shr|Rcl|Rcr|Rol|Ror|Sar)(%s)1$" % _ALT),
  426. None, ["r:%(2)s", "i:1"], {0xD0, 0xD1}),
  427. (re.compile(r"^Stc$"), "stc", [], {0xF9}),
  428. (re.compile(r"^Cmc$"), "cmc", [], {0xF5}),
  429. (re.compile(r"^Clc$"), "clc", [], {0xF8}),
  430. (re.compile(r"^Lahf$"), "lahf", [], {0x9F}),
  431. (re.compile(r"^Sahf$"), "sahf", [], {0x9E}),
  432. (re.compile(r"^LdChDiOff$"), "mov", ["r:Ch", "m:Di"], {0x8A}),
  433. (re.compile(r"^PushBp(4|6)$"), "push", ["m:Bp+%(1)s"], {0xFF}),
  434. (re.compile(r"^IncBxByte$"), "inc", ["m:Bx"], {0xFE}),
  435. (re.compile(r"^Adc(%s)(%s)$" % (_ALT16, _ALT16)),
  436. "adc", ["r:%(1)s", "r:%(2)s"], {0x11, 0x13}),
  437. (re.compile(r"^Sbb(%s)(%s)$" % (_ALT16, _ALT16)),
  438. "sbb", ["r:%(1)s", "r:%(2)s"], {0x18, 0x19, 0x1A, 0x1B}),
  439. (re.compile(r"^(Shl|Shr|Rcl|Rcr)(Al|Cl|Dl|Bl|Ah|Ch|Dh|Bh)Cl$"),
  440. None, ["r:%(2)s", "r:Cl"], {0xD2}),
  441. (re.compile(r"^(Shl|Shr|Rcl|Rcr)(Ax|Bx|Cx|Dx|Si|Di|Bp|Sp)Cl$"),
  442. None, ["r:%(2)s", "r:Cl"], {0xD3}),
  443. (re.compile(r"^Cbw$"), "cbw", [], {0x98}),
  444. (re.compile(r"^Imul(%s)$" % _ALT), "imul", ["r:%(1)s"], {0xF7}),
  445. # --- CMP reg,0 as a flag test (the 8086 has no OR reg,reg FCML can
  446. # decode here; CMP sets the same ZF/SF/CF for a non-negative reg) ---
  447. (re.compile(r"^CmpAl0$"), "cmp", ["r:Al", "i:0"], {0x3C}),
  448. (re.compile(r"^Cmp(Cl|Dl|Bl|Ah|Ch|Dh|Bh)0$"),
  449. "cmp", ["r:%(1)s", "i:0"], {0x80}),
  450. (re.compile(r"^MovAh(0|1)$"),
  451. "mov", ["r:Ah", "i:%(1)s"], {0xB4}),
  452. (re.compile(r"^MovAl(0|1)$"),
  453. "mov", ["r:Al", "i:%(1)s"], {0xB0}),
  454. # --- MUL/IMUL r/m16, accumulator implicit, /5 and /4 -----------------
  455. # The destination is AX:DX and the operand is the named register, so
  456. # "MulAxCx" means CX := CX, i.e. AX:DX := AX * CX. FCML prints only the
  457. # one explicit operand ("imul cx"), which is why the spec has one token.
  458. (re.compile(r"^Mul(%s)(%s)$" % (_ALT, _ALT)),
  459. "imul", ["r:%(2)s"], {0xF7}),
  460. # --- CmpArg<N>W0 : CMP WORD [BP+N],0 between MOV BP,SP / MOV SP,BP -----
  461. # An odd but exact shape: [SP] is not encodable, so the frame pointer is
  462. # borrowed for the one comparison and handed back untouched. The pair of
  463. # saves at the ends is what makes it correct, so the whole sequence is
  464. # checked - a missing MOV SP,BP would leave BP clobbered for the caller.
  465. #
  466. # The displacement is a CAPTURED GROUP, not a constant, and the name has to
  467. # spell it out. That is the rule that keeps CmpArg2W0 and CmpArg4W0
  468. # honest: they differ by one byte, and one byte is the difference between
  469. # reading a BOOLEAN and reading a character, so the name -- which is the
  470. # only thing this audit can read -- has to carry it. A single helper
  471. # taking the displacement as a Modula-2 parameter would decode correctly
  472. # under either name and be unchecked under both.
  473. (re.compile(r"^CmpArg(2|4)W0$"),
  474. None, [["mov", ["r:Bp", "r:Sp"]],
  475. ["cmp", ["m:Bp+%(1)s", "i:0"]],
  476. ["mov", ["r:Sp", "r:Bp"]]], {0x8B}),
  477. # --- a "Mov<reg>Sp" that is a POP/PUSH pair, not a memory access ----
  478. # MOV AX,[SP] does not exist on the 8086 at all, so the stack top is read
  479. # with POP and given back with PUSH: an observational no-op that leaves SP
  480. # where it found it. Named MovAxSp because that is the OPERATION, and the
  481. # two-step shape is recorded here because it is the only correct encoding.
  482. (re.compile(r"^Mov(%s)Sp$" % _ALT),
  483. None, [["pop", ["r:%(1)s"]], ["push", ["r:%(1)s"]]], None),
  484. # --- IDIV: CWD then IDIV r/m16, sign-extending into DX:AX ---------
  485. # A two-instruction shape, so the specs are a list of one list per
  486. # instruction. The CWD is not incidental: F7 /7 divides the 32-bit value
  487. # in DX:AX, and a signed dividend is only in DX:AX if CWD ran. Drop the
  488. # 99h and the divisor goes to the same place, so the pair is checked
  489. # together - which is the reason the sequence form exists.
  490. # FCML prints BOTH ends of a /r divide ("idiv ax, cx"), because the
  491. # accumulator is not implicit in the mnemonic the way it is for MUL, so
  492. # the spec needs two operands and the AX comes first.
  493. (re.compile(r"^I?Div(%s)(%s)$" % (_ALT, _ALT)),
  494. None, [["cwd", []], ["idiv", ["r:%(1)s", "r:%(2)s"]]], {0xF7}),
  495. # --- load a register from memory, with a displacement ------------
  496. # The displacement makes the name unambiguous, so any load opcode works.
  497. (re.compile(r"^Mov(%s)(%s)(\d+)$" % (_ALT, _BASE)),
  498. "mov", ["r:%(1)s", "m:%(2)s+%(3)s"], _LOAD),
  499. # --- two registers: MOV BP,SP / CMP SI,BX / XOR AX,AX / ADD DI,AX -
  500. # Tried before the bare-base reading below, because for a word move
  501. # "MovBpSp" means BP := SP and only "MovDlSi" means DL := [SI]. The two
  502. # readings cannot both match, because one demands a register operand
  503. # where the other demands a memory operand.
  504. # Xchg is NOT in this list. It has its own pattern above, which compares
  505. # the two registers as a SET because 87 /r is symmetric and FCML always
  506. # prints the r/m operand first - so the name's order carries no
  507. # information and must not be checked positionally. Listing Xchg here as
  508. # well gave it a second, ordered reading, and the audit then reported
  509. # "2 name readings fit the same bytes" for XchgAxCx: not a real ambiguity
  510. # in the code, but two overlapping rows in the grammar saying the same
  511. # thing. A grammar that can be satisfied two ways for one name is a
  512. # grammar that can be satisfied the wrong way.
  513. (re.compile(r"^(Mov|Cmp|Add|Sub|Xor|And|Or)(%s)(%s)$" % (_ALT, _ALT)),
  514. None, ["r:%(2)s", "r:%(3)s"], None),
  515. # --- byte move from a bare base register: necessarily memory ------
  516. # Restricted to 8A/88 on purpose: if this ever matched an 8B/89 it would
  517. # be a register move already claimed by the pattern above.
  518. (re.compile(r"^Mov(%s)(%s)$" % (_ALT, _BASE)),
  519. "mov", ["r:%(1)s", "m:%(2)s"], (0x8A, 0x88)),
  520. # --- immediate against a register --------------------------------
  521. (re.compile(r"^(Add|Sub|Cmp|Xor|And|Or)(%s)(\d+)$" % _ALT),
  522. None, ["r:%(2)s", "i:%(3)s"], {0x83}),
  523. # --- XOR AL,#imm8 is `34 ib': no ModRM byte at all, and only the
  524. # accumulator. The row above is pinned to 83 /r, which is the word
  525. # `op r, imm8' encoding, so `XorAl01' matched it and was rejected on
  526. # the opcode - leaving the helper with NO reading rather than a wrong
  527. # one, which is how the audit reported it. Narrow on purpose: 34 is
  528. # XOR AL, nothing else, so a name claiming `xor al, 1h' against any
  529. # other byte fails here. This is TPSRC9 neglevel's boolean NOT.
  530. (re.compile(r"^XorAl(\d+)$"),
  531. "xor", ["r:Al", "i:%(1)s"], {0x34}),
  532. # --- one register: INC CX / DEC SI / NOT DX / NEG AX ------------
  533. (re.compile(r"^(Inc|Dec|Not|Neg|Shl|Shr|Sar)(%s)$" % _ALT),
  534. None, ["r:%(2)s"], None),
  535. ]
  536. def register_word(tok):
  537. """Map an FCML operand token to a REGS/SEGS word, or None."""
  538. t = tok.strip().lower()
  539. if not t or t.startswith("word ptr ") or t.startswith("byte ptr "):
  540. return None
  541. t = t.split()[-1]
  542. for r in REGS + SEGS:
  543. if t == r.lower():
  544. return r
  545. return None
  546. def match_operand(spec, tok):
  547. """Does one FCML operand token satisfy one token spec? Returns None on
  548. success, or a human-readable reason on failure."""
  549. kind, arg = spec.split(":", 1)
  550. t = tok.strip()
  551. if kind in ("i", "ih"):
  552. # FCML prints an immediate as hex with an h suffix ("0h", "2h", "21h")
  553. raw = t.lower().rstrip("h")
  554. try:
  555. v = int(raw, 16)
  556. except ValueError:
  557. return "%r is not an immediate" % t
  558. want = int(arg, 16) if kind == "ih" else int(arg, 10)
  559. if v != want:
  560. return "immediate is %d, name says %d" % (v, want)
  561. return None
  562. if kind == "r":
  563. got = register_word(t)
  564. if got != arg:
  565. return "%r is not the register %s" % (t, arg)
  566. return None
  567. if kind == "m":
  568. base, _, disp = arg.partition("+")
  569. real = {"Arg": "bp", "Data": "si"}.get(base, base.lower())
  570. if base == "Arg" and not disp:
  571. disp = "2" # the first word argument lives at [BP+2]
  572. u = t.lower()
  573. u = re.sub(r"^(word|byte) ptr ", "", u)
  574. if not (u.startswith("[") and u.endswith("]")):
  575. return "%r is not a memory reference" % t
  576. body = u[1:-1]
  577. if not body.startswith(real):
  578. return "memory base is %r, name says [%s]" % (body, real)
  579. if not disp:
  580. if body != real:
  581. return ("memory is %r, name says [%s] with no displacement"
  582. % (body, real))
  583. return None
  584. got = body[len(real):].strip()
  585. m = re.fullmatch(r"\+\s*([0-9a-f]+)h?", got)
  586. if not m:
  587. return "displacement %r is not a number" % got
  588. if int(m.group(1), 16) != int(disp):
  589. return "displacement is %d, name says %s" \
  590. % (int(m.group(1), 16), disp)
  591. return None
  592. return "bad spec %r" % spec
  593. def operands_of(decode):
  594. """Split an FCML decode like "mov si,bx" or "cmp word ptr [bp+4h],0h"
  595. into operand tokens. Returns (mnemonic, [tokens])."""
  596. i = decode.find(" ")
  597. if i < 0:
  598. return decode.strip(), []
  599. return decode[:i].strip(), decode[i + 1:].split(",")
  600. # --- the Vx helpers, which the one-line grammar cannot see ---------------
  601. #
  602. # find_helpers takes a body spanning any number of lines, so a helper written
  603. # as
  604. #
  605. # PROCEDURE LdBxVx (delta : CARDINAL ) ;
  606. # BEGIN
  607. # B (8BH) ; B (01EH) ; Dd (delta)
  608. # END LdBxVx ;
  609. #
  610. # does not reach the one-line pass. That is a gap rather than a documented
  611. # exclusion, because these are exactly the helpers that are hard
  612. # to get right and easy to get subtly wrong:
  613. #
  614. # - the operand is a D_ data-block offset, so the immediate is a FIXUP.
  615. # Emitting W (delta) instead of Dd (delta) produces a byte-identical
  616. # instruction with the raw offset in it: `MOV BX,001Ch` for
  617. # `MOV BX,<D_PUSH>`, pointing into the entry JMP. That happened, and
  618. # the result was a hang rather than a crash.
  619. # - in 16-bit mode there is no [BX] memory form, so a data address has to
  620. # go through mod=00 / rm=110, which is ModRM 1E. rm=111 is [BX+SI], and
  621. # it decodes cleanly, so a wrong ModRM here is invisible to every other
  622. # check in the tree.
  623. # - LdBxVx and StVxBx are a load/store pair on the same word and MUST use
  624. # the same ModRM and the same fixup kind, or one of them silently
  625. # addresses something else.
  626. # - LdBxVx and MovBxVx differ only in the ModRM's mod field, i.e. "the word
  627. # at the address" versus "the address". Both mistakes above have been
  628. # made here, in this pair.
  629. #
  630. # So they get their own check, keyed on "Vx" - the operand word Runtime.mod
  631. # reserves for "a 16-bit absolute address into the data block" - and every one
  632. # of them is declared explicitly, so ADDING a Vx helper is a deliberate act:
  633. # an undeclared one is reported rather than ignored.
  634. #
  635. # The header, the body and the closing name are separated by `(.*?)` rather
  636. # than by `\s*`, because two of these helpers carry an explanatory comment
  637. # between the parameter list and BEGIN. A `\s*` there skips them silently -
  638. # which is the failure this whole check exists to prevent - so the pattern is
  639. # deliberately loose about whitespace and strict about the `END <name>`.
  640. RE_VX = re.compile(
  641. r"^PROCEDURE\s+(\w*Vx\w*)\s*\([^)]*\)\s*;.*?\bBEGIN\b(.*?)\bEND\s+\1\s*;",
  642. re.MULTILINE | re.DOTALL)
  643. # (opcode, shape). "address" = MOV reg,imm16: the register is in the opcode
  644. # and there is no ModRM. "load"/"store" = opcode + ModRM 1E + the
  645. # Dd-supplied immediate.
  646. VX_KINDS = {
  647. "MovBxVx": (0xBB, "address"), # MOV BX, Vx
  648. "MovDxVx": (0xBA, "address"), # MOV DX, Vx
  649. "LdBxVx": (0x8B, "load"), # MOV BX, WORD PTR [Vx]
  650. "StVxBx": (0x89, "store"), # MOV WORD PTR [Vx], BX
  651. }
  652. def audit_vx_helpers(src, mod, verbose):
  653. """Check the multi-line *Vx helpers. Returns (n_checked, problems)."""
  654. found = {}
  655. for m in RE_VX.finditer(src):
  656. found[m.group(1)] = m.group(2)
  657. problems = []
  658. # The count reported is the number of helpers ACTUALLY examined, not the
  659. # size of VX_KINDS. Those were the same number when only Runtime.mod was
  660. # swept, and printing the constant for Compiler.mod - which defines no Vx
  661. # emitter at all - made the report claim four subjects in a module that
  662. # has none. A count that does not change when the module does is a
  663. # cosmetic lie, and this report exists to be believed.
  664. checked = 0
  665. for name in sorted(set(found) | set(VX_KINDS)):
  666. # A *Vx helper is a Runtime.mod concept (the V* data word at a fixed
  667. # offset). Compiler.mod has none, so VX_KINDS is only a floor for the
  668. # module that is supposed to define them; asking Compiler.mod for four
  669. # Vx emitters it should not have produces four phantom failures.
  670. if mod != "Runtime.mod" and name in VX_KINDS and name not in found:
  671. continue
  672. if name not in VX_KINDS:
  673. problems.append("%s: a Vx helper with no declared kind - add it "
  674. "to VX_KINDS with its opcode and shape" % name)
  675. continue
  676. if name not in found:
  677. # The module is named rather than hardcoded: these four live in
  678. # Runtime.mod, and reporting them as missing from Compiler.mod
  679. # would be a false alarm about a module that has no business
  680. # defining them.
  681. problems.append("%s: declared in VX_KINDS but not defined in %s"
  682. % (name, mod))
  683. continue
  684. checked += 1
  685. body = found[name]
  686. want_op, shape = VX_KINDS[name]
  687. by = [int(b, 16) for b in RE_BYTE.findall(body)]
  688. hexs = " ".join("%02X" % b for b in by[:2])
  689. if not by or by[0] != want_op:
  690. problems.append("%s: starts %s, expected opcode %02X (%s)"
  691. % (name, hexs or "<nothing>", want_op, shape))
  692. continue
  693. # The two shapes need different numbers of literal bytes, and asking
  694. # for a ModRM on an address move is a false alarm: MOV reg,imm16
  695. # carries the register in the opcode and has no ModRM at all.
  696. if shape == "address":
  697. if len(by) != 1:
  698. problems.append("%s: emits %s, expected just the opcode - an "
  699. "address move is `MOV reg,imm16` and has no "
  700. "ModRM" % (name, hexs))
  701. continue
  702. elif len(by) < 2:
  703. problems.append("%s: emits %s, expected opcode and ModRM"
  704. % (name, hexs))
  705. continue
  706. elif by[1] != 0x1E:
  707. problems.append(
  708. "%s: ModRM is %02X, expected 1E. 16-bit mode has no [BX] "
  709. "form, so a data address must use mod=00 / rm=110; rm=111 "
  710. "is [BX+SI] and decodes cleanly, so nothing else would "
  711. "notice" % (name, by[1]))
  712. continue
  713. if "Dd" not in body:
  714. problems.append(
  715. "%s: does not call Dd, so the D_ offset is emitted raw "
  716. "instead of as a fixup - the instruction will be "
  717. "byte-correct and point at the wrong place" % name)
  718. continue
  719. if re.search(r"\bW\s*\(", body):
  720. problems.append("%s: calls W as well as Dd - the offset must be "
  721. "placed by Dd alone" % name)
  722. continue
  723. if verbose:
  724. print("%-20s %-16s %s (%s, Dd fixup)"
  725. % (name, hexs, shape,
  726. "no ModRM" if shape == "address" else "mod=00 rm=110"))
  727. return checked, problems
  728. def sweep(src, mod, verbose):
  729. """Audit every one-line emitter helper in one module. Returns
  730. (n_ok, n_unparsed, n_bad, problems)."""
  731. n_ok = n_unparsed = n_bad = 0
  732. problems = []
  733. seen = set()
  734. # EmitData is a documented EXCLUSION, not a gap: its B(...) calls are DATA
  735. # (the D_ block), not instructions, so running the code buffer through a
  736. # disassembler produces nonsense like "add [bx+si],al" and a DECODE ERROR.
  737. # It is listed here rather than left implicit, because an exclusion that is
  738. # not written down is indistinguishable from a helper nobody audited.
  739. #
  740. # Its D_ offsets are asserted against this block by check_runtime.py's
  741. # golden, so "excluded from the name audit" is not "unchecked": the bytes
  742. # are pinned, only the name-to-opcode correspondence does not apply.
  743. excluded = {"EmitData", "EmitFileOps", "EmitMemCopy", "EmitRealToInt",
  744. "EmitMsdos", "MovBxAx", "EmitFileMore", "EmitLowLevel"}
  745. if verbose:
  746. print("-- %s" % mod)
  747. print("name bytes FCML decode")
  748. for name, body in find_helpers(src):
  749. # Compiler.mod prefixes its emitters with Em. The name grammar is
  750. # shared with Runtime.mod and has no Em entries, so the prefix is
  751. # stripped here and the module name only ever appears in output. It
  752. # is stripped for PATTERNS matching and for the Vx pass alike, which
  753. # is why a single RE_BYTE and a single PATTERNS table can serve both
  754. # modules instead of two drifting copies.
  755. if mod != "Runtime.mod" and name.startswith("Em"):
  756. name = name[2:]
  757. seen.add(name)
  758. if name in excluded:
  759. continue
  760. by = [int(b, 16) for b in RE_BYTE.findall(body)]
  761. if not by:
  762. continue
  763. raw = bytes(by)
  764. # Disassemble; helpers are 1-2 bytes so one instruction is the norm,
  765. # but StCx / Shl / Div style helpers can be longer, so decode all.
  766. decodes = []
  767. off = 0
  768. while off < len(raw):
  769. text, length = disasm16.decode(raw[off:], off)
  770. if length == 0:
  771. decodes.append(("<DECODE ERROR>", ""))
  772. break
  773. decodes.append(operands_of(text or "?"))
  774. off += length
  775. hexs = " ".join("%02X" % b for b in raw)
  776. dec_text = "; ".join(d[0] + " " + ", ".join(d[1]) for d in decodes)
  777. if verbose:
  778. print("%-20s %-16s %s" % (name, hexs, dec_text))
  779. # A name can have more than one reading (MovBpSp: BP := SP, or
  780. # BP := [SP]). Take the first reading whose operand specs the decode
  781. # actually satisfies; if two readings both fit, the name is genuinely
  782. # ambiguous and that is itself reported, because it would mean the
  783. # audit could be satisfied by the wrong one.
  784. reads = []
  785. nopattern = True
  786. for rx, pmnem, specs, ops in PATTERNS:
  787. mm = rx.match(name)
  788. if not mm:
  789. continue
  790. # The opcode gate applies to the FIRST byte. That is right for a
  791. # one-instruction helper, and wrong for a sequence, where the first
  792. # byte belongs to the setup step: IDivAxCx starts with 99h (CWD)
  793. # and its F7h /7 is the second instruction, so a first-byte gate
  794. # rejected the one helper whose opcode matters most. Skipping the
  795. # gate for a sequence loses nothing, because the sequence check
  796. # below compares every decoded mnemonic against its own spec.
  797. is_seq = (isinstance(specs, list) and specs
  798. and isinstance(specs[0], list))
  799. if ops is not None and not is_seq and by[0] not in ops:
  800. continue
  801. nopattern = False
  802. gmap = {str(i): g for i, g in enumerate(mm.groups(), 1)}
  803. mn = pmnem
  804. if mn is None:
  805. # A pattern with no capture group has no mnemonic to infer
  806. # from the name, and a pattern with none either names the
  807. # mnemonic outright (a sequence) or is checked by operands
  808. # alone. A group-less pattern with pmnem None is a grammar
  809. # bug, not a helper bug, so say so instead of crashing.
  810. if not mm.re.groups:
  811. reads.append((mm, None, specs,
  812. "pattern %r has no capture group and names "
  813. "no mnemonic" % rx.pattern))
  814. continue
  815. mn = mm.group(1).lower()
  816. elif "%(" in mn:
  817. mn = mn % gmap
  818. if len(decodes) != 1:
  819. # A pattern may describe a fixed SEQUENCE of instructions by
  820. # passing a list of specs, one per instruction - IDIV is the
  821. # only user (CWD, then IDIV r/m16), and without this it was
  822. # reported as "emits 2 instructions, name describes one",
  823. # which is a true observation and a useless one: the two
  824. # instructions are one operation and the pair is the thing
  825. # that has to be checked, because a lone F7 /7 divides by an
  826. # un-sign-extended dividend.
  827. if not is_seq or len(specs) != len(decodes):
  828. reads.append((mm, mn, specs,
  829. "emits %d instructions, name describes %s"
  830. % (len(decodes),
  831. len(specs) if is_seq else "one")))
  832. continue
  833. why = []
  834. for k, sub in enumerate(specs):
  835. smn, sspecs = sub
  836. gmnem, got = decodes[k]
  837. if gmnem != smn:
  838. why.append("step %d is %r, name says %s"
  839. % (k + 1, gmnem, smn))
  840. break
  841. if len(got) != len(sspecs):
  842. why.append("step %d: name asserts %d operand(s), "
  843. "FCML decoded %d"
  844. % (k + 1, len(sspecs), len(got)))
  845. break
  846. w = [x for x in
  847. (match_operand(s % gmap if "%(" in s else s, t)
  848. for s, t in zip(sspecs, got)) if x]
  849. if w:
  850. why.append("step %d: %s" % (k + 1, "; ".join(w)))
  851. break
  852. reads.append((mm, mn, specs, "; ".join(why) if why else None))
  853. continue
  854. gmnem, got = decodes[0]
  855. if gmnem != mn:
  856. reads.append((mm, mn, specs,
  857. "FCML decodes %r, name says %s" % (gmnem, mn)))
  858. continue
  859. if len(specs) == 1 and specs[0].startswith("rx:"):
  860. # XCHG: compare the two registers as a set, not in order.
  861. # This has to come before the operand-COUNT check, because
  862. # the "rx:" spec packs both registers into one token while
  863. # the decode has two operands - which is the whole point of
  864. # it.
  865. want = set((specs[0] % gmap)[3:].split("|"))
  866. got_regs = [register_word(t) for t in got]
  867. if len(got_regs) != 2 or None in got_regs:
  868. reads.append((mm, mn, specs,
  869. "XCHG needs two bare registers, decoded %s"
  870. % ", ".join(got)))
  871. continue
  872. if set(got_regs) != want:
  873. reads.append((mm, mn, specs,
  874. "exchanges %s, name says %s"
  875. % (" and ".join(sorted(got_regs)),
  876. " and ".join(sorted(want)))))
  877. continue
  878. reads.append((mm, mn, specs, None))
  879. continue
  880. if len(got) != len(specs):
  881. reads.append((mm, mn, specs,
  882. "name asserts %d operand(s), FCML decoded %d"
  883. % (len(specs), len(got))))
  884. continue
  885. why = [w for w in
  886. (match_operand(sp % gmap if "%(" in sp else sp, tok)
  887. for sp, tok in zip(specs, got)) if w]
  888. if why:
  889. reads.append((mm, mn, specs, "; ".join(why)))
  890. continue
  891. reads.append((mm, mn, specs, None))
  892. if nopattern:
  893. n_unparsed += 1
  894. problems.append("%s: no name pattern accepts it (emits %s %r) - "
  895. "extend PATTERNS or fix the name"
  896. % (name, hexs, dec_text))
  897. continue
  898. fitting = [r for r in reads if r[3] is None]
  899. if len(fitting) > 1:
  900. n_bad += 1
  901. problems.append("%s: %d name readings fit the same bytes - the "
  902. "name is ambiguous" % (name, len(fitting)))
  903. continue
  904. if not fitting:
  905. n_bad += 1
  906. problems.append("%s: %s" % (name, reads[0][3]))
  907. continue
  908. n_ok += 1
  909. n_vx, vx_problems = audit_vx_helpers(src, mod, verbose)
  910. problems.extend(vx_problems)
  911. n_vx_ok = n_vx - len(vx_problems)
  912. # Every PROCEDURE that EMITS BYTES must have been looked at. A helper
  913. # whose body the parser skipped is the failure mode that has bitten this
  914. # file three times now: the Em prefix, the missing parameter list, and a
  915. # comment in the wrong place - each time the suite stayed green while the
  916. # helper went unexamined and the reported subject count quietly dropped.
  917. #
  918. # The inventory is scanned INDEPENDENTLY of find_helpers, and that is the
  919. # whole design of this check. Deriving both lists from the same parser
  920. # makes it vacuous: any input that stops find_helpers from matching also
  921. # removes the helper from the inventory, so the two lists agree and the
  922. # check reports nothing - which is exactly the failure it exists to catch.
  923. # So the inventory is a separate, deliberately dumb scan: every
  924. # `PROCEDURE <name>` at the start of a line, with everything up to the
  925. # next one as its text. It makes no attempt to parse Modula-2, so it
  926. # cannot fail in the same way twice. A helper the audit cannot reach is
  927. # therefore in `declared` and not in `seen`, which is a finding.
  928. #
  929. # The filter is the byte-call regex, so a parser, a lexer or a symbol
  930. # lookup that emits nothing is not expected to be audited - only helpers
  931. # that actually put bytes in the code buffer. A helper that emitted bytes
  932. # under some spelling RE_BYTE does not know would be missed here too, which
  933. # is why RE_BYTE accepts both B(...) and Ebyte(...) and why a new spelling
  934. # has to be added there rather than relied on to be found by accident.
  935. strip = (lambda s: s[2:] if (mod != "Runtime.mod" and s.startswith("Em")) else s)
  936. declared = {strip(nm) for nm, txt in scan_emitters(src)
  937. if RE_BYTE.search(txt)}
  938. # EmitData is a documented EXCLUSION, not a gap: its B(...) calls are DATA
  939. # (the D_ block), not instructions, so running the code buffer through a
  940. # disassembler produces nonsense like "add [bx+si],al" and a DECODE ERROR.
  941. # It is listed here rather than left implicit, because an exclusion that is
  942. # not written down is indistinguishable from a helper nobody audited.
  943. #
  944. # Its D_ offsets are asserted against this block by check_runtime.py's
  945. # golden, so "excluded from the name audit" is not "unchecked": the bytes
  946. # are pinned, only the name-to-opcode correspondence does not apply.
  947. excluded = {"EmitData", "EmitFileOps", "EmitMemCopy", "EmitRealToInt",
  948. "EmitMsdos", "MovBxAx", "EmitFileMore", "EmitLowLevel"}
  949. strip = (lambda s: s[2:] if (mod != "Runtime.mod" and s.startswith("Em")) else s)
  950. # The table is written in the names the audit uses (Runtime.mod's own
  951. # names, Compiler.mod's with the Em prefix already removed), so it is NOT
  952. # stripped again here. Stripping it twice turns "AddSp" into "dSp", which
  953. # matches nothing - and a wrong match here is the dangerous direction: the
  954. # entries would stop suppressing their helpers and the report would look
  955. # like a genuine finding rather than like a broken table.
  956. documented = set(NON_ONE_LINE.get(mod, {}))
  957. if mod == "CompilerEmit.mod":
  958. # The primitive emitters moved out of Compiler.mod unchanged, so the
  959. # same documented exclusions apply. Their names are already in the
  960. # stripped form the audit uses.
  961. documented |= set(NON_ONE_LINE.get("Compiler.mod", {}))
  962. audited_elsewhere = ({strip(n) for n in _NONLINE_VX}
  963. if mod == "Runtime.mod" else set())
  964. unexamined = (declared - seen - {strip(e) for e in excluded}
  965. - documented - audited_elsewhere)
  966. for n in sorted(unexamined):
  967. problems.append("%s: emits bytes but the audit never examined it - "
  968. "the report would be green and the helper unchecked"
  969. % n)
  970. print("\n %s: %d one-line helpers audited, %d agree with their name, "
  971. "%d disagree, %d outside the grammar"
  972. % (mod, n_ok + n_unparsed + n_bad, n_ok, n_bad, n_unparsed))
  973. print(" %s: %d multi-line *Vx helpers audited, %d agree, %d problem(s)"
  974. % (mod, n_vx, n_vx_ok, len(vx_problems)))
  975. return n_ok + n_vx_ok, n_unparsed, n_bad, problems
  976. def main(argv):
  977. verbose = "-v" in argv
  978. files = [a for a in argv[1:] if not a.startswith("-")]
  979. # No argument means "audit every module that emits bytes", not
  980. # "audit Runtime.mod". The single-module default is why the audit was
  981. # green while EmXchgAxCx encoded XCHG BX,AX under a name that says
  982. # XCHG AX,CX - a fault that broke every two-variable arithmetic operation
  983. # in every program the compiler could produce.
  984. mods = [os.path.basename(f) for f in files] if files else list(MODULES)
  985. tot_ok = tot_unparsed = tot_bad = 0
  986. problems = []
  987. for mod in mods:
  988. path = os.path.join(SHELL, mod)
  989. if not os.path.exists(path):
  990. print("FAIL: no such module: %s" % path)
  991. return 1
  992. with open(path) as f:
  993. src = f.read()
  994. n_ok, n_unp, n_bad, pr = sweep(src, mod, verbose)
  995. tot_ok += n_ok
  996. tot_unparsed += n_unp
  997. tot_bad += n_bad
  998. problems.extend("%s: %s" % (mod, p) for p in pr)
  999. total = tot_ok + tot_unparsed + tot_bad
  1000. print("\nAUDITED %d emitter helpers across %d module(s): %d agree with "
  1001. "their name, %d disagree, %d outside the grammar"
  1002. % (total, len(mods), tot_ok, tot_bad, tot_unparsed))
  1003. if problems:
  1004. print("FAIL: %d problem(s)" % len(problems))
  1005. for p in problems:
  1006. print(" - %s" % p)
  1007. return 1
  1008. print("PASS: every audited helper emits what its name says")
  1009. return 0
  1010. if __name__ == "__main__":
  1011. sys.exit(main(sys.argv))