nonvacuity.sh 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914
  1. #!/bin/sh
  2. # nonvacuity.sh -- prove the runtime checks can actually fail.
  3. #
  4. # A test that has never been seen red is not a test. This script breaks the
  5. # runtime on purpose, once per check, and asserts that the check goes red and
  6. # says something useful about the breakage. Then it restores the source and
  7. # asserts everything is green again.
  8. #
  9. # Each mutation below is a real bug that was in this file at some point, not an
  10. # invented one. That is the point: these are the mistakes we actually make
  11. # with 16-bit ModRM, so these are the ones the checks have to catch.
  12. #
  13. # audit_helpers.py name-versus-decode: catches a wrong ModRM that still
  14. # decodes cleanly
  15. # audit_helpers.py coverage: catches a helper that has silently
  16. # dropped OUT of the audit, which is a green report about
  17. # a subject nobody looked at
  18. # run_com_tests.sh the .COM layout: catches a header that cannot be
  19. # located, a runtime size that disagrees with the image,
  20. # and an entry jump that starts in the wrong place
  21. # check_runtime.py golden: catches the same thing in the built
  22. # image
  23. # check_runtime.py decode sweep: catches a wrong instruction LENGTH
  24. # check_runtime.py branch targets: catches a wrong fixup
  25. # check_runtime.py entry goldens: catches a broken prologue
  26. # probe/modrm11.py the mod=11 table: catches the ModRM column itself
  27. # going wrong, which no amount of decoding will show
  28. # check_framedisp.py the BP disp rule: catches a displacement that reads
  29. # a different address than the symbol table named
  30. # rt_exec.py the BP contract: catches an entry that borrows BP to
  31. # reach its argument and does not hand it back. The bytes
  32. # are well formed, the golden is satisfied, the audit says
  33. # every helper emits what its name says, and the machine
  34. # triple-faults on the second call - so nothing short of
  35. # running it, or of stating the register contract
  36. # explicitly, can see it.
  37. # run_com_exec.py behaviour: catches a*b that emits an ADD, `>'
  38. # and `>=' swapped, REPEAT..UNTIL that stops after one
  39. # pass, and two procedures whose parameters collide.
  40. # All four sat in fixtures that COMPILED and were never
  41. # RUN, with every byte-level check green.
  42. #
  43. # The mod=11 cases do not need a rebuild -- they read the probe sources
  44. # directly -- so they are cheap, and they are the ones that matter most: the
  45. # table they guard is the one thing in this project that was wrong in the
  46. # documentation while the code was right, and a table that is wrong in the
  47. # code produces bytes that decode perfectly.
  48. #
  49. # Usage: tests/nonvacuity.sh (from shell/; leaves Runtime.mod restored)
  50. set -u
  51. cd "$(dirname "$0")/.." || exit 1
  52. GM2=/home/eric/bin/Modula2/Gm2/bin/gm2
  53. SAVED=/tmp/opencode/nonvacuity.Runtime.mod
  54. PROBE=/tmp/opencode/nonvacuity.rtprobe
  55. DUMP=/tmp/opencode/nonvacuity.dump
  56. cp Runtime.mod "$SAVED" || exit 1
  57. trap 'cp "$SAVED" Runtime.mod; "$GM2" -fiso -c Runtime.mod >/dev/null 2>&1' EXIT
  58. pass=0
  59. fail=0
  60. # mutate <file> <sed-expr> -- apply a deliberate breakage and INSIST it landed.
  61. #
  62. # Four cases in this file were already dead when first run, all the same way:
  63. # the helper they name had been renamed or reformatted since the case was
  64. # written, the sed matched nothing, the source was unchanged, and the check
  65. # correctly passed - so the harness reported "NOT NON-VACUOUS" and, worse, a
  66. # reader skimming the output could take "the check still passed" for a passing
  67. # test. A case that cannot fire is worse than no case: it is a claim of
  68. # coverage that was never tested.
  69. #
  70. # So the mutation is verified, not assumed. If the file is byte-identical
  71. # afterwards, that is reported as a FAILURE of the harness, naming the sed, and
  72. # the case is not run - because running it would only produce a meaningless
  73. # green. The message says what to do (fix the sed) rather than what it found.
  74. mutate () {
  75. mf=$1
  76. msed=$2
  77. cp "$mf" /tmp/opencode/nonvacuity.mut.bak
  78. sed -i "$msed" "$mf"
  79. if cmp -s "$mf" /tmp/opencode/nonvacuity.mut.bak; then
  80. echo " BROKEN CASE: the mutation did not change $mf"
  81. echo " sed: $msed"
  82. echo " the named code has probably been renamed or reformatted -"
  83. echo " fix this case, it is asserting nothing"
  84. fail=$((fail + 1))
  85. return 1
  86. fi
  87. return 0
  88. }
  89. # rebuild <label> -- re-emit the runtime and dump it
  90. rebuild () {
  91. "$GM2" -fiso -c Runtime.mod >/dev/null 2>&1 || return 1
  92. "$GM2" -fiso -o "$PROBE" tests/RtProbe.mod Runtime.o Posix.o \
  93. >/dev/null 2>&1 || return 1
  94. "$PROBE" > "$DUMP" || return 1
  95. return 0
  96. }
  97. # expect_red <label> <pattern> <checker-cmd...>
  98. # <pattern> is a grep the failure output must match, so a check cannot
  99. # "pass" by failing for some unrelated reason.
  100. expect_red () {
  101. label=$1
  102. want=$2
  103. shift 2
  104. if out=$("$@" 2>&1); then
  105. echo "NOT NON-VACUOUS: $label -- the check still passed"
  106. fail=$((fail + 1))
  107. elif ! printf '%s\n' "$out" | grep -qi "$want"; then
  108. echo "WRONG FAILURE: $label -- went red, but not for the stated reason"
  109. printf '%s\n' "$out" | sed 's/^/ /'
  110. fail=$((fail + 1))
  111. else
  112. echo " ok: $label"
  113. printf '%s\n' "$out" | grep -im1 "$want" | sed 's/^/ /'
  114. pass=$((pass + 1))
  115. fi
  116. }
  117. echo "== each mutation must turn the named check red"
  118. echo
  119. # --- 1. name-versus-decode -------------------------------------------
  120. # MovSiBx was `89 DC`, which is MOV SP,BX. Two bytes either way, decodes
  121. # cleanly, and no structural check can see it.
  122. cp "$SAVED" Runtime.mod
  123. mutate Runtime.mod 's|B (0DEH) END MovSiBx|B (0DCH) END MovSiBx|'
  124. expect_red "audit_helpers catches MovSiBx emitting MOV SP,BX" \
  125. "MovSiBx" python3 tests/audit_helpers.py
  126. # CmpSiBx had the identical mistake, which is how you know a single fix is
  127. # not enough -- the same misreading was written twice.
  128. cp "$SAVED" Runtime.mod
  129. mutate Runtime.mod 's|B (39H) ; B (0DEH) END CmpSiBx|B (39H) ; B (0DCH) END CmpSiBx|'
  130. expect_red "audit_helpers catches CmpSiBx emitting CMP SP,BX" \
  131. "CmpSiBx" python3 tests/audit_helpers.py
  132. # --- 2. golden, and entry goldens ------------------------------------
  133. # MovDlAl was `88 C0` = MOV AL,AL instead of MOV DL,AL. This is the case that
  134. # motivated runtime.golden: the sweep stayed in sync, every branch target
  135. # stayed on a boundary, no entry's first bytes moved, and the size did not
  136. # change. The target helper was MovAlDh when this case was written, which is
  137. # the fourth way a case here can rot - see the note on `mutate` below.
  138. cp "$SAVED" Runtime.mod
  139. mutate Runtime.mod 's|PROCEDURE MovDlAl ; BEGIN B (88H) ; B (0C2H)|PROCEDURE MovDlAl ; BEGIN B (88H) ; B (0C0H)|'
  140. rebuild
  141. expect_red "runtime.golden catches MOV AL,AL" \
  142. "mov al,al" python3 tests/check_runtime.py "$DUMP"
  143. # initmem opened with the mis-emitted MovSiAx, so its entry golden was the
  144. # thing that noticed the prologue was a no-op.
  145. cp "$SAVED" Runtime.mod
  146. mutate Runtime.mod 's|B (0F0H) END MovSiAx|B (0C0H) END MovSiAx|'
  147. rebuild
  148. expect_red "check_runtime catches a broken initmem prologue" \
  149. "mov ax,ax" python3 tests/check_runtime.py "$DUMP"
  150. # --- 3. decode sweep / length ----------------------------------------
  151. # StDiDl was `88 97` = [BX+disp16],DL: mod=10, so the instruction needs a
  152. # disp16 it was not given, and the sweep loses sync two bytes later.
  153. cp "$SAVED" Runtime.mod
  154. mutate Runtime.mod 's|PROCEDURE StDiDl ; BEGIN B (88H) ; B (15H)|PROCEDURE StDiDl ; BEGIN B (88H) ; B (97H)|'
  155. rebuild
  156. expect_red "decode sweep catches a mod=10 byte move with no displacement" \
  157. "mov byte ptr \[bx+5b5fh\],dl" python3 tests/check_runtime.py "$DUMP"
  158. # --- 4. branch targets ------------------------------------------------
  159. # FixUp measures a rel8 from the end of the instruction, one byte past the
  160. # displacement field. Drop the +1 and every short branch lands one byte into
  161. # its target, which for a 3-byte instruction means the middle of it. The
  162. # bytes themselves are all perfectly well formed -- only the fixups are
  163. # wrong -- so this is the one failure mode the golden cannot be expected to
  164. # catch on its own.
  165. cp "$SAVED" Runtime.mod
  166. mutate Runtime.mod 's|rel := (t + 100H - (fix \[i\].place + 1)) MOD 100H|rel := (t + 100H - fix [i].place) MOD 100H|'
  167. rebuild
  168. expect_red "branch check catches rel8 fixups measured from the wrong byte" \
  169. "not an instruction boundary" \
  170. python3 tests/check_runtime.py "$DUMP"
  171. echo
  172. echo "== everything restored and green again"
  173. cp "$SAVED" Runtime.mod
  174. if rebuild; then
  175. if python3 tests/audit_helpers.py >/dev/null 2>&1 &&
  176. python3 tests/check_runtime.py "$DUMP" >/dev/null 2>&1; then
  177. echo " ok: both checks pass on the restored source"
  178. pass=$((pass + 1))
  179. else
  180. echo "NOT RESTORED: a check is red after restoring Runtime.mod"
  181. fail=$((fail + 1))
  182. fi
  183. else
  184. echo "NOT RESTORED: the runtime would not rebuild"
  185. fail=$((fail + 1))
  186. fi
  187. echo
  188. echo "== the mod=11 table (probe/modrm11.py)"
  189. # These mutate the probe's own sources, not the runtime, so there is no
  190. # rebuild in the loop. SAVED_PY / SAVED_S are restored after each case.
  191. SAVED_PY=/tmp/opencode/nonvacuity.modrm11.py
  192. SAVED_S=/tmp/opencode/nonvacuity.modrm11.s
  193. cp tests/probe/modrm11.py "$SAVED_PY" || exit 1
  194. cp tests/probe/modrm11.s "$SAVED_S" || exit 1
  195. M11="python3 tests/probe/modrm11.py"
  196. restore_probe () {
  197. cp "$SAVED_PY" tests/probe/modrm11.py
  198. cp "$SAVED_S" tests/probe/modrm11.s
  199. }
  200. # 1. one cell of the table moved
  201. mutate tests/probe/modrm11.py 's|"Si", "Di"\]$|"Bp", "Di"]|'
  202. expect_red "anchor pins a moved table cell" \
  203. "anchor ADD SI, 2" $M11
  204. restore_probe
  205. # 2. the table this project actually shipped: AX dropped off the front and a
  206. # duplicate BX invented at the end, which shifts every code down by one
  207. mutate tests/probe/modrm11.py 's|^REG = .*$|REG = ["Cx", "Dx", "Bx", "Sp", "Bp", "Si", "Di", "Bx"]|'
  208. expect_red "the table shifted by one (AX dropped, BX duplicated)" \
  209. "anchor MOV SP, BP" $M11
  210. restore_probe
  211. # 3. the .s edited to contradict the table. This is the case that shows why
  212. # the hard-coded EXPECT bytes exist: the assembler encodes the new claim
  213. # correctly, so comparing the .s against `as` alone can never fail here.
  214. mutate tests/probe/modrm11.s 's|movw %sp, %di # reg 100|movw %bp, %di # reg 100|'
  215. expect_red "probe source edited away from the recorded bytes" \
  216. "expected 89 E7" $M11
  217. restore_probe
  218. # 4. the 8-bit list edited, which is a different table from the word one
  219. mutate tests/probe/modrm11.s 's|movb %al, %dl # 88 C2 -> DL := AL|movb %al, %bl # was DL|'
  220. expect_red "the 8-bit register list edited" \
  221. "expected 88 C2" $M11
  222. restore_probe
  223. # 5. an anchor's recorded byte corrupted, so the anchor can no longer
  224. # corroborate itself
  225. mutate tests/probe/modrm11.py 's|"8B EC", "8B E5"|"8B ED", "8B E5"|'
  226. expect_red "anchor byte no longer matches the emitted code" \
  227. "expected 8B ED" $M11
  228. restore_probe
  229. if $M11 >/dev/null 2>&1; then
  230. echo " ok: modrm11.py passes on the restored probe sources"
  231. pass=$((pass + 1))
  232. else
  233. echo "NOT RESTORED: modrm11.py is red after restoring its sources"
  234. $M11 2>&1 | sed 's/^/ /'
  235. fail=$((fail + 1))
  236. fi
  237. echo
  238. echo "== the BP displacement rule (check_framedisp.py)"
  239. # This one is about Compiler.mod rather than the runtime, and it needs the
  240. # whole toolchain rebuilt (comtest, not rtprobe), so it gets its own rebuild.
  241. SAVED_C=/tmp/opencode/nonvacuity.Compiler.mod
  242. cp Compiler.mod "$SAVED_C" || exit 1
  243. rebuild_compiler () {
  244. $GM2 -fiso -c Compiler.mod >/dev/null 2>&1 || return 1
  245. $GM2 -fiso -fgen-module-list=tests/ct.lst -o /dev/null \
  246. tests/ComTest.mod TextBuf.o Posix.o Compiler.o Runtime.o Linker.o \
  247. >/dev/null 2>&1
  248. $GM2 -fiso -fuse-list=tests/ct.lst -o comtest \
  249. tests/ComTest.mod TextBuf.o Posix.o Compiler.o Runtime.o Linker.o \
  250. >/dev/null 2>&1 || return 1
  251. return 0
  252. }
  253. # 1. the original bug: `off MOD 100H`, always disp8. Restores exactly the code
  254. # that was there before EmBpDisp existed. t28's [BP+128] read becomes
  255. # [BP-128], which is the failure this whole check is named after.
  256. python3 - "$SAVED_C" <<'PYEOF'
  257. import sys
  258. p = 'Compiler.mod'
  259. s = open(p).read()
  260. old = """BEGIN
  261. IF off <= 127 THEN
  262. Ebyte (46H) ; Ebyte (VAL (BYTE, off))
  263. ELSE
  264. Ebyte (86H) ; Eword (off)
  265. END
  266. END EmBpDisp ;"""
  267. new = """VAR disp : CARDINAL ;
  268. BEGIN
  269. disp := off MOD 100H ;
  270. Ebyte (46H) ; Ebyte (VAL (BYTE, disp))
  271. END EmBpDisp ;"""
  272. assert old in s, "EmBpDisp body not found -- update this mutation"
  273. open(p, 'w').write(s.replace(old, new))
  274. PYEOF
  275. if rebuild_compiler; then
  276. expect_red "displacement truncation reads a different address" \
  277. "no 8B access at \[BP+128\]" python3 tests/check_framedisp.py
  278. else
  279. echo " FAIL: the compiler would not rebuild with the truncation"
  280. fail=$((fail + 1))
  281. fi
  282. cp "$SAVED_C" Compiler.mod
  283. # 2. the other half of the rule: always use the 4-byte form, ignoring the
  284. # <= 127 case. This is over-cautious rather than wrong, so the checker must
  285. # still be happy -- which is worth asserting, because a check that only
  286. # ever fails on a smaller encoding is a check that pins one answer instead
  287. # of the rule.
  288. python3 - <<'PYEOF'
  289. p = 'Compiler.mod'
  290. s = open(p).read()
  291. old = """ IF off <= 127 THEN
  292. Ebyte (46H) ; Ebyte (VAL (BYTE, off))
  293. ELSE
  294. Ebyte (86H) ; Eword (off)
  295. END"""
  296. new = """ Ebyte (86H) ; Eword (off)"""
  297. assert old in s, "EmBpDisp branch not found -- update this mutation"
  298. open(p, 'w').write(s.replace(old, new))
  299. PYEOF
  300. if rebuild_compiler; then
  301. if python3 tests/check_framedisp.py >/dev/null 2>&1; then
  302. echo " ok: always-disp16 is accepted, so the check pins the rule"
  303. echo " and not one particular encoding"
  304. pass=$((pass + 1))
  305. else
  306. echo " FAIL: check_framedisp rejects a safe, over-long encoding"
  307. python3 tests/check_framedisp.py 2>&1 | sed 's/^/ /'
  308. fail=$((fail + 1))
  309. fi
  310. else
  311. echo " FAIL: the compiler would not rebuild with always-disp16"
  312. fail=$((fail + 1))
  313. fi
  314. cp "$SAVED_C" Compiler.mod
  315. if rebuild_compiler; then
  316. if python3 tests/check_framedisp.py >/dev/null 2>&1; then
  317. echo " ok: check_framedisp passes on the restored source"
  318. pass=$((pass + 1))
  319. else
  320. echo "NOT RESTORED: check_framedisp is red after restoring Compiler.mod"
  321. python3 tests/check_framedisp.py 2>&1 | sed 's/^/ /'
  322. fail=$((fail + 1))
  323. fi
  324. else
  325. echo "NOT RESTORED: the compiler would not rebuild"
  326. fail=$((fail + 1))
  327. fi
  328. # --- 3. the operator bugs the nine dead fixtures were hiding ------------
  329. echo
  330. echo "== the four bugs the nine never-executed fixtures were hiding"
  331. echo
  332. # A different KIND of case from everything above. The others break the code
  333. # and assert a byte-level check notices; these break the code and assert a
  334. # BEHAVIOURAL check notices, which is the only kind that could have found them.
  335. # All four shipped with a green compile matrix, a passing .COM layout check, a
  336. # passing golden and a passing emitter audit:
  337. #
  338. # OpMul = 1 `a * b` emitted ADD AX,CX. `*' and `+' both numbered
  339. # their operator 1, and BinOpEmit cannot see which
  340. # precedence level called it, so every multiplication
  341. # dispatched to the addition. The constant-folding arm was
  342. # correct, which is why `n * n' with n a CONST was right and
  343. # `a * a' with a a variable was not -- and t08_const is the
  344. # only fixture that ever multiplied.
  345. # 9Dh / 9FH `>' got SETGE and `>=' got SETG: swapped, one letter
  346. # apart in the mnemonic. Only a==b could see it.
  347. # JNZ -> body REPEAT..UNTIL looped back while the condition was TRUE,
  348. # which is WHILE, so the body ran once and stopped.
  349. #
  350. # They are mutated back to the original defect and run_com_exec.py must go red
  351. # on the exact fixture that pins the behaviour. The fourth (HideLocals) is a
  352. # scoping bug rather than an operator bug; it was hiding in the same place.
  353. mutate_compiler () { # reuse mutate's verified-change discipline on Compiler.mod
  354. mf=Compiler.mod
  355. msed=$1
  356. cp "$SAVED_C" /tmp/opencode/nonvacuity.mut2.bak
  357. sed -i "$msed" "$mf"
  358. if cmp -s "$mf" /tmp/opencode/nonvacuity.mut2.bak; then
  359. echo " BROKEN CASE: the mutation did not change Compiler.mod"
  360. echo " sed: $msed"
  361. echo " the named code has probably been renamed or reformatted -"
  362. echo " fix this case, it is asserting nothing"
  363. fail=$((fail + 1))
  364. return 1
  365. fi
  366. return 0
  367. }
  368. # The SETcc swap and the HideLocals removal are done in python rather than
  369. # with sed: both need to match source text containing `*` and `(` in a way that
  370. # is tedious and fragile as a regex, and a case whose only failure mode is a
  371. # malformed sed is a case that silently asserts nothing.
  372. #
  373. # And a python helper fails in a way sed does not: a syntax error in the helper
  374. # is a non-zero exit, `if mutate_foo; then` is simply false, and the case is
  375. # SKIPPED -- with no failure counted and nothing on stdout but whatever python
  376. # printed. That is how the SETcc case spent its first run: an apostrophe in an
  377. # assert message ("the `>' arm") closed the string early, python died, the
  378. # compiler was never broken, and the suite still reported 0 failed. A skipped
  379. # case and a passing case look the same in the total. So each helper below
  380. # fails LOUDLY: a non-zero exit from python is reported as a BROKEN CASE and
  381. # counted, never swallowed.
  382. #
  383. # Each one also counts its targets before replacing. `assert s != before' only
  384. # says the file changed; with two edits it would pass if just one of them
  385. # landed, and with two identical HideLocals call sites it would happily delete
  386. # the wrong one -- still a changed file, still a working compiler, still green
  387. # for the wrong reason.
  388. mutate_cc_swap () {
  389. if python3 - <<'PYX'
  390. p = 'Compiler.mod'
  391. s = open(p).read()
  392. GT = 'EmSetcc (9FH) ; (* > SETG *)' # the greater-than arm
  393. GE = 'EmSetcc (9DH) ; (* >= SETGE *)' # the greater-equal arm
  394. assert s.count(GT) == 1, 'expected 1 greater-than arm, found %d' % s.count(GT)
  395. assert s.count(GE) == 1, 'expected 1 greater-equal arm, found %d' % s.count(GE)
  396. s = s.replace(GT, GT.replace('9FH', '9DH'))
  397. s = s.replace(GE, GE.replace('9DH', '9FH'))
  398. open(p, 'w').write(s)
  399. PYX
  400. then
  401. return 0
  402. fi
  403. echo " BROKEN CASE: the SETcc swap did not apply"
  404. echo " the two EmSetcc arms are probably renamed or reformatted -"
  405. echo " fix this case, it is asserting nothing"
  406. fail=$((fail + 1))
  407. return 1
  408. }
  409. mutate_no_hidelocals () {
  410. if python3 - <<'PYX'
  411. p = 'Compiler.mod'
  412. s = open(p).read()
  413. # Two HideLocals calls exist. Only the body-exit one may go: deleting the
  414. # FORWARD one instead would still change the file, still rebuild, and still
  415. # leave t13_proc compiling, so the case would go green for the wrong reason.
  416. HL = ' HideLocals (nestMark) ; (* parameters and locals stop here *)\n'
  417. assert s.count(HL) == 1, 'expected 1 body-exit HideLocals, found %d' % s.count(HL)
  418. open(p, 'w').write(s.replace(HL, ''))
  419. PYX
  420. then
  421. return 0
  422. fi
  423. echo " BROKEN CASE: HideLocals was not removed"
  424. echo " the call or its comment has probably been reformatted -"
  425. echo " fix this case, it is asserting nothing"
  426. fail=$((fail + 1))
  427. return 1
  428. }
  429. cp "$SAVED_C" Compiler.mod
  430. if mutate_compiler 's|^ op := OpMul ; DropCh| op := OpAdd ; DropCh|'; then
  431. if rebuild_compiler; then
  432. expect_red "execution catches '*' emitting an ADD (t08_const, n*n)" \
  433. "t08_const" python3 tests/run_com_exec.py t08_const
  434. # t08 only ever multiplied two CONSTANTS, which is the one path that was
  435. # never wrong, because BinOpEmit folds it. So the case above is close
  436. # to vacuous: it proves the mutation changed the binary, not that the
  437. # emitted multiply is covered. The check that matters needs a
  438. # VARIABLE operand, and no shipped fixture has one -- which is why the
  439. # bug survived at all. So this writes a throwaway fixture that
  440. # multiplies a variable, runs it, and asserts the multiply is right.
  441. # The fixture is deleted afterwards; it is here to close the coverage
  442. # hole, not to become a permanent test (that is what a real fixture
  443. # with a `*' in it would be for).
  444. cat > tests/fixtures/zzmul.pas <<'ZZEOF'
  445. program zzmul;
  446. var a : integer ;
  447. begin
  448. a := 7 ;
  449. writeln (a * 6)
  450. end.
  451. ZZEOF
  452. printf '42\r\n' > tests/fixtures/zzmul.out
  453. expect_red "execution catches '*' on a VARIABLE (the unfolded path)" \
  454. "zzmul" python3 tests/run_com_exec.py zzmul
  455. rm -f tests/fixtures/zzmul.pas tests/fixtures/zzmul.out
  456. else
  457. echo " FAIL: the compiler would not rebuild with OpAdd for '*'"
  458. fail=$((fail + 1))
  459. fi
  460. fi
  461. cp "$SAVED_C" Compiler.mod
  462. cp "$SAVED_C" Compiler.mod
  463. if mutate_cc_swap; then
  464. if rebuild_compiler; then
  465. expect_red "execution catches '>' and '>=' swapped (t09_if)" \
  466. "t09_if" python3 tests/run_com_exec.py t09_if
  467. else
  468. echo " FAIL: the compiler would not rebuild with the SETcc swap"
  469. fail=$((fail + 1))
  470. fi
  471. fi
  472. cp "$SAVED_C" Compiler.mod
  473. cp "$SAVED_C" Compiler.mod
  474. if mutate_compiler 's| DropC (EmJcc (84H, L1)) ; (\* JZ -> body again \*)| zj := EmJcc (85H, L1) ;|'; then
  475. if rebuild_compiler; then
  476. expect_red "execution catches REPEAT..UNTIL exiting after one pass (t12)" \
  477. "t12_repeat" python3 tests/run_com_exec.py t12_repeat
  478. else
  479. echo " FAIL: the compiler would not rebuild with the JNZ repeat"
  480. fail=$((fail + 1))
  481. fi
  482. fi
  483. cp "$SAVED_C" Compiler.mod
  484. # The fourth: sibling procedures shared one parameter namespace, because a
  485. # finished procedure's symbols were left at a level Search still accepts.
  486. # Removing HideLocals puts two procedures' `a : integer' back in collision.
  487. cp "$SAVED_C" Compiler.mod
  488. if mutate_no_hidelocals; then
  489. if rebuild_compiler; then
  490. expect_red "a duplicate parameter in two procedures is a compile error again" \
  491. "ERROR 41" python3 tests/run_com_exec.py t13_proc
  492. else
  493. echo " FAIL: the compiler would not rebuild without HideLocals"
  494. fail=$((fail + 1))
  495. fi
  496. else
  497. echo " FAIL: could not remove HideLocals to test the scoping fix"
  498. fail=$((fail + 1))
  499. fi
  500. cp "$SAVED_C" Compiler.mod
  501. if rebuild_compiler; then
  502. if python3 tests/run_com_exec.py >/dev/null 2>&1; then
  503. echo " ok: all 30 executed fixtures pass on the restored compiler"
  504. pass=$((pass + 1))
  505. else
  506. echo "NOT RESTORED: run_com_exec.py is red after restoring Compiler.mod"
  507. python3 tests/run_com_exec.py 2>&1 | grep -i fail | head -3 | sed 's/^/ /'
  508. fail=$((fail + 1))
  509. fi
  510. else
  511. echo "NOT RESTORED: the compiler would not rebuild"
  512. fail=$((fail + 1))
  513. fi
  514. echo
  515. echo "== the emitter-name audit of Compiler.mod (audit_helpers.py)"
  516. # These need no rebuild: the audit reads the SOURCE, not the built object, so
  517. # they are the cheapest cases here and they cover the module the audit used
  518. # not to look at at all. That is the point of the section: the audit reported
  519. # "every helper agrees with its name" for a module it had never examined, and
  520. # EmXchgAxCx was `93` (XCHG BX,AX) under a name that says XCHG AX,CX for the
  521. # whole life of the project. Two of these five are for faults that were real.
  522. SAVED_C2=/tmp/opencode/nonvacuity.Compiler.mod.2
  523. SAVED_R2=/tmp/opencode/nonvacuity.Runtime.mod.2
  524. cp Compiler.mod "$SAVED_C2" || exit 1
  525. cp Runtime.mod "$SAVED_R2" || exit 1
  526. restore_audit_sources () {
  527. cp "$SAVED_C2" Compiler.mod
  528. cp "$SAVED_R2" Runtime.mod
  529. }
  530. AUD="python3 tests/audit_helpers.py"
  531. # 1. THE fault. 91h is XCHG AX,CX; 93h is XCHG BX,AX. Both are one byte, so
  532. # the compile matrix never moved and the byte counts never moved.
  533. cp "$SAVED_C2" Compiler.mod
  534. mutate Compiler.mod 's|^ Ebyte (91H)$| Ebyte (93H)|'
  535. expect_red "audit catches XchgAxCx emitting XCHG BX,AX" \
  536. "exchanges Ax and Bx" $AUD
  537. restore_audit_sources
  538. # 2. the coverage check itself. A parameter list that find_helpers does not
  539. # accept is exactly how the real emitter was missed, and the inventory is
  540. # scanned separately on purpose so this can be caught. Without the
  541. # independent scan this case is silent, because both lists would come from
  542. # the same parser and agree that the helper does not exist.
  543. cp "$SAVED_C2" Compiler.mod
  544. mutate Compiler.mod 's|^PROCEDURE EmXchgAxCx () ;$|PROCEDURE EmXchgAxCx (why : CARDINAL) ;|'
  545. expect_red "audit reports an emitter it cannot reach, rather than skipping it" \
  546. "never examined it" $AUD
  547. restore_audit_sources
  548. # 3. EmXchgAxDx was named EmMoveAxDx, which said MOV where the bytes say XCHG.
  549. # 93h here is XCHG AX,BX - one letter away, the exact class of mistake the
  550. # name is supposed to make impossible.
  551. cp "$SAVED_C2" Compiler.mod
  552. mutate Compiler.mod 's|^ Ebyte (92H)$| Ebyte (93H)|'
  553. expect_red "audit catches XchgAxDx emitting XCHG BX,AX" \
  554. "XchgAxDx" $AUD
  555. restore_audit_sources
  556. # 4. CmpArgW0's [BP+2] written as the 386 SIB form, which decodes on a 8086 as
  557. # [SI+24h]. A real bug: the runtime was clearing the wrong memory.
  558. cp "$SAVED_R2" Runtime.mod
  559. mutate Runtime.mod 's| B (83H) ; B (7EH) ; B (2) ; B (0) ;| B (83H) ; B (7CH) ; B (24) ; B (0) ; B (0) ;|'
  560. expect_red "audit catches the [SI+24h] encoding of [BP+2]" \
  561. "memory base is 'si" $AUD
  562. restore_audit_sources
  563. # 5. MovAxSp is POP then PUSH, because MOV AX,[SP] does not exist on an 8086.
  564. # Dropping the POP leaves the stack one word short - a fault in the shape,
  565. # not in a byte value.
  566. cp "$SAVED_C2" Compiler.mod
  567. python3 - <<'PYEOF'
  568. p='Compiler.mod'; s=open(p).read()
  569. a=" Ebyte (58H) ; (* POP AX *)\n"
  570. assert s.count(a)==1, "EmMovAxSp POP line not found -- update this mutation"
  571. open(p,'w').write(s.replace(a, ""))
  572. PYEOF
  573. expect_red "audit catches MovAxSp with its POP missing" \
  574. "MovAxSp" $AUD
  575. restore_audit_sources
  576. # 6. The two-instruction shape: IDIV is CWD then IDIV, and dropping the CWD
  577. # leaves an un-sign-extended dividend in DX:AX. Both are still present as
  578. # a two-step spec, so a missing step has to be visible.
  579. cp "$SAVED_C2" Compiler.mod
  580. mutate Compiler.mod 's| Ebyte (99H) ; Ebyte (0F7H) ; Ebyte (0F9H)| Ebyte (0F7H) ; Ebyte (0F9H)|'
  581. expect_red "audit catches IDiv without the CWD that extends the dividend" \
  582. "IDivAxCx" $AUD
  583. restore_audit_sources
  584. if $AUD >/dev/null 2>&1; then
  585. echo " ok: the audit passes on both restored sources"
  586. pass=$((pass + 1))
  587. else
  588. echo "NOT RESTORED: the audit is red after restoring the sources"
  589. $AUD 2>&1 | sed 's/^/ /'
  590. fail=$((fail + 1))
  591. fi
  592. echo
  593. echo "== the BP contract rt_exec.py checks before it starts a machine"
  594. # wrchar and wrbool both borrowed BP to reach their argument -- [SP] is not
  595. # encodable in 16-bit mode -- and neither saved it. The driver's cursor into
  596. # the case record lives in BP precisely because BP is the one register an entry
  597. # may keep, so "wrchar borrowed it and did not give it back" sent the second
  598. # call to a garbage address, the machine triple-faulted, and the run printed the
  599. # record header twice and hung. Both the golden and the audit call that shape
  600. # CORRECT: the bytes are well formed, every branch is on a boundary, the size
  601. # is unchanged, and the decode says exactly what it says. So the rule is now
  602. # checked directly, and these two cases are what make that check more than a
  603. # claim.
  604. #
  605. # rt_exec.py needs the whole runtime rebuilt and then boots 36 machines, so this
  606. # section is the slow one. The baseline comes first and is asserted: a case
  607. # that mutates a red tree proves nothing.
  608. SAVED_R3=/tmp/opencode/nonvacuity.Runtime.mod.3
  609. cp Runtime.mod "$SAVED_R3" || exit 1
  610. if rebuild; then
  611. if python3 tests/rt_exec.py >/dev/null 2>&1; then
  612. echo " ok: baseline - rt_exec.py passes on the unmutated runtime"
  613. pass=$((pass + 1))
  614. else
  615. echo " FAIL: the baseline is already red, so the cases below prove"
  616. echo " nothing - fix the baseline before reading them"
  617. python3 tests/rt_exec.py 2>&1 | tail -3 | sed 's/^/ /'
  618. fail=$((fail + 1))
  619. fi
  620. else
  621. echo " FAIL: could not rebuild the runtime for the baseline"
  622. fail=$((fail + 1))
  623. fi
  624. # 1. THE fault: drop the PUSH, exactly as EmitWrChar was written. This is the
  625. # shape the behavioural case found, so the byte-level check must find it too
  626. # -- a check that only the expensive test can trip is a check that has not
  627. # been made to earn its place.
  628. cp "$SAVED_R3" Runtime.mod
  629. python3 - <<'PYEOF'
  630. p = 'Runtime.mod'
  631. s = open(p).read()
  632. a = ' M ("wrchar") ;\n PushBp ; MovBpSp ;\n'
  633. assert s.count(a) == 1, "EmitWrChar prologue not found exactly once"
  634. open(p, 'w').write(s.replace(a, ' M ("wrchar") ;\n MovBpSp ;\n'))
  635. PYEOF
  636. if rebuild; then
  637. expect_red "the BP contract catches wrchar borrowing BP unsaved" \
  638. "borrows BP but does not open with" python3 tests/rt_exec.py
  639. else
  640. echo " FAIL: the runtime would not rebuild with the PUSH removed"
  641. fail=$((fail + 1))
  642. fi
  643. cp "$SAVED_R3" Runtime.mod
  644. # 2. The mirror image: push it and never pop it. A different one-instruction
  645. # omission with the same consequence for a caller, and the reason the rule
  646. # asks for the 5D and not just the 55.
  647. cp "$SAVED_R3" Runtime.mod
  648. mutate Runtime.mod 's|^ MovSpBp ; PopBp ;$| MovSpBp ;|'
  649. if rebuild; then
  650. expect_red "the BP contract catches a BP that is pushed and never popped" \
  651. "pushes BP but never pops it" python3 tests/rt_exec.py
  652. else
  653. echo " FAIL: the runtime would not rebuild with the POP removed"
  654. fail=$((fail + 1))
  655. fi
  656. cp "$SAVED_R3" Runtime.mod
  657. # 3. The scan's other silent failure: a rule that matches nothing looks exactly
  658. # like a rule that passes. The pattern the check looks for is changed to one
  659. # the blob does not contain -- MOV BP,DI, which the runtime has no reason to
  660. # emit -- so the check has examined thirteen entries and matched nothing and
  661. # MUST say so rather than report a clean sweep. This is the general shape of
  662. # the fault this project keeps making: a check whose SUBJECT has drifted
  663. # reports a confident answer about the wrong thing.
  664. cp "$SAVED_R3" Runtime.mod
  665. SAVED_X=/tmp/opencode/nonvacuity.rt_exec.py
  666. cp tests/rt_exec.py "$SAVED_X" || exit 1
  667. mutate tests/rt_exec.py 's|^MOV_BP_SP = b"\\x8b\\xec" .*$|MOV_BP_SP = b"\\x8b\\xed" # MOV BP,DI: never emitted|' \
  668. expect_red "a check that matched nothing is a failure, not a pass" \
  669. "matched nothing" python3 tests/rt_exec.py
  670. cp "$SAVED_X" tests/rt_exec.py
  671. if rebuild; then
  672. if python3 tests/rt_exec.py >/dev/null 2>&1; then
  673. echo " ok: rt_exec.py passes on the restored source"
  674. pass=$((pass + 1))
  675. else
  676. echo "NOT RESTORED: rt_exec.py is red after restoring Runtime.mod"
  677. python3 tests/rt_exec.py 2>&1 | tail -3 | sed 's/^/ /'
  678. fail=$((fail + 1))
  679. fi
  680. else
  681. echo "NOT RESTORED: the runtime would not rebuild"
  682. fail=$((fail + 1))
  683. fi
  684. # 4. The GOLDEN and the entry goldens, which are the other half of the same
  685. # rule. rt_exec.py states the contract; check_runtime.py pins the bytes.
  686. # They are separate mechanisms and the case below is what shows the golden
  687. # one works on its own -- a re-baseline of runtime.golden would otherwise
  688. # have absorbed the new prologues silently, and the next person to bless it
  689. # would have no way to know the PUSH and POP were ever missing.
  690. cp "$SAVED_R3" Runtime.mod
  691. python3 - <<'PYEOF'
  692. p = 'Runtime.mod'
  693. s = open(p).read()
  694. a = ' M ("wrchar") ;\n PushBp ; MovBpSp ;\n MovAlArg4 ;\n MovSpBp ; PopBp ;\n'
  695. assert s.count(a) == 1, "EmitWrChar frame not found exactly once"
  696. open(p, 'w').write(s.replace(a, ' M ("wrchar") ;\n MovBpSp ;\n MovAlArg2 ;\n MovSpBp ;\n'))
  697. PYEOF
  698. if rebuild; then
  699. expect_red "the entry golden catches wrchar without its PUSH BP" \
  700. "entry wrchar starts 8B EC" python3 tests/check_runtime.py "$DUMP"
  701. else
  702. echo " FAIL: the runtime would not rebuild with wrchar's frame removed"
  703. fail=$((fail + 1))
  704. fi
  705. cp "$SAVED_R3" Runtime.mod
  706. echo
  707. echo "== the .COM layout check, and the runtime size it now measures"
  708. # The checker used to RESTATE the runtime's size as a literal. It was wrong
  709. # by 41 bytes for an unknown time, and every one of the 30 .COM files "failed"
  710. # on a header read out of the code stream. A duplicated constant that has
  711. # drifted does not fail loudly; it re-reports the same falsehood, in which the
  712. # real failures hide. The size is now MEASURED from the image.
  713. #
  714. # These cases corrupt a real emitted .COM and require the checker to notice.
  715. # They need the images, so they are built once and copied; the checker has a
  716. # --check-only mode for exactly this, because its scratch directory is normally
  717. # deleted on exit and a check that has only ever seen the truth is not a check.
  718. KEEPDIR=/tmp/opencode/nonvacuity.com
  719. rm -rf "$KEEPDIR"
  720. TP_COM_KEEP=1 tests/run_com_tests.sh >/tmp/opencode/nonvacuity.com.log 2>&1
  721. KEEP=$(sed -n 's/^TP_COM_KEEP=1: images left in //p' \
  722. /tmp/opencode/nonvacuity.com.log | tail -1)
  723. if [ -z "$KEEP" ] || [ ! -d "$KEEP" ]; then
  724. echo " FAIL: could not obtain emitted .COM images for the layout cases"
  725. fail=$((fail + 1))
  726. else
  727. COMCHK="tests/run_com_tests.sh --check-only"
  728. # 0. The baseline. Every case below is a claim that a specific assertion
  729. # turns red, and none of them means anything if the copies of untouched
  730. # images already fail. (The stale RT_SZ produced exactly that: 30
  731. # failures that were not findings.) So this is asserted first, and a
  732. # failure here is reported as a broken baseline rather than a red test.
  733. #
  734. # The baseline is also WHERE THE HEADER OFFSET COMES FROM. Cases 1 and 2
  735. # used to carry it as the literal 435 and 439, which were ENT_SZ + the
  736. # runtime size at the time -- 432. Two 4-byte changes to the runtime
  737. # later, both cases were editing the wrong bytes: 435 had become four
  738. # bytes inside the runtime itself, so case 1 had stopped testing "the
  739. # header cannot be found" and had started testing "the checker also
  740. # notices you scribbled on the code", and case 2 had become a no-op
  741. # that wrote the header where it already was. Both still went red, for
  742. # reasons the messages did not name, which is the whole problem: a
  743. # hard-coded offset is a claim about the world that expires silently.
  744. #
  745. # So the offset is read back out of the checker's own report on the
  746. # untouched images, and the expected numbers below are ARITHMETIC ON IT.
  747. # If the runtime grows again, these cases still test what they say.
  748. rm -rf "$KEEPDIR"; mkdir -p "$KEEPDIR"
  749. cp "$KEEP"/*.COM "$KEEP"/raw.txt "$KEEPDIR"/
  750. BASE_OUT=$($COMCHK "$KEEPDIR" 2>&1)
  751. if [ $? -eq 0 ]; then
  752. echo " ok: baseline - untouched copies of the real images all pass"
  753. pass=$((pass + 1))
  754. else
  755. echo " FAIL: the baseline is already red, so the cases below prove"
  756. echo " nothing - fix the baseline before reading them"
  757. printf '%s\n' "$BASE_OUT" | grep FAIL | head -3 | sed 's/^/ /'
  758. fail=$((fail + 1))
  759. fi
  760. BASE_RT=$(printf '%s\n' "$BASE_OUT" \
  761. | sed -n 's/.*measured runtime size: \([0-9][0-9]*\) bytes.*/\1/p' \
  762. | head -1)
  763. if [ -z "$BASE_RT" ]; then
  764. echo " FAIL: the checker did not report the runtime size it measured,"
  765. echo " so the cases below cannot find the header to edit"
  766. fail=$((fail + 1))
  767. BASE_RT=0
  768. fi
  769. # The layout constants are ENT_SZ 3 and HDR_SZ 16 (run_com_tests.sh), so the
  770. # header sits at hdrOff = 3 + rtSz and the entry jump's displacement must be
  771. # (hdrOff + 16) - 3. The wanted number is therefore computed from the header
  772. # OFFSET, not from rtSz: the two differ by 3, and getting that backwards
  773. # produces a plausible-looking expectation three bytes out, which is how
  774. # this case came to expect "want 452" and then be reported as not proving
  775. # what it said.
  776. BASE_OFF=$((3 + BASE_RT))
  777. # Case 2 claims a runtime four bytes LONGER, so the header - and with it the
  778. # demanded jump target - moves four bytes further on.
  779. SHIFTED_WANT=$((BASE_OFF + 4 + 16 - 3))
  780. echo " (measured runtime size $BASE_RT, header at image offset $BASE_OFF)"
  781. # 1. Break hdrDS so it no longer ties the header to its own offset. The
  782. # header must become UNFINDABLE and be reported as such - a checker that
  783. # fell back to a remembered offset would report a confident number here,
  784. # which is the failure mode the measurement was introduced to remove.
  785. rm -rf "$KEEPDIR"; mkdir -p "$KEEPDIR"
  786. cp "$KEEP"/*.COM "$KEEP"/raw.txt "$KEEPDIR"/
  787. python3 - "$KEEPDIR/t01_minimal.COM" "$BASE_OFF" <<'PYEOF'
  788. import sys
  789. p, off = sys.argv[1], int(sys.argv[2])
  790. d = bytearray(open(p, 'rb').read())
  791. d[off + 4:off + 6] = (0x1234).to_bytes(2, 'little') # hdrDS, no longer self-consistent
  792. open(p, 'wb').write(bytes(d))
  793. PYEOF
  794. expect_red "a header that cannot be located is reported, not assumed" \
  795. "no program header found" $COMCHK "$KEEPDIR"
  796. # 2. A complete, self-consistent header four bytes later, so the measured
  797. # runtime size becomes $((BASE_RT + 4)) instead of $BASE_RT. This is the
  798. # positive half of the same check: the derivation must FOLLOW the file,
  799. # and the entry jump assertion - expressed in terms of the measurement -
  800. # must follow it too, demanding $SHIFTED_WANT rather than $((BASE_OFF + 16 - 3)).
  801. #
  802. # hdrCS is copied from the header already in the file rather than written
  803. # as a literal. It used to be the literal 464+100h, which was the image
  804. # length when the image was 720 bytes; four bytes of runtime later it was
  805. # 464+100h against a 724-byte image, so this case was ALSO failing on
  806. # hdrCS, for a reason three lines below the one it was written to test.
  807. rm -rf "$KEEPDIR"; mkdir -p "$KEEPDIR"
  808. cp "$KEEP"/*.COM "$KEEP"/raw.txt "$KEEPDIR"/
  809. python3 - "$KEEPDIR/t01_minimal.COM" "$BASE_OFF" "$SHIFTED_WANT" <<'PYEOF'
  810. import sys
  811. p, off, want = sys.argv[1], int(sys.argv[2]), int(sys.argv[3])
  812. d = bytearray(open(p, 'rb').read())
  813. hdrCS = int.from_bytes(d[off + 2:off + 4], 'little') # unchanged: still the image end
  814. off += 4
  815. ds = off + 0x1000 + 0x100
  816. w = [1, hdrCS, ds, ds + 4, 0, 0, 0, 0]
  817. for i, x in enumerate(w):
  818. d[off + 2 * i:off + 2 * i + 2] = x.to_bytes(2, 'little')
  819. open(p, 'wb').write(bytes(d))
  820. PYEOF
  821. expect_red "the measured runtime size follows the image ($BASE_RT -> $((BASE_RT + 4)))" \
  822. "want $SHIFTED_WANT" $COMCHK "$KEEPDIR"
  823. # 3. The entry jump's opcode. One byte, and the only assertion in the
  824. # project that can see where execution STARTS.
  825. rm -rf "$KEEPDIR"; mkdir -p "$KEEPDIR"
  826. cp "$KEEP"/*.COM "$KEEP"/raw.txt "$KEEPDIR"/
  827. python3 - "$KEEPDIR/t01_minimal.COM" <<'PYEOF'
  828. import sys
  829. p = sys.argv[1]
  830. d = bytearray(open(p, 'rb').read())
  831. d[0] = 0xEA
  832. open(p, 'wb').write(bytes(d))
  833. PYEOF
  834. expect_red "the entry jump must be E9, not a near JMP" \
  835. "not the E9 of the entry jump" $COMCHK "$KEEPDIR"
  836. # 4. The entry jump's target, moved one instruction earlier. A .COM that
  837. # lands in the middle of the prologue runs, prints something and exits
  838. # cleanly, so no size or structure check can see this.
  839. rm -rf "$KEEPDIR"; mkdir -p "$KEEPDIR"
  840. cp "$KEEP"/*.COM "$KEEP"/raw.txt "$KEEPDIR"/
  841. python3 - "$KEEPDIR/t01_minimal.COM" <<'PYEOF'
  842. import sys
  843. p = sys.argv[1]
  844. d = bytearray(open(p, 'rb').read())
  845. d[1:3] = (100).to_bytes(2, 'little')
  846. open(p, 'wb').write(bytes(d))
  847. PYEOF
  848. expect_red "the entry jump must land on the first instruction" \
  849. "entry jump rel16=100" $COMCHK "$KEEPDIR"
  850. rm -rf "$KEEPDIR"
  851. fi
  852. echo
  853. echo "non-vacuity: $pass ok, $fail failed"
  854. [ "$fail" -eq 0 ]