| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159 |
- #!/usr/bin/env python3
- """run_modrm19.py -- re-run the mod=00/01/10 ModR/M probe and check the result.
- modrm19.s derives the 16-bit effective-address table by EXECUTION: it stores a
- marker through each candidate encoding on a real 8086 under qemu, then scans
- memory for the word and reports the offset it landed at. This script builds
- the bootable image, runs it, and requires the measured offsets to equal the
- table computed from first principles here -- which is the point, because the
- computation in EXPECTED is written from the register arithmetic (BX+SI and so
- on) and not from a remembered table.
- usage: run_modrm19.py [-v] [--keep]
- Requires qemu-system-i386, as and objcopy. This is not part of run_all.sh:
- it needs a hand-built floppy image and a 60-second qemu timeout, and its
- result is already recorded in the table in Runtime.mod, which
- tests/probe/modrm11.py checks on every run. Re-run this when you doubt the
- memory forms.
- mod=11 is not measured here -- it names a register, not an address. See
- modrm11.s for why, and modrm11.py for how that half is established.
- """
- import os
- import shutil
- import subprocess
- import sys
- HERE = os.path.dirname(os.path.abspath(__file__))
- SRC = os.path.join(HERE, "modrm19.s")
- WORK = "/tmp/opencode/modrm19"
- # The probe's register setup, from modrm19.s. Distinct values, so the offset
- # a marker lands at identifies the effective address by arithmetic alone.
- BX, DI, SI, BP = 0x1000, 0x2000, 0x0030, 0x0040
- DISP8, DISP16 = 0x44, 0x1234
- # The effective address for (mod, rm), computed from the arithmetic. This is
- # the prediction; the probe's output is the measurement, and the two are
- # compared. mod=00 rm=110 is the direct disp16 form, so the address is the
- # displacement itself.
- def expected(mod, rm):
- base = {0: BX + SI, 1: BX + DI, 2: BP + SI, 3: BP + DI,
- 4: SI, 5: DI, 7: BX}
- if mod == 0:
- return DISP16 if rm == 6 else base[rm]
- disp = DISP8 if mod == 1 else DISP16
- if rm == 6: # [BP]+disp
- return BP + disp
- return base[rm] + disp
- # The one cell execution cannot answer. mod=10 rm=001 is BX+DI+disp16 =
- # 0x4234, and the probe's scan window stops at 0x3600, so the marker is
- # written but never seen. Recorded as a gap, not as a result; the cell is
- # covered statically by the recipes in Runtime.mod.
- GAP = {(2, 1)}
- def build():
- subprocess.run(["as", "--32", "-o", WORK + ".o", SRC], check=True)
- subprocess.run(["objcopy", "-O", "binary", "-j", ".text",
- WORK + ".o", WORK + ".bin"], check=True)
- with open(WORK + ".bin", "rb") as f:
- code = f.read()
- # A boot sector is 512 bytes: EB 3C at 0, code at 0x3E, 55 AA at 0x1FE.
- if 0x3E + len(code) > 512:
- raise SystemExit("probe code is %d bytes, does not fit after the "
- "0x3E header" % len(code))
- img = bytearray(512)
- img[0:2] = b"\xeb\x3c"
- img[0x3E:0x3E + len(code)] = code
- img[0x1FE:0x200] = b"\x55\xaa"
- with open(WORK + ".img", "wb") as f:
- f.write(bytes(img))
- return len(code)
- def run():
- """boot the image under qemu, return the captured serial bytes"""
- ser = WORK + ".ser"
- if os.path.exists(ser):
- os.remove(ser)
- # The guest does all its work in the first few milliseconds and then halts;
- # qemu keeps running, so the timeout is what ends it. rc=124 is the normal
- # outcome. Any other non-zero rc is a real failure.
- rc = subprocess.run(["timeout", "10", "qemu-system-i386",
- "-drive", "file=%s.img,format=raw,if=floppy" % WORK,
- "-serial", "file:" + ser,
- "-display", "none", "-no-reboot"],
- stdout=subprocess.DEVNULL,
- stderr=subprocess.DEVNULL).returncode
- if rc not in (0, 124):
- raise SystemExit("qemu exited %d; the probe did not run to the end"
- % rc)
- with open(ser, "rb") as f:
- return f.read()
- def main(argv):
- verbose = "-v" in argv
- for tool in ("as", "objcopy", "qemu-system-i386"):
- if not shutil.which(tool):
- print("SKIP: %s not installed" % tool)
- return 0
- os.makedirs(os.path.dirname(WORK), exist_ok=True)
- n = build()
- data = run()
- # 24 groups of "lo hi 0x20", then a blank line. The probe writes 0A 0A
- # but qemu can lose the last byte when it tears down, so require the tail
- # to be newline(s) and not insist on both.
- if len(data) < 24 * 3 + 1 or set(data[24 * 3:]) - {0x0A}:
- print("FAIL: serial capture is %d bytes and does not end in the "
- "expected blank line" % len(data))
- return 1
- got = [int.from_bytes(data[i:i + 2], "little")
- for i in range(0, 24 * 3, 3)]
- if any(data[i + 2] != 0x20 for i in range(0, 24 * 3, 3)):
- print("FAIL: a group separator is not 0x20")
- return 1
- bad = []
- for mod in range(3):
- for rm in range(8):
- g = got[mod * 8 + rm]
- if (mod, rm) in GAP:
- if g != 0xFFFF:
- bad.append("mod=%d rm=%d: expected the known gap "
- "(0xFFFF, outside the scan window), got %04X"
- % (mod, rm, g))
- continue
- w = expected(mod, rm)
- if g != w:
- bad.append("mod=%d rm=%d: measured %04X, arithmetic says %04X"
- % (mod, rm, g, w))
- if verbose:
- for mod in range(3):
- row = []
- for rm in range(8):
- g = got[mod * 8 + rm]
- row.append(" gap " if (mod, rm) in GAP else "%04X" % g)
- print("mod=%02d : %s" % (mod, " ".join(row)))
- print("mod=00/01/10: %d of 24 cells measured by execution on a real 8086"
- % (24 - len(GAP)))
- print(" %d cell is a documented gap, not a result"
- % len(GAP))
- if bad:
- print("FAIL: %d problem(s)" % len(bad))
- for b in bad:
- print(" - %s" % b)
- return 1
- print("PASS: measured effective addresses match the arithmetic in "
- "Runtime.mod")
- return 0
- if __name__ == "__main__":
- sys.exit(main(sys.argv))
|