disasm16.py 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. #!/usr/bin/env python3
  2. """disasm16.py -- linear-sweep 16-bit x86 disassembler built on FCML.
  3. Why this exists
  4. ---------------
  5. We need an *independent* check on the bytes our Modula-2 compiler and runtime
  6. emit. The obvious candidates all turned out to be unusable:
  7. * objdump / binutils: has no 16-bit x86 disassembler at all. `-m i8086`
  8. silently falls back to the 32-bit i386 rules.
  9. * unicorn 2.1.4: UC_MODE_16 mis-decodes 16-bit ModRM memory operands.
  10. FCML (libfcml, Debian package `fcml`) does have a real 16-bit x86
  11. disassembler, and we validated it against GNU as's `.code16` *encoder*
  12. (assemble there, disassemble here). See tests/FCML_PROBE.md.
  13. FCML is driven through the `fcml-disasm` CLI because the package ships no
  14. development headers, so ctypes struct layout would be guesswork. The CLI
  15. happens to be a linear sweeper that reports "Instruction code length", which
  16. is all this driver needs.
  17. The length agreement is the important part: if we emit a 3-byte instruction
  18. where a 4-byte one was required, the sweep desynchronises and the very next
  19. line points straight at the offending offset.
  20. Usage:
  21. disasm16.py FILE [BASE] # BASE defaults to 0, decimal or 0x-hex
  22. disasm16.py --bytes '89 6e 04' # inline bytes, for one-off probes
  23. """
  24. import re
  25. import subprocess
  26. import sys
  27. FCML = "fcml-disasm"
  28. # The Debian 1.3.0 `fcml-disasm` wrapper aborts (SIGABRT, rc=134) on any
  29. # buffer of 16 bytes or more: it linearly sweeps the *whole* input and blows
  30. # up on the resulting instruction list. Reproduce with 16 NOPs:
  31. # fcml-disasm -m16 0x90909090909090909090909090909090 # rc=134
  32. # 15 bytes is safe, and 15 >= the longest real-mode instruction (7 bytes,
  33. # e.g. `EA off16 seg16`), so a 15-byte window always contains the whole first
  34. # instruction. We only ever read the first instruction's length.
  35. MAX_WINDOW = 15
  36. RE_LEN = re.compile(r"^\s*Instruction code length:\s*(\d+)\s*$")
  37. RE_TEXT = re.compile(r"^\s*Disassembled instruction:\s*(.*?)\s*$")
  38. def decode(code, base):
  39. """Decode the first instruction of `code` (bytes). Returns (text, length).
  40. `code` is the remaining stream; at most MAX_WINDOW bytes are handed to
  41. FCML. No padding is invented: an under-length buffer must surface as a
  42. decode error rather than being silently completed with made-up bytes.
  43. """
  44. win = code[:MAX_WINDOW]
  45. hexs = "".join("%02X" % b for b in win)
  46. out = subprocess.run(
  47. [FCML, "-m16", "-rh", "-rz", "-ip", hex(base), "0x" + hexs],
  48. capture_output=True, text=True)
  49. if out.returncode != 0:
  50. return None, 0
  51. text = None
  52. length = None
  53. for line in out.stdout.splitlines():
  54. m = RE_TEXT.match(line)
  55. if m:
  56. text = m.group(1)
  57. m = RE_LEN.match(line)
  58. if m:
  59. length = int(m.group(1))
  60. if length is None or length == 0 or length > len(code):
  61. return text, 0
  62. return text, length
  63. def disasm(code, base=0, out=sys.stdout):
  64. """Linear-sweep `code` from `base`, printing one line per instruction."""
  65. pc = 0
  66. ninstr = 0
  67. while pc < len(code):
  68. text, length = decode(code[pc:], base + pc)
  69. if length == 0:
  70. out.write("%04X: %-24s <DECODE ERROR>\n"
  71. % (base + pc, " ".join("%02X" % b
  72. for b in code[pc:pc + 8])))
  73. return ninstr, False
  74. shown = code[pc:pc + length]
  75. out.write("%04X: %-24s %s\n"
  76. % (base + pc,
  77. " ".join("%02X" % b for b in shown),
  78. text if text else "<no text>"))
  79. pc += length
  80. ninstr += 1
  81. return ninstr, True
  82. def main(argv):
  83. if len(argv) < 2:
  84. sys.stderr.write(__doc__)
  85. return 2
  86. if argv[1] == "--bytes":
  87. code = bytes.fromhex(argv[2].replace(" ", ""))
  88. base = int(argv[3], 0) if len(argv) > 3 else 0
  89. else:
  90. with open(argv[1], "rb") as f:
  91. code = f.read()
  92. base = int(argv[2], 0) if len(argv) > 2 else 0
  93. ninstr, ok = disasm(code, base)
  94. sys.stderr.write("-- %d instructions, %d bytes\n" % (ninstr, len(code)))
  95. return 0 if ok else 1
  96. if __name__ == "__main__":
  97. sys.exit(main(sys.argv))