docs: run the r5.01 C toolchain end to end under 2ine

The null-selector fix in the 2ine fork unblocks gspac/gspopt/gspcg, so
document the full pipeline: build 2ine, extract the tools, the four C
stages, absolute placement, and the firmware round-trip.

Record the two gotchas found while testing: gspcl still stops after
gspac because the children exit 139 at teardown, and GSPA 5.01 rejects
b15, so the 32 KB round-trip differs at one SP access (same physical
register, A15/B15).
This commit is contained in:
arti committed 2026-11-01 00:18:06 +02:00
1 parent b75048fb87
commit 9deada6cef
3 files changed
+258 -4

No files matched your search

+2 -1
View File
@@ -572,6 +572,7 @@ also why a server-side change cannot cause or cure it.
| `reference/third-party-code.md` | what open-source code is in the ROM |
| `reference/tms34010-sdk.md` | the 1987 toolchain, buildable today |
| `tools/tms34010-sdk/OS2-501.md` | using the 1990 r5.01 toolchain (ANSI C + optimizer) |
| `tools/tms34010-sdk/2INE-501.md` | the r5.01 C toolchain end to end under 2ine, no VM |
| `EXPLOIT_NOTES.md` | the original code-execution audit and the crash decode |
| `NEXT_SESSION.md` | the LAT soak handoff (older; see the reports for later work) |
@@ -591,7 +592,7 @@ vt1200/
│ ├── ptr_constants.py # find/resolve bit-address constants
│ ├── smart34010/ # dis34 (two encoding fixes)
│ ├── as34/ # TMS34010 binutils, ported to 64-bit Linux
│ ├── tms34010-sdk/ # TI SDKs: 1987 DOS + 1990 OS/2 (see OS2-501.md)
│ ├── tms34010-sdk/ # TI SDKs: 1987 DOS + 1990 OS/2 (see OS2-501.md, 2INE-501.md)
│ └── ghidra-tms34010/ # patched processor module + install script
├── harness/
│ ├── mame/ # the MAME driver, Lua probes, reports
+252
View File
@@ -0,0 +1,252 @@
# Running the TMS340 r5.01 toolchain under 2ine
This is the working **no-VM** route to TI's 1990 **TMS340 Family Code
Generation Tools r5.01**. The tools are 16-bit OS/2 1.x `NE` console programs;
the patched fork of [2ine](https://github.com/icculus/2ine) at
<https://git.wut.ee/opencode.energia.arti.ee/2ine-tms34010> runs them on a
64-bit Linux host.
Unlike the assembler-only state documented when the fork was first published,
the **full C pipeline works**: front end, optimizer, code generator,
assembler, linker and converter. `gspcl` (the one-command driver) still does
not chain its children reliably; drive the four C stages explicitly.
Verified 2026-11-01 on x86_64 Arch, against the VT1200 firmware image.
| tool | result |
|---|---|
| `gspac` (C front end) | **works** |
| `gspopt` (intermediate optimizer) | **works** |
| `gspcg` (code generator) | **works** |
| `gspa` (COFF assembler) | **works** — 11,852-line firmware disassembly, no errors |
| `gsplnk` (linker) | **works** — absolute placement; `rts.lib` + `flib.lib` |
| `gsprom` (COFF → Tek-hex) | **works** — plain and `-i` EPROM pair |
| `gspar` (archiver) | **works** — lists `rts.lib` members |
| `gspcl` (driver) | **partial** — runs `gspac`, then stops (see §7) |
---
## 1. Build 2ine
The fork's `main` carries the three fixes this needs: the null-selector
mapping (commit `91b78f2`), Arch/CMake-4 ncurses discovery (`a440128`), and
this document's status update (`3bcdfc3`).
Dependencies (Arch):
```sh
sudo pacman -S --needed base-devel cmake lib32-glibc lib32-gcc-libs \
lib32-ncurses sdl2-compat
```
Debian/Ubuntu names:
```sh
sudo apt install build-essential cmake libncurses-dev libncurses-dev:i386 \
libc6-dev-i386 gcc-multilib
```
Build 32-bit. On CMake 4.x the policy floor must be passed explicitly:
```sh
git clone https://git.wut.ee/opencode.energia.arti.ee/2ine-tms34010
cd 2ine-tms34010
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_POLICY_VERSION_MINIMUM=3.5
cmake --build build -j8
```
Result: `build/lx_loader` (32-bit), `build/lx_dump`, and `build/lib*.so`.
## 2. Extract the r5.01 tools
Disk 1 is the executables, disk 2 the headers, runtimes and library sources.
Extract them anywhere:
```sh
cd /path/to/vt1200
SDK="tools/tms34010-sdk/1990-11 TMS340 Family SDK"
mkdir -p os2-501/bin os2-501/lib os2-501/work
unzip -o "$SDK/1990-11-19 TMS340 Family Code Generation Tools (1 of 2) r5.01 rev2564062-1641.zip" -d os2-501/bin
unzip -o "$SDK/1990-11-19 TMS340 Family Code Generation Tools (2 of 2) r5.01 rev2564062-1641.zip" -d os2-501/lib
```
## 3. Prepare the run directory
`lx_loader` `dlopen()`s the native modules as `./lib<module>.so`, so the
working directory matters. The OS/2 tools also cannot open Unix absolute
paths, so keep sources, objects and libraries in the same directory and pass
**bare relative names**.
```sh
cd os2-501/work
ln -sf /path/to/2ine-tms34010/build/lib*.so .
cp ../bin/*.exe .
cp ../lib/rts.lib ../lib/flib.lib .
```
The tools consume their intermediate `.if` and `.opt` files on success — even
with `-k` — so regenerate them for every build.
## 4. End-to-end C build
Given `HELLO.C`:
```c
int twice(int n) { return n + n; }
int sum(int *p, int n) {
int s = 0;
int i;
for (i = 0; i < n; i++) s += p[i];
return s + twice(n);
}
```
Run each stage. `LX` is the absolute path to `lx_loader`:
```sh
LX=/path/to/2ine-tms34010/build/lx_loader
$LX ./gspac.exe -k HELLO.C HELLO.IF # C front end -> intermediate
$LX ./gspopt.exe -k -O2 HELLO.IF HELLO.OPT # optimizer -> intermediate
$LX ./gspcg.exe -m HELLO.OPT HELLO.ASM HELLO.TMP # codegen -> .asm
$LX ./gspa.exe HELLO.ASM # assembler -> HELLO.obj
$LX ./gsplnk.exe HELLO.obj rts.lib flib.lib \
-o hello.out -m hello.map # linker -> absolute COFF
$LX ./gsprom.exe hello.out hello.hex # converter -> Tek-hex
```
Notes:
* `gspa` writes the object as **lowercase** `HELLO.obj`, even though the
source is `HELLO.ASM`. Keep the names consistent on a case-sensitive FS.
* `-O2` means level 2. `-O0` is the lowest level, not "off"; with no `-o`/`-O`
stage at all the optimizer is not run.
* Link `flib.lib` as well as `rts.lib` when the code pulls in `_sqrt`,
`_frexp`, `_ldexp` and friends: the float primitives (`FD$ADD`, `FD$DIV`,
…) live in `flib.lib`.
### Placing the image at a TMS34010 bit address
`gsplnk` accepts a command file whose `SECTIONS` origin is a **bit address**.
COFF section sizes are in bits too (16 bits per instruction word): a `.text`
of `0x8000` is 4096 bytes, not 32 KB.
**Write the command file with CRLF**, even on Linux: with LF-only line ends the
linker merges the `-m` line into the next one and silently loses options.
```
-a
-o hello.out
-m hello.map
HELLO.obj
SECTIONS
{
.text 3F208000h: {}
}
```
```sh
$LX ./gsplnk.exe hello.cmd
$LX ./gsprom.exe -i hello.out hi lo # high/low EPROM pair
```
`0x3F208000` is the VT1200's firmware base. The converter's own hex record
addresses wrap at 64 KB; take placement from the COFF file or the map, never
from the hex.
## 5. Assembler / linker / converter on a firmware region
The 1987 round-trip flow in `harness/sdk-scratch/roundtrip.sh` works with the
5.01 tools, with one source-level change (`b15` → `sp`, §6):
```sh
# disassemble 32 KB at bit 3F208000 to RT.ASM, as roundtrip.sh does
# then, in the 2ine run directory:
$LX ./gspa.exe RT.ASM
$LX ./gsplnk.exe RT.CMD
$LX ./gsprom.exe -i rt.out rthi rtlo
# interleave low/high and compare with the original region
```
Result on `vt1200_v22.bin`, region `0x1000` / 32,768 bytes / base `3F208000`:
* **1987 GSPA:** 0 differing bytes,
`md5 43adb6669571041985f3e26fd9ff7cb9` (BYTE-EXACT)
* **5.01 GSPA:** 32,768/32,768 bytes, **1 differing byte** at `0x33ec`
(`3f` vs `2f`)
The difference is the A/B register-file namespace bit of the single
`rl 09h, sp` in the region, not a semantic difference (§6).
## 6. Why `b15` becomes `sp`, and why one byte differs
`dis2as34.py` emits `b15` for the B-file stack pointer because **GSPA 2.00**
accepts `b15` and uses a bare `sp` for the A-file form. **GSPA 5.01 rejects
`b15`** with `INVALID REGISTER VALUE`; its `sp` encodes the A15 namespace.
The TMS34010 has one stack-pointer register, addressable as **A15 or B15**, so
`rl 09h, sp` (5.01, `2f31`) and `rl 09h, b15` (2.00, `3f31`) address the same
register. The 5.01 object is therefore functionally identical to the ROM, but
not byte-identical at that instruction. This is the one exception to the
otherwise one-way assembler compatibility noted in `OS2-501.md`.
## 7. `gspcl` and the exit-status trap
`gspcl` finds `gspac` and runs it, then stops without invoking `gspopt` /
`gspcg` / `gspa`. The individual tools frequently exit non-zero after
successful work: `gspac` and `gspcg` return **139** (segfault during teardown),
`gsprom` can return 1. `gspcl` treats the child status as failure.
**Check for output files, not exit codes.** The manual pipeline in §4 produces
correct objects and images; the one-command driver does not yet.
## 8. What the null-selector fix was
The original fork asserted in `lxConvert1616to32` when the C front end passed
`0003:0000`. That is the OS/2 **null far pointer**: selector index 0 with RPL
3. The converter special-cased only `addr1616 == 0`, so it indexed `ldt[0]`
and asserted.
The fix in `lx_loader.c` is:
```c
const uint16 selector = (uint16) (addr1616 >> 19);
const uint16 offset = (uint16) (addr1616 % 0x10000);
if (selector == 0) // segment 0x0000 or 0x0003: the OS/2 null selector.
return NULL;
```
Segment index 0 is the null selector by definition, so returning `NULL` is
correct, not a workaround.
## 9. Measured results
Bench source with a sum loop, indexed loop, `switch`, struct update, helper
call and bit loop (`BENCH.C`), compiled four ways:
| build | `.text` | `.obj` | `.asm` |
|---|---:|---:|---:|
| no optimizer | 490 B | 950 B | 4396 B |
| `-O0` | 394 B | 854 B | 4497 B |
| `-O1` | 394 B | 854 B | 4499 B |
| **`-O2`** | **376 B** | **836 B** | **4236 B** |
`-O2` is ~23 % smaller than no optimizer on this source. Assembly of the
11,852-line firmware disassembly takes ~0.22 s wall through `lx_loader`
(loader startup included).
## 10. Reproduce
A compact end-to-end run (from `os2-501/work`, with the libs symlinked and the
tools copied as in §3):
```sh
LX=/path/to/2ine-tms34010/build/lx_loader
$LX ./gspac.exe -k HELLO.C HELLO.IF
$LX ./gspopt.exe -k -O2 HELLO.IF HELLO.OPT
$LX ./gspcg.exe -m HELLO.OPT HELLO.ASM HELLO.TMP
$LX ./gspa.exe HELLO.ASM
$LX ./gsplnk.exe HELLO.obj rts.lib flib.lib -o hello.out -m hello.map
$LX ./gsprom.exe hello.out hello.hex
```
+4 -3
View File
@@ -142,9 +142,10 @@ cd 2ine/build # lx_loader dlopen()s ./lib<module>.so, so cwd matters
./lx_dump gspa.exe # NE header/segments/imports
```
`gspcl` (the C compiler) does **not** work yet there: the `gspac` child hits
`lxConvert1616to32` with a selector 2ine has not mapped. Use 86Box when you need
to compile C.
The C pipeline **does** work under the patched fork: a null-selector fix in
`lx_loader.c` unblocks `gspac`, `gspopt` and `gspcg`. `gspcl` itself still
stops after `gspac` because the children exit non-zero at teardown, so drive
the stages explicitly. See `2INE-501.md` for the full end-to-end flow.
---