diff --git a/tools/as34/.gitignore b/tools/as34/.gitignore new file mode 100644 index 0000000..567609b --- /dev/null +++ b/tools/as34/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/tools/as34/README.md b/tools/as34/README.md new file mode 100644 index 0000000..a4248ed --- /dev/null +++ b/tools/as34/README.md @@ -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. diff --git a/tools/as34/as34-port.patch b/tools/as34/as34-port.patch new file mode 100644 index 0000000..520c8ac --- /dev/null +++ b/tools/as34/as34-port.patch @@ -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 ++#include + #include "link.h" + #include "branches.h" + +@@ -46,7 +47,7 @@ + #define QS_INSQUOTE 1 + #define QS_INDQUOTE 2 + +-char *imalloc(), *mkstr(), *malloc(); ++#include + + 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 + #include ++#include + #include + #include + #include +@@ -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); + } + diff --git a/tools/as34/build.sh b/tools/as34/build.sh new file mode 100755 index 0000000..ed58d49 --- /dev/null +++ b/tools/as34/build.sh @@ -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" diff --git a/tools/as34/expected-payload.bin b/tools/as34/expected-payload.bin new file mode 100644 index 0000000..7d61291 Binary files /dev/null and b/tools/as34/expected-payload.bin differ diff --git a/tools/as34/extract.py b/tools/as34/extract.py new file mode 100755 index 0000000..62c91ef --- /dev/null +++ b/tools/as34/extract.py @@ -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("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 diff --git a/tools/as34/proto.h b/tools/as34/proto.h new file mode 100644 index 0000000..248a96f --- /dev/null +++ b/tools/as34/proto.h @@ -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 *, ...); diff --git a/tools/as34/test.sh b/tools/as34/test.sh new file mode 100755 index 0000000..7a32f87 --- /dev/null +++ b/tools/as34/test.sh @@ -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