run_com_tests.sh 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. #!/bin/bash
  2. # Build and run the .COM linker harness (tests/ComTest.mod), then verify every
  3. # .COM it produced with an INDEPENDENT checker.
  4. #
  5. # The independent pass matters: ComTest computes the expectations from the same
  6. # Compiler state it is testing, so a bug in the compiler would be invisible to
  7. # it. The Python pass re-derives what the file must contain - the runtime's
  8. # first bytes, a zero gap, a size that covers the data area - from the
  9. # constants only, and cross-checks.
  10. set -u
  11. D=/home/eric/Projets/Projets-Modula2/MyWork/TP3-comp/shell
  12. GM2=/home/eric/bin/Modula2/Gm2/bin/gm2
  13. cd "$D" || exit 9
  14. FLAGS="-fiso"
  15. # build logs go beside the tree (TP3-comp/tmp), never in /tmp
  16. mkdir -p ../tmp
  17. # --check-only DIR -- skip the build and the link, and run only the
  18. # independent Python checker over the .COM files already in DIR (plus a
  19. # hand-written raw.txt in the same shape the linker emits).
  20. #
  21. # This exists for non-vacuity, and it is the only reason to make it. The
  22. # checker normally runs over a scratch directory that the EXIT trap deletes,
  23. # so there is no way to hand it a DELIBERATELY WRONG .COM and see whether it
  24. # notices. A check that has only ever been shown the truth is not a check:
  25. # it could be reporting the truth about every file because it says nothing at
  26. # all. tests/nonvacuity.sh uses this to feed it a corrupted image and
  27. # require the named assertion to go red.
  28. CHECK_ONLY=""
  29. if [ "${1:-}" = "--check-only" ]; then
  30. CHECK_ONLY=${2:-}
  31. shift 2
  32. fi
  33. echo "== support modules =="
  34. if [ -n "$CHECK_ONLY" ]; then
  35. echo "check-only mode: not rebuilding, not linking"
  36. OUT="$CHECK_ONLY"
  37. [ -d "$OUT" ] || { echo "RESULT: FAIL (no such directory: $OUT)"; exit 1; }
  38. else
  39. [ -f Posix.o ] || cc -c Posix.c || exit 1
  40. for m in TextBuf Compiler Runtime Linker; do
  41. $GM2 $FLAGS -c $m.mod >../tmp/cm_c_$m 2>&1 \
  42. || { echo "COMPILE_FAIL $m"; grep -m5 "error:" ../tmp/cm_c_$m; exit 1; }
  43. done
  44. $GM2 $FLAGS -c tests/ComTest.mod >../tmp/cm_c_ComTest 2>&1 \
  45. || { echo "COMPILE_FAIL ComTest"; grep -m5 "error:" ../tmp/cm_c_ComTest; exit 1; }
  46. rm -f tests/ct.lst comtest
  47. $GM2 $FLAGS -fgen-module-list=tests/ct.lst -o /dev/null \
  48. tests/ComTest.mod TextBuf.o Posix.o Compiler.o Runtime.o Linker.o \
  49. >../tmp/cm_p1 2>&1
  50. p1=$?
  51. $GM2 $FLAGS -fuse-list=tests/ct.lst -o comtest \
  52. tests/ComTest.mod TextBuf.o Posix.o Compiler.o Runtime.o Linker.o \
  53. >../tmp/cm_p2 2>&1
  54. p2=$?
  55. if [ $p2 -ne 0 ]; then
  56. echo "LINK_FAIL p1_rc=$p1 p2_rc=$p2"
  57. grep -E "error:|undefined" ../tmp/cm_p2 | head -10
  58. exit 1
  59. fi
  60. echo "comtest built (p1_rc=$p1, phase 1 rc=1 is the expected rollup)"
  61. # .COM files are written beside the harness, so run it in a scratch dir
  62. OUT=$(mktemp -d) || exit 9
  63. # TP_COM_KEEP=1 leaves the scratch dir behind, for tests/nonvacuity.sh to
  64. # corrupt a copy of a real image and hand it back through --check-only. The
  65. # alternative - rebuilding the whole toolchain inside the non-vacuity script
  66. # to produce one throwaway byte - is slow for no benefit, and the point of
  67. # the case is the CHECKER's sensitivity, not the compiler's.
  68. if [ "${TP_COM_KEEP:-0}" = "1" ]; then
  69. echo "TP_COM_KEEP=1: images left in $OUT"
  70. else
  71. trap 'rm -rf "$OUT"' EXIT
  72. fi
  73. cd "$OUT" || exit 9
  74. ls "$D"/tests/fixtures/*.pas | "$D"/comtest > "$OUT/raw.txt" 2>&1
  75. sed 's/^.*fixtures\///' "$OUT/raw.txt"
  76. echo "----------------------------------------------------------------"
  77. nok=$(grep -c " OK com=" "$OUT/raw.txt")
  78. nerr=$(grep -c " ERROR " "$OUT/raw.txt")
  79. nbad=$(grep -cE "WRITE_COM_FAILED|CANNOT" "$OUT/raw.txt")
  80. echo "linked: $nok .COM files, $nerr fixtures rejected at compile time, $nbad harness failures"
  81. if [ "$nbad" -ne 0 ]; then
  82. echo "RESULT: FAIL (harness could not link every compiling fixture)"
  83. exit 1
  84. fi
  85. fi
  86. # ---- independent verification of the bytes on disk ------------------------
  87. python3 - "$OUT" "$D/tests" <<'PYEOF'
  88. import sys, os, re, glob
  89. out = sys.argv[1]
  90. # The layout of a linked image - ENT_SZ, HDR_SZ, the load bias, initmem's
  91. # first bytes, and the function that MEASURES where the header is in a given
  92. # file - comes from tests/comimage.py and is not written down here. This
  93. # checker carried its own copy of all of it and tests/comtest.py carried a
  94. # second; a duplicated constant that has silently drifted is not an
  95. # independent check, it is a second source of truth that lies, and it lies in
  96. # the direction of looking like the compiler is broken. The runtime's SIZE
  97. # was such a constant (a literal 391 beside a comment saying it tracked
  98. # Runtime.RT_Size()), so the header was read out of the middle of the code and
  99. # every linked fixture "failed" on a header full of code bytes.
  100. #
  101. # The measurement is still independent of the compiler: find_header reads the
  102. # emitted file, not a Modula-2 variable, so the code under test cannot satisfy
  103. # it by agreeing with itself. tests/check_runtime.py pins the runtime's size
  104. # explicitly, which is where a deliberate size change should be noticed.
  105. #
  106. # The image starts with a three-byte JMP at offset 0 (Compiler.Inittur): a
  107. # .COM is entered at file offset 0, and until that jump existed this checker
  108. # ASSERTED that the runtime was at offset 0, which was precisely the bug -
  109. # every .COM began by executing initmem with whatever the loader left in AX.
  110. # A checker that pins a wrong invariant is worse than no checker, because it
  111. # makes the wrong thing look tested. RTSZ (the header's image offset), PROLOG
  112. # (RTSZ + HDR_SZ, where the entry jump must land) and DATAB (RTSZ + 1000h, the
  113. # compiler's data base) are all DERIVED PER FILE by find_header below.
  114. #
  115. # The load bias is the THIRD bias of the same family in this file, and the
  116. # subtlest: the entry jump (a jump that landed on the end of the code), the
  117. # RT_Entry offsets (CALLs that landed inside a neighbouring runtime entry) and
  118. # absolute addresses (which landed 0100h low, inside the runtime) all produce
  119. # a program that STARTS, RUNS and PRINTS something. Only running it finds the
  120. # last one; the byte checks below are all satisfied by an address that is
  121. # consistently 0100h wrong. See comimage.LOAD_BIAS and Runtime.LoadBias.
  122. sys.path.insert(0, sys.argv[2])
  123. from comimage import (ENT_SZ, HDR_SZ, LOAD_BIAS, HEAD, HDR_DS_WORD,
  124. HDR_HEAP_WORD, find_header)
  125. raw = open(os.path.join(out, 'raw.txt')).read()
  126. rows = re.findall(r'(\S+\.pas)\s+OK\s+com=(\d+)\s+image=(\d+)\s+data=(\d+)\s+nonzeroInGap=(\d+)', raw)
  127. if not rows:
  128. print('RESULT: FAIL (no linked fixtures found in output)')
  129. sys.exit(1)
  130. bad = 0
  131. last_rt = None
  132. for name, com, image, data, nzg in rows:
  133. com, image, data, nzg = int(com), int(image), int(data), int(nzg)
  134. # ComTest writes the .COM by BASENAME beside itself (it cannot graft a
  135. # directory onto a source path), so the checker must look for the bare
  136. # name, not the full source path the fixture was read from.
  137. path = os.path.join(out, os.path.basename(name)[:-4] + '.COM')
  138. errs = []
  139. if not os.path.exists(path):
  140. errs.append('no .COM file')
  141. d = b''
  142. else:
  143. d = open(path, 'rb').read()
  144. # Everything below is expressed in terms of where the header actually is,
  145. # measured from this file, rather than where a literal says it should be.
  146. hdrOff = find_header(d)
  147. if hdrOff is None:
  148. errs.append('no program header found: the layout this checker knows '
  149. 'how to look for is not the one in the file')
  150. RTSZ, PROLOG, DATAB = ENT_SZ, ENT_SZ + HDR_SZ, ENT_SZ + 0x1000
  151. else:
  152. RTSZ = hdrOff
  153. PROLOG = hdrOff + HDR_SZ
  154. DATAB = hdrOff + 0x1000
  155. if RTSZ != last_rt:
  156. last_rt = RTSZ
  157. print(' measured runtime size: %d bytes (header at image offset '
  158. '%d)' % (RTSZ - ENT_SZ, RTSZ))
  159. # The entry jump. This is the one assertion in the whole project that can
  160. # see where execution STARTS, because it is the only one that cares. It
  161. # has caught THREE real bugs, all in the same three bytes, and all of them
  162. # invisible to every other check here:
  163. #
  164. # 1. no jump at all, so a .COM began by executing the runtime's initmem
  165. # with whatever the loader left in AX;
  166. # 2. a jump to `pc`, one byte past the last instruction, into the
  167. # zero-filled code/data gap, where the CPU slides through
  168. # `ADD [BX+SI],AL` until it faults;
  169. # 3. a jump to RTSZ, which is the program HEADER - sixteen bytes of DATA
  170. # that the CPU then decodes as instructions. This one is the reason
  171. # the target is pinned to PROLOG and not to RTSZ: whether it works
  172. # depends entirely on how those sixteen bytes happen to decode, so
  173. # writeln('hi') ran correctly by sliding through them while t07 hung
  174. # on a LOCK-prefixed ADD with a displacement crossing a page. The
  175. # correct target is the first instruction, and there is no reason for
  176. # a checker to accept a range.
  177. if len(d) >= ENT_SZ:
  178. if d[0] != 0xE9:
  179. errs.append('byte 0 is %02X, not the E9 of the entry jump' % d[0])
  180. # The jump's displacement is measured from the end of the jump.
  181. want_rel = PROLOG - ENT_SZ
  182. got_rel = int.from_bytes(d[1:ENT_SZ], 'little')
  183. if got_rel != want_rel:
  184. errs.append('entry jump rel16=%d, want %d; it lands on image '
  185. 'offset %d, want %d (the first instruction, %d bytes '
  186. 'past the header - not the header at %d, and not the '
  187. 'end of the code at %d)'
  188. % (got_rel, want_rel, ENT_SZ + got_rel, PROLOG,
  189. HDR_SZ, RTSZ, image))
  190. # The runtime's own first bytes must follow the jump, and the jump must be
  191. # the only thing before them.
  192. if d[ENT_SZ:ENT_SZ + len(HEAD.split())].hex(' ').upper() != HEAD:
  193. errs.append('runtime not at offset %d (bytes there %s, want %s)'
  194. % (ENT_SZ,
  195. d[ENT_SZ:ENT_SZ + len(HEAD.split())].hex(' ').upper(),
  196. HEAD))
  197. if len(d) != com:
  198. errs.append('file is %d bytes, harness said %d' % (len(d), com))
  199. if DATAB + data != len(d):
  200. errs.append('size %d != dataBase+data %d' % (len(d), DATAB + data))
  201. # the gap between the image and the data area must be entirely zero
  202. gap = d[image:DATAB]
  203. if any(gap):
  204. errs.append('%d non-zero bytes in the code/data gap' % sum(1 for b in gap if b))
  205. if nzg != 0:
  206. errs.append('harness itself reported %d non-zero gap bytes' % nzg)
  207. # the program must start exactly where the runtime ends
  208. if image < RTSZ:
  209. errs.append('image %d shorter than the runtime %d' % (image, RTSZ))
  210. # the program header sits at offset rtSz, and its words must describe the
  211. # image that is actually in the file
  212. if len(d) > RTSZ + 8:
  213. hdr = d[RTSZ:RTSZ+16]
  214. flag = int.from_bytes(hdr[0:2], 'little')
  215. cs = int.from_bytes(hdr[2:4], 'little')
  216. ds = int.from_bytes(hdr[HDR_DS_WORD:HDR_DS_WORD+2], 'little')
  217. heap = int.from_bytes(hdr[HDR_HEAP_WORD:HDR_HEAP_WORD+2], 'little')
  218. if flag != 1:
  219. errs.append('hdrFlag=%d' % flag)
  220. # hdrCS is the end of the code, as a SEGMENT offset like every other
  221. # offset in the header, so the load bias comes off before comparing it
  222. # with the harness's `image` (= rtSz + code, already an image offset).
  223. # Adding RTSZ here would count the runtime twice.
  224. if cs - LOAD_BIAS != image:
  225. errs.append('hdrCS=%d, want %d (end of image, as a segment '
  226. 'offset)' % (cs, image + LOAD_BIAS))
  227. if ds != DATAB + LOAD_BIAS:
  228. errs.append('hdrDS=%d, want %d (= data base %d + load bias %d)'
  229. % (ds, DATAB + LOAD_BIAS, DATAB, LOAD_BIAS))
  230. if heap != DATAB + data + LOAD_BIAS:
  231. errs.append('hdrHeap=%d, want %d (= data base %d + %d + load bias %d)'
  232. % (heap, DATAB + data + LOAD_BIAS, DATAB, data,
  233. LOAD_BIAS))
  234. # initmem must read hdrDS and hdrHeap, not some other pair of header
  235. # words. (It read +8, hdrMax, which the compiler patches to 0, so
  236. # the range it cleared was empty and every global kept whatever the
  237. # loader left in it.)
  238. if [d[ENT_SZ + 4], d[ENT_SZ + 7]] != [HDR_DS_WORD, HDR_HEAP_WORD]:
  239. errs.append('initmem reads header words +%d/+%d, but the data '
  240. 'base and data end are at +%d/+%d'
  241. % (d[ENT_SZ + 4], d[ENT_SZ + 7],
  242. HDR_DS_WORD, HDR_HEAP_WORD))
  243. if errs:
  244. bad += 1
  245. print(' %-24s FAIL %s' % (os.path.basename(name)[:-4], '; '.join(errs)))
  246. else:
  247. print(' %-24s PASS %d bytes' % (os.path.basename(name)[:-4], com))
  248. print('----------------------------------------------------------------')
  249. print('independent .COM check: %d checked, %d failed' % (len(rows), bad))
  250. sys.exit(1 if bad else 0)
  251. PYEOF
  252. rc=$?
  253. [ "$rc" -eq 0 ] || exit 1
  254. echo "RESULT: ALL PASS"