| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116 |
- #!/usr/bin/env python3
- """disasm16.py -- linear-sweep 16-bit x86 disassembler built on FCML.
- Why this exists
- ---------------
- We need an *independent* check on the bytes our Modula-2 compiler and runtime
- emit. The obvious candidates all turned out to be unusable:
- * objdump / binutils: has no 16-bit x86 disassembler at all. `-m i8086`
- silently falls back to the 32-bit i386 rules.
- * unicorn 2.1.4: UC_MODE_16 mis-decodes 16-bit ModRM memory operands.
- FCML (libfcml, Debian package `fcml`) does have a real 16-bit x86
- disassembler, and we validated it against GNU as's `.code16` *encoder*
- (assemble there, disassemble here). See tests/FCML_PROBE.md.
- FCML is driven through the `fcml-disasm` CLI because the package ships no
- development headers, so ctypes struct layout would be guesswork. The CLI
- happens to be a linear sweeper that reports "Instruction code length", which
- is all this driver needs.
- The length agreement is the important part: if we emit a 3-byte instruction
- where a 4-byte one was required, the sweep desynchronises and the very next
- line points straight at the offending offset.
- Usage:
- disasm16.py FILE [BASE] # BASE defaults to 0, decimal or 0x-hex
- disasm16.py --bytes '89 6e 04' # inline bytes, for one-off probes
- """
- import re
- import subprocess
- import sys
- FCML = "fcml-disasm"
- # The Debian 1.3.0 `fcml-disasm` wrapper aborts (SIGABRT, rc=134) on any
- # buffer of 16 bytes or more: it linearly sweeps the *whole* input and blows
- # up on the resulting instruction list. Reproduce with 16 NOPs:
- # fcml-disasm -m16 0x90909090909090909090909090909090 # rc=134
- # 15 bytes is safe, and 15 >= the longest real-mode instruction (7 bytes,
- # e.g. `EA off16 seg16`), so a 15-byte window always contains the whole first
- # instruction. We only ever read the first instruction's length.
- MAX_WINDOW = 15
- RE_LEN = re.compile(r"^\s*Instruction code length:\s*(\d+)\s*$")
- RE_TEXT = re.compile(r"^\s*Disassembled instruction:\s*(.*?)\s*$")
- def decode(code, base):
- """Decode the first instruction of `code` (bytes). Returns (text, length).
- `code` is the remaining stream; at most MAX_WINDOW bytes are handed to
- FCML. No padding is invented: an under-length buffer must surface as a
- decode error rather than being silently completed with made-up bytes.
- """
- win = code[:MAX_WINDOW]
- hexs = "".join("%02X" % b for b in win)
- out = subprocess.run(
- [FCML, "-m16", "-rh", "-rz", "-ip", hex(base), "0x" + hexs],
- capture_output=True, text=True)
- if out.returncode != 0:
- return None, 0
- text = None
- length = None
- for line in out.stdout.splitlines():
- m = RE_TEXT.match(line)
- if m:
- text = m.group(1)
- m = RE_LEN.match(line)
- if m:
- length = int(m.group(1))
- if length is None or length == 0 or length > len(code):
- return text, 0
- return text, length
- def disasm(code, base=0, out=sys.stdout):
- """Linear-sweep `code` from `base`, printing one line per instruction."""
- pc = 0
- ninstr = 0
- while pc < len(code):
- text, length = decode(code[pc:], base + pc)
- if length == 0:
- out.write("%04X: %-24s <DECODE ERROR>\n"
- % (base + pc, " ".join("%02X" % b
- for b in code[pc:pc + 8])))
- return ninstr, False
- shown = code[pc:pc + length]
- out.write("%04X: %-24s %s\n"
- % (base + pc,
- " ".join("%02X" % b for b in shown),
- text if text else "<no text>"))
- pc += length
- ninstr += 1
- return ninstr, True
- def main(argv):
- if len(argv) < 2:
- sys.stderr.write(__doc__)
- return 2
- if argv[1] == "--bytes":
- code = bytes.fromhex(argv[2].replace(" ", ""))
- base = int(argv[3], 0) if len(argv) > 3 else 0
- else:
- with open(argv[1], "rb") as f:
- code = f.read()
- base = int(argv[2], 0) if len(argv) > 2 else 0
- ninstr, ok = disasm(code, base)
- sys.stderr.write("-- %d instructions, %d bytes\n" % (ninstr, len(code)))
- return 0 if ok else 1
- if __name__ == "__main__":
- sys.exit(main(sys.argv))
|