tools: add as34/ld34 TMS34010 binutils, ported to 64-bit Linux

Andrew Morton's Unix-hosted assembler/linker for the TMS34010, fetched from the gsptools archive and patched to build on x86-64. The K&R sources silently truncate pointers there: implicit int parameter declarations and fixed-argument printf wrappers. The patch fixes those and the flex 2.6 yy_init mismatch.

hello.as34.g is harness/hello-world/hello.asm in as34 syntax. as34/ld34 reproduce the GSPA-built payload byte-for-byte (82 bytes, md5 db91788991ebba476b55a11127a36475), and the MAME hello.lua run reports the same markers, the same 1024-word VRAM block and a byte-identical frame.

build.sh downloads the upstream tarball at build time (sha256 pinned); the tree is not vendored.
This commit is contained in:
arti committed 2026-10-22 14:19:14 +03:00
1 parent f3d26ad687
commit 6338095683
9 files changed
+713

No files matched your search

+1
View File
@@ -0,0 +1 @@
build/
+104
View File
@@ -0,0 +1,104 @@
# as34 / ld34 — Morton's Unix-hosted TMS34010 binutils
`as34` (assembler) and `ld34` (linker) are Andrew Morton's 1990s TMS34010
tools, shipped with his GCC 2.5.x port. They are **not TI tools** and not
related to `GSPA`/`GSPLNK`; they run natively on Linux, with no DOSBox.
`build.sh` fetches the original `gsp.tar.gz` from the Wayback Machine,
applies `as34-port.patch`, and builds three programs:
| tool | what it does |
|---|---|
| `as34` | assembles `.g` source to a `.go` object (`LINKMAGIC 0x3abf782e`) |
| `ld34` | links `.go` objects to `g.out` (same linkhdr container), with a map |
| `dis34` | disassembles `g.out` (kept for inspection; the repo's `tools/smart34010/dis34` is a better independent decoder) |
The compiled `.text` is extracted from `g.out` by `extract.py`. `g.out` is
**not COFF** — `ld34/coff.c` is dead code that is neither compiled nor called.
The live writer is `ld34/header.c`, which emits the same packed `linkhdr`
format that `as34` produces.
## Build
```sh
tools/as34/build.sh
```
Everything is fetched and built under `tools/as34/build/` (git-ignored;
override with `AS34_BUILD_DIR`). The tools end up at:
```
tools/as34/build/src/gsp/binutils/as34/as34
tools/as34/build/src/gsp/binutils/ld34/ld34
tools/as34/build/src/gsp/binutils/ld34/dis34
```
The tarball is 2.1 MB and is an **uncompressed tar with a `.tar.gz` name** —
that is how the archive is stored. Its SHA-256 is pinned in `build.sh`.
## Test (byte comparison, then MAME)
```sh
tools/as34/test.sh
REPO=$PWD MAME=/home/arti/code/k-space/mame0289/vt1200 tools/as34/test.sh
```
`hello.as34.g` is `harness/hello-world/hello.asm` ported to `as34` syntax.
The result is **byte-identical** to the GSPA-built
`harness/hello-world/payload.bin` (82 bytes, md5
`db91788991ebba476b55a11127a36475`), and the MAME run reports the same
markers, the same 1024-word VRAM block and the same frame as the original
payload.
### Syntax differences from GSPA
| | GSPA | as34 |
|---|---|---|
| hex | `0DEADh` | `>DEAD` (also `0xdead`) |
| immediate width | `movi x, a0, L` | no `L`/`W` suffix; `movi` is always the 32-bit form |
| 16-bit immediate add | `addi` | `addiw` — as34's `addi` is the **32-bit** immediate form |
| immediate order | same (`op src, dst`) | same |
Everything else — labels, `@absolute`, `*a2+`, `dsjs`, bit addresses, field
specifiers — is unchanged. `dsjs` displacement encoding matches GSPA (checked
against `tools/smart34010/dis34`).
## What the port patch changes
The code is K&R C written for 32-bit SunOS. On x86-64 the original build
produces truncated pointers, so the patch is not cosmetic:
* **Pointer parameters declared implicitly as `int`** (`Xwrite`, `openfiles`,
`emitslcode`/`emitnslcode`/`emitswcode`/`emitnswcode`). On a 32-bit host
the implicit `int` happened to be pointer-sized; on x86-64 it truncates.
* **K&R printf wrappers** (`amprintf`, `ameprintf`, `amfprintf`, `eprintf`)
declared `p1…p6` as `int`, truncating `%s` arguments. Rewritten as real
variadic functions with `va_list`.
* **`imalloc`** likewise made variadic, and declared in `proto.h` (included
with `-include` for the assembler's translation units).
* **flex 2.6**: `yyGetReady()` used to set `yy_init = 1`; with the modern
skeleton that leaves `yy_c_buf_p` NULL and segfaults. It is now a no-op —
`YY_INPUT`/`am_input` still supplies the line buffer.
* `abort(1)` → `abort()`; `hash()` uses unsigned arithmetic; two
`exit(eprintf(...))` calls split.
## Upstream quirks worth knowing
* **`-e` takes its argument attached**: `-e_start`. The space form
(`-e _start`) is broken in the original parser: it sets the entry name to
the empty string and treats `_start` as an object file.
* **The container format depends on the host `long` size.** `link.h` stores
addresses and sizes as `long`; this build writes 8-byte fields, so the
header is 156 bytes rather than the historical 80. `as34`, `ld34` and
`extract.py` all agree, but these files are not interchangeable with
32-bit-host `g.out` files. Making the format stable would mean changing
`long` to `int32_t` in `link.h`.
* **`ld34` runs `as34` from `PATH`** when generating C++ ctor/dtor tables.
`test.sh` passes `-c` to skip that.
* `as34` is **not** GSPA-compatible at the token level (see the table above).
## Licence
The upstream tarball contains GPLv2-era GNU binutils code plus Morton's own
sources. It is downloaded at build time, not vendored here; the patch is the
only derived file committed.
+353
View File
@@ -0,0 +1,353 @@
--- a/binutils/as34/as.l
+++ b/binutils/as34/as.l
@@ -268,7 +268,9 @@
yyGetReady()
{
-#ifdef BISON
- yy_init = 1;
-#endif
+ /* The original flex 2.5 code set yy_init = 1 here, because the old
+ * skeleton created its input buffer some other way. With flex 2.6,
+ * setting yy_init without a buffer leaves yy_c_buf_p NULL. Leave the
+ * flag alone: yylex initialises normally on first use, and YY_INPUT
+ * (am_input) supplies bytes from linebuf, not from yyin. */
}
--- a/binutils/as34/as.y
+++ b/binutils/as34/as.y
@@ -96,12 +96,12 @@
return 0;
}
-emitslcode(pexpr)
+emitslcode(expr *pexpr)
{
return doemitl(pexpr, 0);
}
-emitnslcode(pexpr)
+emitnslcode(expr *pexpr)
{
return doemitl(pexpr, 1);
}
@@ -120,11 +120,11 @@
emitsl(mode ? ~(pexpr->abspart) : pexpr->abspart);
}
-emitswcode(pexpr)
+emitswcode(expr *pexpr)
{
return doemitw(pexpr, 0);
}
-emitnswcode(pexpr)
+emitnswcode(expr *pexpr)
{
return doemitw(pexpr, 1);
}
--- a/binutils/as34/main.c
+++ b/binutils/as34/main.c
@@ -1,4 +1,5 @@
#include <stdio.h>
+#include <stdarg.h>
#include "link.h"
#include "branches.h"
@@ -46,7 +47,7 @@
#define QS_INSQUOTE 1
#define QS_INDQUOTE 2
-char *imalloc(), *mkstr(), *malloc();
+#include <stdlib.h>
main(argc, argv)
int argc;
@@ -284,7 +285,7 @@
for (inci = 0; inci < ninclpaths; inci++)
{
path = imalloc(strlen(inclpaths[inci]) +
- strlen(copyfile) + 2);
+ strlen(copyfile) + 2, "include path");
sprintf(path, "%s/%s", inclpaths[inci], copyfile);
if ((inputfiles[incldepth + 1] = fopen(path, "r"))
== NULL)
@@ -388,14 +389,15 @@
* Error handler
*/
-eprintf(str, p1, p2, p3, p4)
-char *str;
-int p1, p2, p3, p4;
+void eprintf(char *str, ...)
{
char buf[300];
+ va_list ap;
nerrors++;
- sprintf(buf, str, p1, p2, p3, p4);
+ va_start(ap, str);
+ vsnprintf(buf, sizeof buf, str, ap);
+ va_end(ap);
ameprintf("%s: %s:%d: %s\n", ourname, filenames[incldepth], linenos[incldepth], buf);
if (listing && pass2)
{
@@ -469,15 +471,18 @@
}
char *
-imalloc(nbytes, str, p1, p2)
-char *str;
+imalloc(int nbytes, char *str, ...)
{
char *ret;
+ va_list ap;
+
ret = (char *)malloc(nbytes);
if (ret == 0)
{
char buf[300];
- sprintf(buf, str, p1, p2);
+ va_start(ap, str);
+ vsnprintf(buf, sizeof buf, str, ap);
+ va_end(ap);
eprintf("Cannot allocate memory for %s\n", buf);
exit(-1);
}
@@ -503,53 +508,23 @@
VERSION >> 4, VERSION & 15);
}
-amprintf(str, p1, p2, p3, p4, p5, p6)
-char *str;
+void amprintf(char *str, ...)
{
-#ifdef A1616
- char buf[1000];
- register char *cp;
+ va_list ap;
- sprintf(buf, str, p1, p2, p3, p4, p5, p6);
- cp = buf;
- while (*cp)
- {
- if (*cp != '\r')
- {
- if (*cp == '\n')
- putchar('\r');
- putchar(*cp);
- }
- cp++;
- }
-#else
- printf(str, p1, p2, p3, p4, p5, p6);
-#endif
+ va_start(ap, str);
+ vprintf(str, ap);
+ va_end(ap);
}
-ameprintf(str, p1, p2, p3, p4, p5, p6)
-char *str;
+void ameprintf(char *str, ...)
{
-#ifdef A1616
- char buf[1000];
- register char *cp;
+ va_list ap;
- sprintf(buf, str, p1, p2, p3, p4, p5, p6);
- cp = buf;
- while (*cp)
- {
- if (*cp != '\r')
- {
- if (*cp == '\n')
- putc('\r', stderr);
- putc(*cp, stderr);
- }
- cp++;
- }
-#else
- fprintf(stderr, str, p1, p2, p3, p4, p5, p6);
-#endif
+ va_start(ap, str);
+ vfprintf(stderr, str, ap);
+ va_end(ap);
}
#ifdef A1616
--- a/binutils/as34/reloc.c
+++ b/binutils/as34/reloc.c
@@ -35,7 +35,10 @@
greloc areloc;
if (cursection > 1)
- exit(eprintf("Panic in emitreloc"));
+ {
+ eprintf("Panic in emitreloc");
+ exit(1);
+ }
if (pass2)
{
areloc.offset = offset;
@@ -62,7 +65,7 @@
} filesegs[3];
-openfiles(name)
+openfiles(char *name)
{
outfd = creat(name, 0666);
filesegs[0].filepos = LINKHDRSIZE;
@@ -315,7 +318,7 @@
close(outfd);
}
-Xwrite(fd, buf, nbytes)
+Xwrite(int fd, char *buf, int nbytes)
{
if (write(fd, buf, nbytes) != nbytes)
{
--- a/binutils/as34/support.c
+++ b/binutils/as34/support.c
@@ -278,5 +278,5 @@
char *file;
{
fprintf(stderr, "PANIC at %s:%d\n", file, line);
- abort(1);
+ abort();
}
--- a/binutils/as34/sym.c
+++ b/binutils/as34/sym.c
@@ -53,7 +53,10 @@
register int i;
if (inname == (char *)0)
- exit(eprintf("Panic1 in addsym"));
+ {
+ eprintf("Panic1 in addsym");
+ exit(1);
+ }
s = mksym();
s->n.name = inname;
--- a/binutils/ld34/gld.h
+++ b/binutils/ld34/gld.h
@@ -67,11 +67,11 @@
arfile *mkaddarfile(arfile **plist, char *name);
void readGoFile(gofile *pgofile, int readTextDataRelocStab);
void doexit(int code);
-void *imalloc();
+void *imalloc(size_t, char *, ...);
char *mkstr(char *s);
-void amprintf();
-void ameprintf();
-void amfprintf();
+void amprintf(char *, ...);
+void ameprintf(char *, ...);
+void amfprintf(FILE *, char *, ...);
void pass1(void);
void addtomastersymtab(gofile *pgofile);
void addonesym(symbol *psymbol, gofile *pgofile);
--- a/binutils/ld34/pass2.c
+++ b/binutils/ld34/pass2.c
@@ -356,9 +356,9 @@
static int
hash(char *name)
{
- register int res = 0;
+ register unsigned int res = 0;
while (*name)
- res = res * 33 + *name++;
+ res = res * 33 + (unsigned char)*name++;
return res & HASHSIZE;
}
--- a/binutils/ld34/source.c
+++ b/binutils/ld34/source.c
@@ -90,7 +90,7 @@
char *_name = name;
fixName(&_name);
- name = imalloc(strlen(sp->path) + strlen(_name) + 2);
+ name = imalloc(strlen(sp->path) + strlen(_name) + 2, "source path");
sprintf(name, "%s/%s", sp->path, _name);
fd = open(name, O_RDONLY);
free(name);
--- a/binutils/ld34/utils.c
+++ b/binutils/ld34/utils.c
@@ -1,5 +1,6 @@
#include <sys/types.h>
#include <stdio.h>
+#include <stdarg.h>
#include <link.h>
#include <fcntl.h>
#include <stdlib.h>
@@ -267,16 +268,18 @@
}
void *
-imalloc(nbytes, m, p1, p2, p3)
-char *m;
+imalloc(size_t nbytes, char *m, ...)
{
char buf[400];
char *p;
+ va_list ap;
p = (char *)malloc(nbytes);
if (p == 0)
{
- sprintf(buf, m, p1, p2, p3);
+ va_start(ap, m);
+ vsnprintf(buf, sizeof buf, m, ap);
+ va_end(ap);
ameprintf("Not enough memory to allocate %s\n", buf);
doexit(-1);
}
@@ -295,26 +298,34 @@
}
void
-amprintf(str, p1, p2, p3, p4, p5, p6)
-char *str;
+amprintf(char *str, ...)
{
- amfprintf(stdout, str, p1, p2, p3, p4, p5, p6);
+ va_list ap;
+
+ va_start(ap, str);
+ vfprintf(stdout, str, ap);
+ va_end(ap);
}
void
-ameprintf(str, p1, p2, p3, p4, p5, p6)
-char *str;
+ameprintf(char *str, ...)
{
- amfprintf(stderr, "%s: ", ourname);
- amfprintf(stderr, str, p1, p2, p3, p4, p5, p6);
+ va_list ap;
+
+ fprintf(stderr, "%s: ", ourname);
+ va_start(ap, str);
+ vfprintf(stderr, str, ap);
+ va_end(ap);
}
void
-amfprintf(stream, str, p1, p2, p3, p4, p5, p6)
-FILE *stream;
-char *str;
+amfprintf(FILE *stream, char *str, ...)
{
- fprintf(stream, str, p1, p2, p3, p4, p5, p6);
+ va_list ap;
+
+ va_start(ap, str);
+ vfprintf(stream, str, ap);
+ va_end(ap);
fflush(stream);
}
+61
View File
@@ -0,0 +1,61 @@
#!/bin/sh
# Build as34 (assembler), ld34 (linker) and dis34 (disassembler) from Andrew
# Morton's gsptools, patched to build on a 64-bit Linux host.
#
# Everything is fetched/built under tools/as34/build (override with
# AS34_BUILD_DIR) so the repository tree stays clean.
#
# The upstream tarball is an UNCOMPRESSED tar with a .tar.gz name; that is
# how the Wayback Machine archived it.
set -eu
here=$(cd "$(dirname "$0")" && pwd)
builddir=${AS34_BUILD_DIR:-"$here/build"}
tarball="$builddir/gsp.tar.gz"
srcdir="$builddir/src"
treesrc="$srcdir/gsp/binutils"
TARBALL_SHA256=efff8e59f7e3bf0285c4bd44d63e2357d6c26a85d939d4843d6be3471af9ef46
TARBALL_URL="https://web.archive.org/web/20070206220404if_/http://www.zip.com.au/~akpm/gsp.tar.gz"
mkdir -p "$builddir"
if [ ! -f "$tarball" ]; then
echo "fetching $tarball"
curl -fL --max-time 300 -o "$tarball" "$TARBALL_URL"
fi
echo "$TARBALL_SHA256 $tarball" | sha256sum -c -
if [ ! -d "$treesrc" ]; then
mkdir -p "$srcdir"
tar xf "$tarball" -C "$srcdir"
fi
if [ ! -f "$srcdir/.patched" ]; then
patch -p1 -d "$srcdir/gsp" < "$here/as34-port.patch"
touch "$srcdir/.patched"
fi
# -std=gnu17: C23 removed K&R definitions, which this 1998 code uses.
# -fcommon: the sources rely on tentative definitions being merged.
# as34 needs -DBISON to select the flex/bison input path in as.l.
CC_BASE="gcc -std=gnu17 -O2 -fcommon \
-Wno-implicit-function-declaration -Wno-int-conversion \
-Wno-incompatible-pointer-types -Wno-builtin-declaration-mismatch \
-Wno-implicit-int -Wno-return-mismatch -Wno-format -Wno-unused-result"
echo "building as34"
make -C "$treesrc/as34" clean >/dev/null 2>&1 || true
make -C "$treesrc/as34" \
CC="$CC_BASE -DBISON" \
XCFLAGS="-include $here/proto.h"
echo "building ld34 and dis34"
make -C "$treesrc/ld34" clean >/dev/null 2>&1 || true
make -C "$treesrc/ld34" ld34 CC="$CC_BASE"
make -C "$treesrc/ld34" dis34 CC="$CC_BASE"
echo
echo "built:"
ls -l "$treesrc/as34/as34" "$treesrc/ld34/ld34" "$treesrc/ld34/dis34"
Binary file not shown.
+74
View File
@@ -0,0 +1,74 @@
#!/usr/bin/env python3
"""Extract the loadable .text bytes from an as34/ld34 g.out file.
g.out is not COFF. It uses the .go/linkhdr format from binutils/link.h
(LINKMAGIC 0x3abf782e), packed with #pragma pack(2).
IMPORTANT: link.h declares the header fields that hold addresses and sizes
as `long`. On the original 32-bit Unix hosts that is 4 bytes; on x86-64 it
is 8, so the written header is 156 bytes here instead of the historical 80.
as34 and ld34 are both rebuilt with the same compiler, so they agree with
each other and with this parser. A stable on-disk format would need `long`
changed to int32_t in link.h.
Layout as written by this 64-bit build (offsets in bytes):
off field
0 short swapped
2 long magic
10 short flags
12 long time
20 long textstart file position of text
28 long textsize size in BYTES
36 long datastart
44 long datasize
52 long bsssize
60 long relocstart
68 long nrelocs
76 long symtabstart
84 long nsyms
92 long stabstart
100 long nstabs
108 long stringstart
116 long stringSize
124 long textLoadAddress BIT address
132 long dataLoadAddress
140 long bssLoadAddress
148 long entrypoint BIT address
"""
import hashlib
import struct
import sys
LINKMAGIC = 0x3ABF782E
def main(out_path, bin_path):
d = open(out_path, "rb").read()
fields = struct.unpack_from("<hqh" + "q" * 18, d, 0)
(swapped, magic, flags, _time, textstart, textsize,
_datastart, _datasize, _bsssize, _relocstart, _nrelocs,
_symtabstart, _nsyms, _stabstart, _nstabs,
_stringstart, _stringsize,
textload, _dataload, _bssload, entry) = fields
if magic != LINKMAGIC:
sys.exit("extract: bad magic %#010x" % magic)
if swapped:
sys.exit("extract: byte-swapped header not handled")
payload = d[textstart:textstart + textsize]
if len(payload) != textsize:
sys.exit("extract: truncated text (%d of %d bytes)"
% (len(payload), textsize))
open(bin_path, "wb").write(payload)
print("origin=%08X (bits) size=%d bytes entry=%08X file_offset=%#x md5=%s"
% (textload, textsize, entry, textstart,
hashlib.md5(payload).hexdigest()))
if __name__ == "__main__":
if len(sys.argv) != 3:
sys.exit("usage: extract.py hello.out payload.bin")
main(sys.argv[1], sys.argv[2])
+47
View File
@@ -0,0 +1,47 @@
; hello.as34.g -- hello.asm ported to Andrew Morton's as34 syntax.
;
; Differences from harness/hello-world/hello.asm (TI GSPA syntax):
; * hex constants use >DEAD, not 0DEADh
; * no L/W immediate-width suffix; as34's movi is always the 32-bit form,
; which is what the GSPA source asked for with ", L"
; * @absolute operands use the same @ syntax
; Everything else (labels, mnemonics, bit addresses, dsjs loops) is unchanged.
;
; Addresses in operands are BIT addresses, like every TMS34010 pointer.
.text
.globl _start
_start:
dint ; stay deterministic: no firmware ISR
setf 16, 0, 0 ; field 0 = 16 bits, zero-extended
setf 32, 0, 1 ; field 1 = 32 bits, zero-extended
; ---- channel 1: RAM markers -----------------------------------
movi >DEAD, a0
move a0, @>1810000, 0 ; MAGIC1 (bytes AD DE in a dump)
movi >BEEF, a0
move a0, @>1810010, 0 ; MAGIC2 (bytes EF BE)
; ---- channel 2: solid block in video RAM ----------------------
movi >4019040, a2 ; 04000000h + 100*1024 + 64
movi 64, a3 ; 64 rows
movi 16, a4 ; 16 words = 256 pixels per row
sub a1, a1 ; counter = 0
movi >FFFF, a0
row_loop:
move a4, a5
word_loop:
move a0, *a2+, 0 ; store word, postincrement by 16 bits
addiw 1, a1 ; count this store
dsjs a5, word_loop
addiw >300, a2 ; skip 768 bits to the next row
dsjs a3, row_loop
; The counter is 0400h only if all 64*16 stores executed.
move a1, @>1810020, 0 ; COUNT
spin:
nop
jruc spin
.end
+8
View File
@@ -0,0 +1,8 @@
/* Prototypes for functions the original K&R sources declared implicitly.
* On a 32-bit host an implicit int return was pointer-sized; on x86-64 it is
* not. Included with -include for every translation unit. */
char *imalloc(int, char *, ...);
char *mkstr(char *);
void amprintf(char *, ...);
void ameprintf(char *, ...);
void eprintf(char *, ...);
+65
View File
@@ -0,0 +1,65 @@
#!/bin/sh
# Assemble hello.as34.g with as34, link it with ld34, extract .text, then
# compare against the TI-GSPA-built payload and (optionally) run MAME.
#
# tools/as34/test.sh
# REPO=$PWD MAME=/path/to/vt1200 tools/as34/test.sh
#
# Without REPO/MAME only the byte comparison runs.
set -eu
here=$(cd "$(dirname "$0")" && pwd)
builddir=${AS34_BUILD_DIR:-"$here/build"}
as34="$builddir/src/gsp/binutils/as34/as34"
ld34="$builddir/src/gsp/binutils/ld34/ld34"
if [ ! -x "$as34" ] || [ ! -x "$ld34" ]; then
echo "tools not built; run tools/as34/build.sh first" >&2
exit 1
fi
work="$builddir/work"
mkdir -p "$work" "$work/evidence"
cp "$here/hello.as34.g" "$work/hello.as34.g"
cd "$work"
"$as34" hello.as34.g
"$ld34" -c -T 1800000 -e_start hello.as34.go -o hello.out -m hello.map
python3 "$here/extract.py" hello.out hello.bin
echo
if [ -n "${REPO:-}" ] && [ -f "$REPO/harness/hello-world/payload.bin" ]; then
expected="$REPO/harness/hello-world/payload.bin"
else
expected="$here/expected-payload.bin"
fi
if cmp hello.bin "$expected"; then
echo "PASS: hello.bin is byte-identical to $expected"
else
echo "FAIL: hello.bin differs from $expected" >&2
exit 1
fi
if [ -n "${MAME:-}" ] && [ -n "${REPO:-}" ]; then
echo
echo "running MAME acceptance test"
out=$(cd "$REPO" && HELLO_END=150 HELLO_PAYLOAD="$work/hello.bin" \
VT1200_DUMP_FRAME=145 VT1200_DUMP_RAM=145 \
VT1200_DUMP_DIR="$work/evidence" \
"$MAME" vt1200 -rompath harness/mame/roms -video none -sound none \
-nothrottle -debug -debugger none \
-autoboot_script harness/mame/hello.lua -autoboot_delay 0 2>&1)
echo "$out" | grep -E "MAGIC|COUNT|VRAM block|PC at|lit ="
if echo "$out" | grep -q "WRONG"; then
echo "FAIL: MAME reported WRONG" >&2
exit 1
fi
if ! echo "$out" | grep -q "1024 stores ok"; then
echo "FAIL: counter did not reach 0x400" >&2
exit 1
fi
if ! echo "$out" | grep -q "VRAM block 1024/1024 words are FFFF, 0 holes"; then
echo "FAIL: VRAM block not complete" >&2
exit 1
fi
echo "PASS: MAME markers and VRAM block"
fi