Handoff - ND TCP/IP (211185) firmware RE and D02 segment recovery¶
Date: 2026-08-04
Status of record for: the Ethernet II TCP/IP card firmware work and the damaged-D02 recovery.
Companion docs: TCPIP-211185-B05-MEDIA-RECOVERED-2026-07-30.md,
TCPIP-D02-SEGMENT-RECOVERY-2026-07-30.md, TCPIP-B05-FIRMWARE-RE-2026-07-30.md,
WRITING-A-TCPIP-STACK-ON-SINTRAN.md.
1. Where we got to¶
DONE - the media is found and proven¶
ND's 211185 TCP/IP product, revision B05 (1988), was recovered from the Tingo MFM disc dump
(\\Nas9t\data\NorskData\FloppyImages\Tingo\Tingo-HDD\raw\tingo_raw_debug.img, user TCP-IP).
This was the top item on the artifact hunt list.
All four BPUN banks pass the documented checksum rule ("arithmetic sum of all the words in the Data field, modulo 2^16, big endian"). 8 of 8 stored checksums match.
SCOPE LIMIT (do not overstate this): only the BPUN files carry a checksum. The .prog,
.brf, .mode and .symb files on the same disc have no checksum, so their integrity is
unproven. "The media is intact" is true of the BPUNs only.
DONE - the 512 KB Ghidra image¶
E:\Dev\Ronny\NDInsight\Installation\Communication\Ethernet\x\stripped\tcp-ser-all-banks-b05-68k.bin
sha256 6d2a76f061859612f0134fda7fead17d1472c646a5c5f5438ab7667667fd04a4, 524288 bytes.
Built as bpun[0x44 : 0x44+0x20000] per bank, concatenated bank 0 -> 1 -> 2 -> 3.
Load in Ghidra as 68000, big endian, base 0x00000000, length 0x80000.
Provenance and the record layout are in the README beside it.
Measured layout (this replaces the earlier bank-as-subsystem guess, which was WRONG - all the code is one contiguous span and the subsystems are interleaved):
0x00000-0x003FF 68000 vector table
0x00400-0x0465F low DRAM / BSS, plus a 2-byte stub run at 0x1F24+
0x04660-0x23F38 CODE - one contiguous span, all subsystems interleaved
0x24000 onward tables and strings (dispatch 0x24A86, port names 0x24A5C)
0x7C3A0 onward the embedded ND symbol table (463 records)
There is no CODE/DRAM overlap. An earlier warning to that effect was arithmetic I got wrong:
BUFFER_END (DRAM, 0x1A00) is simply below END_PIOCOS (CODE, 0x4660). One flat space.
DONE - the flow fix¶
PlancFixFlow.java ran headless against the image: 16 rounds to stable, 653 fallthroughs
corrected, 464 epilogues marked RETURN, 589 sites disassembled. The 9 sites it declined are
inline ON ROUTINEERROR handlers - that refusal is correct, leave it. The 3 "won't disassemble"
sites are code, not data.
The script now serves GUI and headless from one file (argument apply in headless, dialog in GUI),
so the old duplicate *Headless.java copies are obsolete.
DONE - findings so far¶
- ~~
set DIX mode= AIP request-block type 12, handler at 0x7096~~ CORRECTED 2026-08-08: opcode 12 @0x7096 is DEFINE/ADD MULTICAST ADDRESS, not "set DIX mode". The 6-byte argument atRB+0x08is a full MAC address; the "bit 0 of byte 0" that is tested is the Ethernet I/G (group) bit (a non-multicast address is rejected -18), and the other 5 bytes ARE used. TheSTOPMA/STARTMApair reprograms the LANCE multicast filter, which is what made the original reader guess "DIX mode". Sibling opcode 14 @0x7162 = REMOVE MULTICAST. The real DIX-vs-802.3 gate is the 0x1888A mode word, a separate mechanism. SeeSINTRAN\XMSG\DOC\NDIX-XMSG-VS-SINTRAN-ETHII-CROSSCHECK-FINDINGS-2026-08-08.mdsection 4b. - The host seam is
*TCP- a 4-character PIOC-OS port name at0x7B90A, copied byTCPNETINITintoTCPPORTNAM. - ND shipped TCP on the ND-100, IP on the card, DIX 2.0 only (quoted from ND-860284-1 sec 1.3).
- PIOCOS - the card's RTOS - is byte-identical between the COSMOS/ENCOS load and the TCP/IP load.
NOT DONE¶
- ~20 functions are pinned with one-byte bodies (the stale-body trap - see phase 1).
- The ND-100 side of the
*TCPseam is undetermined: does the ND-100 register the port or only look it up?PO100PORTS/POMS100POOare the next targets. - ~~Five of the six
set DIX modeargument bytes are undecoded.~~ RESOLVED 2026-08-08: opcode 12 is define-multicast; all 6 argument bytes are the MAC address (see the corrected finding above). - AIP request blocks attach / start / stop are only partly decoded (ops 22/24/26).
2. D02 (1992) - the damaged copy¶
Found on D:\ND\HDD\extract-ronny.img. Damaged.
Bank 3 RECOVERED and PROVEN. Pulled from SEGFIL0 and verified against the damaged file's own
stored checksum 0x99ee. 3008 non-zero bytes against 2082 in the damaged original.
Banks 0/1/2 not recovered. Bank 0 was located at page 5015 but the checksum did not match. Banks 1 and 2 have their checksum words zeroed.
The method I used was the fallback, not the method¶
I located the segment copies by content-scanning SEGFIL0. The sintran-carving skill
explicitly forbids this: "Never locate code by scanning for a constant." The documented method is
segment bytes = SEGFIL0[ MADR*2048 : MADR*2048 + SEGLE*2048 ]
with MADR and SEGLE taken from the Segment Table (bank 3, offset 0o124000),
entry = 8 words, word[2]=LOGAD word[3]=SEGLE word[4]=MADR word[5]=FLAG (top 3 bits = SEGFIL#).
EXTRACTING-SEGMENTS.md lines 20-22 state the blocker plainly: those values live only in the
in-memory Segment Table - "there is no fixed disk header to parse offline." So content-scanning
was the only thing available to me statically, but it is why bank 0 came out ambiguous. Bank 3 only
landed because the checksum could verify a guess.
Correct route for bank 0: boot the image under RetroCore/nd100x, dump the Segment Table over
DAP, read SEGLE/MADR for segment 146 octal (TCPS0B0), cut exactly, and check against
bank 0's stored checksum 0x2090.
Bank 1 is permanently unverifiable - its checksum word is zeroed, so no candidate can ever be proven. Do not produce a reconstruction for it; an unfalsifiable artifact is worse than a gap.
3. The reusable lesson¶
The segment file holds a second copy of every loaded BPUN, and the BPUN's own stored checksum survives independently of the file-system damage. That gives a general recovery path for any damaged pack:
- Read the mode-run
:LISToutput to get the segment number (do not search for it). - Get
MADR/SEGLEfor that segment from the Segment Table. - Cut the segment out of
SEGFIL0. - Verify against the damaged file's stored checksum.
fsck validating cleanly proves nothing here - it validates pointers, not contents.
This belongs in EXTRACTING-SEGMENTS.md and is not yet written there. See phase 4.
4. Plan - phases and todos¶
Phase 1 - finish the Ghidra symbol pass (DONE 2026-08-08)¶
- [x] Ronny: close the program in Ghidra (headless cannot take the project lock while it is open)
- [x] Run
PlancApplyNdSymbols.javaheadless as a dry run first - [x] Read the dry-run failure list before applying anything
- [x] Re-run with the
applyargument - [ ] Ronny: reopen Ghidra so the RE agent can resume
Result (2026-08-08): 436 defined records parsed (317 CODE + 119 DRAM), 0 failures.
143 stale bodies removed and recreated, 94 entry points newly disassembled, 119 DRAM labels
placed. Function count 230 -> 404. Verified persisted with a fresh -readOnly dry run.
Correction found on the way: the script's default table base 0x7C3A4 was WRONG - at that
base the name-length byte reads 0 and the parser rejects every record (first dry run parsed 0).
The real base is 0x7C3A0, pinned unambiguously by the +0 pointer field incrementing by
0x20 across records. 463 slots, of which 27 are kind=0xFF undefined/marker records (NIL,
NONE_x) and are correctly skipped - so "463 symbols" in this doc means 436 placeable ones.
The script default and its header comment are fixed
(C:\Users\ronny\ghidra_scripts\PlancApplyNdSymbols.java).
Ghidra project: E:\Dev\Repos\Ronny\RetroGhidra\ETH_II\ND_ETH_II.gpr, program
tcp-ser-all-banks-b05-68k.bin.
C:\Utils\Ghidra\ghidra_12.0.4_PUBLIC\support\analyzeHeadless.bat <project> <name> ^
-process <prog> -noanalysis -postScript PlancApplyNdSymbols.java
apply after the script name for the real run.
Optional one-time GUI fix: Script Manager -> Manage Script Directories -> add
C:\Users\ronny\ghidra_scripts, so all 7 PLANC scripts appear again. They vanished because a
previous session deleted the osgi folder, which de-registers every script directory.
Never delete the osgi folder - delete only osgi\felixcache and osgi\compiled-bundles.
Phase 2 - finish the firmware RE (needs phase 1)¶
- [x]
PO100PORTS/POMS100POO- SETTLED 2026-08-08: the CARD registers*TCP, the ND-100 only looks it up.TCPNETINIT->SKPOPENPOR(net flag) ->PORTOPEN->PORTCREATE(port type 2 ->PO100PORTS, the ND-100-facing pool) ->PONAREGIST. Port type 0/1 goes toPOPIOCPORT(on-card only). Plate comments set onTCPNETINIT,PO100PORTS@0x2692C. - [x] The MA command-port dispatch enumerated -
MACMDPORTH@0x6D2E, tabletbl_maCommandDispatch@0x24A86 (27 entries, only EVEN opcodes populated; odd -> error 0x727C). Opcodes 0,2,4,6,8,10,12,14,22,24,26. Opcode 12 @0x7096 = DEFINE/ADD MULTICAST ADDRESS (verified, corrects the old "set DIX mode" label - see the strike-through above), opcode 14 @0x7162 = REMOVE MULTICAST. This is the on-card AIP(IP)->MA seam, a 6-bit opcode space, NOT the XMSG EXMTY 128..132 family and NOT the*ENUMXMSG server (which is a separate product, absent from both images - explains the no-ENUM-string scan). - [ ] Decode the remaining AIP request blocks: attach, start, stop = opcodes 22/24/26 @0x71EA / 0x6FF2 / 0x7050 (partly decoded).
- [ ] Fold the results into
WRITING-A-TCPIP-STACK-ON-SINTRAN.mdpart 4.
Full cross-check against the NDIX (ND-500 Unix, 1988) source is in
SINTRAN\XMSG\DOC\NDIX-XMSG-VS-SINTRAN-ETHII-CROSSCHECK-FINDINGS-2026-08-08.md (sections 4a, 4b);
the RetroCore decoder MON_200_XMSG.cs now carries the func 45/48 version-gate and XFRMR
annotations.
Phase 3 - D02 bank 0 (independent, needs only the emulator)¶
- [ ] Boot
extract-ronny.imgunder RetroCore/nd100x - [ ] Dump the Segment Table (bank 3, offset
0o124000) over DAP - [ ] Read
SEGLE= word[3],MADR= word[4] for segment 146 octal (TCPS0B0) - [ ] Cut
SEGFIL0[MADR*2048 : +SEGLE*2048], check against stored checksum0x2090 - [ ] If it passes, rebuild the 512 KB D02 image. If not, say so and stop.
Bank 1 is out of scope - see section 2.
Phase 4 - write-up and decisions¶
- [ ] Add the section-3 technique to
EXTRACTING-SEGMENTS.md - [ ] Update
TCPIP-D02-SEGMENT-RECOVERY-2026-07-30.mdwith whatever phase 3 finds - [ ] Ronny decides: commit or not, and whether the ~1 MB of firmware
.binfiles belong in the repo at all
5. Traps for whoever picks this up¶
- The stale-body trap. Ghidra does not recompute an existing function's body when control flow
changes underneath it, and
createFunction()on an existing entry point silently returns "already exists". Renaming such a function pins the broken body. Order must be: disassemble -> remove -> create -> name. RunPlancFixFlowbeforePlancApplyNdSymbols. createLabelADDS, it does not replace. Re-running a script that uses it stacks duplicates.PlancApplyNdSymbolsusessetNameon the function instead, so it is idempotent.- The 5-character symbol truncation trap. The 463 embedded ND symbols are full-length (up to
10 chars, e.g.
RCVCOMPLET,XMTRINGAPP). ButSYMBOL-1-LIST/SYMBOL-2-LISTare 5-char truncated - cross-referencing against them on a full name will miss and mean nothing. ERRCODEwidth sets the first-parameter offset.move.w D0w,(0x10,A6)-> first param at0x12;move.l D0,(0x10,A6)->0x14. Both are correct, decided per routine. Version F changed the word size. This is not a contradiction in the docs.jmp (A5)is the runtime ERROR vector (#XRET/#ERET), not a continuation address. A5 permanently holds it.4E EA 00 02IS the return. The caller resumes at RETLINK+2 - that is the PLANC-MC skip return, not a mid-function jump.ndtool -xflattens all users into one directory and silently skipped 35 files on the Tingo extract. Extraction itself is byte-perfect (proved with a control) - the flattening is the bug. This is separate from the--putstructure note.- Read the mode-run
:LISToutput for segment numbers. Do not search the image for them.