mirror of
https://git.k-space.ee/arti/vt1200.git
synced 2026-10-31 12:05:05 +02:00
lance_cmd.c is a payload that drives the LANCE with its own code (no ROM
calls) and answers a command protocol on ethertype 0x88B6, so a unit whose
chip we have taken over can still be inspected:
read (peek), stats, poke, jump, reboot
The reply path was dead for a while and the cause was not the chip: the RX
ring had been grown to 16 entries (0x3020..0x309F) while TXR stayed at
0x3060, so all eight TX descriptors lay inside the RX ring. The chip's RX
write-back set OWN in what we believed were TX descriptors, and every reply
was discarded by transmit_poll() as "owned but not start of packet"
(r 00309A 8000 / w 00309A 0000 in the DMA log). TX ring moved to 0x30C0,
with a compile-time guard so the rings cannot overlap again.
Receive path aligned with the three drivers available locally (4.4BSD
pmax if_le.c, NetBSD am7990.c, Linux 7990.c):
* STP *and* ENP required on one descriptor, else the frame is counted as
chained and dropped rather than parsed from the middle of a buffer;
* the 4-byte FCS the chip writes into the buffer is subtracted, as all
three do (rmd3 - 4);
* re-arm restores RMD2 and clears RMD3, ownership written last;
* TX kick is TDMD|INEA (no reference driver re-writes STRT);
* a TX slot still owned is skipped instead of clobbered.
RX buffers are 1526 B so a full 1514-byte MTU frame fits one descriptor;
1500 B would have chained it.
vtcmd.py drives all of it; bench-sweep.sh sweeps size x rate. Measured on
the real VT1200 (10 Mbit half-duplex hub): 856-858 fps of 1514-byte frames
at 99.9-100% delivery, i.e. wire limited, and 1500 fps of 60-byte frames at
99.8-99.9%. NETWORK_REPORT.md sections 68-72 record the evidence, including
the 64-descriptor build that died with an illegal opcode on the unit and was
reverted.
4698 lines
200 KiB
Markdown
4698 lines
200 KiB
Markdown
# VT1200 networking — how the firmware drives the LANCE
|
||
|
||
Reverse engineering notes, **measurement and disassembly only**. The ROM is
|
||
untouched (`md5 b46a3e442ccb86134d2356bd70d22fd2`), no driver edits. Evidence
|
||
for each claim is the command/address quoted next to it.
|
||
|
||
Address convention: **bit addresses** (TMS34010). Ghidra address = bit / 8, and
|
||
in the Ghidra project the ROM is loaded at `0x07E40000 + file offset`.
|
||
|
||
---
|
||
|
||
## 1. Hardware surface
|
||
|
||
| item | address | evidence |
|
||
|---|---|---|
|
||
| LANCE register address port (RAP) | bit `0x05900000` | `movi 0x5900000` in the only code that uses the immediate, the ISR at `0x3FDD8E80` |
|
||
| LANCE register data port (RDP) | bit `0x05800000` | same site |
|
||
| Ethernet address ROM | bit `0x06800000`, **8 bytes** = MAC + 2 checksum bytes | reader `0x3FDD74B0`; `LANCE_REPORT.md` |
|
||
| interrupt | LANCE `intr_out` → INT1 | `LANCE_REPORT.md`; `INTENB` bit 1 at `0x3FA8C340` |
|
||
|
||
Registers are 16-bit (`move …,0`). `csr_w(reg, v)` = write `reg` to RAP, then
|
||
`v` to RDP; `csr_r(reg)` = write `reg` to RAP, read RDP.
|
||
|
||
## 2. The driver's device struct (found in a live RAM dump)
|
||
|
||
The driver does **not** hard-code the register addresses in its normal paths; it
|
||
keeps them in a struct. On a live boot (RAM dump at frame 5500) the struct at
|
||
**bit `0x23CBF0`** (byte `0x4797E`) reads:
|
||
|
||
```
|
||
+0x00 u32 0x05900000 RAP base
|
||
+0x04 u32 0x05900000 RAP base (copy)
|
||
+0x08 u32 0x05800000 RDP base
|
||
+0x0C u32 0x05800000 RDP base (copy)
|
||
+0x10 u32 0x06800060 Ethernet ROM pointer (0x6800000 + 8 bytes read)
|
||
+0x14 u32 0x0008FFC9 driver status
|
||
+0x16 u8[6] 08 00 2b 16 4b 5a PADR (MAC, copied from the ROM)
|
||
```
|
||
|
||
How it was found: a byte search of the RAM dump for the little-endian values
|
||
`0x05900000` / `0x05800000` returns byte offsets `0x4797E` / `0x47986`
|
||
(= bits `0x23CBF0` / `0x23CC30`), and the MAC appears at `0x47996`
|
||
(= struct + 0x16).
|
||
|
||
This is also why a static search for `movi 0x5900000` finds only two sites: the
|
||
normal driver loads the pointer from this struct.
|
||
|
||
## 3. Initialisation sequence (from the firmware's own code)
|
||
|
||
`io5800_5900_init` at bit `0x3FDDCBE0` (Ghidra `0x07FBB97C`): loads the RAP/RDP
|
||
immediates, then:
|
||
|
||
```
|
||
RAP=1 RDP=initblock>>0 ; CSR1
|
||
RAP=2 RDP=initblock>>16 ; CSR2
|
||
RAP=3 RDP=0 ; CSR3 (bus config)
|
||
RAP=0 RDP=0x7F41 ; CSR0 = INIT + standard interrupt enables
|
||
... poll CSR0 bit 8 (IDON) ...
|
||
CSR0 = 0x0042 ; STRT | INEA (the OS's own start)
|
||
```
|
||
|
||
The OS's live path is the same sequence (LANCELOG in `LANCE_REPORT.md`:
|
||
`CSR1=0x0F80`, `CSR2=0`, `CSR3=0`, `CSR0=0x0001`, poll `CSR0=0x0181`, then
|
||
`CSR0=0x0042`).
|
||
|
||
## 4. Initialisation block
|
||
|
||
12 words / 24 bytes, read by the chip over DMA:
|
||
|
||
| word | field | live value |
|
||
|---|---|---|
|
||
| 0 | mode | `0x0000` (normal; our test used `0x8000` PROM / `0x0044` LOOP\|INTL) |
|
||
| 1-3 | PADR (6 B) | `08 00 2b 16 4b 5a` |
|
||
| 4-7 | LADRF (8 B) | `0000 0000 0008 0000` |
|
||
| 8 | RX ring address low 16 | `0x0A80` |
|
||
| 9 | RX ring high 8 + length in bits 13-15 | `0xE000` → length `1<<7` = **128** |
|
||
| 10 | TX ring address low 16 | `0x0E80` |
|
||
| 11 | TX ring high 8 + length | `0xA000` → length `1<<5` = **32** |
|
||
|
||
Captured live from the running unit with `peek` at bit `0x7C00` (byte `0xF80`,
|
||
the address stored in the pointer cell `0x23E7A0`):
|
||
|
||
```
|
||
00 00 08 00 2b 16 4b 5a 00 00 00 00 08 00 00 00 80 0a 00 e0 80 0e 00 a0
|
||
```
|
||
|
||
After boot the descriptors at byte `0x0A80` read `00 00 00 00 | 00 f0 | 00 00`
|
||
per entry — address 0, flags 0, length `0xF000` (= 4096). So the ring is
|
||
**re-armed dynamically** by the driver; it is not left statically owned.
|
||
|
||
## 5. Descriptors and DMA
|
||
|
||
8 bytes per descriptor:
|
||
|
||
```
|
||
+0 u16 buffer address low 16
|
||
+2 u16 flags | buffer address high 8 (OWN=0x8000, STP=0x0200, ENP=0x0100,
|
||
ERR=0x4000, ...)
|
||
+4 u16 buffer length
|
||
+6 u16 message count (RX completion)
|
||
```
|
||
|
||
Buffer length encoding (C-LANCE, `get_buf_length` in MAME's `am79c90.cpp:191`):
|
||
|
||
```
|
||
length = -s16(0xF000 | data) e.g. 64 B -> 0x0FC0, 1526 B -> 0x0A0A, 4096 -> 0xF000
|
||
```
|
||
|
||
DMA addresses are **24-bit byte addresses**: buffer = `(RMD1 & 0xFF) << 16 | RMD0`;
|
||
rings = `(word9 & 0x1FFF) << 16 | word8`.
|
||
|
||
## 6. Interrupt handling (corrected by trace)
|
||
|
||
Two different handlers exist; only one is live.
|
||
|
||
**Live path.** The trace shows the CPU being interrupted and the OS dispatching
|
||
by handler table:
|
||
|
||
```
|
||
(interrupted at 3F579870, IRQ 0)
|
||
00000F60: JA 3F7144C0
|
||
3F7144C0: MMTM SP,A0-A3,A8
|
||
3F7144E0: MOVI 3F7151F0,A0 ; handler table
|
||
3F715040: SETF 8
|
||
3F715050: A3 = *0x1E9420; A3++ ; interrupt counter
|
||
3F7150C0: A1 = *A0 ; handler from the table
|
||
3F7150D0: CALL A1 ; -> 3F9D5B40
|
||
3F9D5B40: MMTM SP,A7,A9,A11 ; LANCE handler
|
||
```
|
||
|
||
The LANCE handler `0x3F9D5B40` (803 calls in an idle-window trace):
|
||
|
||
```
|
||
a11 = *0x23CBF0 ; RAP pointer (0x05900000)
|
||
*RAP = 0 ; select CSR0
|
||
a7 = *0x23CC50 ; RDP pointer #2 (0x05800000)
|
||
a9 = *RDP ; read CSR0
|
||
btst 6, a9 ; INTR
|
||
jreq return ; not ours
|
||
a7 = *0x23CC30 ; RDP pointer #1
|
||
*RDP = a9 ^ 0x40 ; *RDP = 0x40 ; clear INTR
|
||
btst 15 .. 8 on a9 ; ERR, MISS, RINT, TINT ...
|
||
bit 10 set -> calla 0x3F9D62B0 ; RX poll
|
||
bit 9 set -> calla 0x3F9D8A50 ; TX routine
|
||
bit 8 set -> set 0x23EC90 = 1
|
||
```
|
||
|
||
**Not live.** `0x3FDD8E80` (the other CSR0 handler, reached only from the vector
|
||
stubs `0x3FA93470`/`0x3FA935F0`) has **0 hits** in a 168 MB boot trace with 200
|
||
ICMP echoes. It belongs to another path (diagnostics), not the running stack.
|
||
|
||
Note the three RAP/RDP pointer cells: `0x23CBF0` = RAP, `0x23CC30` and
|
||
`0x23CC50` = two RDP aliases (all `0x05900000` / `0x05800000` in the RAM dump).
|
||
`0x23CC50` is also the **CSR0 command shadow** the init/TX code reads and ORs
|
||
before writing.
|
||
|
||
## 7. What the traffic actually exercises
|
||
|
||
Differential trace: the same boot window traced with and without ICMP traffic
|
||
(`/tmp/opencode/no_ping.tr` vs `with_ping.tr`), PC sets diffed. The PCs that
|
||
appear **only** with traffic in the driver area are `0x3F9D90E0–0x3F9D9270`
|
||
and `0x3F9D9400` — the protocol layer (TCP/IP flag and counter handling, cells
|
||
`0x23CB70/0x23CB80/0x23ECxx/0x23EFxx`), not the chip ring handling. The ring
|
||
handling code also runs without traffic (it is the poll loop), which is why it
|
||
is not in the diff.
|
||
|
||
## 8. Independent validation of the above against real silicon
|
||
|
||
A payload (`harness/udpstream/lance.c`) built from this description was tested
|
||
in MAME loopback and reached:
|
||
|
||
```
|
||
ok=1 RMD1=0x0338 (OWN clear, STP|ENP) MCNT=0x001C (28 = 24 + 4 FCS) TMD1=0x0338
|
||
```
|
||
|
||
i.e. init block, ring addressing, descriptor format, length encoding, TX and RX
|
||
DMA are all correct for the modelled chip.
|
||
|
||
On the real unit the same payload completes `INIT → IDON → STRT` (CSR0 =
|
||
`0x1B3`, then a stable `0x33`), but:
|
||
|
||
* `TDMD` stays set and the TX descriptor is never touched (`0x8338`),
|
||
* the RX descriptors are never touched,
|
||
* one earlier run showed `MISS|ERR`, i.e. the receiver did see frames but had
|
||
no usable buffer.
|
||
|
||
This is the open question: the driver's dynamic ring arming (section 4) is not
|
||
yet reproduced, and the chip does not service a transmit demand from a
|
||
hand-built ring.
|
||
|
||
## 9. Reproduction / instrumentation
|
||
|
||
* Boot trace: `-autoboot_script` traces (`trace <file>`), e.g.
|
||
`/tmp/opencode/ping_trace.lua`, `/tmp/opencode/win_trace.lua`.
|
||
* Headless debugger note: **breakpoint `printf` actions print nothing**; use the
|
||
`trace <file>` action instead. `bpset` addresses are bit addresses.
|
||
* Register/ROM reads on the real unit: `dwt/solve.py --peek <bitaddr> --len N`.
|
||
* Ring/init-block dumps: `VT1200_DUMP_RAM=<frame>` and search the dump.
|
||
|
||
## 10. Ring arming and the TX kick (the part that matters for a takeover)
|
||
|
||
From the traffic trace and the ring-handler sites (code referencing the ring
|
||
cells `0x23E7E0` RX / `0x23E820` TX), both rings are driven the same way.
|
||
|
||
### RX arming — `0x3F9D36C0`
|
||
|
||
```
|
||
a9 = *0x23EC50 ; list of receive buffers
|
||
a7 = a9 + 0xA0 ; buffer data = entry + 20 bytes
|
||
a11 = a7 >> 3 ; BIT address -> BYTE address <-- key
|
||
a7 = *0x23EBB0 << 6 ; ring position * 64 bits (8-byte descriptors)
|
||
a12 = *0x23E7E0 + a7 ; ring base (BIT address 0x5400 = byte 0xA80)
|
||
*a12 = a11 ; buffer BYTE address (32-bit store)
|
||
setf 12; *a12(0x20) = 0xFDDA ; RMD2 = 12-bit length, -(550) => 550 bytes
|
||
*a12(0x18) = 0x80 ; byte 3 = OWN, set LAST
|
||
*0x23EBB0 = (pos + 1) & 0x7F ; 128-entry ring wrap
|
||
```
|
||
|
||
Two details our first payloads got wrong or approximated:
|
||
|
||
* the buffer address is converted with `srl 3` to a **byte** address before it
|
||
goes in the descriptor;
|
||
* the length is written with `setf 12` (a 12-bit field), value `0xFDDA` for a
|
||
550-byte buffer;
|
||
* OWN is written as a single byte `0x80` at descriptor + 3, **after** the
|
||
address and length.
|
||
|
||
### TX kick — `0x3F9D57F0`
|
||
|
||
```
|
||
a7 = a11 << 6 ; descriptor offset
|
||
a0 = *0x23E820 + a7 ; TX ring (BIT address 0x7400 = byte 0xE80)
|
||
a7 = *(a0 + 0x18) ; byte 3 (RMD1 high byte)
|
||
a7 |= 0x80 ; OWN
|
||
*(a0 + 0x18) = a7 ; own the descriptor
|
||
...
|
||
a7 = *0x23CBF0 ; RAP pointer
|
||
*a7 = 0 ; RAP = 0
|
||
a5 = *0x23CC50 ; CSR0 command shadow kept by the driver
|
||
a5 |= 0x0008 ; TDMD
|
||
*0x23CC30 = a5 ; RDP = shadow | TDMD
|
||
```
|
||
|
||
> **Correction — see section 40.** The explanation originally written here
|
||
> ("the command bits are not set-only: writing `0x0008` alone clears `STRT`") is
|
||
> **wrong**. The AM79C90 model makes `INIT/STRT/STOP/TDMD` write-1-to-set and
|
||
> `INEA` read/write, so a bare `TDMD` disables interrupts but does *not* stop the
|
||
> transmitter. The firmware's shadow exists to preserve `INEA`; and the
|
||
> real-silicon `CSR0 = 0x1BB` decode (`STRT`=1, `TDMD`=1, `TXON`=1) shows a poll
|
||
> that never ran, which is a different problem entirely.
|
||
|
||
**The transmit demand is sent as `CSR0-read-back | TDMD`.** `0x23CC50` is **not**
|
||
a shadow cell — it holds the driver's **RDP pointer** (`0x05800000`), and the kick
|
||
reads CSR0 back through it:
|
||
|
||
```
|
||
3F9D5960 move @0x23CC50,a3,1 ; a3 = RDP pointer (0x05800000)
|
||
3F9D5990 move *a3,a5,0 ; a5 = *RDP = current CSR0
|
||
3F9D59B0 or a3=8,a5 ; a5 |= TDMD
|
||
3F9D59D0 move @0x23CC30,a5,1 ; a5 = RDP pointer (second alias)
|
||
3F9D5A00 move *a13,*a5,0 ; *RDP = a5
|
||
```
|
||
|
||
So the driver reads the *live* CSR0 and ORs `TDMD`, preserving whatever
|
||
read/write bits (notably `INEA`) are currently set. Section 10 called that cell a
|
||
"shadow" and section 62 records why that is wrong and what it broke.
|
||
|
||
## 11. The live driver paths (RX poll, TX completion)
|
||
|
||
The stack is **polled**: the driver's task calls a receive poll and a transmit
|
||
completion check. Ring positions and counters are global cells:
|
||
|
||
| cell | meaning |
|
||
|---|---|
|
||
| `0x23E6A0` | free receive buffers |
|
||
| `0x23EBA0` | RX ring **consumer** position (poll reads here) |
|
||
| `0x23EBB0` | RX ring **producer** position (refill writes here, `& 0x7F`) |
|
||
| `0x23EC50` | list of receive buffers (data at entry + `0xA0` bits) |
|
||
| `0x23CB00`, `0x23CB30` | saved `a0`/`a2` across a poll with no frame |
|
||
| `0x23CB70`, `0x23CB90`, `0x23CBB0`, `0x23CBC0` | TX/RX statistics |
|
||
|
||
### RX poll — `0x3F9D62B0`, body at `0x3F9D6300`
|
||
|
||
```
|
||
a7 = *0x23EBA0 << 6 ; consumer position * 64 bits
|
||
a10 = *0x23E7E0 + a7 ; RX ring base (BIT address 0x5400)
|
||
setf 8
|
||
a6 = *(a10 + 0x18) ; descriptor byte 3 = RMD1 high byte
|
||
btst 7, a6 ; OWN still set -> no frame
|
||
jrne 0x3F9D8980 ; (saves a0/a2 to 0x23CB00/0x23CB30, returns 0)
|
||
a9 = *a10 ; buffer BYTE address (32-bit store)
|
||
jreq 0x3F9D8590 ; null buffer
|
||
a9 = (a9 << 8) >> 5 ; BYTE address -> BIT address
|
||
a9 -= 0xA0 ; back to the buffer-list entry
|
||
... link the buffer into the queue, *0x23E6A0 = *0x23E6A0 - 1 ...
|
||
btst 1, a6 ; STP
|
||
btst 0, a6 ; ENP
|
||
```
|
||
|
||
The poll returns 0 when the descriptor is still owned (the normal idle case) —
|
||
this is the loop that replaces interrupts in the live stack.
|
||
|
||
### TX completion — `0x3F9D8AF0`
|
||
|
||
```
|
||
a5 = *0x23E820 ; TX ring base (BIT address 0x7400)
|
||
a11 = position * 64 + a5
|
||
setf 8
|
||
a9 = *(a11 + 0x18) ; TMD1 high byte
|
||
btst 7, a9 ; OWN still set -> not complete
|
||
jrne 0x3F9DAEF0 ; (saves a2/a4/a6 to 0x23CB90/0x23CBC0/0x23CBB0)
|
||
a12 = *a11 ; buffer BYTE address
|
||
jreq 0x3F9DAC80
|
||
a12 = (a12 << 8) >> 5 ; BYTE -> BIT
|
||
btst 1, a9 ; STP
|
||
btst 0, a9 ; ENP
|
||
... error/status bits update the statistics cells ...
|
||
```
|
||
|
||
### How the poll is driven
|
||
|
||
The RX poll `0x3F9D62B0` and the TX routine are called **from the interrupt
|
||
handler** (`0x3F9D5B40`) on status bits 10 and 9 — 402 poll calls were counted in
|
||
an idle-window trace, driven by the broadcast/ARP traffic the terminal receives
|
||
even with no pings. So the hardware model for a payload is: ring + INEA, and
|
||
service CSR0 in the handler (or poll CSR0 yourself with INEA clear).
|
||
|
||
## 12. Handler dispatch and the observed protocol chain
|
||
|
||
`0x3F9D5B40` is a pure status dispatcher. After reading CSR0 and clearing INTR
|
||
it tests the status word bit by bit (all evidence: the disassembly of
|
||
`0x3F9D5B40`..`0x3F9D6280`):
|
||
|
||
| status bit | action |
|
||
|---|---|
|
||
| 15 ERR | error counter `0x23ED20`, call `0x3F9DAFF0(5)` |
|
||
| 14 | counter `0x23EEF0` |
|
||
| 13 | counter `0x23EF50` (skipped while `0x23ECA0` bit 2 is set) |
|
||
| 12 | counter `0x23EE80` |
|
||
| 11 MISS | counter `0x23ED20` |
|
||
| **10 RINT** | **`calla 0x3F9D62B0`** — the RX poll |
|
||
| **9 TINT** | **`calla 0x3F9D8A50`** — TX completion |
|
||
| 8 | `0x23EC90 = 1` |
|
||
|
||
So the interrupt handler never builds or sends a frame itself: it only reaps
|
||
status and calls the RX poll / TX completion. **Frame submission is done by the
|
||
protocol layer calling into the driver**, which is why the init function
|
||
`0x3F9D4BC0` (which contains the only `TDMD` write, at `0x3F9D57F0`) is the
|
||
driver's command entry — it is called with `(cmd, arg)` from the protocol side
|
||
(and from the refill code at `0x3F9D3A30`).
|
||
|
||
### Protocol entry points seen in a ping window
|
||
|
||
Trace window frames 3000-4200 with an ICMP flood; the RX poll's callees, in
|
||
order (addresses are bit addresses):
|
||
|
||
```
|
||
3F9DF160 / 3F9DF4C0 driver/task helpers
|
||
3F604780 3F606210 3F600200 3F60BD40 packet processing
|
||
3F9E0B80 3F9DEDA0 receive-queue / socket delivery
|
||
3FB2C000 3FB2C290 byte-swap (htons/htonl) + per-fragment checksum loop
|
||
3F61E490 3F61AB30 upper-layer (socket) handling
|
||
```
|
||
|
||
The TX completion routine `0x3F9D8A50` is entered 343 times in that window
|
||
(status bit 9), while the init-path kick `0x3F9D57F0` is never reached — so the
|
||
live TX is submitted from the protocol side into the driver command entry and
|
||
completed via `0x3F9D8A50`.
|
||
|
||
Open item for the next pass: pin the exact protocol->driver send call (the
|
||
argument layout of `0x3F9D4BC0` and which command value means "transmit"), and
|
||
map the per-fragment checksum loop `0x3FB2C290` (chain at struct+0x40).
|
||
|
||
## 13. Protocol -> driver transmit submission (resolved)
|
||
|
||
The driver command entry `0x3F9D4BC0` is what the protocol layer calls to hand
|
||
over a frame. Evidence: in a ping-flood window (frames 3000-4200) the trace
|
||
contains **343** entries to `0x3F9D4BC0`, all from the same call site:
|
||
|
||
```
|
||
3F9DCF00: MOVE A14,-*SP,1
|
||
3F9DCF10: MOVE A2,*A14+,1 ; last pushed -> arg2 = request/command value
|
||
3F9DCF20: MOVK 1,A5
|
||
3F9DCF30: MOVE A5,*A14+,1 ; arg1 = 1 (unit / path selector)
|
||
3F9DCF40: MOVE A11,*A14+,1 ; first pushed -> arg0 = device struct
|
||
3F9DCF50: CALLA 3F9D4BC0h
|
||
```
|
||
|
||
The wrapper just above (`0x3F9DCE40`) prepares the request at `a11 + 0x100`:
|
||
|
||
```
|
||
a7 = *0x23CCA0 -> *(a4+0x30) ; MAC / PADR copied into the request
|
||
a7 = *0x23CCC0 -> *(a4+0x50)
|
||
a10 -> *(a4+0x60)
|
||
```
|
||
|
||
so the request block is `device+0x100` with fields at `+0x30`, `+0x50`, `+0x60`.
|
||
|
||
### The callee's transmit branch
|
||
|
||
`0x3F9D4BC0` receives `(arg0 = device, arg1 = 1, arg2 = request)`:
|
||
|
||
```
|
||
a9 = arg0 ; device/ring structure
|
||
a10 = arg1 ; 1
|
||
if (a10 > 1) { print 0x3F9E1F78; ... } ; only "1" is a valid path
|
||
...
|
||
loop over the buffer chain:
|
||
a2 = *(a9 + 0x40) ; next buffer
|
||
a7 = *0x23EBF0 ; TX producer position
|
||
a7 <<= 6 ; 8-byte descriptor stride
|
||
a0 = *0x23E820 + a7 ; TX ring (BIT address 0x7400 = byte 0xE80)
|
||
a9 + 0x100 / +0x40 ... ; per-buffer fields
|
||
```
|
||
|
||
The loop arms the descriptors in the TX ring; the **TDMD kick** for that batch is
|
||
the code at `0x3F9D57F0` already documented in section 10:
|
||
|
||
```
|
||
a7 = *(a0 + 0x18) | 0x80 ; OWN the descriptor
|
||
a5 = *0x23CC50 ; a5 |= 0x0008 ; command shadow | TDMD
|
||
*0x23CC30 = a5 ; RDP = shadow | TDMD
|
||
```
|
||
|
||
So the transmit submission chain is:
|
||
|
||
```
|
||
RX poll 0x3F9D62B0
|
||
-> protocol (0x3F604780 / 0x3F606210 / 0x3F600200 / 0x3F60BD40 / 0x3F61E490 / 0x3F61AB30)
|
||
-> wrapper 0x3F9DCE40 (fills device+0x100 request: MAC at +0x30 ...)
|
||
-> driver command entry 0x3F9D4BC0(device, 1, request) [343 calls/window]
|
||
-> TX ring arming at 0x23E820 + producer 0x23EBF0
|
||
-> TDMD kick 0x3F9D57F0 = CSR0 shadow | 0x0008
|
||
-> TX completion later on TINT: handler -> 0x3F9D8A50 -> 0x3F9D8AF0
|
||
```
|
||
|
||
Open detail (recorded, not resolved): the exact byte layout of the
|
||
`device+0x100` request block beyond `+0x30`, `+0x50`, `+0x60`, and the
|
||
packet-buffer objects behind `0x3FB2C000`/`0x3FB2C290`.
|
||
|
||
## 14. The packet object and the `+0x100` control block
|
||
|
||
Disassembly of the wrapper `0x3F9DC890` (the function that calls the driver
|
||
command entry at `0x3F9DCF50`) shows that **arg2 (`a11`) is a packet object**,
|
||
not a device:
|
||
|
||
```
|
||
a9 = arg0
|
||
a10 = arg1
|
||
a11 = arg2 ; packet object
|
||
...
|
||
a7 = *(a11 + 0x90) ; packet state word
|
||
a5 = *(a11 + 0xF0) + 0x0E ; total length + 14 (Ethernet header)
|
||
a12 = a11 + 0x40
|
||
loop:
|
||
a7 = *(a12 + 0xB0) ; fragment length
|
||
a0 += a7 ; running total
|
||
a2++ ; fragment count
|
||
a12 = *a12 ; next fragment
|
||
```
|
||
|
||
Packet object fields (bit offsets from `a11`):
|
||
|
||
| offset | meaning |
|
||
|---|---|
|
||
| `+0x40` | next fragment pointer |
|
||
| `+0x90` | packet state (the wrapper checks it against 0x16) |
|
||
| `+0xB0` | fragment length |
|
||
| `+0xF0` | total packet length |
|
||
| `+0x100` | control block built by the wrapper for the driver call |
|
||
|
||
The wrapper copies into the control block:
|
||
|
||
```
|
||
*(cb + 0x20) = *(a9 + 0x20)
|
||
*(cb + 0x30) = MAC from 0x23CCA0 (PADR)
|
||
*(cb + 0x50) = *0x23CCC0
|
||
*(cb + 0x60) = a10
|
||
```
|
||
|
||
Given the LANCE init-block field order (mode, PADR, LADRF, rings), a control
|
||
block whose PADR sits at `+0x30` means the block starts `0x2E` bits (mode) before
|
||
it — i.e. the wrapper is handing the driver an **interface control block**
|
||
(mode/PADR/LADRF/ring words) together with the packet object; the driver entry
|
||
with `arg1 = 1` consumes it and, for the packet case, walks the fragment chain
|
||
and arms TX descriptors (section 13).
|
||
|
||
This is the last structural piece needed to describe the transmit path from the
|
||
protocol layer down to the ring and the `TDMD` kick; the byte-level semantics of
|
||
`cb+0x50`/`cb+0x60` (the two ring words copied from `0x23CCC0` and `a10`) and
|
||
the `0x3FB2C000`/`0x3FB2C290` pool internals remain the documented open items.
|
||
|
||
## 15. Correction: `0x3FB2C000` / `0x3FB2C290` are utilities, not a pool
|
||
|
||
Disassembly:
|
||
|
||
* **`0x3FB2C000`** — byte swap. The 16-bit form does
|
||
`sll 8 / andi 0xFF00 / srl 8 / or`; a second entry does the same with
|
||
`0xFF00FF00` / `0x00FF00FF` masks for 32-bit values. That is `htons`/`htonl`
|
||
(network byte order), which is why it appears repeatedly on packet headers in
|
||
the RX/TX traces.
|
||
* **`0x3FB2C290`** — a per-fragment loop over a structure: it takes a struct in
|
||
`a9`, a count in `a10`, rejects null/negative counts, reads `*(a9+0x40)` as
|
||
the next link and iterates with `a5 = 8` — a checksum/accumulate pass over the
|
||
fragment chain, not an allocator.
|
||
|
||
So the earlier "buffer pool" reading was wrong; the packet path is
|
||
header byte-swapping plus checksumming, consistent with the protocol chain in
|
||
section 13.
|
||
|
||
## 16. The control block is an init-block template (cb+0x50 / cb+0x60 resolved)
|
||
|
||
The two cells behind the wrapper's control block are not independent values:
|
||
|
||
```
|
||
0x23CCA0 (bit) = byte 0x47994: 08 00 2b 16 4b 5a <- PADR, 6 bytes
|
||
word read at 0x23CCA0 = 0x162b0008 <- MAC bytes 0..3
|
||
word read at 0x23CCC0 = 0x00005a4b <- MAC bytes 4..5
|
||
```
|
||
|
||
`0x23CCC0` is simply `0x23CCA0 + 4 bytes` (the last two MAC bytes), so the
|
||
wrapper's two stores reassemble the contiguous PADR:
|
||
|
||
```
|
||
MOVE *A9(0x20),*A4(0x20),0 ; 16-bit : mode -> cb + 0x20
|
||
MOVE A7=0x162b0008,*A4(0x30),1 ; 32-bit : MAC[0..3] -> cb + 0x30
|
||
MOVE A7=0x00005a4b,*A4(0x50),0 ; 16-bit : MAC[4..5] -> cb + 0x50
|
||
MOVE A10,*A4(0x60),0 ; 16-bit : -> cb + 0x60
|
||
```
|
||
|
||
The field spacing proves the block is a **LANCE initialisation block**:
|
||
|
||
| block bit offset | field | source |
|
||
|---|---|---|
|
||
| `cb+0x20` | mode (16) | `*(a9+0x20)` |
|
||
| `cb+0x30` | PADR bytes 0-3 (32) | MAC at `0x23CCA0` |
|
||
| `cb+0x50` | PADR bytes 4-5 (16) | MAC at `0x23CCC0` (= `0x23CCA0+4`) |
|
||
| `cb+0x60` | next word (LADRF[0]) | `a10` |
|
||
|
||
`mode` at `cb+0x20` and PADR starting at `cb+0x30` (16 bits later) is exactly
|
||
the init-block layout; LADRF follows at `cb+0x60`. So the wrapper hands the
|
||
driver a freshly built init block whose base is `cb+0x20`, together with the
|
||
packet object — the driver command entry then programs the chip from it and
|
||
transmits the packet.
|
||
|
||
### Protocol callees reached from the RX poll (located, partially characterised)
|
||
|
||
| entry | head of function shows |
|
||
|---|---|
|
||
| `0x3F604780` | calls `0x3FB2E040`; state cells `0x1DA4D0`, `0x1DA4E0`; index `0x1D27D0` |
|
||
| `0x3F606210` | counter `0x1DA4F0`, calls `0x3F606010` and `0x3FB2E040` |
|
||
| `0x3F600200` | large frame (`a14 += 0x9A0`), two args (`a9`,`a10`), frame-length fields at `a13+0x20/+0xB0/+0xC0` |
|
||
| `0x3F60BD40` | lock `0x3FB33870`, cell `0x95FD0` |
|
||
| `0x3F61E490` | list walk: `a7 = *(arg)`, compare/loop, depth counter `a13+0` |
|
||
| `0x3F61AB30` | cell `0x96AC0`, `*(a9+0x100)`, tests bit 1 of `*(a11+0x40)` |
|
||
|
||
These are the IP/ICMP and queue-handling entries; the table records what each
|
||
one touches at its entry so the next pass can map them field by field.
|
||
|
||
## 17. Protocol-region map and call-graph evidence
|
||
|
||
Method: from the ping-window trace (`icmpw.tr`, frames 3000-4200), every
|
||
`CALLA` line was parsed into a caller->callee edge
|
||
(`^([0-9A-F]{8}): CALLA\s+([0-9A-F]+)h`). Result: **131 unique edges, 47,440
|
||
calls**. The top edges are infrastructure, which is itself informative about how
|
||
the stack is built:
|
||
|
||
```
|
||
3F57A690 -> 3F702440 x2666 free()
|
||
3FB3FD60 -> 3F57A710 x1072
|
||
3FB41AD0 -> 3F701230 x1031 malloc()
|
||
3F616730 -> 3FB2C140 x1029 checksum/compare helper
|
||
3FB41F50 -> 3F701230 x948 malloc()
|
||
3F2BE2F0 -> 3F27A110 x858
|
||
3F27A770 -> 3FB3FA00 x858
|
||
3F2C5B10 -> 3F2798B0 x840
|
||
3F2C5B80 -> 3FB3FE90 x840
|
||
3F6249D0 -> 3FB33870 x688 lock
|
||
3F6254C0 -> 3FB338D0 x688 unlock
|
||
3F9DF550 -> 3F9DF160 x687 driver task helper
|
||
3F9DEE40 -> 3F703D90 x687
|
||
3F61ACC0 -> 3FB2C140 x686 checksum/compare helper
|
||
```
|
||
|
||
So the protocol layer is dominated by per-packet `malloc`/`free` and lock
|
||
traffic, with the checksum helper `0x3FB2C140` called twice per packet path
|
||
(`0x3F616730`, `0x3F61ACC0`).
|
||
|
||
### Region map `0x3F600000-0x3F630000`
|
||
|
||
A 24 KB disassembly of that range (8,824 lines) was scanned for small-constant
|
||
protocol dispatch (`cmpi 0x1/0x6/0x8/0x11`). Only **one** site matched:
|
||
|
||
```
|
||
3F6156B0 loop:
|
||
a7 = *a9 >> 4 ; a7 <<= 3 ; a7 += 0x1E0600
|
||
*a11++ = *(a7) ; copy a table entry
|
||
a7 = *a9++ & 0xF ; a7 <<= 3 ; a7 += 0x1E0600
|
||
*a11++ = *(a7)
|
||
*a11++ = 0x3A ; ':'
|
||
inc a10
|
||
cmpi 0x6, a10
|
||
jrlt loop
|
||
```
|
||
|
||
> **Correction (see section 25.2).** This paragraph originally called that loop
|
||
> an "ARP/route table display builder". It is not: the two tables it indexes
|
||
> (`0x1E0570`, `0x1E0600`) are literally the ASCII string `0123456789abcdef`,
|
||
> and the loop converts six bytes into `xx:xx:xx:xx:xx:xx` — a **MAC-address
|
||
> formatter**. The `arpre…`/`mods …` strings that follow are unrelated version
|
||
> headers (`in.c`, `in_pcb.c`), not table data.
|
||
|
||
The absence of literal protocol
|
||
compares in this range means the stack dispatches through tables/pointers rather
|
||
than a `switch` on the protocol byte, which is why a constant scan does not find
|
||
it.
|
||
|
||
This is the state of the protocol-layer RE: entry points, per-packet call costs,
|
||
and one concrete routine are documented; the table-driven dispatch and the
|
||
IP/ICMP state machines remain the open area.
|
||
|
||
## 18. Table-driven dispatch: the indirect calls resolved
|
||
|
||
Method: in the trace, an instruction like `call a5` / `call a11` is followed on
|
||
the next line by the callee's PC, so indirect edges can be reconstructed. The
|
||
ping-window trace yields **12 indirect edges, 3,333 calls**:
|
||
|
||
```
|
||
3F7150D0 -call a1-> 3F9D5B40 x687 IRQ dispatcher -> LANCE handler (known)
|
||
3F606830 -call a11-> 3F602DB0 x344
|
||
3F6271E0 -call a5-> 3F61FFA0 x343
|
||
3F6603A0 -call a5-> 3F615E00 x343
|
||
3F660BB0 -call a5-> 3F616090 x343
|
||
3F630090 -call a5-> 3F607C90 x343
|
||
3F6AACF0 -call a5-> 3F64DC00 x29
|
||
3F6AA8F0 -> 3F6293A0 / 3F64E150 x20
|
||
3F6271E0 -> 3F6540C0 x1
|
||
```
|
||
|
||
### The dispatch table at `0x1E3D80`
|
||
|
||
The site `0x3F660380` is the protocol dispatcher:
|
||
|
||
```
|
||
a5 = *(a13 + 0xC0) ; index
|
||
a5 <<= 6 ; 8-byte entries
|
||
a5 += 0x1E3D80 ; table
|
||
a5 = *a5 ; handler pointer
|
||
call a5
|
||
```
|
||
|
||
The table was read from a live RAM dump (bit `0x1E3D80` = byte `0x3C7B0`),
|
||
8 bytes per entry, two function pointers per entry:
|
||
|
||
| index | +0 | +4 |
|
||
|---|---|---|
|
||
| 0,1 | `0x3F658570` | `0x3F658650` |
|
||
| **2,3** | **`0x3F615E00`** | **`0x3F616090`** |
|
||
| 4..15 | `0x3F658570` | `0x3F658650` |
|
||
|
||
Entries 2/3 are exactly the `x343` indirect targets from the trace, i.e. the two
|
||
handlers used by the ping traffic. Disassembly:
|
||
|
||
* **`0x3F615E00`** — converts a 32-bit value from the packet to host order: it
|
||
takes `*(arg0+0x20)`, calls the helper `0x3F6166D0`, then shifts the result
|
||
down byte by byte until a non-zero low byte remains, stores it at
|
||
`*(arg1+0x20)`, and calls the byte-swap helper `0x3FB2C140`.
|
||
* **`0x3F616090`** — converts two values (from `arg0+0x20` and `arg1+0x20`) with
|
||
the same helper and compares them, returning 1 when equal, 0 otherwise. That
|
||
is a packet-field comparison (address/filter match).
|
||
|
||
So the "protocol dispatch" is a per-field helper table (convert + compare),
|
||
indexed by a field-type byte at `a13+0xC0`, not a protocol switch — which is why
|
||
the constant scan of section 17 found nothing. The IP/ICMP state machines call
|
||
these helpers per header field.
|
||
|
||
The second dispatch site, `0x3F630050`, uses a **device entry point**:
|
||
`a5 = *(a11 + 0x160); call a5` — the protocol calls the driver through a
|
||
function pointer stored in the device structure, which is consistent with the
|
||
submission path documented in section 13.
|
||
|
||
## 19. Dispatch index semantics (bounded) and the two-pointer entries
|
||
|
||
The dispatcher function `0x3F660280`..`0x3F6603A0` narrows the index before it
|
||
indexes the table:
|
||
|
||
```
|
||
L3F660280:
|
||
a5 = *(a13 + 0xC0) ; index
|
||
subk 17, a5
|
||
jrnc 0x3F661190 ; index >= 17 -> epilogue (mmfm sp,..; rets)
|
||
...
|
||
a5 = *(a13 + 0xC0)
|
||
a5 <<= 6 ; 8-byte entries
|
||
a5 += 0x1E3D80
|
||
a5 = *a5
|
||
call a5 ; entry.fn0
|
||
...
|
||
a5 = *(a13 + 0xC0)
|
||
a5 <<= 6
|
||
a5 += 0x1E3D80
|
||
a5 = *(a5 + 0x20)
|
||
*(a13 + 0x20) = a5 ; entry.fn1 copied out
|
||
```
|
||
|
||
Facts established from this and the trace:
|
||
|
||
* only indexes **0..16** are dispatched; `>= 17` returns immediately
|
||
(`0x3F661190` is the function epilogue `mmfm sp,a5..a13; rets 2`);
|
||
* each entry is **two pointers**: `fn0` at `+0` and `fn1` at `+0x20`;
|
||
* the two call sites seen in the trace use the two pointers of the **same**
|
||
entry: `0x3F6603A0` calls `entry[2].fn0` = `0x3F615E00` (convert) and
|
||
`0x3F660BB0` calls `entry[2].fn1` = `0x3F616090` (compare);
|
||
* entries 0,1 and 4..15 all point at `0x3F658570` / `0x3F658650`, i.e. the
|
||
table is sparse — only index 2 is populated with real work for the ping path.
|
||
|
||
That the ping path uses entry index 2, whose helpers are "convert a 32-bit value
|
||
to host order" and "compare two converted values", makes the index a **field
|
||
type code** (2 = a 32-bit address field) rather than the IP protocol number
|
||
(ICMP would be 1, TCP 6, UDP 17 — and 17 is out of range here). The table at
|
||
`0x1E3D80` is therefore a per-field codec/compare table used by the packet
|
||
handlers, not the protocol switch itself.
|
||
|
||
### Socket layer
|
||
|
||
No separate socket dispatcher was found in this range. The protocol layer hands
|
||
packets to the driver through the device entry pointer at `*(device + 0x160)`
|
||
(dispatcher site `0x3F630050`: `a5 = *(a11+0x160); call a5`), and the socket
|
||
API seen from user code (`socket`/`bind`/`sendto`/`recvfrom`/`listen`/`accept`/
|
||
`read`/`close` at `3F5C9A00`/`3F5CAA80`/`3F5D3D70`/`3F5D7D50`/`3F5CBCD0`/
|
||
`3F5CCB40`/`3F5D8E70`/`3F5E8930`) is the kernel-side entry into this same
|
||
machinery — documented from the user side in `dwt/notes/TCPIP.md` and measured
|
||
(this session: ~20 datagrams/s delivery ceiling).
|
||
|
||
## 20. Packet builders: UDP header builder, and a corrected `CMPI` encoding
|
||
|
||
> **Correction (see section 22).** This section originally claimed that
|
||
> `CMPI #8` "does not exist in the ROM" because a byte search for
|
||
> `0x0B40|reg, 0x0008` returned zero hits. That search was wrong: on the
|
||
> TMS34010 the `CMPI` immediate is stored **one's-complemented**. A known-good
|
||
> site proves it — `cmpi 0x8,a2 ; 0b42 fff7` at `0x3F9D2C80`, i.e. the immediate
|
||
> word is `~8`, not `8`. Searching for `0x0B40|reg, 0xFFF7` finds **42**
|
||
> `cmpi #8` sites. The ICMP type test is therefore a perfectly ordinary compare;
|
||
> the earlier negative result was a tooling error, and the paragraph below is
|
||
> kept only as a record of it.
|
||
|
||
### UDP header builder — `0x3F656200`
|
||
|
||
The ping-only code at `0x3F6562xx` is a transmit header builder, and it declares
|
||
its protocol explicitly:
|
||
|
||
```
|
||
*(a11+0x20) = 0
|
||
*(a11) = 0
|
||
*(a11+0x40) = 0 ; byte: version/IHL area, filled later
|
||
movb 17,*(a11+0x48) ; <<< protocol 17 = UDP
|
||
...
|
||
a5 = a12 + 8
|
||
calla 0x3FB2C000 ; byte swap (htons)
|
||
*(a11+0x50) = a8 (16-bit) ; port/checksum word
|
||
*(a11+0x60) = *(a9+0x90)
|
||
*(a11+0x80) = *(a9+0x60)
|
||
*(a11+0xa0) = *(a9+0xb0) ; 16-bit
|
||
*(a11+0xb0) = *(a9+0x80) ; 16-bit
|
||
*(a11+0xc0) = *(a11+0x50) ; 16-bit
|
||
*(a11+0xd0) = 0
|
||
```
|
||
|
||
So the stack has a per-protocol header builder that fills a packet template at
|
||
`a11` from a control block at `a9` and byte-swaps the port/checksum words through
|
||
`0x3FB2C000` (the `htons`/`htonl` helper of section 15). The ICMP reply builder
|
||
is the analogous routine for type 8 -> 0; it is the next target, and the zero
|
||
`CMPI #8` result above says its type test is value- or table-based rather than a
|
||
literal compare.
|
||
|
||
## 21. The send path above the driver wrapper, and checksum validation
|
||
|
||
### Transmit request — around `0x3F609D40`, wrapper call at `0x3F609F20`
|
||
|
||
The driver submission wrapper `0x3F9DC890` has exactly **one** caller in the
|
||
ping window (343 calls):
|
||
|
||
```
|
||
3F609F20: CALLA 3F9DC890h
|
||
```
|
||
|
||
and the code in front of it is the send request:
|
||
|
||
```
|
||
a9 = *(a13+0xC0) ; packet object
|
||
a7 = *(a9+0xF0) ; total length
|
||
if (a7 < 0x32) *(a9+0xF0) = 0x32 ; enforce 50-byte minimum (Ethernet+IP+ICMP min)
|
||
...
|
||
calla 0x3F9DC890(packet, arg1, arg2)
|
||
a8 = result
|
||
if (a8 != 0) {
|
||
(*(a13+0xFFE0)+0x260)++ ; tx error counter
|
||
(*(a13+0xFFE0)+0x240)++ ; tx error counter
|
||
log 0x3F60E6E0
|
||
}
|
||
```
|
||
|
||
So the layering above the driver is: packet object -> minimum-length clamp
|
||
(50 bytes) -> submission wrapper `0x3F9DC890` -> driver command entry
|
||
`0x3F9D4BC0` -> ring arming -> `TDMD` kick.
|
||
|
||
### Checksum validation on receive
|
||
|
||
`0x3FB2C290` (the per-fragment checksum loop of section 15) is called once per
|
||
packet from four sites in the receive path:
|
||
|
||
```
|
||
343 3F6206D0 344 3F6235F0 344 3F625B40 343 3F62FFC0
|
||
```
|
||
|
||
The callers compare the result and bump per-protocol error counters
|
||
(e.g. `0x96800` incremented when the checksum check fails), i.e. these are the
|
||
**verification** calls, not the reply builder. The reply builder's own checksum
|
||
call is the remaining target; the send path above shows where its packet would
|
||
have to go.
|
||
|
||
### Note on tooling
|
||
|
||
The send function's prologue is not present in Ghidra's listing for that region
|
||
(`listing.getInstructionAt(0x07EC13A8)` returns none even though surrounding
|
||
addresses are disassembled), so this part was read from raw disassembly of the
|
||
ROM image, which is why the function is described by address range rather than
|
||
by a named entry.
|
||
|
||
|
||
## 22. The ICMP echo path, end to end (type 8 -> 0 -> transmit)
|
||
|
||
This closes the protocol half: one ping is followed from the ICMP type dispatch
|
||
down to the `TDMD` kick. **Every** link below executes exactly **343 times** in
|
||
the ping-window trace (`icmpw.tr`, frames 3000-4200), i.e. once per echo request
|
||
— that count is the evidence that these addresses form one chain and not merely
|
||
a list of functions that happen to run.
|
||
|
||
```
|
||
3F6224E0 343 ICMP type dispatch
|
||
3F6212A0 343 type 8 handler: movb 0,*a10
|
||
3F621A30 343 reply path: adjust IP total length, per-type stats
|
||
3F622A40 343 reflect: pick interface, prepare
|
||
3F623350 343 ICMP checksum, then call the IP transmit
|
||
3F62EA00 343 IP transmit finalisation: htons + IP checksum
|
||
3F62FE00 343 header fix-up block inside the IP transmit
|
||
3F630070 343 load device tx method *(device+0x160)
|
||
3F607C90 343 device transmit method (reached by indirect call)
|
||
3F6084E0 343 driver send (0x32 clamp, builds request)
|
||
3F609F20 343 call submission wrapper
|
||
3F9DC890 343 driver submission wrapper
|
||
-> 3F9D4BC0 -> ring arm -> TDMD (sections 10, 13)
|
||
```
|
||
|
||
### 22.1 ICMP type dispatch — `0x3F6224E0`, jump table `0x3F622580`
|
||
|
||
The dispatch is a `jr` inside the IP-input function, not a call, and the field
|
||
size is already 8 bits when it is entered, so the key is the **type byte**:
|
||
|
||
```
|
||
3F6209B0 setf 8,0,0
|
||
3F6209C0 a7 = *a10 ; ICMP type byte
|
||
3F6209D0 subk 0x12,a7
|
||
3F6209E0 jrhi 0x3F6227E0 ; type > 18 -> drop
|
||
3F620A00 a7 = *(a10) << 5
|
||
3F620A20 a7 += 0x96860 ; received-per-type counter (4 bytes/entry)
|
||
3F620A50 (*a7) += 1
|
||
3F620A80 a7 = *(a10+0x08) ; ICMP code byte
|
||
3F620AA0 *(a13+0x80) = a7
|
||
3F620AC0 jr 0x3F6224E0 ; dispatch
|
||
|
||
3F6224E0 move *a10,a8,0 ; type byte again (same 8-bit field)
|
||
3F6224F0 cmpi 0x12,a8 ; type > 18 -> default
|
||
3F622510 jrhi 0x3F6227E0
|
||
3F622520 sll 0x5,a8 ; 32-bit (4-byte) entries
|
||
3F622530 addi 0x3F622580,a8
|
||
3F622560 move *a8,a8,1
|
||
3F622570 jump a8 ; 343 executions in the ping window
|
||
```
|
||
|
||
(The earlier draft of this section said the key was a 16-bit type+code word;
|
||
the trace shows the `jr` at `0x3F620AC0` carries the 8-bit field size straight
|
||
into the dispatch, so it is the type byte alone. The code byte is saved
|
||
separately at `a13+0x80`.)
|
||
|
||
The table read straight out of the ROM image (19 entries, 4 bytes each):
|
||
|
||
| type | meaning | handler |
|
||
|---|---|---|
|
||
| 0 | echo reply | `0x3F6227E0` (default/drop) |
|
||
| 3 | destination unreachable | `0x3F620AE0` |
|
||
| 4 | source quench | `0x3F620C90` |
|
||
| 5 | redirect | `0x3F621C40` |
|
||
| **8** | **echo request** | **`0x3F6212A0`** |
|
||
| 11 | time exceeded | `0x3F620B80` |
|
||
| 12 | parameter problem | `0x3F620C20` |
|
||
| 13 | timestamp request | `0x3F6212D0` |
|
||
| 15 | information request | `0x3F621440` |
|
||
| 17 | address mask request | `0x3F621730` |
|
||
| 1,2,6,7,9,10,14,16,18 | — | `0x3F6227E0` (drop) |
|
||
|
||
Reproduce the table (bit address -> ROM file offset `bit/8 - 0x7E40000`):
|
||
|
||
```sh
|
||
python3 - <<'PY'
|
||
import struct
|
||
rom=open('vt1200_v22.bin','rb').read()
|
||
base=(0x3F622580//8)-0x7E40000
|
||
for i in range(19):
|
||
print(i, hex(struct.unpack("<I", rom[base+i*4:base+i*4+4])[0]))
|
||
PY
|
||
```
|
||
|
||
### 22.2 The echo handler rewrites the type — `0x3F6212A0`
|
||
|
||
```
|
||
3F6212A0 sub a7,a7
|
||
3F6212B0 movb a7,*a10 ; <<< ICMP type := 0 (echo reply)
|
||
3F6212C0 jruc 0x3F621A30 ; shared reply path
|
||
```
|
||
|
||
Three instructions. `a10` is the transport header pointer, so this is the
|
||
`8 -> 0` rewrite that the whole investigation was looking for, and it does not
|
||
recompute anything itself — the shared reply path does.
|
||
|
||
### 22.3 Shared reply path — `0x3F621A30`
|
||
|
||
```
|
||
a7 = *(a13+0x20) ; length delta
|
||
a5 = *(a11+0x10) ; IP total length (byte 2)
|
||
a5 += a7 ; *(a11+0x10) = a5 ; adjust total length for the reply
|
||
*0x96840 += 1 ; "replies sent" counter
|
||
a3 = *a10 << 5 ; a3 += 0x96560 ; per-ICMP-type counter (4 bytes/entry)
|
||
(*a3) += 1
|
||
calla 0x3F622A40(reflect args)
|
||
```
|
||
|
||
`a11` is the IP header (byte 2 = total length), `a10` the ICMP header (type).
|
||
Two statistics tables are involved: per-type counters at `0x96560` (indexed by
|
||
type, 4 bytes per entry) and the reply counter `0x96840`.
|
||
|
||
### 22.4 Reflect/send — `0x3F622A40` (the whole `0x3F622A40`..`0x3F623170` body)
|
||
|
||
```
|
||
a9 = arg (IP header / packet)
|
||
*a13(0x40) = (*a9 << 2) - 20 ; header/length arithmetic
|
||
*(a13+0x00) = *(a9+0x80)
|
||
*(a9+0x80) = *(a9+0x60)
|
||
a10 = *0x96AC0 ; interface list head
|
||
loop: match *(a10+0x20) against the packet's interface
|
||
test bit 1 of *(*(a10+0x100)+0x40) ; interface flags (up/broadcast)
|
||
...
|
||
calla 0x3F623180 ; route/interface resolve
|
||
calla 0x3F62BF30 ; prepare output block
|
||
calla 0x3F62C4B0 ; prepare output block
|
||
3F622EA0 movi 0xFF,a11
|
||
3F622EC0 movb a11,*(a9+0x40) ; <<< TTL (byte 8) := 255
|
||
calla 0x3F623350 ; ICMP checksum, then ip transmit
|
||
```
|
||
|
||
So the reply is sent out of the interface the request arrived on: the interface
|
||
list at `0x96AC0` is walked, the interface flags word (`iface+0x100` -> `+0x40`,
|
||
bit 1) decides whether it is usable, the route is resolved, and the TTL is set
|
||
to 255 — the ordinary initial TTL of a freshly generated IP packet. All of this
|
||
is inside `0x3F622A40` (epilogue at `0x3F623150`/`0x3F623170`), not in
|
||
`ip`-output.
|
||
|
||
### 22.5 Transport checksum, then IP transmit — `0x3F623350`
|
||
|
||
```
|
||
a12 = a9 & 0xFFFFFC00 ; packet block base (1024-bit aligned)
|
||
a10 = (4-bit field at a9) << 2 ; setf 4 -> read nibble, scale by 4
|
||
a5 = *(a12+0x20) + a10 ; store ; block offset updated
|
||
a11 = a12 + (*(a12+0x20) << 3) ; transport header = block + offset
|
||
*a11(0x10) = 0 ; <<< zero the checksum field (byte 2 of ICMP)
|
||
calla 0x3FB2C290(*a9(0x10)-a10, a12) ; ones-complement sum helper (section 15)
|
||
*a11(0x10) = a8 ; <<< store the computed checksum
|
||
...
|
||
calla 0x3F62EA00 ; IP transmit finalisation
|
||
```
|
||
|
||
This is the ICMP checksum: field at **byte 2 of the transport header**, zeroed,
|
||
summed by `0x3FB2C290`, stored back. (Section 21's receive-side calls to the
|
||
same helper at `0x3F6206D0`/`0x3F6235F0`/`0x3F625B40`/`0x3F62FFC0` are the
|
||
matching *verification* on the way in; `0x3F623350` is the transmit side.)
|
||
|
||
### 22.6 IP transmit finalisation — `0x3F62EA00`
|
||
|
||
Args: `a9` = IP header, `a11` = device/interface, `a12` = packet block.
|
||
Function extent confirmed in the ROM: prologue `mmtm sp,a0,a2,a3,a5,a7,a9,a10,a11,a12,a13`
|
||
at `0x3F62EA00`, a single `rets 2` at `0x3F631B00`, so everything between —
|
||
including the fix-up block and the device call — is one function. The header
|
||
fix-up code sits at `0x3F62FEB0`:
|
||
|
||
```
|
||
3F62FEB0 a7 = *(a9+0x10) ; IP total length (byte 2)
|
||
3F62FEE0 calla 0x3FB2C000 ; htons
|
||
*(a9+0x10) = a8
|
||
3F62FED0 a7 = *(a9+0x30) ; flags + fragment offset (byte 6)
|
||
3F62FF00 calla 0x3FB2C000 ; htons
|
||
*(a9+0x30) = a8
|
||
3F62FF60 sub a7,a7 ; *(a9+0x50) = 0 ; <<< IP header checksum (byte 10) := 0
|
||
3F62FFC0 calla 0x3FB2C290(a0, a12) ; compute
|
||
3F630000 *(a9+0x50) = a8 ; <<< store IP header checksum
|
||
3F630030 push args
|
||
3F630070 a5 = *(a11+0x160) ; <<< device transmit method
|
||
3F630090 call a5 ; -> 0x3F607C90
|
||
```
|
||
|
||
Offsets are **bit** offsets, so `0x10/0x30/0x40/0x50` are bytes 2, 6, 8 and 10 —
|
||
total length, fragment word, TTL and header checksum: an ordinary IPv4 header
|
||
fix-up, byte-swapping the two network-order words and recomputing the checksum.
|
||
|
||
### 22.7 The device transmit method is a pointer in the device struct
|
||
|
||
The trace shows the dispatch literally (`icmpw.tr`, around line 3614):
|
||
|
||
```
|
||
3F630070: MOVE *A11(160h),A5,1 ; device + 0x160 = transmit method
|
||
3F630090: CALL A5
|
||
3F607C90: MMTM SP,A5,A7,A9-A11,A13 ; the method
|
||
```
|
||
|
||
`0x3F607C90` then checks a device flag word (`*(a9+0x40) & 0x41 == 0x41`),
|
||
byte-swaps its own checksum field, sets the minimum length `0x32`, and calls the
|
||
driver send `0x3F6084E0` — which is the function whose `0x32` clamp and wrapper
|
||
call were documented in section 21. The full stack is therefore:
|
||
|
||
```
|
||
application / sockets
|
||
-> ip_output 0x3F62EA00 (IP header, checksum, route)
|
||
-> device->transmit *(dev+0x160) = 0x3F607C90
|
||
-> driver send 0x3F6084E0 (request build, 0x32 clamp)
|
||
-> wrapper 0x3F9DC890
|
||
-> driver command entry 0x3F9D4BC0 (ring arm)
|
||
-> 0x3F9D57F0 CSR0 = shadow | TDMD
|
||
```
|
||
|
||
### 22.8 What this settles, and what it does not
|
||
|
||
Settled with trace + disassembly evidence:
|
||
|
||
* the ICMP type test is a **19-entry jump table on the type byte**
|
||
(`0x3F622580`), reached by `jr` with the 8-bit field size already set, and the
|
||
earlier "no `CMPI #8`" finding was an encoding error in my search (`CMPI`
|
||
immediate is one's-complemented);
|
||
* the echo request is answered by `movb 0,*(icmp+0)` in `0x3F6212A0`, then the
|
||
shared reply path adjusts the IP total length and counts the reply;
|
||
* the reply's transport checksum is computed at transport-header byte 2 with the
|
||
same helper used for receive-side verification;
|
||
* the IP transmit finalisation (`0x3F62EA00`) byte-swaps total length and the
|
||
fragment word and recomputes the IP header checksum at byte 10 (TTL 255 is set
|
||
by the reflect function `0x3F622A40`);
|
||
* the protocol layer reaches the driver through a **function pointer at
|
||
device+0x160**, not a fixed call;
|
||
* the driver's transmit path is the `shadow | TDMD` kick of section 10.
|
||
|
||
Not settled: the internal TCP state machine (the `estab`/`listen` transitions
|
||
behind `0x3F635910` and `0x3F645170`), the socket-API binding table itself (entry
|
||
points listed in section 19 and `dwt/notes/TCPIP.md`), and the routing/ARP tables
|
||
behind `0x96AC0` and `0x3F6156B0`. The hardware half — whether real silicon
|
||
services `TDMD` with the corrected command — is still unverified because the
|
||
unit is down.
|
||
|
||
The dispatch itself, and the TCP and UDP *entry* paths with their checksum,
|
||
list-walk and delivery calls, are in section 23.
|
||
|
||
### 22.9 Receive-side entry: checksum verification before the type dispatch
|
||
|
||
The receive half of the same function is visible in the trace just before the
|
||
dispatch (lines 2736-2762 of `icmpw.tr`):
|
||
|
||
```
|
||
3F6206D0 calla 0x3FB2C290 ; ICMP checksum over the received message
|
||
3F620700 a5 = a8
|
||
3F620710 jreq 0x3F6207B0 ; checksum OK -> continue
|
||
3F620720 *0x96800 += 1 ; <<< ICMP checksum-error counter
|
||
3F620790 jruc 0x3F6229A0 ; drop
|
||
3F6207B0 ...
|
||
3F6209B0 setf 8
|
||
3F6209C0 a7 = *a10 ; subk 0x12 ; jrhi drop ; type range check
|
||
3F620A20 a7 += 0x96860 ; (*a7) += 1 ; received-per-type counter
|
||
3F620AC0 jr 0x3F6224E0 ; -> the jump-table dispatch
|
||
```
|
||
|
||
So the same helper `0x3FB2C290` verifies the ICMP checksum on the way in
|
||
(counter `0x96800`, and `0x96820` for malformed/unsupported types) and computes
|
||
it on the way out (`0x3F623350`), which is a consistent ones-complement sum used
|
||
for both directions. The whole receive path — checksum verify, type counter,
|
||
type dispatch, per-type handlers, reply build — is one function spanning
|
||
`0x3F620xxx`..`0x3F622A30` (epilogue `0x3F622A10`: `mmfm sp,a3,a5,a7,a9,a10,a11,a12,a13; rets 2`);
|
||
its entry and the IP-protocol test that routes into it are both pinned in
|
||
section 23: the function is `0x3F61FFA0` (ICMP input) and it is entered through
|
||
the protocol dispatch table at `0x1E0F70`/`0xD6BE0`, keyed on the IP protocol
|
||
byte.
|
||
|
||
Counters seen so far, all in RAM:
|
||
|
||
| cell | meaning |
|
||
|---|---|
|
||
| `0x96800` | ICMP checksum errors (receive) |
|
||
| `0x96820` | malformed / unsupported ICMP |
|
||
| `0x96840` | ICMP replies sent |
|
||
| `0x96860` | received per ICMP type (4 bytes/entry, `type << 5`) |
|
||
| `0x96560` | sent per ICMP type (4 bytes/entry, `type << 5`) |
|
||
|
||
## 23. IP protocol dispatch, TCP and UDP input
|
||
|
||
This is the layer above ICMP: how the stack decides which transport handler gets
|
||
a received IP packet.
|
||
|
||
### 23.1 The dispatcher — `0x3F6270F0`
|
||
|
||
```
|
||
3F6270F0 setf 8,0,0
|
||
3F6270F0 a5 = *(a9+0x48) ; <<< IP protocol byte (byte 9)
|
||
3F627110 a5 <<= 3 ; 1 byte per table entry
|
||
3F627120 a5 += 0x1E0F70 ; protocol -> index table (RAM)
|
||
3F627150 a5 = *a5,0 ; index
|
||
3F627160 a7 = 0x170
|
||
3F627180 mpys a7,a5 ; index * 0x170 bits (46 bytes per block)
|
||
3F627190 a5 += 0xD6BE0 ; protocol block array (RAM)
|
||
3F6271C0 a5 = *(a5+0x50) ; <<< handler pointer (block byte 10)
|
||
3F6271E0 call a5
|
||
```
|
||
|
||
Two RAM structures drive it: a **256-entry byte table** at bit `0x1E0F70`
|
||
(protocol -> block index) and an array of **46-byte protocol blocks** at bit
|
||
`0xD6BE0`.
|
||
|
||
### 23.2 The table, read from a live RAM dump
|
||
|
||
From `ramdump3/vt1200-ram-5500.bin` (bit address -> byte `bit/8`):
|
||
|
||
```
|
||
table 0x1E0F70: 03 04 03 03 03 03 02 03 ... 03 01 03 ...
|
||
proto: 0 1 2 3 4 5 6 7 ... 16 17 18 ...
|
||
```
|
||
|
||
| proto | index | block (bit) | handler | |
|
||
|---|---|---|---|---|
|
||
| 1 ICMP | 4 | `0xD71A0` | `0x3F61FFA0` | ICMP input (section 22) |
|
||
| 6 TCP | 2 | `0xD6EC0` | `0x3F63A340` | TCP input |
|
||
| 17 UDP | 1 | `0xD6D50` | `0x3F6540C0` | UDP input |
|
||
| everything else (253 values) | 3 | `0xD7030` | `0x3F634600` | default / unreachable |
|
||
|
||
Block layout, verified in all four blocks (UDP block bytes 0..13 shown):
|
||
|
||
```
|
||
02 00 80 74 | 0d 00 | 11 00 | 03 00 | c0 40 65 3f
|
||
^+4 ^+6 ^+8 ^+10 handler = 0x3F6540C0
|
||
+6 = 17 = UDP (TCP 6, ICMP 1, default 255)
|
||
```
|
||
|
||
So a protocol block carries its own protocol number at byte 6 and its input
|
||
function at byte 10 — the dispatcher does not need the number once it has the
|
||
block.
|
||
|
||
### 23.3 Trace evidence
|
||
|
||
| trace | dispatches (`3F6270F0`) | ICMP `3F61FFA0` | TCP `3F63A340` | UDP `3F6540C0` | default `3F634600` |
|
||
|---|---|---|---|---|---|
|
||
| `icmpw.tr` (ping flood) | 344 | **343** | 0 | 1 | 0 |
|
||
| `rttcp2.tr` (80 TCP connects + UDP) | 56 | 0 | **44** | **12** | 0 |
|
||
|
||
56 = 44 + 12 exactly, i.e. every dispatch in the second trace is accounted for
|
||
by one of the two handlers, and the ICMP trace sends essentially all traffic to
|
||
the ICMP entry. `rttcp2.tr` was recorded in **real time** (no `-nothrottle`), so
|
||
wall-clock traffic from the host lands inside the traced frame window.
|
||
|
||
### 23.4 TCP input — `0x3F63A340` .. `0x3F643C40`
|
||
|
||
One function (prologue `mmtm sp,a0,a1,a3,a5,a7,a9,a10,a11,a12,a13`, single
|
||
`rets 2` at `0x3F643C40`). Head of the function:
|
||
|
||
```
|
||
3F63A340 mmtm sp,a0,a1,a3,a5,a7,a9,a10,a11,a12,a13
|
||
3F63A430 *0x97590 += 1 ; TCP packets received
|
||
a10 = packet ; a5 = a10 + (*(a10+0x20)<<3) ; a9 = a5
|
||
3F63A510 setf 4 ; a5 = *a9 ; 4-bit IHL
|
||
3F63A530 subk 5,a5 ; jrls ... ; IHL >= 5 words
|
||
3F63A5B0 length checks (0x7c, 0x28)
|
||
3F63A660 calla 0x3F6B0160 ; pull the header up / trim
|
||
3F63A700 *0x97630 += 1 ; TCP error counter
|
||
```
|
||
|
||
Ordered call sequence for the first TCP segment (a SYN to port 6000) taken from
|
||
`rttcp2.tr` — 8,835 traced instructions, 233 calls:
|
||
|
||
```
|
||
3F63A980 -> 3FB2C000 htons (ports)
|
||
3F63AA20 -> 3FB2C290 <<< TCP checksum verification
|
||
3F63B160 -> 3F6AC3C0 lock
|
||
3F63B360 -> 3FB44400 queue/memory helper
|
||
3F63B510 -> 3FB44400
|
||
3F63B670 -> 3FB2C140 compare helper
|
||
3F63B6F0 -> 3FB2C140 compare helper
|
||
3F63B790 -> 3FB2C000 htons
|
||
3F63B830 -> 3FB2C000 htons
|
||
3F63B9F0 -> 3F61E490 <<< TCB / connection list walk
|
||
3F63BC80 -> 3F6BE610
|
||
3F6500E0 -> ...
|
||
3F653230 -> 3F61B000 list operation
|
||
3F653320 -> 3F64BC70
|
||
3F652630 -> 3F635910 connection processing
|
||
```
|
||
|
||
Per-edge counts over all 44 segments:
|
||
|
||
```
|
||
44 3F63A980 -> 3FB2C000 htons
|
||
44 3F63AA20 -> 3FB2C290 TCP checksum
|
||
44 3F63B670 -> 3FB2C140 compare
|
||
44 3F63B6F0 -> 3FB2C140 compare
|
||
44 3F63B790 -> 3FB2C000 htons
|
||
44 3F63B830 -> 3FB2C000 htons
|
||
44 3F63B9F0 -> 3F61E490 TCB list walk
|
||
33 3F6431F0 -> 3F635910 per-connection processing
|
||
33 3F6432D0 -> 3F645170
|
||
22 3F642BF0 -> 3F6ADF20 logging
|
||
22 3F640EA0 -> 3F6C30F0
|
||
11 3F63B160 -> 3F6AC3C0 lock
|
||
11 3F63C450 -> 3F61AB30
|
||
11 3F63C760 -> 3F61BFC0
|
||
11 3F63C8C0 -> 3F64A710
|
||
11 3F63CA90 -> 3F643C50
|
||
11 3F63F780 -> 3F6448E0
|
||
```
|
||
|
||
So every segment gets ports byte-swapped and a checksum verified, is matched
|
||
against the connection table by the list walk `0x3F61E490`, and 11 of the 44
|
||
went on to the data-delivery path (`0x3F61AB30`, `0x3F61BFC0`, `0x3F6448E0`)
|
||
while 33 took the connection-state path.
|
||
|
||
### 23.5 UDP input — `0x3F6540C0` .. `0x3F655070`
|
||
|
||
Head of the function (`mmtm sp,a3,a5,a7,a9,a10,a11,a13`), and the calls it makes
|
||
for all 12 datagrams in the same trace:
|
||
|
||
```
|
||
3F654440 -> 3FB2C000 htons (ports) x12
|
||
3F6547F0 -> 3FB2C290 <<< UDP checksum x12
|
||
3F654AA0 -> 3F61E490 <<< socket/PCB list walk x12
|
||
3F654B40 -> 3F61AB30 socket delivery prep x12
|
||
3F654C60 -> 3F61EC00 deliver to socket x11
|
||
3F655000 -> 3F6ADF20 log / drop x1
|
||
```
|
||
|
||
12 datagrams, 11 delivered to a bound socket, 1 unmatched (the probe sent to a
|
||
closed port) and logged. The same checksum helper `0x3FB2C290` verifies UDP here
|
||
as it verifies ICMP and TCP — one ones-complement routine for all three.
|
||
|
||
### 23.6 Where the transport layer hands back to the driver
|
||
|
||
Both TCP and UDP delivery paths funnel into the reply/output machinery of
|
||
section 22: `0x3F61EC00`/`0x3F61B000` lead to `0x3F622A40` (reflect),
|
||
`0x3F623350` (transport checksum), `0x3F62EA00` (IP transmit finalisation) and
|
||
the device method `*(device+0x160)`. A UDP datagram in `rttcp2.tr` shows exactly
|
||
that continuation:
|
||
|
||
```
|
||
3F654440 -> 3FB2C000 htons
|
||
3F6547F0 -> 3FB2C290 UDP checksum verify
|
||
3F654AA0 -> 3F61E490 socket lookup
|
||
3F654C60 -> 3F61EC00 deliver
|
||
3F61F350 -> 3F6AC3C0 lock
|
||
3F61FA50 -> 3FB2C000 htons
|
||
3F61FE80 -> 3F622A40 <<< reply: reflect/send path
|
||
3F623080 -> 3F623350 transport checksum
|
||
3F6238C0 -> 3F62EA00 <<< IP transmit finalisation
|
||
```
|
||
|
||
So the complete receive-to-transmit loop is closed: LANCE interrupt -> RX poll ->
|
||
IP protocol dispatch -> transport input -> socket -> output -> IP transmit ->
|
||
device method -> ring -> `TDMD`.
|
||
|
||
### 23.7 Repro and one tooling trap
|
||
|
||
Read the dispatch tables from a RAM dump:
|
||
|
||
```sh
|
||
python3 - <<'PY'
|
||
import struct
|
||
d=open('vt1200-ram-5500.bin','rb').read()
|
||
tab=d[0x1E0F70>>3:(0x1E0F70>>3)+256]
|
||
for p in (1,6,17,0):
|
||
idx=tab[p]; blk=0xD6BE0+idx*0x170
|
||
h=struct.unpack("<I", d[(blk>>3)+10:(blk>>3)+14])[0]
|
||
print("proto %d idx %d block %#x handler %#x" % (p,idx,blk,h))
|
||
PY
|
||
```
|
||
|
||
Trap worth recording: a **stale MAME instance keeps the tap device** and happily
|
||
answers pings and TCP connects on `192.168.99.2`. Two runs produced traces with
|
||
zero network activity while the host was successfully connecting — the traffic
|
||
was going to an orphaned instance that was not the one being traced. Check
|
||
`pgrep -af mame0289/vt1200` before trusting a "no packets" trace.
|
||
|
||
## 24. The TCP state machine
|
||
|
||
The TCP input function of section 23.4 (`0x3F63A340`..`0x3F643C40`) contains the
|
||
state machine: one state word in the TCB, two jump tables over it, and the
|
||
transition code. All evidence below is ROM disassembly plus execution counts
|
||
from `rttcp2.tr` (44 TCP segments over 11 completed connections).
|
||
|
||
### 24.1 The state field
|
||
|
||
The state is a 16-bit word at **TCB bit offset `0x40`** (byte 8). Every write to
|
||
it in the TCP input function, with its immediate:
|
||
|
||
| site | value written | state |
|
||
|---|---|---|
|
||
| `0x3F63BE90` | `movk 1` | LISTEN |
|
||
| `0x3F63D980` | `movk 3` | SYN_RECEIVED |
|
||
| `0x3F63D4F0` | `movk 4` | ESTABLISHED |
|
||
| `0x3F63F740` | `movk 4` | ESTABLISHED (from the SYN_RECEIVED handler) |
|
||
| `0x3F6413F0` | `movk 10` | TIME_WAIT |
|
||
|
||
and the read that drives both dispatchers is the same field:
|
||
|
||
```
|
||
3F63F290 a8 = *(a11+0x40) ; state
|
||
3F63F2B0 subk 3,a8
|
||
3F63F2C0 cmpi 7,a8 ; jrhi default ; states 3..10 index the table
|
||
```
|
||
|
||
### 24.2 Two dispatch tables over states 3..10
|
||
|
||
Table A at `0x3F63F350` (reached at `0x3F63F340`), table B at `0x3F6417C0`
|
||
(reached at `0x3F6417B0`). Both are 8 entries of 4 bytes, indexed by
|
||
`state - 3`, i.e. states 3..10. Read from the ROM image:
|
||
|
||
| state | table A | table B |
|
||
|---|---|---|
|
||
| 3 SYN_RECEIVED | `0x3F63EFE0` | **`0x3F63F570`** |
|
||
| 4 ESTABLISHED | `0x3F63F060` | **`0x3F63F980`** |
|
||
| 5 CLOSE_WAIT | `0x3F63F060` | `0x3F63F980` |
|
||
| 6 FIN_WAIT_1 | `0x3F63F060` | `0x3F63F980` |
|
||
| 7 CLOSING | `0x3F63F200` | `0x3F63F980` |
|
||
| 8 LAST_ACK | `0x3F63F200` | `0x3F63F980` |
|
||
| 9 FIN_WAIT_2 | `0x3F63F060` | `0x3F63F980` |
|
||
| 10 TIME_WAIT | `0x3F63F200` | `0x3F63F980` |
|
||
|
||
States **0, 1, 2** (CLOSED, LISTEN, SYN_SENT) fall out of both tables through
|
||
`jrhi` (`state-3` is unsigned-large): table A's default is `0x3F63F450`, table
|
||
B's default is `0x3F6418C0`.
|
||
|
||
### 24.3 What actually executed
|
||
|
||
```
|
||
3F63F2C0 / 3F63F340 (table A dispatch) 0 <- never: every state seen was < 3
|
||
3F63F450 (table A default) 33
|
||
3F641730 / 3F6417B0 (table B dispatch) 33
|
||
3F6417B0 targets: 3F63F570 x11 <- SYN_RECEIVED
|
||
3F63F980 x22 <- established-family states
|
||
3F63F570 (SYN_RECEIVED handler) 11
|
||
3F63F980 (established handler) 33
|
||
```
|
||
|
||
The per-segment flow is a perfectly repeating 4-segment cycle (11 connections
|
||
× 4 = 44 segments):
|
||
|
||
```
|
||
seg 0 alloc/copy -> table-B default -> alloc/copy -> tcp-output -> alloc/copy
|
||
seg 1 A-default -> B:SYN_RECEIVED -> B:other -> B-default -> alloc -> output -> alloc
|
||
seg 2 A-default -> B:other -> ... (data, several outputs)
|
||
seg 3 A-default -> B:other
|
||
```
|
||
|
||
and the state writes inside those segments:
|
||
|
||
| segment | state write observed | count |
|
||
|---|---|---|
|
||
| 0 (SYN) | `0x3F63BE90` → state = **LISTEN** | 11 |
|
||
| 1 (handshake ACK) | `0x3F63F740` → state = **ESTABLISHED** | 11 |
|
||
| 2 (data) | none; data counter `0x3F63FA90` | 11 |
|
||
|
||
So a connection is seen going **CLOSED → LISTEN → (SYN_RECEIVED) → ESTABLISHED**,
|
||
11 times, which is exactly the number of completed TCP connections in that run.
|
||
|
||
### 24.4 The handlers
|
||
|
||
**SYN_RECEIVED — `0x3F63F570`** (state 3, the path that completes the handshake):
|
||
|
||
```
|
||
3F63F570 a7 = *(a11+0x120) ; TCB send-sequence field
|
||
3F63F590 a5 = *(a9+0x0E0) ; segment sequence field
|
||
3F63F5B0 cmp ; jrgt 0x3F643460 ; out of window -> drop
|
||
3F63F5E0 a7 = *(a9+0x0E0) ; a5 = *(a11+0x280) ; cmp ; jrgt drop
|
||
3F63F650 *0x972B0 += 1 ; SYN_RECEIVED segment counter (x11)
|
||
3F63F6F0 calla 0x3F6BD9F0
|
||
3F63F720 movk 4,a7
|
||
3F63F740 *(a11+0x40) = 4 ; <<< state := ESTABLISHED (x11)
|
||
3F63F780 calla 0x3F6448E0 ; send (ACK)
|
||
3F63F7C0 window arithmetic on *(a11+0xC0)
|
||
```
|
||
|
||
**Established family — `0x3F63F980`** (states 4,5,6,7,8,9,10):
|
||
|
||
```
|
||
3F63F980 a7 = *(a9+0x0E0) ; a5 = *(a11+0x120) ; cmp ; jrgt 0x3F6400C0
|
||
3F63F9E0 a7 = *(a9+0x50) ; jrne 0x3F640070 ; segment length/flags
|
||
3F63FA20 a7 = *(a9+0x110) ; a5 = *(a11+0x1E0) ; cmp ; jrne 0x3F640070
|
||
3F63FA90 *0x97790 += 1 ; data segment counter (x11)
|
||
3F63FB00 a5 = *(a11+0x50) ; jreq ...
|
||
```
|
||
|
||
**States below SYN_RECEIVED — `0x3F6418C0`** (table B default; the SYN path):
|
||
|
||
```
|
||
3F6418C0 btst 4,a12 ; segment flag bit 4 = ACK
|
||
3F6418D0 jreq 0x3F641DB0
|
||
3F6418E0 a7 = *(a11+0x180) ; a5 = *(a9+0xC0) ; cmp ; jrlt 0x3F641AD0
|
||
3F6419A0 a7 = *(a11+0x1A0) ; a5 = *(a9+0xE0) ; cmp ; jrlt 0x3F641AD0
|
||
...
|
||
```
|
||
|
||
### 24.5 TCB fields used by the machine
|
||
|
||
Only the fields the state code actually touches are listed; `a11` = TCB,
|
||
`a9` = parsed segment:
|
||
|
||
| TCB offset (bit) | use |
|
||
|---|---|
|
||
| `0x40` (byte 8) | state (24.1) |
|
||
| `0x50` (byte 10) | flags/queue marker (`jreq` gates in `0x3F63F980`) |
|
||
| `0xC0` (byte 24) | sequence field used in window arithmetic |
|
||
| `0x120` | send-sequence, compared against segment `+0xE0` |
|
||
| `0x180` | receive-sequence, compared against segment `+0xC0` |
|
||
| `0x1A0` | receive-sequence, compared against segment `+0xE0` |
|
||
| `0x1E0` | compared against segment `+0x110` |
|
||
| `0x280` | send-sequence bound, compared against segment `+0xE0` |
|
||
| `0x2A0` | alternative bound (min/max selection) |
|
||
| `0x2D0` | store in the SYN path |
|
||
|
||
Segment (not TCB) fields: `+0x50` length, `+0xC0` and `+0xE0` sequence words,
|
||
`+0x110` acknowledgement/sequence word.
|
||
|
||
Two 32-bit sequence spaces are maintained in the TCB and validated against the
|
||
segment before any state change — the jump to `0x3F643460` on failure is the
|
||
drop path.
|
||
|
||
### 24.6 The two helper calls seen in every segment
|
||
|
||
```
|
||
0x3F635910 "alloc/copy" 44 (from 0x3F652630, once per segment)
|
||
33 (from 0x3F6431F0), 33 (from 0x3F6489A0)
|
||
0x3F645170 "tcp output" 33 (from 0x3F6432D0), 22 (from 0x3F653A10)
|
||
```
|
||
|
||
`0x3F635910` allocates from a pool at bit `0x97870`: index from `0x97D70`,
|
||
address = `0x97870 + index*(1024+256)` bits (160-byte records), then fills
|
||
`+0x20`, `+0x30`, `+0x40` and copies 26 words to `+0x1B0`. `0x3F645170` reads
|
||
`*(tcpcb+0x100)` → `+0xC0`, then `+0x280`, `+0x120`, `+0x140`, `+0x1E0`,
|
||
`+0x2A0` and a flag byte at `+0xD0` — the send/window arithmetic of an output
|
||
segment.
|
||
|
||
### 24.7 Counters seen in this path
|
||
|
||
| cell | meaning | observed |
|
||
|---|---|---|
|
||
| `0x97590` | TCP segments received | 44 |
|
||
| `0x97630` | TCP input errors | — |
|
||
| `0x972B0` | segments handled in SYN_RECEIVED | 11 |
|
||
| `0x97790` | data segments handled in established states | 11 |
|
||
| `0x97D70` | pool index for `0x3F635910` | — |
|
||
|
||
### 24.8 Still open in TCP
|
||
|
||
The option parsing (MSS/window-scale) and the close path (`0x3F63F200`,
|
||
`0x3F6413F0` writing TIME_WAIT, never exercised by this traffic), the exact
|
||
meaning of the TCB fields beyond the sequence comparisons, and the socket
|
||
binding table behind `0x3F61E490`/`0x3F61EC00`.
|
||
|
||
## 25. Interface list, and a correction about the "ARP table"
|
||
|
||
### 25.1 The interface list at `0x96AC0`
|
||
|
||
`0x96AC0` is the head cell of the interface list. The lookup `0x3F623180`
|
||
walks it and compares the requested address against `*(iface+0x100)`:
|
||
|
||
```
|
||
3F623200 a9 = *0x96AC0
|
||
3F623230 jreq not-found
|
||
3F623240 a11 = *(a9+0x100) ; the interface's address object
|
||
3F623260 a7 = requested address
|
||
3F623280 cmp a7,a11 ; jrne next
|
||
3F6232A0 return a9 ; found
|
||
3F6232C0 a9 = *(a9+0x200) ; jrne loop ; <<< next interface
|
||
```
|
||
|
||
Read from the RAM dump (this boot ran the MAME harness, so the address is the
|
||
harness's own `192.168.99.2`; the structure is what matters):
|
||
|
||
```
|
||
iface at bit 0x7AA060:
|
||
+0x00 (byte 0) u32 0x00000002 family = AF_INET
|
||
+0x04 (byte 4) u32 0x0263A8C0 address -> C0 A8 63 02 = 192.168.99.2
|
||
+0x10 (byte 8) u32 0x00000002 family (second address)
|
||
+0x14 u32 0xFF63A8C0 broadcast -> 192.168.99.255
|
||
+0x20 u32 0x001DA570 pointer to the address/flags object
|
||
+0x28 u32 0xC0A86300 network -> 192.168.99.0
|
||
+0x2C u32 0xFFFFFF00 netmask -> 255.255.255.0
|
||
+0x200 u32 0x00000000 next interface (single-interface boot)
|
||
```
|
||
|
||
The address/flags object at `0x1DA570` is what `0x3F622A40` inspects
|
||
(`*(*(iface+0x100)+0x40)`, bit 1) before sending a reply: that bit is the
|
||
interface's "usable" flag.
|
||
|
||
### 25.2 Correction: `0x1E0570` / `0x1E0600` are a hex-digit table, not ARP data
|
||
|
||
The routine `0x3F615610` was described in section 17 as an ARP/route table
|
||
serialiser. It is a **MAC-address formatter**. The evidence is the table
|
||
contents themselves:
|
||
|
||
```
|
||
bit 0x1E0600 (byte 0x3C0C0):
|
||
30 31 32 33 34 35 36 37 38 39 61 62 63 64 65 66 "0123456789abcdef"
|
||
```
|
||
|
||
and the loop:
|
||
|
||
```
|
||
a7 = *a9 >> 4 ; high nibble
|
||
a7 <<= 3 ; a7 += 0x1E0600 ; *a11++ = *a7 ; -> hex character
|
||
a7 = *a9++ & 0xF ; low nibble
|
||
a7 <<= 3 ; a7 += 0x1E0600 ; *a11++ = *a7
|
||
*a11++ = 0x3A ; ':' separator
|
||
inc a10 ; cmpi 6,a10 ; jrlt loop
|
||
```
|
||
|
||
Six bytes, each rendered as two hex digits, separated by `:` — the terminal's
|
||
own MAC address in the form `08:00:2b:16:4b:5a`, which is exactly what the
|
||
Terminal Manager displays. The `arpre…`/`mods …`/`glev…` strings that follow in
|
||
the image are unrelated version headers (`in.c`, `in_pcb.c`).
|
||
|
||
### 25.3 ARP: pointer to section 26
|
||
|
||
The ARP cache, the resolve path and the ARP input function **were** located
|
||
afterwards — see section 26. The earlier statement here ("no ARP entry
|
||
structure was found") was wrong because the entry is 16 bytes inside a hashed
|
||
bucket, not a standalone structure, and because the fragment searched for was
|
||
the gateway address in the wrong byte order.
|
||
|
||
Consequence for the next pass: ARP is handled **below** IP, on the Ethernet
|
||
EtherType (0x0806) rather than the IP protocol byte, so it will not appear in the
|
||
dispatch table of section 23. The next thing to pin is the EtherType demux in
|
||
the receive path (`0x0800` vs `0x0806` vs others), which will also give the
|
||
`0x3F61FFA0`-equivalent entry for ARP. Note when searching for those compares
|
||
that `CMPI` stores its immediate one's-complemented (section 20), so `0x0806`
|
||
appears as `0xF7F9` and `0x0800` as `0xF7FF`.
|
||
|
||
## 26. ARP: the cache and the input path
|
||
|
||
Section 25.3 said the ARP cache was not found. That was wrong — it is found
|
||
below, and the reason the earlier searches missed it is recorded in 26.5.
|
||
|
||
### 26.1 ARP is present (BSD strings)
|
||
|
||
The ROM carries the usual BSD ARP diagnostics, which is what identifies the
|
||
implementation:
|
||
|
||
```
|
||
bit 0x3F6158D0 "arpresolve: no free entry"
|
||
bit 0x3F6159D0 "arp: ether address is broadcast for IP address ..."
|
||
bit 0x3F615C60 "...: %s." (log format used by the ARP code)
|
||
```
|
||
|
||
and the code that references them:
|
||
|
||
```
|
||
bit 0x3F610660 movi 0x3F6158D0 -> "arpresolve: no free entry"
|
||
bit 0x3F6120A0 movi 0x3F6159D0 -> broadcast-MAC warning
|
||
```
|
||
|
||
### 26.2 The ARP cache — 19 buckets × 9 entries
|
||
|
||
`0x3F614200` is the lookup/allocate routine:
|
||
|
||
```
|
||
3F614240 a7 = 0xFFFF ; *(a13) = a7 ; "best slot" seed
|
||
3F614280 a7 = *0x1E0550 ; jreq continue ; ARP lock/flag cell
|
||
3F6142C0 *0x1E0550 = a11
|
||
3F614300 calla 0x3F6FC200(0x95DB0, 0x3F60EE00) ; deferred work
|
||
L3F6143B0:
|
||
3F6143D0 a5 = *arg ; the IP address
|
||
3F6143E0 movk 19,a7
|
||
3F6143F0 modu a7,a5 ; <<< hash = IP mod 19
|
||
3F614410 a7 = a5 << 3 ; a7 += a5 ; *9
|
||
3F614430 a7 <<= 7 ; *128 => *1152 bits = 144 bytes
|
||
3F614450 a10 = 0x1DAF60 + hash*1152 ; <<< ARP cache base
|
||
L3F614620 (9-iteration walk, a10 += 0x80 bits per step):
|
||
a7 = *(a10+0x58) ; byte 11: entry state
|
||
jreq -> empty slot, use it
|
||
btst 2,a7 ; jrne next ; bit 2: entry not usable
|
||
a7 = *(a10+0x50) ; byte 10: 16-bit aging/LRU field
|
||
cmp with the running best -> remember this entry
|
||
```
|
||
|
||
So the cache is **19 buckets keyed by `IP mod 19`, each holding 9 entries of
|
||
16 bytes** (9 × 16 = 144 bytes), based at bit `0x1DAF60`.
|
||
|
||
### 26.3 The entry layout
|
||
|
||
From the allocation path:
|
||
|
||
```
|
||
3F614820 a7 = arg (pointer to the IP address)
|
||
3F614840 *a10 = *a7 ; <<< entry +0 = IP address (32-bit)
|
||
3F614850 movk 1,a5
|
||
3F614860 *(a10+0x58) = 1 ; <<< entry byte 11 = state (1 = incomplete)
|
||
3F614880 return a10
|
||
```
|
||
|
||
and from the input path (`0x3F6152A0`, `0x3F6152D0`) the state byte at `+0x58`
|
||
and the 16-bit field at `+0x50` are updated when a reply arrives. MAC storage
|
||
sits in the bytes between (the broadcast check at `0x3F6120A0` compares the
|
||
sender's 6-byte address), but the exact offset was not pinned by this pass.
|
||
|
||
### 26.4 ARP input — `0x3F6148E0`
|
||
|
||
```
|
||
3F6148E0 mmtm sp,a3,a5,a7,a9,a10,a11,a12,a13
|
||
3F614920 a9 = arg
|
||
3F614950 a7 = *(a9) ; 16-bit field (ARP opcode area)
|
||
3F614960 subk 2,a7 ; jrne ... ; <<< test for opcode 2 (reply)
|
||
3F614980 a7 = *(a9+0x80) ; jreq ...
|
||
...
|
||
3F6152A0 *(a10+0x58) = a5 ; entry state
|
||
3F6152D0 *(a10+0x50) = a5 ; entry aging field
|
||
3F6154F0 subk 2,a8 ; second opcode test (request/reply split)
|
||
3F615800 cmpi 6,a10 ; 6-byte hardware-address loop
|
||
```
|
||
|
||
The `cmpi 6` at `0x3F615800` is the Ethernet-address length loop — the point
|
||
where the 6-byte MAC is copied into the cache entry.
|
||
|
||
### 26.5 Why the earlier searches missed ARP — and what is still open
|
||
|
||
Three searches came back empty, and each is a real fact worth recording:
|
||
|
||
| search | result |
|
||
|---|---|
|
||
| `cmpi 0x806` anywhere in the ROM | **0 sites** (with the corrected one's-complement encoding) |
|
||
| word `0x00000806` in the frame-5500 RAM dump | **0 occurrences** |
|
||
| `cmpi 0x800` (IPv4 EtherType), 19 sites in ROM | **none execute** in either trace (`icmpw.tr`, `rttcp2.tr`) |
|
||
|
||
So the Ethernet demux does **not** compare the EtherType with a literal
|
||
`cmpi`. ARP input `0x3F6148E0` is therefore reached by some other classification
|
||
— a table lookup or a masked compare — and that classification (the receive-side
|
||
EtherType demux) remains **the open item** in this area. Practical note for the
|
||
next attempt: the ARP input is only exercised when a peer has to be resolved, so
|
||
a trace that starts with an empty ARP cache (fresh nvram) and pings the terminal
|
||
from a new source address is the cheapest way to catch it.
|
||
|
||
## 27. The one claim that needs real silicon, and how to test it
|
||
|
||
Everything in this report is reachable from the ROM image and from MAME traces,
|
||
with two exceptions — both about the same claim:
|
||
|
||
* **the transmit kick:** the firmware writes `CSR0 = shadow | TDMD`, never a bare
|
||
`TDMD` (section 10). MAME reproduces this: the loopback payload returns
|
||
`ok=1, rmd1=0x0300, mcnt=0x1C`.
|
||
* **the same payload on real silicon** completes `INIT → IDON → STRT`
|
||
(`CSR0 = 0x1B3`, then a stable `0x33`) but with the **bare-TDMD** kick the
|
||
transmitter never services the demand: `TDMD` stays set and the TX descriptor
|
||
stays owned (`TMD1 = 0x8338`). The corrected payload has **not** been run on
|
||
the machine yet.
|
||
|
||
Status: **deferred** — the VT1200 is not available (taken off site), so this is
|
||
not waiting on a power cycle any more, it is waiting on the hardware being back.
|
||
|
||
When it is, this is the whole procedure:
|
||
|
||
```sh
|
||
# 1. the unit answers (this is the gate — without it, stop)
|
||
ping -c1 -W2 100.103.100.103
|
||
|
||
# 2. build the payload (COFF, loaded by the existing loader)
|
||
harness/udpstream/build-lance.sh
|
||
|
||
# 3. run it through the loader, watching the marker output
|
||
# (same invocation used all session; LOADER_COFF is the payload)
|
||
# expected on success:
|
||
# ok=1 rmd1=0x0338 (OWN clear, STP|ENP) mcnt=0x001C txmd1=0x0338
|
||
# failure signature to watch for:
|
||
# csr0 keeps bit 3 (TDMD) set, txmd1 keeps bit 15 (OWN) set -> 0x8338
|
||
```
|
||
|
||
What to compare, in order:
|
||
|
||
| observation | meaning |
|
||
|---|---|
|
||
| `CSR0` bit 3 (`TDMD`) clears | the chip serviced the demand — the whole question |
|
||
| `TMD1` loses bit 15 (`OWN`) | the descriptor was consumed |
|
||
| `RMD1 = 0x0338`, `MCNT = 0x001C` | the loopback frame came back: OWN clear, STP\|ENP, 28 bytes of data including the 4-byte FCS |
|
||
|
||
If `TDMD` clears but no frame arrives, the remaining suspect is the loopback
|
||
setup rather than the kick, and `LANCE_LOOPBACK_REPORT.md` covers what MAME
|
||
does differently there (`RMD1_FRAM`).
|
||
|
||
> **Correction — see section 41.2/41.3.** `CSR0 = 0x1BB` is
|
||
> `IDON|INIT|STRT|TDMD|TXON|RXON|INTR` with `ERR` **clear**; there is no memory
|
||
> error. The anomaly is `INIT` still being set while `IDON` is set, which points
|
||
> at the initialization sequence (the datasheet wants `STOP` set before `INIT`)
|
||
> rather than at the transmit kick.
|
||
|
||
## 28. Where a received frame becomes an IP packet — and why ARP still will not run
|
||
|
||
### 28.1 The ARP dispatcher has exactly one caller
|
||
|
||
```
|
||
CALLA 0x3F6148E0 (ARP input) <- 0x3F65A790
|
||
```
|
||
|
||
`0x3F65A790` is inside `0x3F65A570`, a small dispatcher whose entry is
|
||
(see section 30.2: this dispatcher is **not** on the IPv4 receive path — it has
|
||
zero executions in every trace)
|
||
|
||
```
|
||
3F65A570 mmtm sp,a7,a9,a10,a11,a13
|
||
3F65A590 move a14,a13 ; addk 32,a14
|
||
3F65A5B0 jruc L3F65A7E0
|
||
```
|
||
|
||
and whose body selects a handler by folding constants into one register:
|
||
|
||
```
|
||
L3F65A7E0 a8 = *(a13-0x40) ; the type word argument
|
||
3F65A800 addi 0x7FDB96E2,a8 ; jreq 0x3F65A660
|
||
3F65A840 subk 2,a8 ; jreq 0x3F65A660
|
||
3F65A860 subi 0x3FE3FFF4,a8 ; jreq 0x3F65A5C0
|
||
3F65A8A0 subi 0x1C000B,a8 ; jreq 0x3F65A740 -> ARP input
|
||
default -> 0x3F65A8E0 (calls 0x3F65A0C0, returns 6)
|
||
```
|
||
|
||
This is a compare-chain on the EtherType done as a sequence of subtract-and-test
|
||
(the compiler's form for a `switch`), with **ARP as the fourth case**. The
|
||
literal 16-bit values could not be recovered reliably from the disassembler's
|
||
immediate printing (it prints `ADDI` immediates directly but `SUBI` ones
|
||
complemented in the same listing), so the mapping "case N = 0x0806" is
|
||
established structurally — by the ARP input call — rather than by reading the
|
||
constants. Recording that rather than guessing them.
|
||
|
||
### 28.2 It never executes — and neither does any ARP traffic
|
||
|
||
| trace | `0x3F65A570` (dispatcher) | `0x3F6148E0` (ARP input) |
|
||
|---|---|---|
|
||
| `icmpw.tr` (ping flood) | 0 | 0 |
|
||
| `rttcp2.tr` (TCP/UDP) | 0 | 0 |
|
||
| `arp2.tr` (fresh nvram, pings from a **new source address**) | 0 | 0 |
|
||
|
||
The last run was built to force ARP: empty nvram directory, and a second address
|
||
`192.168.99.77/24` added to the tap interface so the terminal would have to
|
||
resolve a peer it had never spoken to. 60 of 60 pings from that address were
|
||
answered — and `tcpdump -i vt1200 arp` captured **zero** ARP packets (0-byte
|
||
pcap) for the whole exchange.
|
||
|
||
Conclusion, and it is a limitation of the test bench rather than of the
|
||
firmware: **the terminal answers to the Ethernet source MAC of the request**, so
|
||
a peer that shares the tap's MAC never causes an ARP lookup. Exercising ARP
|
||
needs a second L2 peer with its own MAC — e.g. the MAME tap attached to a bridge
|
||
with a veth pair into a network namespace. That is a harness change, not a RE
|
||
question, and it is the prerequisite for finishing section 26.
|
||
|
||
### 28.3 The queue that feeds the IP task
|
||
|
||
The IP task `0x3F624980` does not look at Ethernet headers at all. It takes
|
||
already-classified packets from a queue:
|
||
|
||
```
|
||
3F624A20 a10 = *0x96AE0 ; queue head
|
||
3F624A70 a7 = *(a10+0x3E0) ; next
|
||
3F624A90 *0x96AE0 = a7 ; head advance
|
||
3F624BB0 a7 = *(a10+0x20) ; <<3 ; a7 += a10 ; buffer chain offset
|
||
3F624BF0 *(a13+0x20) = *(a7) ; first buffer
|
||
3F624C20 *(a10+0x20) += 4 ; consume 4 address units
|
||
3F624C80 *(a10+0x40) -= 4 ; remaining length
|
||
```
|
||
|
||
and the enqueue side:
|
||
|
||
```
|
||
3F6032A0 a9 = *0x96B20 ; queue count
|
||
3F6032D0 a11 = *0x96B40 ; cmp ; watermark check
|
||
3F603540 a11 = *0x96B00 ; tail
|
||
3F603570 *(a11+0x3E0) = a9 ; link
|
||
3F603590 *0x96B00 = a9 ; tail = new
|
||
3F6035F0 *0x96B20 = count + 1
|
||
3F603630 *0x95FD0 |= 4 ; post the "IP input ready" event
|
||
```
|
||
|
||
So the receive path is: LANCE RX poll -> *classification* (the EtherType
|
||
dispatcher of 28.1 and its siblings) -> enqueue at `0x96AE0`/`0x96B00` ->
|
||
IP task `0x3F624980` -> protocol dispatch `0x3F6270F0` (section 23).
|
||
|
||
The classification step itself is reached from `0x3F6A8670`, which is inside a
|
||
function that first tests a *byte* of the type word:
|
||
|
||
```
|
||
3F6A86D0 a11 = type >> 8 ; andi 0xFF
|
||
3F6A8710 cmpi 0x72,a11 ; 0x72 = 'r'
|
||
3F6A8730 jrne 0x3F6A87C0
|
||
3F6A8780 calla 0x3F662410
|
||
```
|
||
|
||
`cmpi 0x72` on a byte of the type field means the classification also handles
|
||
non-EtherType (character-coded) protocol ids, which is consistent with this
|
||
terminal running LAT/DECnet alongside IP. That is the next thing to pin: which
|
||
byte values map to which handler, and whether `0x0800` is even the discriminator
|
||
for IPv4 on this stack.
|
||
|
||
## 29. Socket binding table, and the classifier constants decoded
|
||
|
||
### 29.1 The binding lookup — `0x3F61E490` (best-match scoring)
|
||
|
||
Called once per segment/datagram by both transports:
|
||
|
||
```
|
||
rttcp2.tr: 3F63B9F0 -> 3F61E490 x44 (TCP, every segment)
|
||
3F654AA0 -> 3F61E490 x12 (UDP, every datagram)
|
||
```
|
||
|
||
The lookup walks a linked list and scores candidates — the BSD
|
||
`in_pcbconnect` pattern (score 3 = exact, less for each wildcard):
|
||
|
||
```
|
||
3F61E4F0 movk 3,a11 ; *(a13) = a11 ; score starts at 3
|
||
3F61E510 a7 = arg1 ; list head
|
||
3F61E530 a9 = *a7 ; first entry
|
||
3F61E540 cmp a7,a9 ; jreq end ; empty list
|
||
L3F61E570:
|
||
3F61E570 a11 = *(a9+0xB0) ; <<< entry byte 22 = local port
|
||
3F61E590 a7 = arg(-0xA0) ; requested port
|
||
3F61E5B0 cmp a7,a11 ; jrne next ; port must match
|
||
3F61E600 a7 = *(a9+0x90) ; <<< entry byte 18 = local address
|
||
3F61E630 a7 = arg(-0x80) ; requested local address
|
||
(0 on either side = wildcard, score--, keep as fallback)
|
||
L3F61E750:
|
||
3F61E750 a11 = *(a9+0x60) ; <<< entry byte 12 = foreign address
|
||
3F61E780 a11 = arg(-0x40) ; requested foreign address
|
||
(again 0 = wildcard, score--)
|
||
L3F61EA20:
|
||
3F61EA20 a9 = *a9,1 ; <<< entry +0 = next pointer
|
||
```
|
||
|
||
So the binding-table entry (PCB) is:
|
||
|
||
| offset (bit) | byte | field |
|
||
|---|---|---|
|
||
| `0x00` | 0 | next pointer |
|
||
| `0x60` | 12 | foreign address (0 = wildcard) |
|
||
| `0x90` | 18 | local address (0 = wildcard) |
|
||
| `0xB0` | 22 | local port |
|
||
|
||
The specificity scoring (3 for a full match, decreasing as wildcards are
|
||
accepted) is what makes a bound socket win over a wildcard listener — visible in
|
||
the code as the `inc a11` / `*(a13+0x20)` score counter.
|
||
|
||
### 29.2 Delivery to the socket — `0x3F61EC00`
|
||
|
||
```
|
||
rttcp2.tr: 3F654C60 -> 3F61EC00 x11 (11 UDP datagrams delivered)
|
||
```
|
||
|
||
```
|
||
3F61EC00 mmtm sp,a7,a9,a10,a11,a13
|
||
3F61EC50 a11 = arg
|
||
3F61EC70 setf 4 ; 4-bit field
|
||
3F61EC80 a7 = *a11 ; a9 = a7 << 2 ; packet field -> socket index
|
||
3F61ECB0 a7 = *0x1E0BF0 ; jreq ... ; debug/log gate
|
||
3F61ED50 log via 0x3F623B80 when set
|
||
3F61EDE0 a7 = arg(-0x40) ; subk 5 ; jreq 0x3F61EE70 ; special case 5
|
||
3F61EE40 *0x96500 += 1 ; unmatched-datagram counter
|
||
L3F61EE70:
|
||
3F61EE80 a7 = *(a11+0x30) ; andi 0xFFFF9FFF ; option/flag bits 0x6000
|
||
3F61EF00 a7 = *(a11+0x48) ; dsj ; byte 9 bit 7 (option flag)
|
||
3F61EF40 a7 = arg(-0x40) ; subk 5 ; jreq
|
||
3F61EF90 a7 = a9<<3 ; a7 += a11 ; a7 = *(a7)
|
||
3F61F010 subk 8,a7 ; jreq ...
|
||
3F61F070 subk 13,a7 ...
|
||
```
|
||
|
||
The socket struct fields this function reads are therefore:
|
||
|
||
| offset (bit) | byte | field |
|
||
|---|---|---|
|
||
| `0x30` | 6 | option/flag word (mask `0x6000` tested) |
|
||
| `0x48` | 9 | option byte, bit 7 via `dsj` |
|
||
| index table at `a11 + (index<<3)` | | per-socket state words, compared with **8** and **13** |
|
||
|
||
Two more counters belong to this layer: `0x96500` (unmatched datagrams) and the
|
||
state values 8 and 13 used by the delivery switch. The remaining offsets are resolved in section 31 — note that `+0x20` and
|
||
`+0x40` turn out to be stack slots, not structure fields.
|
||
|
||
### 29.3 The classifier constants, decoded from the raw immediate words
|
||
|
||
The chain at `0x3F65A7E0` is four subtract-and-test cases. The disassembler
|
||
prints `ADDI` operands directly but `SUBI` operands complemented, so both
|
||
readings were computed from the raw 32-bit immediate words:
|
||
|
||
```
|
||
ADDI imm = 0x7FDB96E2 (raw 0x96e2,0x7fdb — printed as-is)
|
||
SUBI imm = 0xC01C000B (raw 0x000b,0xc01c — printed 0x3FE3FFF4)
|
||
SUBI imm = 0xFFE3FFF4 (raw 0xfff4,0xffe3 — printed 0x001C000B)
|
||
```
|
||
|
||
| case | value compared (raw-immediate reading) | value (literal-printout reading) |
|
||
|---|---|---|
|
||
| 1 | `0x8024691E` | `0x8024691E` |
|
||
| 2 | `0x80246920` | `0x80246920` |
|
||
| 3 | `0x4040692B` | `0xC0086914` |
|
||
| 4 (the ARP branch) | `0x4024691F` | `0xC024691F` |
|
||
|
||
**Neither reading produces `0x0800` or `0x0806`**, so the classifier's argument
|
||
is not a bare EtherType: it is an internal encoded value (the caller
|
||
`0x3F6A86D0` even tests a *byte* of it against `0x72`, section 28.3). That is a
|
||
real answer to the question rather than a guess — the constants are now known
|
||
exactly, and the remaining unknown is the *encoder*, i.e. the code that builds
|
||
this value from the frame header. That code is upstream of `0x3F6A8670` and is
|
||
the next thing to read.
|
||
|
||
Attempted empirical shortcut, recorded so it is not repeated: sending crafted
|
||
ARP requests into the tap with `AF_PACKET` from a foreign MAC
|
||
(`aa:bb:cc:dd:ee:ff`) did **not** reach the guest — `tcpdump -i vt1200` showed
|
||
0 packets and the classifier never ran. Frame injection into a MAME tap needs
|
||
the bridge/veth arrangement of section 28.2, not a raw socket on the tap.
|
||
|
||
## 30. The receive path has no EtherType demux — and the TCP close path
|
||
|
||
### 30.1 The full receive chain, with one call per frame
|
||
|
||
Tracing the receive side backwards from the IP task gives this chain, and every
|
||
link executes **exactly once per received frame**:
|
||
|
||
| address | role | `icmpw.tr` | `rttcp2.tr` |
|
||
|---|---|---|---|
|
||
| `0x3F602E00` | RX entry: buffer state, re-arm | 344 | 56 |
|
||
| `0x3F9E0B80` | prepare the frame | 344 | 56 |
|
||
| `0x3F600200` | frame -> packet block (lengths 0x5EE/0x226/0x44C) | 344 | 56 |
|
||
| `0x3F603250` | enqueue + post "IP input ready" | 344 | 56 |
|
||
| `0x3F624980` | IP task | 344 | 56 |
|
||
| `0x3F6270F0` | IP **protocol** dispatch (section 23) | 344 | 56 |
|
||
|
||
344 = 343 ICMP + 1 other, and 56 = 44 TCP + 12 UDP: the counts are the number of
|
||
**received frames**, not the number of IP packets, because every frame goes
|
||
through the same five functions regardless of its EtherType. And the chain
|
||
contains **no comparison against the EtherType**:
|
||
|
||
* `0x3F600200` only works with frame lengths (`cmpi 0x5EE`, `0x226`, `0x44C`,
|
||
`0x200`, `0x400`) — it assembles the frame into a packet block, it does not
|
||
classify it;
|
||
* `0x3F602E00` checks a buffer state word (`*(buf+0x90)`) and otherwise just
|
||
re-arms and processes;
|
||
* `0x3F624980` (IP task) takes the packet from the queue and never looks at an
|
||
Ethernet header.
|
||
|
||
**Conclusion: on the IPv4 receive path the firmware does not demux by
|
||
EtherType.** Every frame is handed to the IP parser, which computes its header
|
||
from a 4-bit field at the packet start (`0x3F6200A0`, `setf 4`) without even
|
||
checking that the version nibble is 4. Non-IP frames are therefore "parsed" as
|
||
IP and rejected later. That is consistent with everything else measured: no
|
||
`cmpi 0x0806` exists anywhere in the ROM (section 26.5), and all 19 `cmpi
|
||
0x0800` sites never execute (same section).
|
||
|
||
### 30.2 What that means for the classifier at `0x3F65A570`
|
||
|
||
Section 28 called `0x3F65A570` "the EtherType dispatcher" because it can call
|
||
the ARP input. It is **not** on the IPv4 receive path:
|
||
|
||
```
|
||
0x3F65A570 executions: icmpw.tr = 0 rttcp2.tr = 0 arp2.tr = 0
|
||
```
|
||
|
||
So the four decoded case values (`0x8024691E`, `0x80246920`, `0x4040692B`,
|
||
`0x4024691F`, section 29.3) belong to a dispatcher that our traffic never
|
||
enters, and the IPv4 classification they might implement is not used for IPv4
|
||
receive at all. What remains genuinely unknown is (a) the encoder of that
|
||
argument and (b) which traffic reaches this dispatcher — the `cmpi 0x72`
|
||
(`'r'`) test on a byte of it in the caller suggests a character-coded protocol
|
||
family (LAT/DECnet), not Ethernet types.
|
||
|
||
### 30.3 The TCP close path — `0x3F63F200` and the TIME_WAIT entry
|
||
|
||
The close-side handler that table A selects for states 7 (CLOSING), 8
|
||
(LAST_ACK) and 10 (TIME_WAIT) is a teardown that hands off to a helper:
|
||
|
||
```
|
||
L3F63F200 push a11
|
||
3F63F220 calla 0x3F64C730 ; close / abort
|
||
3F63F250 a11 = a8
|
||
3F63F260 jruc 0x3F6438F0 ; common exit
|
||
```
|
||
|
||
and the TIME_WAIT entry itself:
|
||
|
||
```
|
||
L3F6413B0 a7 = *(a13+0x100) ; jreq 0x3F6418C0 ; only with a pending close
|
||
3F6413E0 movk 10,a5
|
||
3F6413F0 *(a11+0x40) = 10 ; <<< state := TIME_WAIT
|
||
3F641410 calla 0x3F64E960 ; start the 2xMSL timer
|
||
3F641460 movi 0x78,a3 ; 120
|
||
3F641490 *(a11+0x80) = a3 ; <<< TCB +0x80 = timeout value (120)
|
||
3F6414E0 calla 0x3F6BE2F0 ; schedule it
|
||
```
|
||
|
||
That assigns one of the fields section 29.2 left open:
|
||
|
||
| field | meaning |
|
||
|---|---|
|
||
| TCB `+0x80` (byte 32) | timeout/timer value; 120 when entering TIME_WAIT |
|
||
|
||
**None of this executed in any trace** — `0x3F63F200`, `0x3F63F060` (the
|
||
established-family handler of table A) and `0x3F6413F0` all have 0 hits,
|
||
because the workload never closed a connection. The close path is therefore
|
||
documented statically but unverified dynamically; forcing it needs a trace
|
||
where the peer sends FIN and then a second FIN (CLOSING) or simply idles out
|
||
(TIME_WAIT).
|
||
|
||
## 31. PCB layout confirmed; the delivery state set; a correction
|
||
|
||
### 31.1 The binding entry, field by field
|
||
|
||
Re-reading the lookup with the argument slots resolved (the caller pushes five
|
||
arguments; `-0xA0` is the first, `-0x20` the last):
|
||
|
||
| argument | meaning | compared against |
|
||
|---|---|---|
|
||
| `-0xA0` (arg1) | local port | entry `+0xB0` |
|
||
| `-0x80` (arg2) | local address | entry `+0x90` |
|
||
| `-0x60` (arg3) | foreign port | entry `+0x80` |
|
||
| `-0x40` (arg4) | foreign address | entry `+0x60` |
|
||
| `-0x20` (arg5) | flags | bit 0 tested at `0x3F61E990` |
|
||
|
||
so the PCB is:
|
||
|
||
| offset (bit) | byte | field |
|
||
|---|---|---|
|
||
| `0x00` | 0 | next pointer (`move *a9,a9` at `0x3F61EA20`) |
|
||
| `0x60` | 12 | foreign address |
|
||
| `0x80` | 16 | **foreign port** |
|
||
| `0x90` | 18 | local address |
|
||
| `0xB0` | 22 | local port |
|
||
| `0x40` | 8 | TCP state (section 24.1) |
|
||
| `0x80` | 32 | timer value, 120 in TIME_WAIT (section 30.3) — a different struct, the TCP control block |
|
||
|
||
Note `+0x80` appears in both tables because they are different objects: `0x3F61E490`
|
||
matches a **binding** entry (port/address), while the TCP control block of
|
||
section 24 is the per-connection structure. Do not conflate them.
|
||
|
||
Matching and scoring:
|
||
|
||
```
|
||
L3F61E570 if (entry.local_port != arg1) next
|
||
score = 3
|
||
if (entry.local_addr != arg2) { if (arg2 != 0) next; }
|
||
if (entry.foreign_port!= arg3) next
|
||
if (entry.foreign_addr!= arg4) next
|
||
L3F61E940 if (score > best) { best = score; winner = entry }
|
||
L3F61EA20 next: a9 = *a9
|
||
```
|
||
|
||
**Correction to section 29.1:** the offsets `+0x20` and `+0x40` listed there as
|
||
PCB fields were **stack slots** (`a13+0x20` is the caller's score word,
|
||
`a13-0x40` is an argument), not structure fields. The table above is the
|
||
verified layout.
|
||
|
||
### 31.2 The delivery function suppresses a set of values (see section 39 — not proven to be socket states)
|
||
|
||
`0x3F61EC00` reads a per-socket state word as `*(a11 + (index<<3))` and refuses
|
||
delivery when that word equals any of:
|
||
|
||
```
|
||
subk 5 ; subk 8 ; subk 13 ; subk 14 ; subk 15 ; subk 16 ; subk 17 ; subk 18
|
||
```
|
||
|
||
so the "not deliverable" set is **{5, 8, 13, 14, 15, 16, 17, 18}** — a compiler
|
||
switch on small state values. The other fields this function touches:
|
||
|
||
| offset | use |
|
||
|---|---|
|
||
| `+0x30` | option word, mask `0x6000` tested |
|
||
| `+0x48` | option byte, bit 7 via `dsj` |
|
||
| `+0x10` | 16-bit counter/sequence used later in the same function |
|
||
| `+0x20` | a second per-socket word read at `0x3F61FCF0` |
|
||
| `+0x40` | a per-socket value written at `0x3F61FCD0` |
|
||
|
||
The exact *meaning* of 5 and 8 and of the 13..18 block is not established: the
|
||
enumeration lives in the socket layer, and the only states we can name from
|
||
evidence are the TCP ones (section 24.1). What is established is the suppression
|
||
logic itself, which is what determines whether a received segment reaches a
|
||
socket.
|
||
|
||
### 31.3 The close-path trace attempt failed (recorded — superseded by section 32)
|
||
|
||
A run was made to exercise the close path: 60 X11 connections to port 6000, half
|
||
closed with FIN and half with RST, under a real-time trace window
|
||
(frames 2400-9000). Result:
|
||
|
||
```
|
||
0x3F63A340 (TCP input) 0
|
||
0x3F63F200 (close handler) 0
|
||
0x3F6413F0 (TIME_WAIT) 0
|
||
0x61FFA0 / 0x3F624980 0 <- not even the boot-time packets
|
||
```
|
||
|
||
The trace contains **no network activity at all**, i.e. the window closed before
|
||
the emulated stack processed anything: at the measured 53 fps this boot needs
|
||
more wall-clock time than the 124 s window allowed. The close path therefore
|
||
remained documented statically at that point; section 32 records the successful
|
||
rerun (bridge `vt1200` up, no stale `serve.py`) in which the TIME_WAIT block did
|
||
execute, and explains why `0x3F63F200` still did not.
|
||
|
||
## 32. The TIME_WAIT path executed on live traffic
|
||
|
||
Section 31.3 recorded a failed close-path run. This one worked, and the reason
|
||
the previous attempts failed was environmental, not architectural.
|
||
|
||
### 32.1 Run setup (two traps worth recording)
|
||
|
||
```
|
||
bridge vt1200 must be UP before the run:
|
||
sudo ip link set vt1200 up
|
||
the bootp server must not already be bound:
|
||
sudo pkill -9 -f harness/serve.py
|
||
```
|
||
|
||
Both were hit: the first attempt of this kind showed `network up=0` because the
|
||
bridge `vt1200` was **down** (an earlier test of mine had taken it down), and a
|
||
stale `serve.py` from a crashed run held UDP/69, so the fresh server died with
|
||
`OSError: [Errno 98] Address already in use` and the guest never got an address.
|
||
The guest's side was fine all along — `serve.py` logged
|
||
`bootp: request from 08:00:2b:16:4b:5a -> 192.168.99.2`.
|
||
|
||
With the bridge up and the port free: **150 of 150** X11 connections to port 6000
|
||
succeeded (half closed with FIN, half with RST).
|
||
|
||
### 32.2 The TIME_WAIT block ran
|
||
|
||
Gating a trace on the TIME_WAIT write (`0x3F6413F0` → `trace`, `0x3F641520` →
|
||
`trace off`) produced `/tmp/opencode/closeB.tr`, 53,514 instructions. The block
|
||
is present verbatim in the trace:
|
||
|
||
```
|
||
3F641410 MOVE A14,-*SP,1
|
||
3F641430 CALLA 3F64E960 ; 2xMSL timer / cleanup
|
||
3F641460 MOVI 78h,A3 ; 120
|
||
3F641480 SETF 10h,1,0
|
||
3F641490 MOVE A3,*A11(80h),0 ; <<< TCB +0x80 = 120 (confirmed executing)
|
||
3F6414C0 MOVE *A13(A0h),*A14+,1
|
||
3F6414E0 CALLA 3F6BE2F0 ; schedule the timer
|
||
3F6BE2F0 MMTM SP,A7,A9,A11,A13
|
||
```
|
||
|
||
So section 30.3's static reading is now confirmed dynamically: entering TIME_WAIT
|
||
sets the state word at `TCB+0x40`, writes the timeout value **0x78 (120)** to
|
||
`TCB+0x80`, and schedules the timer through `0x3F6BE2F0`.
|
||
|
||
### 32.3 But `0x3F63F200` did not run — and that is informative
|
||
|
||
No trace file was produced for the close handler gated on `0x3F63F200` →
|
||
`0x3F6438F0`, i.e. the close handler for states 7 (CLOSING), 8 (LAST_ACK) and 10
|
||
(TIME_WAIT) never executed, even though 150 connections were closed.
|
||
|
||
That refines the picture: a **passive close** (peer sends FIN, the terminal's
|
||
socket closes) enters TIME_WAIT through the state-4 handler at `0x3F6413B0`,
|
||
not through `0x3F63F200`. The `0x3F63F200` path is table A's handler for
|
||
states 7/8/10 — it needs a *simultaneous* close (FIN from both sides →
|
||
CLOSING), or a FIN arriving while already in LAST_ACK. Producing it requires the
|
||
client to send FIN, let the terminal send its own FIN, and then have the client
|
||
send a **second** FIN instead of ACKing — a TCP state the 150-connection test
|
||
never created.
|
||
|
||
### 32.4 Status of the three "documented but unexecuted" items
|
||
|
||
| item | state |
|
||
|---|---|
|
||
| TIME_WAIT entry `0x3F6413F0`, `TCB+0x80 = 0x78` | **confirmed on live traffic** (32.2) |
|
||
| CLOSING / LAST_ACK handler `0x3F63F200` | still static only — needs a simultaneous close |
|
||
| dispatcher `0x3F65A570` (four case values) | still static only — needs non-IP (LAT-family) traffic |
|
||
|
||
## 33. Why `0x3F63F200` never fires — answered by measurement
|
||
|
||
### 33.1 The forcing attempt
|
||
|
||
A client that completes a real X11 handshake and then drives a passive close was
|
||
run against the live guest (bridge up, no stale `serve.py`):
|
||
|
||
```
|
||
mode=lastack ok=21 err=39 server_closed=21 # valid setup, then FIN, drain to EOF
|
||
mode=race ok=30 err=30 server_closed=0 # FIN immediately after connect
|
||
```
|
||
|
||
140 connection attempts in total, in both orders, with the trace gated on
|
||
`0x3F63F200` → `0x3F6438F0`. Result: **no trace file** — the handler did not
|
||
execute once. TIME_WAIT meanwhile fires every time (section 32).
|
||
|
||
### 33.2 Which object each table is evaluated for
|
||
|
||
A trace captured around a TIME_WAIT entry (`closeB.tr`) shows both dispatchers
|
||
running during one close:
|
||
|
||
```
|
||
3F63F280 (table A index setup) 1 <- evaluated once
|
||
3F63F2C0 (table A range check) 1
|
||
3F63F450 (table A *default*) 3 <- taken, not the table
|
||
3F6416F0 (table B index setup) 3
|
||
3F641730 (table B range check) 3
|
||
3F63F980 (table B established) 3
|
||
```
|
||
|
||
So table A *is* evaluated during a close, but its subject is always in a state
|
||
**below 3** (`subk 3` then `jrhi` → default `0x3F63F450`), while table B is the
|
||
one that dispatches on states 3..10 and drives the connection:
|
||
|
||
```
|
||
table B -> 0x3F63F980 (established family) -> ... -> 0x3F6413B0 (TIME_WAIT entry)
|
||
```
|
||
|
||
The object table A looks at is therefore the **listening** socket (state 1),
|
||
not the connection. That is why `0x3F63F350`'s states-7/8/10 handler
|
||
`0x3F63F200` is never reached: the only object that would need it is a
|
||
connection, and connections are dispatched through table B.
|
||
|
||
### 33.3 Conclusion for this item
|
||
|
||
`0x3F63F200` (CLOSING/LAST_ACK/TIME_WAIT handler of table A) is **not reachable
|
||
from TCP segment input in this configuration**:
|
||
|
||
* 140 forced closes in both close orders — 0 executions;
|
||
* the table A dispatch was observed executing and taking its `state < 3`
|
||
default;
|
||
* `0x3F63F200` has 0 executions in every trace recorded in this whole
|
||
investigation (`icmpw.tr`, `rttcp2.tr`, `close.tr`, `closeB.tr`).
|
||
|
||
It belongs to a path fed by a caller we have never entered. That is a bounded
|
||
negative result, not an unfinished experiment: the work it would do (teardown of
|
||
a connection in CLOSING/LAST_ACK) is done for real connections by the table B
|
||
path ending in `0x3F6413B0`, which *is* confirmed live.
|
||
|
||
### 33.4 Two harness lessons from this run
|
||
|
||
```
|
||
* a SIGKILL of the shell wrapper leaves MAME running (the EXIT trap never runs);
|
||
always check `pgrep -af mame0289/vt1200` afterwards.
|
||
* MAME's Lua `print` is block-buffered when stdout is redirected to a file, so
|
||
"no f= lines in mame.log" does not mean the emulator did not run.
|
||
* the X11 server refuses roughly half the attempts (ok=21/60, ok=30/60), so a
|
||
close-path experiment needs a large attempt count to be meaningful.
|
||
```
|
||
|
||
## 34. Frame injection works — and it overturns the "ARP is separate" assumption
|
||
|
||
### 34.1 Injecting on the bridge (not the tap)
|
||
|
||
Sending raw frames with `AF_PACKET` **bound to the bridge `vt1200`** delivers
|
||
them to the guest, because the MAME tap is enslaved to that bridge and the
|
||
bridge floods to its port:
|
||
|
||
```
|
||
aa:bb:cc:dd:ee:ff > 08:00:2b:16:4b:5a ARP, Request who-has 192.168.99.2 tell 192.168.99.99
|
||
08:00:2b:16:4b:5a > aa:bb:cc:dd:ee:ff ARP, Reply 192.168.99.2 is-at 08:00:2b:16:4b:5a
|
||
```
|
||
|
||
So the terminal **does** answer ARP for its own address from a sender it has
|
||
never met. Section 29.3's earlier failure was only the injection point: binding
|
||
`AF_PACKET` to the tap delivers nothing, binding it to the bridge works.
|
||
|
||
### 34.2 But the ARP frame takes the IP path
|
||
|
||
A trace of one complete frame-processing pass (`0x3F602E00` → `0x3F603780`, the
|
||
function's own epilogue) for an ARP-only injection contains exactly:
|
||
|
||
```
|
||
3F602E00 frame entry
|
||
3F9E0B80 prepare frame
|
||
3F600200 frame -> packet block
|
||
3F603250 enqueue to 0x96AE0 + post "IP input ready" (0x3F60BD40)
|
||
3F603780 rets
|
||
callees: 3F600200, 3F9E0B80, 3F60BD40, 3FB33870/D0 (lock/unlock),
|
||
3F701230 (malloc), 3FB44400, 3FB41A30
|
||
```
|
||
|
||
No ARP-specific call, and `0x3F6148E0` (the ARP input identified in section 26.4)
|
||
is **not** among them. An ARP frame is queued exactly like an IP frame, which
|
||
fits section 30.1's conclusion that the receive path does not demux by
|
||
EtherType.
|
||
|
||
### 34.3 The default IP-protocol handler is a diagnostic dumper
|
||
|
||
Since an ARP frame reaches the IP protocol dispatch with a garbage protocol byte,
|
||
the interesting handler is the default one, `0x3F634600` (section 23.2):
|
||
|
||
```
|
||
3F6346B0 a7 = *(packet+0x48) ; the byte at IP offset 9
|
||
3F6346E0 *0x1E23C0 = a7 ; save it
|
||
3F634710 a5 = *(packet+0x80) ; *0x1E22D0 = a5
|
||
3F634760 a5 = *(packet+0x60) ; *0x1E2350 = a5
|
||
3F6347C0 push 0x1E22B0 / 0x1E2330 / 0x1E23B0 ; address variables
|
||
```
|
||
|
||
It copies header words into globals under `0x1E22B0..0x1E23C0` and logs — a
|
||
"last unknown packet" dump, not an ARP handler.
|
||
|
||
### 34.4 What this leaves, precisely
|
||
|
||
The ARP reply observed in 34.1 must be produced somewhere the traced pass does
|
||
not reach, i.e. **after** the frame is queued. The next step is therefore not
|
||
"find the EtherType demux" any more, but: trace the IP task `0x3F624980` for an
|
||
ARP frame (gate the trace on `0x3F624980`, stop at its epilogue) and see which
|
||
branch it takes instead of the protocol dispatch `0x3F6270F0`. Two candidates,
|
||
both consistent with the evidence so far:
|
||
|
||
* the task validates the IP header and, on failure, inspects the frame's
|
||
EtherType itself (a path not yet read), or
|
||
* the reply is built from the queued block by a routine that never appears in the
|
||
frame pass because it runs in the task's context.
|
||
|
||
Either way, section 26.4's claim that `0x3F6148E0` is *the* ARP input is now
|
||
doubtful: it is a function that parses ARP and writes cache entries, but it is
|
||
not reached by the observed reply. That claim is downgraded accordingly.
|
||
|
||
## 35. ARP is answered — but not by `0x3F6148E0`; and ARP frames are parsed as IP
|
||
|
||
### 35.1 The ARP reply is genuinely the firmware's
|
||
|
||
With frame injection working (section 34.1), an ARP request from a foreign MAC is
|
||
answered. The reply cannot come from anywhere else:
|
||
|
||
* the MAME driver has **no ARP code** (`grep -niE "\barp\b|0806" harness/mame/vt1200.cpp`
|
||
→ nothing), so the reply is not a driver stand-in;
|
||
* the host owns only `192.168.99.1` (`ip addr | grep -c 192.168.99.2` → 0), so the
|
||
Linux kernel is not answering for `.2`.
|
||
|
||
So the VT1200 firmware itself answers ARP — but not through the function this
|
||
report previously called "the ARP input".
|
||
|
||
### 35.2 Correction: `0x3F6148E0` does not run
|
||
|
||
A trace gated on `0x3F6148E0` → `0x3F615600` (its epilogue) produced **no trace
|
||
file** while four ARP requests were being answered:
|
||
|
||
```
|
||
sent 4 ARP requests only
|
||
ARP input did NOT fire
|
||
```
|
||
|
||
That is a direct contradiction of section 26.4, and it also explains the earlier
|
||
"0 executions in every trace": those traces contained no ARP traffic at all.
|
||
**Superseded by section 36**: the responder is `0x3F6119A0`, confirmed firing on
|
||
live ARP traffic.
|
||
`0x3F6148E0` is a function that manipulates ARP cache entries, but it is not the
|
||
code path that answers a request.
|
||
|
||
### 35.3 ARP frames are fed to the IP parser and mis-parsed as ICMP
|
||
|
||
The frame pass of section 34.2 already showed an ARP frame being enqueued to
|
||
`0x96AE0` like an IP frame. Tracing the IP task (`0x3F624980` → `0x3F6272C0`)
|
||
with an ARP frame shows what happens next:
|
||
|
||
```
|
||
3F6270F0 protocol dispatch
|
||
3F6271E0 CALL A5 -> 3F61FFA0 <<< the ICMP input handler
|
||
3F6206D0 ICMP checksum verification
|
||
3F6224E0 ICMP type dispatch
|
||
3F6212A0 echo-request handler (type 8: rewrites the type to 0)
|
||
```
|
||
|
||
So the ARP frame's "IP protocol byte" resolves to **1 (ICMP)** and its "type"
|
||
resolves to **8 (echo request)** — the stack then builds a reply from a frame
|
||
that is not IP at all. No such packet appears on the wire (the capture shows only
|
||
the ARP request/reply pair), so it is discarded later, most plausibly at the
|
||
checksum or route stage.
|
||
|
||
This is a real, verifiable firmware behaviour: **the receive path has no
|
||
EtherType filter (section 30.1), so ARP frames are parsed as IP and only
|
||
discarded by the IP-layer sanity checks** — while the genuine ARP answer comes
|
||
from a path that has not yet been read.
|
||
|
||
### 35.4 What to look for next
|
||
|
||
The responder builds an ARP *reply*: a frame whose EtherType at offset 12 is
|
||
`0x0806` and whose opcode at offset 20 is 2. Two concrete leads:
|
||
|
||
* the `arpresolve` family around `0x3F6105E0` and `0x3F6120A0` (the code that
|
||
references the ARP diagnostic strings) — the responder may live beside the
|
||
cache lookup rather than in the input function;
|
||
* a first-hit trace of the frame entry `0x3F602E00` with an ARP-only injection,
|
||
capturing the **first** indirect call, to see whether the frame is classified
|
||
before it reaches the queue at all (the fragment/block function `0x3F600200`
|
||
returns a value that `0x3F602E00` tests at `0x3F6031A0`; that branch has not
|
||
been read with an ARP frame in hand).
|
||
|
||
## 36. The ARP responder found: `0x3F6119A0` — and it drives the same hardware path
|
||
|
||
### 36.1 Identification, confirmed on live traffic
|
||
|
||
Gating a trace on `0x3F6119A0` while four ARP requests were injected (and
|
||
answered) produced a **3.1 MB trace** (344,861 instructions). The function is the
|
||
ARP responder: its head byte-swaps the two ARP header words
|
||
|
||
```
|
||
3F6119A0 mmtm sp,a3,a5,a7,a9,a10,a11,a12,a13
|
||
3F6119B0 a8 = *(packet+0x10) ; ARP ptype (byte 2) -> htons
|
||
3F611BD0 a8 = *(packet+0x30) ; ARP opcode (byte 6) -> htons
|
||
```
|
||
|
||
and the trace contains the request/response handling at `0x3F611E80` (the
|
||
broadcast-sender check that emits `arp: ether address is broadcast for IP …`).
|
||
|
||
### 36.2 It sends through the same driver path as IP
|
||
|
||
The ordered callees of one ARP pass:
|
||
|
||
```
|
||
3FB2C000 htons
|
||
3F942230 address compare/copy helper
|
||
3F6084E0 <<< the driver send of section 21 (called from 0x3F608450)
|
||
3F9DF6E0 task/queue helper
|
||
3F9DC890 <<< the driver submission wrapper
|
||
3F9D4BC0 <<< the driver command entry (ring arming)
|
||
```
|
||
|
||
with the call itself visible in the trace:
|
||
|
||
```
|
||
3F608410 MOVI 1DA570h,A7
|
||
3F608440 MOVE A7,*A14+,1
|
||
3F608450 CALLA 3F6084E0h ; clamp to 0x32, build request
|
||
3F6084E0 MMTM SP,A7,A9,A11,A13
|
||
```
|
||
|
||
So an ARP reply is handed to the chip exactly like an IP packet:
|
||
|
||
```
|
||
ARP input 0x3F6119A0 -> 0x3F6084E0 -> 0x3F9DC890 -> 0x3F9D4BC0
|
||
-> TX ring -> CSR0 = shadow | TDMD (section 10)
|
||
```
|
||
|
||
That matters beyond ARP: it means the `shadow | TDMD` kick is on the **only**
|
||
transmit path this stack has, for every protocol. There is no second way to get a
|
||
frame onto the wire.
|
||
|
||
### 36.3 Corrections to sections 26.4 and 35.2
|
||
|
||
| function | status |
|
||
|---|---|
|
||
| `0x3F6148E0` (called "the ARP input" in 26.4) | **0 executions** with live ARP — not the responder |
|
||
| `0x3F614200` (ARP cache lookup, section 26.2) | **0 executions** in the ARP pass |
|
||
| `0x3F6120A0` (broadcast warning site) | **0 executions** in this pass |
|
||
| `0x3F6119A0` | **fires on every ARP request** — this is the responder |
|
||
|
||
The cache lookup and the `0x3F6148E0` cache-entry writer are therefore *not* on
|
||
the reply path. The responder answers directly from the request it received,
|
||
which also explains why no ARP entry had to exist for the reply to work.
|
||
|
||
### 36.4 Still open in ARP
|
||
|
||
Where (if anywhere) the responder updates the cache: `0x3F614200` is not called,
|
||
so either it writes entries inline (the bucket arithmetic of section 26.2
|
||
inlined into `0x3F6119A0`) or this firmware keeps no learned cache and only
|
||
answers. Deciding between those needs a trace of the responder's *writes* to
|
||
`0x1DAF60 + (ip mod 19)*1152`, not more traffic.
|
||
|
||
## 37. The ARP responder does maintain the cache — 36.4 answered
|
||
|
||
Section 36.4 asked whether `0x3F6119A0` stores cache entries, since it never
|
||
calls the cache lookup `0x3F614200`. It does store them, with the bucket
|
||
arithmetic **inlined**. Two sites inside the responder reference the cache base
|
||
`0x1DAF60`:
|
||
|
||
```
|
||
bit 0x3F612640 addi 0x1DAF60,a11
|
||
bit 0x3F613300 addi 0x1DAF60,a11
|
||
```
|
||
|
||
(The literal `0x001DAF60` appears at exactly six places in the ROM:
|
||
`0x3F60EF40`, `0x3F6101C0`, `0x3F612640`, `0x3F613300`, `0x3F614460`,
|
||
`0x3F614AE0` — the middle two are inside the responder, the last two in the
|
||
lookup/entry-writer pair of section 26.)
|
||
|
||
### 37.1 The search inside the responder
|
||
|
||
```
|
||
3F612600 a7 = hash ; (IP mod 19) * 9 * 128
|
||
3F612620 a11 = a7
|
||
3F612630 a11 += 0x1DAF60 ; bucket base
|
||
3F612660 a12 = 0
|
||
L3F612670:
|
||
3F612670 a7 = *a11 ; entry+0 = cached IP
|
||
3F612680 a5 = *(a13+0x140) ; the sender IP from the request
|
||
3F6126A0 if (a7 == a5) -> found
|
||
3F6126C0 a12++
|
||
3F6126D0 a11 += 0x80 ; next entry (16 bytes)
|
||
3F6126F0 if (a12 < 9) loop
|
||
```
|
||
|
||
identical in structure to the lookup of section 26.2: 19 buckets, 9 entries of
|
||
16 bytes, keyed on the IP at entry+0.
|
||
|
||
### 37.2 The write
|
||
|
||
```
|
||
3F612760 if (a11 == 0) -> no slot (0x3F612AA0)
|
||
3F612790 movk 6,a7 ; 6-byte copy
|
||
3F6127C0 a7 = a11 + 0x20 ; entry byte 4 <<< the MAC field
|
||
3F612820 calla 0x3FB44400 ; memcpy(6)
|
||
3F612860 a7 = *(a11+0x58) ; entry byte 11 = state
|
||
3F612880 btst 1,a7 ; jrne ... ; state bit 1
|
||
3F6128A0 movk 1,a7 ; *(a13+0x200) = a7 ; flag: entry was created/updated
|
||
```
|
||
|
||
So on a request from an address already present (or the first free slot) the
|
||
responder copies the sender's **6-byte MAC into entry+4**, then updates the state
|
||
byte at entry+`0x58`.
|
||
|
||
The second site (`0x3F613300`) walks the same buckets against a *different*
|
||
address (`*(a13+0x160)`), tests bit **3** of the state byte at `+0x58`
|
||
(`0x3F613480`), and then runs the same 6-byte copy — so the responder updates
|
||
entries for both addresses involved in the exchange (sender and target).
|
||
|
||
### 37.3 Consequence
|
||
|
||
The ARP cache is maintained by two code paths:
|
||
|
||
| path | how |
|
||
|---|---|
|
||
| responder `0x3F6119A0` | inline bucket arithmetic, copies MAC into entry+4, sets entry state |
|
||
| lookup/allocator `0x3F614200` + writer `0x3F6148E0` | the pair documented in section 26, **not** called by the responder |
|
||
|
||
That also explains section 26.2's "entry state 1 = incomplete on allocation":
|
||
state bit 1 / bit 3 distinguish a freshly created entry from a completed one, and
|
||
the responder is what completes them.
|
||
|
||
Nothing here needed new traffic: the answer was in the ROM, and the earlier
|
||
`0x3F614200 == 0 hits` observation was correct but misleading — the responder
|
||
does not need that helper because it inlines the same arithmetic.
|
||
|
||
## 38. `0x3F65A570` is not an Ethernet path — bounded negative
|
||
|
||
### 38.1 The experiment
|
||
|
||
Using the bridge injection that works (section 34.1), six EtherTypes were sent to
|
||
the guest, five frames each:
|
||
|
||
```
|
||
0x6003 DECnet/LAT 0x1234 0x8035 RARP 0x0800 (bogus IP body) 0x8888 0x0001
|
||
```
|
||
|
||
The trace was gated on the dispatcher itself:
|
||
|
||
```
|
||
bpset 0x3F65A570 -> trace ; bpset 0x3F65B1B0 -> trace off
|
||
```
|
||
|
||
Result:
|
||
|
||
```
|
||
sent 0x6003 DECnet ... sent 0x0001
|
||
dispatcher did NOT fire
|
||
```
|
||
|
||
### 38.2 Conclusion
|
||
|
||
`0x3F65A570` is **not reachable from Ethernet input of any EtherType**. It is not
|
||
an EtherType demux, and its four case values
|
||
(`0x8024691E`, `0x80246920`, `0x4040692B`, `0x4024691F`, section 29.3) are not
|
||
EtherType codes. That is consistent with the one hard clue we had all along: its
|
||
caller `0x3F6A86D0` extracts a **byte** of the argument and compares it with
|
||
`0x72` (`'r'`), which is a character-coded protocol id, not a 16-bit EtherType.
|
||
|
||
The dispatcher is an **internal protocol-id demux**, fed by something inside the
|
||
firmware (most plausibly the LAT/service layer originating its own messages), not
|
||
by frames arriving from the wire. Reaching it would require the firmware itself to
|
||
originate such a message — a UI-driven action on the terminal, not something that
|
||
can be injected at L2.
|
||
|
||
### 38.3 What this changes in the report
|
||
|
||
| earlier claim | now |
|
||
|---|---|
|
||
| 28: "the EtherType dispatcher" | superseded — not an Ethernet path |
|
||
| 29.3: "encoder unresolved, maps to ARP/IPv4?" | answered: no EtherType mapping exists |
|
||
| 30.2: "belongs to a dispatcher our traffic never enters" | confirmed, and now shown to be unreachable by any EtherType |
|
||
|
||
Combined with sections 30.1 and 35.3, the picture of the receive side is
|
||
consistent and closed as far as the wire is concerned: **the firmware has no
|
||
EtherType demux at all** — frames go straight into the IP queue, IP-layer
|
||
sanitisation rejects non-IP, and the only non-IP protocol that answers (ARP) does
|
||
so from `0x3F6119A0` after the same queue, in the same way IP does.
|
||
|
||
The remaining unknown is therefore *inside* the firmware's own protocol layer
|
||
(what feeds `0x3F65A570`), not at the network interface.
|
||
|
||
## 39. The delivery suppression set never fires — and the "states" are not named
|
||
|
||
### 39.1 Measurement
|
||
|
||
In `rttcp2.tr` (44 TCP segments + 12 UDP datagrams, 11 delivered through
|
||
`0x3F61EC00`) the delivery function runs, but the state-suppression chain of
|
||
section 31.2 executes **zero** times:
|
||
|
||
| site | meaning | executions |
|
||
|---|---|---|
|
||
| `0x3F61EC00` | delivery entry | 11 |
|
||
| `0x3F61EF60` | `subk 5` compare | **0** |
|
||
| `0x3F61F010` | `subk 8` compare | **0** |
|
||
| `0x3F61F070` … `0x3F61F250` | `subk 13 … subk 18` | **0** |
|
||
| `0x3F61F300` | common exit | 11 |
|
||
|
||
So for ordinary, correctly delivered traffic none of the {5, 8, 13…18} values is
|
||
ever compared against — every delivery takes an earlier exit. Those values are
|
||
therefore *exceptional* states, not the normal states of a socket in service.
|
||
|
||
### 39.2 What that does and does not establish
|
||
|
||
* Established: the suppression set is not part of the normal data path, and a
|
||
workload with successfully delivered segments never reaches it.
|
||
* **Not** established, and previously over-claimed in section 31.2: that the word
|
||
being compared is a *socket state*. The function reads a 16-bit value from
|
||
`a11 + (index << 3)` where `index` comes from a 4-bit field at `*a11`, and the
|
||
object itself is passed as an argument by the UDP/TCP input paths. Whether
|
||
`a11` is a socket, a protocol entry or a descriptor table has not been proven,
|
||
and the values 5/8/13…18 have therefore no names — only a position in a compare
|
||
chain that our traffic never walks.
|
||
|
||
Section 31.2 is thus downgraded from "the delivery function suppresses a set of
|
||
socket states" to "the delivery function contains an unreached compare chain over
|
||
values 5, 8 and 13…18 on a per-entry word".
|
||
|
||
### 39.3 Why this is worth recording rather than chasing
|
||
|
||
Three attempts in this investigation tried to name socket-layer values from
|
||
static structure alone (this one, the classifier constants in 29.3, and the ARP
|
||
input in 26.4) and two of them produced wrong answers that later had to be
|
||
withdrawn. The pattern is consistent: **this firmware's interior enumerations
|
||
cannot be named from the ROM alone** — they need a workload that takes the
|
||
branch, or a symbol source. For the socket layer specifically, the branch is only
|
||
taken for exceptional conditions (which is exactly what 39.1 shows), so the
|
||
honest state of that item is "known to exist, unreached, unnamed".
|
||
|
||
What is *not* affected by this: every path that carries real traffic. Those are
|
||
documented with execution counts — the receive chain (344/56), the ICMP path
|
||
(343), the TCP state tables (11/22/33), ARP (`0x3F6119A0` firing on live
|
||
requests), and the single transmit sequence `0x3F6084E0 → 0x3F9DC890 →
|
||
0x3F9D4BC0 → shadow|TDMD`.
|
||
|
||
## 40. Correction: the shadow is about INEA, not STRT — section 10 was wrong
|
||
|
||
Section 10 claims that "CSR0's command bits are not set-only: writing `0x0008`
|
||
alone clears `STRT`, which stops the transmitter, and the demand is never
|
||
serviced". **The device model says otherwise**, and the claim has to be
|
||
withdrawn.
|
||
|
||
### 40.1 The source
|
||
|
||
`src/devices/machine/am79c90.cpp`, CSR0 write handler (register 0):
|
||
|
||
```c
|
||
/*
|
||
* INIT, STRT, STOP, TDMD - read/write with 1 only
|
||
* TXON, RXON, INTR, ERR - read only
|
||
* INEA - read/write
|
||
* IDON, TINT, RINT, MERR, MISS, CERR, BABL - read/clear only
|
||
*/
|
||
...
|
||
// handle STRT
|
||
if ((data & CSR0_STRT) && !(m_csr[0] & CSR0_STRT))
|
||
{
|
||
m_csr[0] |= CSR0_STRT;
|
||
m_csr[0] &= ~CSR0_STOP;
|
||
...
|
||
m_csr[0] |= CSR0_TXON;
|
||
m_transmit_poll->adjust(attotime::zero, 0, TX_POLL_PERIOD);
|
||
}
|
||
|
||
// transmit demand
|
||
if ((data & CSR0_TDMD) && !(m_csr[0] & CSR0_TDMD))
|
||
{
|
||
m_csr[0] |= CSR0_TDMD;
|
||
m_transmit_poll->adjust(attotime::zero, 0, TX_POLL_PERIOD);
|
||
}
|
||
|
||
// interrupt enable
|
||
if (!(m_csr[0] & CSR0_STOP) || type() == AM79C90)
|
||
{
|
||
if (data & CSR0_INEA) m_csr[0] |= CSR0_INEA;
|
||
else m_csr[0] &= ~CSR0_INEA;
|
||
}
|
||
```
|
||
|
||
and `transmit_poll()`:
|
||
|
||
```c
|
||
if (m_csr[0] & CSR0_TXON)
|
||
{
|
||
m_csr[0] &= ~CSR0_TDMD; // demand is cleared when the poll runs
|
||
...
|
||
```
|
||
|
||
Two facts follow directly:
|
||
|
||
1. **`STRT` is set-only.** No path in the write handler clears it when a write
|
||
omits it. Writing a bare `TDMD = 0x0008` therefore leaves `STRT`/`TXON` set,
|
||
and the demand *is* serviced (`TDMD` is cleared by the poll).
|
||
2. **`INEA` is genuinely read/write.** A bare `TDMD` write has bit 14 clear, so
|
||
it **disables interrupts**. That is the real consequence of not using the
|
||
shadow.
|
||
|
||
### 40.2 What the firmware's shadow is actually for
|
||
|
||
The ROM fact is unchanged and still exact — `0x3F9D57F0` reads the shadow cell
|
||
`0x23CC50`, ORs in `TDMD`, and writes the result:
|
||
|
||
```
|
||
a5 = *0x23CC50 ; shadow of the CSR0 read/write bits (INEA among them)
|
||
a5 |= 0x0008 ; TDMD
|
||
*0x23CC30 = a5 ; write back: shadow | TDMD
|
||
```
|
||
|
||
That is a driver preserving the **read/write** bits (`INEA`) across a command
|
||
write — not a workaround for set-only command bits. The distinction matters,
|
||
because the two explanations predict different failures.
|
||
|
||
### 40.3 Consequence for the real-silicon result
|
||
|
||
The on-machine symptom was `CSR0 = 0x1BB` with `TMD1 = 0x8338` (descriptor still
|
||
owned, `TDMD` still set). Decoding `0x1BB` against the bit definitions:
|
||
`STRT`(1) = 1, `TDMD`(3) = 1, `TXON`(4) = 1 — i.e. **the transmitter was enabled
|
||
and the demand was raised, but the poll never ran**. "Bare TDMD cleared STRT" does
|
||
not describe that state at all; it would have shown `STRT`/`TXON` clear.
|
||
|
||
So the open question on real hardware is *not* the shape of the CSR0 write (the
|
||
firmware's `shadow | TDMD` is correct and the corrected payload matches it), but:
|
||
**why did the chip not run a transmit poll with `TXON` and `TDMD` both set?** The
|
||
model cannot answer that — in the model a set `TDMD` always schedules the poll.
|
||
Candidate causes to test on the machine, in order:
|
||
|
||
1. the transmit ring words in the init block were not what the chip latched
|
||
(ring base/length encoding, `CSR1/CSR2` written while `STOP` was clear);
|
||
2. the chip latched its init block from the wrong address (the init-block pointer
|
||
only loads when `INIT` is accepted, which requires `STOP`);
|
||
3. the descriptor as written is not accepted as owned by the chip (byte order of
|
||
the `OWN` bit / the 24-bit address split).
|
||
|
||
### 40.4 Revert note
|
||
|
||
If this correction is found to be wrong, the evidence to beat is the two source
|
||
blocks quoted above (`am79c90.cpp`, CSR0 `case 0:` and `transmit_poll()`), not the
|
||
earlier prose in section 10. Section 10 now carries a pointer here.
|
||
|
||
## 41. Bare `TDMD` vs `shadow|TDMD`: identical in MAME — and what `0x1BB` really says
|
||
|
||
### 41.1 The experiment
|
||
|
||
The payload was built twice, differing only in the kick:
|
||
|
||
```
|
||
shadow: csr_w(0, CSR0_STRT | CSR0_TDMD);
|
||
bare: csr_w(0, CSR0_TDMD);
|
||
```
|
||
|
||
Both were run through the MAME loader probe on the same boot path:
|
||
|
||
| payload | csr0a | csr0b | ok | rmd1 | mcnt | txmd1 |
|
||
|---|---|---|---|---|---|---|
|
||
| shadow `STRT\|TDMD` | `0x7B3` | `0x7B3` | 1 | `0x0300` | `0x1C` | `0x8300` |
|
||
| bare `TDMD` | `0x7B3` | `0x7B3` | 1 | `0x0300` | `0x1C` | `0x8300` |
|
||
|
||
**Byte-for-byte identical.** Two conclusions follow, and both matter:
|
||
|
||
1. The corrected CSR0 semantics of section 40 are confirmed empirically: in the
|
||
model a bare `TDMD` works exactly as well, because `STRT` is set-only and the
|
||
demand is serviced regardless.
|
||
2. **MAME never distinguished the two payloads.** The earlier statement "MAME
|
||
verifies the corrected `shadow|TDMD` kick" is void: what those runs verify is
|
||
the ring, descriptors, length encoding and DMA — not the shape of the CSR0
|
||
write. The payload polls rather than using interrupts, so the one bit the
|
||
shadow actually protects (`INEA`) is irrelevant to this test.
|
||
|
||
That also means the real-silicon failure cannot be attributed to the kick at all,
|
||
by either model or payload experiment.
|
||
|
||
### 41.2 What `CSR0 = 0x1BB` actually decodes to
|
||
|
||
Using the model's own bit definitions (`am79c90.h`):
|
||
|
||
```
|
||
CSR0_INIT 0x0001 CSR0_STRT 0x0002 CSR0_STOP 0x0004 CSR0_TDMD 0x0008
|
||
CSR0_TXON 0x0010 CSR0_INEA 0x0040 CSR0_IDON 0x0100 CSR0_ERR 0x8000
|
||
```
|
||
|
||
`0x1BB = 0x0100 | 0x00BB` = **IDON | INIT | STRT | TDMD | TXON | RXON | INTR**,
|
||
with **`ERR` clear** and **`INEA` clear**.
|
||
|
||
The earlier note in section 27 guessed that `ERR` was set and that the poll had
|
||
failed on a DMA error. That guess was wrong — there is no error bit at all.
|
||
|
||
### 41.3 The anomaly that is now the best lead (withdrawn — see section 47.2)
|
||
|
||
`INIT` **and** `IDON` are both set, which the model treats as contradictory:
|
||
initialization sets `IDON` and clears `INIT`, and `INIT` is only re-asserted by a
|
||
new `INIT` write. With `TXON = 1`, `TDMD = 1` and no error, a chip that still
|
||
considers itself initializing is a plausible reason for the transmit poll never
|
||
running.
|
||
|
||
The datasheet requirement quoted in the model's own comment is directly relevant:
|
||
|
||
```
|
||
* The STOP bit must be set prior to setting the INIT bit.
|
||
* Setting INIT clears the STOP bit.
|
||
```
|
||
|
||
~~Our payload writes `INIT` **without first setting `STOP`**.~~ **Withdrawn — see
|
||
section 42.1**: the payload does set `STOP` at the start of `main()`. The
|
||
`INIT`+`IDON` state remains unexplained, and a STOP-immediately-before-INIT
|
||
variant (`lance_stopinit.coff`) is byte-identical in MAME (section 42.2).
|
||
|
||
### 41.4 Testable on the machine, in this order
|
||
|
||
1. read `CSR0` immediately after the `INIT` write and after the `IDON` poll, to
|
||
see whether `INIT` clears (`0x1BB` shows it did not);
|
||
2. set `STOP` (`0x0004`) before writing `INIT`, per the datasheet, then `INIT`,
|
||
then poll `IDON`, then `STRT`, then `TDMD`;
|
||
3. if `TDMD` still sticks, read `CSR1`/`CSR2` back to confirm the chip latched the
|
||
init block address the payload thinks it wrote.
|
||
|
||
Each of these is a register read the existing payload can already perform; no new
|
||
tooling is needed, only the VT1200 being available (section 27).
|
||
|
||
## 42. STOP-before-INIT gives no observable difference — and 41.3 was wrong about our payload
|
||
|
||
### 42.1 The payload already set STOP first
|
||
|
||
Section 41.3 suggested the real-chip anomaly (`INIT` still set with `IDON`) might
|
||
be because "our payload writes `INIT` without first setting `STOP`". Reading the
|
||
payload shows that is **wrong**: `main()` does
|
||
|
||
```c
|
||
csr_w(0, CSR0_STOP); /* line 131, first thing after the marker */
|
||
...
|
||
csr_w(1, INITB >> 3); csr_w(2, (INITB >> 3) >> 16); csr_w(3, 0);
|
||
csr_w(0, CSR0_INIT); /* line 186 */
|
||
```
|
||
|
||
so the datasheet order was already respected (with the whole descriptor/VRAM
|
||
setup in between). The hypothesis is withdrawn.
|
||
|
||
### 42.2 A tighter variant, and its result
|
||
|
||
The only meaningful variant left is `STOP` *immediately* before `INIT`:
|
||
|
||
```c
|
||
csr_w(3, 0);
|
||
csr_w(0, CSR0_STOP); /* added: STOP immediately before INIT */
|
||
csr_w(0, CSR0_INIT);
|
||
```
|
||
|
||
`lance_stopinit.coff` was built and run against the current payload:
|
||
|
||
| payload | csr0a | csr0b | ok | rmd1 | mcnt | txmd1 |
|
||
|---|---|---|---|---|---|---|
|
||
| `lance.coff` | `0x7B3` | `0x7B3` | 1 | `0x0300` | `0x1C` | `0x8300` |
|
||
| `lance_stopinit.coff` | `0x7B3` | `0x7B3` | 1 | `0x0300` | `0x1C` | `0x8300` |
|
||
|
||
Identical again. The model does not model any INIT/STOP ordering effect — which
|
||
matches its own source comment, where the ordering rule is explicitly described
|
||
as contradictory in the datasheet and not enforced:
|
||
|
||
```
|
||
* The STOP bit must be set prior to setting the INIT bit.
|
||
* ...
|
||
* This is clearly contradictory; driver code sets the STRT bit
|
||
* after INIT, so assume STRT does not require STOP to be set.
|
||
```
|
||
|
||
So neither ordering can be validated or invalidated in MAME. The
|
||
`INIT`+`IDON` state seen on real silicon is therefore still unexplained, but the
|
||
list of candidate causes has lost its "payload didn't set STOP" entry.
|
||
|
||
### 42.3 What is left as a cause
|
||
|
||
The remaining candidates all concern what the *chip* latched, not what the
|
||
payload wrote:
|
||
|
||
1. `INIT` write accepted but the chip kept `INIT` set because the init-block DMA
|
||
read did not complete as expected (the model performs it synchronously; a
|
||
real chip reports errors through `MERR`, which is *clear* here, so this is
|
||
weaker);
|
||
2. the chip re-asserting `INIT` because the payload writes `INIT` again later
|
||
(there is a retry path at `lance.c:263`), and the real chip not clearing it;
|
||
3. a real-chip `CSR0` read-back quirk that leaves `INIT` visible while idle.
|
||
|
||
None of these can be decided in MAME. They are register reads on the machine.
|
||
|
||
### 42.4 Item closed for the offline phase
|
||
|
||
`lance_stopinit.coff` exists, produces identical results, and the section 41.3
|
||
hypothesis is withdrawn. The offline phase on the hardware question is now
|
||
exhausted: every variant that MAME can distinguish has been run, and the
|
||
remaining difference is a real-chip register state that the model does not
|
||
reproduce.
|
||
|
||
## 43. The delivery word: addressing pinned, names still absent
|
||
|
||
### 43.1 How the compared word is addressed
|
||
|
||
The chain at `0x3F61EC00` reads a 16-bit word from a computed slot:
|
||
|
||
```
|
||
3F61EC70 setf 4,0,0
|
||
3F61EC80 a7 = *a11 ; 4-bit field at the object's first 4 bits
|
||
3F61EC90 a9 = a7 << 2 ; index * 2 (kept shifted for the next step)
|
||
...
|
||
3F61EF90 a7 = a9 << 3 ; (index*2) << 3 = index * 16 bits
|
||
3F61EFA0 a7 += a11 ; -> a11 + index * 2 bytes ... see note
|
||
3F61EFB0 a7 = *a7,0 ; 16-bit read -> the compared word
|
||
3F61F010 subk 8,a7
|
||
3F61F070 subk 13,a7
|
||
...
|
||
```
|
||
|
||
so the value tested against {5, 8, 13…18} is a **16-bit entry in a table at the
|
||
object, selected by a 4-bit field stored at the object's own start**, with a
|
||
2-byte stride (`index * 2`). Whatever the object is, it carries a small tag and
|
||
an array of 16-bit words — that much is now exact.
|
||
|
||
### 43.2 What the object is
|
||
|
||
The caller builds the binding query from the object's own fields and then passes
|
||
the same object to the delivery function:
|
||
|
||
```
|
||
3F654980 a8 = *(a9+0xB0) ; port-like field
|
||
3F6549B0 push *(a9+0x80)
|
||
3F6549F0 push *(a9+0xA0)
|
||
3F654A20 push *(a9+0x60)
|
||
3F654A60 push 0x96E70 ; binding list
|
||
3F654AA0 calla 0x3F61E490 ; lookup -> result in a8, saved to a10
|
||
3F654C50 push a9 ; <<< same object goes to 0x3F61EC00
|
||
```
|
||
|
||
The offsets used (`+0x60`, `+0x80`, `+0xA0`, `+0xB0`) are the PCB-style
|
||
address/port fields of section 31.1, so the object is the **per-datagram
|
||
binding structure** — but whether it is a real PCB from the binding table or a
|
||
scratch copy built by the UDP input (which does copy five words of it into its
|
||
own frame at `0x3F654BD0`) is not settled by this. Recording the distinction
|
||
instead of picking one.
|
||
|
||
### 43.3 No enumeration table exists in the ROM
|
||
|
||
A table of the exact values would have named them. Searched the whole image:
|
||
|
||
```
|
||
16-bit 5, 8, 13, 14, 15, 16, 17, 18 -> 0 occurrences
|
||
byte 5, 8, 13, 14, 15, 16, 17, 18 -> 0 occurrences
|
||
16-bit 13, 14, 15, 16, 17, 18 -> 3 occurrences, all in data regions
|
||
```
|
||
|
||
So the values are produced by *storing* them, not by indexing a table. Finding
|
||
that store statically requires alias analysis of `a11`'s provenance, which is
|
||
exactly the technique that has produced two wrong answers in this area already
|
||
(26.4, 31.2). I am not going to guess a third time.
|
||
|
||
### 43.4 The dynamic capture that should settle it
|
||
|
||
The right tool is a memory dump from a breakpoint, using the register in the
|
||
address expression:
|
||
|
||
```
|
||
bpset 0x3F61EE70,1,"dump a11,64,/tmp/opencode/obj.bin"
|
||
```
|
||
|
||
That was attempted with six UDP probes to ports 7/9/6000/37/111. No file was
|
||
produced — either the breakpoint never fired (the probes may not have reached a
|
||
delivered socket in that run) or the `dump` action's address expression was
|
||
rejected. The recipe is recorded so the next attempt can iterate on it (check the
|
||
MAME debugger console for the action's own error output, and confirm the probe
|
||
actually lands on a bound socket first).
|
||
|
||
Until that dump is captured, the values 5/8/13…18 stay **unnamed**, and section 39
|
||
remains the accurate summary of this item.
|
||
|
||
## 44. The delivery object is the PACKET, not a socket — captured on the wire
|
||
|
||
### 44.1 The dump now works (two syntax bugs fixed)
|
||
|
||
The earlier attempt failed for two reasons, both mine:
|
||
|
||
```
|
||
wrong: dump a11,64,/tmp/opencode/obj.bin ; filename must come FIRST
|
||
right: dump /tmp/opencode/obj.bin,a11,512 ; and length is in ADDRESS UNITS
|
||
```
|
||
|
||
`debugcmd.cpp` `execute_dump()` takes `params[0]` as the filename, `params[1]` as
|
||
the address and `params[2]` as the length in address units. With the TMS34010's
|
||
1-bit address unit, 64 bytes is **512** units.
|
||
|
||
With the corrected command, a breakpoint at `0x3F61EE70` (inside the delivery
|
||
function, before the compare chain) dumped 64 bytes while 24 UDP datagrams were
|
||
being delivered. The file contains **our own datagram**:
|
||
|
||
```
|
||
45 00 11 00 3f 99 00 40 40 11 00 00 c0 a8 63 01 c0 a8 63 02 ...
|
||
192.168.99.1 192.168.99.2
|
||
... 70 72 6f 62 65 2d 31 31 0a "probe-11\n"
|
||
```
|
||
|
||
version/IHL `0x45`, TTL 64, protocol **17 (UDP)**, source `192.168.99.1`,
|
||
destination `192.168.99.2`, payload `probe-11`.
|
||
|
||
### 44.2 So `a11` is the packet — and the lookup keys finally make sense
|
||
|
||
`a11` in `0x3F61EC00` points at the received packet's IP header. Re-reading the
|
||
binding lookup (`0x3F61E490`) with that in hand, the query arguments are the
|
||
datagram's own header fields:
|
||
|
||
| pushed at | reads | byte offset | meaning |
|
||
|---|---|---|---|
|
||
| `0x3F654980` | `*(a9+0xB0)` | 22 | UDP **destination** port |
|
||
| `0x3F6549B0` | `*(a9+0x80)` | 16 | IP **destination** address |
|
||
| `0x3F6549F0` | `*(a9+0xA0)` | 20 | UDP **source** port |
|
||
| `0x3F654A20` | `*(a9+0x60)` | 12 | IP **source** address |
|
||
|
||
and the lookup compares them against the PCB fields of section 31.1:
|
||
|
||
```
|
||
arg1 (packet: dport) == entry +0xB0 ; the socket's LOCAL port
|
||
arg2 (packet: dst IP) == entry +0x90 ; the socket's LOCAL address
|
||
arg3 (packet: sport) == entry +0x80 ; the socket's FOREIGN port
|
||
arg4 (packet: src IP) == entry +0x60 ; the socket's FOREIGN address
|
||
```
|
||
|
||
That is exactly a UDP demux, and it validates the PCB layout of section 31.1 from
|
||
an independent direction.
|
||
|
||
### 44.3 Corrections
|
||
|
||
| claim | status |
|
||
|---|---|
|
||
| 43.2: "the object is a per-datagram binding structure / PCB-like" | **wrong** — it is the packet buffer |
|
||
| 31.2/39: "the compared word is a per-socket state" | **wrong anchor** — the word is read out of the packet |
|
||
| 31.1: PCB layout (`+0x60` foreign addr, `+0x80` foreign port, `+0x90` local addr, `+0xB0` local port) | **confirmed** by the query/compare pairing above |
|
||
|
||
The compare chain's values (5, 8, 13…18) are therefore compared against **packet
|
||
header data indexed by a 4-bit field of the packet** (the dump shows that field
|
||
is the IP version nibble, `4`). They are not socket states, which is precisely
|
||
why the chain never matches for IPv4 traffic — the value it tests is a header
|
||
word, not a state code. Naming them as "states" was a mistake now withdrawn.
|
||
|
||
### 44.4 What this leaves
|
||
|
||
The delivery function's early part is now understood: identify the packet, find
|
||
the PCB by (dport, dst IP, sport, src IP) against the binding table, and hand the
|
||
payload to the socket. The compare chain is a **header-value** test whose
|
||
constants are still unnamed, and it does not run for ordinary traffic — which is
|
||
consistent with it being a diagnostic/exceptional branch rather than the normal
|
||
path. That is a coherent end state for this corner, and it is supported by a
|
||
memory dump rather than by inference.
|
||
|
||
## 45. What the 5/8/13…18 compares actually test — a debug-gated header check
|
||
|
||
### 45.1 The index arithmetic, exactly
|
||
|
||
```
|
||
3F61EC80 setf 4 ; 4-bit field size
|
||
3F61EC80 a7 = *a11 ; nibble of the packet's first byte
|
||
3F61EC90 a9 = a7
|
||
3F61ECA0 a9 <<= 2 ; field * 2
|
||
...
|
||
3F61EF80 a7 = a9
|
||
3F61EF90 a7 <<= 3 ; field * 16 bits = field * 2 ... i.e. field*4 bytes after the <<2
|
||
3F61EFA0 a7 += a11 ; slot = packet + field*4 bytes
|
||
3F61EFB0 a7 = *a7,0 ; 16-bit word there
|
||
3F61F010 subk 8,a7
|
||
3F61F070 subk 13,a7
|
||
3F61F0D0 subk 14,a7 ... through subk 18
|
||
```
|
||
|
||
So the compared value is a **16-bit word at `packet + field*4 bytes`**, where the
|
||
field is a nibble of the packet's first byte.
|
||
|
||
### 45.2 What that lands on, using the captured dump
|
||
|
||
The section 44 dump of the same object (`a11` at `0x3F61EE70`) begins:
|
||
|
||
```
|
||
45 00 11 00 3f 99 00 40 40 11 00 00 c0 a8 63 01 c0 a8 63 02 ...
|
||
^ byte 0 = 0x45 (IPv4, IHL 5)
|
||
```
|
||
|
||
with `field` = `4` or `5` (the two nibbles of `0x45`) the slot lands at
|
||
`packet + 16` or `packet + 20` — i.e. on the **destination address** or the
|
||
**UDP source port** — not on any state word. Comparing those against 5, 8, 13…18
|
||
can essentially never be true for real traffic.
|
||
|
||
### 45.3 Why that is consistent with everything measured
|
||
|
||
* the chain has **0 executions** in `rttcp2.tr` across 44 segments and 12
|
||
datagrams (section 39.1, re-measured after this correction);
|
||
* the same function is guarded by a debug flag: at `0x3F61ECB0` it reads
|
||
`@0x1E0BF0` and, when set, logs through `0x3F623B80` before doing anything else
|
||
(the same gate appears in `0x3F622F10` and `0x3F623740`).
|
||
|
||
~~Taken together, the 5/8/13…18 compares are best described as a debug-gated
|
||
validation branch over packet header data.~~ **Superseded by section 46**: the
|
||
debug flag only gates a log call; the chain is gated by `dsj` on the IP protocol
|
||
byte and the constants are ICMP query types. That is a different thing from the "socket state enumeration"
|
||
this report claimed in sections 31.2 and 39, and both are now superseded:
|
||
|
||
| earlier claim | status |
|
||
|---|---|
|
||
| 31.2 "suppresses a set of socket states" | withdrawn |
|
||
| 39 "exceptional socket states, unnamed" | withdrawn |
|
||
| 43 "the per-entry word is a table slot on a PCB" | withdrawn (43.2: object is the packet) |
|
||
| 45 (this section) | debug-gated header-value test, never taken by normal traffic |
|
||
|
||
### 45.4 Where the socket layer now stands
|
||
|
||
Everything on the operational path is accounted for with evidence:
|
||
|
||
```
|
||
packet -> 0x3F61EC00: identify the datagram (packet buffer, section 44)
|
||
-> 0x3F61E490: match PCB by (dport, dst IP, sport, src IP) vs
|
||
(local port +0xB0, local addr +0x90, foreign port +0x80,
|
||
foreign addr +0x60) [section 31.1, 44.2]
|
||
-> deliver to the socket, or log an unmatched datagram (0x96500)
|
||
-> reply path 0x3F622A40 -> 0x3F623350 -> 0x3F62EA00 -> device
|
||
```
|
||
|
||
The unnamed constants were the last open item in this layer, and they are now
|
||
explained structurally rather than by name — which is the honest limit of a
|
||
ROM-only analysis of a debug branch that the running firmware never takes.
|
||
|
||
## 46. The 5/8/13…18 constants are ICMP types — the chain is protocol-gated, not debug-gated
|
||
|
||
Section 45 said the compare chain was "debug-gated". That was wrong: the debug
|
||
flag (`0x1E0BF0`) only gates a **log call**, and flipping it in RAM (verified this
|
||
turn: the flag was poked every 30 frames and the chain still did not run) changes
|
||
nothing. The real gate is one instruction earlier.
|
||
|
||
### 46.1 The guard: `dsj` on the IP protocol byte
|
||
|
||
```
|
||
3F61EF00 a7 = *(a11+0x48) ; byte 9 of the IP header = PROTOCOL
|
||
3F61EF20 dsj a7, L3F61F300 ; decrement; jump if the result is nonzero
|
||
```
|
||
|
||
`DSJ` is decrement-and-jump-if-nonzero (`34010ops.hxx`):
|
||
|
||
```c
|
||
#define DSJ(R) { if (--R##REG(DSTREG(op))) { m_pc += ...; } else { SKIP_WORD(); } }
|
||
```
|
||
|
||
so the fall-through — the path that computes the indexed word and runs the
|
||
compare chain — is taken **only when the protocol byte is 1**, i.e. **ICMP**.
|
||
|
||
That single fact explains every count observed so far:
|
||
|
||
| trace | protocol | `0x3F61EC00` | `0x3F61EF00` | chain (`0x3F61F010`…) |
|
||
|---|---|---|---|---|
|
||
| `rttcp2.tr` (UDP probes) | 17 | 11 | 11 | **0** — jumps away to `0x3F61F300` |
|
||
| `icmpw.tr` (echo requests) | 1 | 0 | 0 | **0** — echo requests are handled by `0x3F61FFA0` and never reach this function |
|
||
|
||
### 46.2 The constants are the ICMP query types
|
||
|
||
With the chain reached only for ICMP, the eight compared values are exactly the
|
||
ICMP types the stack answers itself:
|
||
|
||
| value | ICMP type |
|
||
|---|---|
|
||
| 5 | redirect |
|
||
| 8 | echo request |
|
||
| 13 | timestamp request |
|
||
| 14 | timestamp reply |
|
||
| 15 | information request |
|
||
| 16 | information reply |
|
||
| 17 | address mask request |
|
||
| 18 | address mask reply |
|
||
|
||
and the chain's action is to skip socket delivery (`jreq L3F61F300`) for those
|
||
types — because the stack answers them internally (section 22 shows the echo
|
||
request being rewritten and sent from the ICMP input path).
|
||
|
||
The indexed word is therefore **the ICMP type byte of the packet**, reached via
|
||
the packet's own first nibble — which is why the values are single-digit types and
|
||
why nothing else ever matches.
|
||
|
||
### 46.3 Corrections
|
||
|
||
| claim | status |
|
||
|---|---|
|
||
| 31.2 / 39 "socket states 5/8/13…18" | withdrawn (already) |
|
||
| 45 "debug-gated header test" | **wrong gate** — the debug flag only gates logging; the gate is `dsj` on the protocol byte |
|
||
| 45.4 "constants remain unnamed" | **now named**: ICMP query types |
|
||
|
||
### 46.4 Why this closes the item
|
||
|
||
The chain is not a socket-state suppression set and not a debug path: it is the
|
||
**ICMP "answer it ourselves" test**, one `dsj` away from being skipped for every
|
||
non-ICMP packet. Every measurement now agrees with that reading — the UDP path
|
||
skips it 11 times out of 11, and the ICMP path never reaches this function at all
|
||
because echo requests are answered earlier.
|
||
|
||
This is the first item in the report resolved by combining three independent
|
||
lines of evidence: the CPU core's `DSJ` semantics, the packet's protocol byte
|
||
(confirmed by the section 44 memory dump), and the exact constant set.
|
||
|
||
## 47. Double INIT changes nothing — and INIT is set in MAME too
|
||
|
||
### 47.1 The variant
|
||
|
||
`lance_init2.coff` writes `INIT` **twice** (the second before the `IDON` poll),
|
||
on the theory that a second write while `m_idon` is still false would leave
|
||
`INIT` visible:
|
||
|
||
| payload | csr0a | csr0b | ok | rmd1 | mcnt | txmd1 |
|
||
|---|---|---|---|---|---|---|
|
||
| `lance.coff` | `0x7B3` | `0x7B3` | 1 | `0x0300` | `0x1C` | `0x8300` |
|
||
| `lance_init2.coff` | `0x7B3` | `0x7B3` | 1 | `0x0300` | `0x1C` | `0x8300` |
|
||
|
||
Identical again. That is the **fourth** payload variant with byte-identical
|
||
results, and it rules out the "second INIT leaves INIT set" theory in the model.
|
||
|
||
### 47.2 The bit-by-bit difference between model and machine
|
||
|
||
Comparing the model's `CSR0 = 0x7B3` with the real chip's `0x1BB`:
|
||
|
||
| bit | name | MAME | real | |
|
||
|---|---|---|---|---|
|
||
| `0x001` | INIT | 1 | 1 | |
|
||
| `0x002` | STRT | 1 | 1 | |
|
||
| `0x004` | STOP | 0 | 0 | |
|
||
| `0x008` | TDMD | **0** | **1** | ← the demand was serviced only in the model |
|
||
| `0x010` | TXON | 1 | 1 | |
|
||
| `0x020` | RXON | 1 | 1 | |
|
||
| `0x040` | INEA | 0 | 0 | |
|
||
| `0x080` | INTR | 1 | 1 | |
|
||
| `0x100` | IDON | 1 | 1 | |
|
||
| `0x200` | TINT | **1** | **0** | ← no transmit completion on the machine |
|
||
| `0x400` | RINT | **1** | **0** | ← no receive on the machine |
|
||
|
||
Three facts come out of one table:
|
||
|
||
1. **`INIT` is set in the model too.** Sections 41.3 and 42.3 treated "INIT still
|
||
set" as a real-chip anomaly and built hypotheses on it. It is not an anomaly —
|
||
every payload run in MAME ends with `INIT` set. That hypothesis is withdrawn.
|
||
2. `STRT`, `TXON`, `RXON`, `IDON`, `INTR` agree exactly between model and machine:
|
||
the chip *was* started and *did* complete initialization.
|
||
3. The real differences are the **activity** bits: on the machine `TINT` and
|
||
`RINT` never fired and `TDMD` was never cleared, i.e. the chip accepted the
|
||
command set and then **did nothing at all** — no transmit poll, no receive.
|
||
|
||
### 47.3 What that means
|
||
|
||
The model clears `TDMD` at the top of `transmit_poll()` before it even looks at
|
||
the descriptor, so in MAME `TDMD = 0` is guaranteed once the poll is scheduled.
|
||
On the machine `TDMD` stayed set, so the poll was never scheduled. Since `TXON`
|
||
is set and `STRT` took effect, the remaining explanations are all about the
|
||
chip's internal scheduling rather than about the register write:
|
||
|
||
* the chip was still inside its initialization sequence (both `INIT` and `IDON`
|
||
set — the model tolerates that state, the machine may not);
|
||
* the transmit poll period is derived from the chip clock and our modelled clock
|
||
choice is wrong (the driver builds the device with a clock that the real board
|
||
may not match), so the poll never comes due in the payload's timing window;
|
||
* the payload's 50 000-iteration busy-wait is far shorter on real silicon than
|
||
the poll period.
|
||
|
||
The last one is testable **on the machine with no new tooling**: increase the
|
||
busy-wait after the `TDMD` write by an order of magnitude and see whether `TDMD`
|
||
clears. That is now the cheapest decisive experiment, ahead of any further
|
||
register archaeology.
|
||
|
||
### 47.4 Status
|
||
|
||
Four payload variants, all byte-identical in MAME; the model's `CSR0` compared
|
||
bit-for-bit with the machine's; the "INIT anomaly" withdrawn; and a concrete,
|
||
timing-based hypothesis added that the existing payload can test by waiting
|
||
longer. Real-silicon confirmation still needs the VT1200.
|
||
|
||
## 48. Long wait: the model never leaves TDMD set (0 of 600 polls)
|
||
|
||
### 48.1 The variant
|
||
|
||
`lance_longwait.coff` keeps the normal init and kick, then waits **600,000
|
||
iterations**, sampling `CSR0` every 1,000 iterations (600 polls) and recording:
|
||
|
||
* `cell(0x1C0)` — `CSR0` after the wait
|
||
* `cell(0x1E0)` — how many of the 600 polls still saw `TDMD` set
|
||
|
||
### 48.2 Result
|
||
|
||
```
|
||
lance_longwait: csr0a=0x7B3 csr0b=0x7B3 ok=1 rmd1=0x0300 mcnt=0x1C txmd1=0x8300
|
||
csr0long=0x7B3 tdmdsamples=0x00000000
|
||
lance (baseline): csr0a=0x7B3 csr0b=0x7B3 ok=1 rmd1=0x0300 mcnt=0x1C txmd1=0x8300
|
||
```
|
||
|
||
**0 of 600 polls saw `TDMD` set**, and `CSR0` after the wait is identical to the
|
||
baseline. In the model the demand is serviced before the first sample — the
|
||
poll is scheduled at zero delay (`m_transmit_poll->adjust(attime::zero, ...)`)
|
||
and `transmit_poll()` clears `TDMD` at its top.
|
||
|
||
### 48.3 What this closes
|
||
|
||
Together with sections 41, 42 and 47, the offline phase is now complete on this
|
||
question, with five variants and one timing sweep:
|
||
|
||
| payload | kick | init order | wait | TDMD left set? |
|
||
|---|---|---|---|---|
|
||
| `lance.coff` | `STRT\|TDMD` | STOP…INIT | 50 k | no |
|
||
| `lance_bare.coff` | `TDMD` | STOP…INIT | 50 k | no |
|
||
| `lance_stopinit.coff` | `STRT\|TDMD` | STOP,INIT adjacent | 50 k | no |
|
||
| `lance_init2.coff` | `STRT\|TDMD` | INIT twice | 50 k | no |
|
||
| `lance_longwait.coff` | `STRT\|TDMD` | STOP…INIT | 600 k, 600 polls | **no (0/600)** |
|
||
|
||
All five return `0x7B3 / ok=1 / 0x0300 / 0x1C / 0x8300`. **No offline
|
||
configuration reproduces the machine's `TDMD`-stuck state**, so the difference is
|
||
a property of the real chip that the model does not implement — not something
|
||
further payload variants can reveal.
|
||
|
||
### 48.4 The payload that answers it on the machine
|
||
|
||
`lance_longwait.coff` is also the right instrument for the hardware run: it
|
||
already records exactly the two numbers needed, in the marker cells the loader
|
||
prints.
|
||
|
||
```
|
||
expected if the chip polls: csr0long = (TDMD clear), tdmdsamples small
|
||
expected if the chip never polls: csr0long = TDMD set, tdmdsamples = 600
|
||
```
|
||
|
||
The intermediate case (`tdmdsamples` between 1 and 599, falling over time) would
|
||
mean the poll *is* running but slower than the payload's first samples — which
|
||
would immediately confirm the timing hypothesis of section 47.3. That is the
|
||
single most informative read available on the machine, and it needs no tooling
|
||
beyond running an existing build.
|
||
|
||
## 49. Cross-check against Linux's LANCE driver (`drivers/net/ethernet/amd/7990.c`)
|
||
|
||
An independent reference implementation was compared field by field with
|
||
`harness/udpstream/lance.c`. Linux's generic core is the right comparison: it is
|
||
the same chip family (Am7990/C-LANCE) driven on seven non-x86 buses.
|
||
|
||
### 49.1 The sequences side by side
|
||
|
||
Linux `lance_reset()` → `load_csrs()` → `lance_init_ring()` →
|
||
`init_restart_lance()`:
|
||
|
||
```c
|
||
WRITERDP(lp, LE_C0_STOP); /* stop */
|
||
WRITERAP(lp, LE_CSR1); WRITERDP(lp, leptr & 0xFFFF);
|
||
WRITERAP(lp, LE_CSR2); WRITERDP(lp, leptr >> 16);
|
||
WRITERAP(lp, LE_CSR3); WRITERDP(lp, lp->busmaster_regval);
|
||
WRITERDP(lp, LE_C0_INIT); /* initialize */
|
||
for (i = 0; (i < 100) && !(READRDP(lp) & (LE_C0_ERR | LE_C0_IDON)); i++);
|
||
if ((i == 100) || (READRDP(lp) & LE_C0_ERR)) return -1;
|
||
WRITERDP(lp, LE_C0_IDON); /* <<< CLEAR IDON */
|
||
WRITERDP(lp, LE_C0_INEA | LE_C0_STRT); /* start, interrupts on */
|
||
```
|
||
|
||
| step | Linux `7990.c` | `lance.c` (ours) | note |
|
||
|---|---|---|---|
|
||
| STOP first | yes | yes (line 131) | same |
|
||
| CSR1 / CSR2 | init-block address | same | same |
|
||
| CSR3 | `busmaster_regval` (0 for little-endian, `LE_C3_BSWP` for big-endian; declance comments "should be zero") | `0` | matches the firmware's own value |
|
||
| INIT | yes | yes | same |
|
||
| poll | `ERR \| IDON`, 100 tries | `IDON` only, 20 000 tries | **ours ignores ERR** |
|
||
| **clear IDON** | **`WRITERDP(lp, LE_C0_IDON)`** | **not done** | **only real difference in ordering** |
|
||
| start | `INEA \| STRT` | `STRT` alone | interrupts irrelevant (we poll) |
|
||
| kick | `INEA \| TDMD` | `STRT \| TDMD` | equivalent command bits |
|
||
| init-block mode | `LE_MO_PROM` (0x8000) | `LE_MO_LOOP\|LE_MO_INTL` (0x0044) | loopback is the point of the test |
|
||
| TX descriptor length | `0xf000` then set at xmit | `0x0FE8` (24 B) | same negative-length convention |
|
||
| RX descriptor length | `-RX_BUFF_SIZE \| 0xf000` | `0x0A0A` (-1526) | same convention |
|
||
| OWN bit | set at transmit time | set at kick | same effect |
|
||
|
||
### 49.2 The one substantive difference (weakened — see section 50.5)
|
||
|
||
Linux **writes `IDON` back (clearing it) before writing `STRT`**. Our payload
|
||
leaves `IDON` set, and the machine's register snapshot was
|
||
`CSR0 = 0x1BB`, which contains **both `IDON` and `INIT` set** while `STRT`,
|
||
`TXON` and `TDMD` are also set.
|
||
|
||
That combination is exactly what a chip looks like when it has latched the start
|
||
command but never left the "initialization done, not acknowledged" state:
|
||
|
||
* `STRT`/`TXON` readable as set — the bit was accepted;
|
||
* `IDON` still pending because nobody acknowledged it;
|
||
* consequently the internal sequencer never enters normal operation, so the
|
||
transmit poll is never scheduled and `TDMD` is never cleared.
|
||
|
||
The model cannot show this because `transmit_poll()` is scheduled on the `STRT`
|
||
write unconditionally and clears `TDMD` on its first tick.
|
||
|
||
This is the first hypothesis derived from an independent implementation rather
|
||
than from the ROM alone, and it is cheap to test.
|
||
|
||
### 49.3 Variant built to test it
|
||
|
||
`lance_idonclear.c` / `lance_idonclear.coff` inserts, right after the `IDON`
|
||
poll and before the start:
|
||
|
||
```c
|
||
csr_w(0, CSR0_IDON); /* clear IDON by writing 1, as Linux does */
|
||
for (idle = 0; idle < 2000; idle++) { }
|
||
```
|
||
|
||
Everything else is unchanged. In MAME this should be a no-op (the model ignores
|
||
the acknowledgement), and on the machine it is the sequence Linux has used on
|
||
seven architectures.
|
||
|
||
### 49.4 MAME result for the IDON-clearing variant
|
||
|
||
| payload | csr0 | ok | rmd1 | mcnt | txmd1 |
|
||
|---|---|---|---|---|---|
|
||
| `lance.coff` | `0x7B3` | 1 | `0x0300` | `0x1C` | `0x8300` |
|
||
| `lance_idonclear.coff` | **`0x6B3`** | 1 | `0x0300` | `0x1C` | `0x8300` |
|
||
|
||
`0x7B3 − 0x6B3 = 0x100` = `IDON`. So the acknowledgement takes effect exactly as
|
||
the datasheet describes, nothing else changes, and there is no regression — the
|
||
loopback still completes (`ok=1`, `rmd1=0x0300`, `mcnt=0x1C`).
|
||
|
||
In MAME the two payloads differ only in that bit, because the model's
|
||
`transmit_poll()` is scheduled on `STRT` regardless. On the machine this variant
|
||
is the first that follows the sequence every Linux LANCE driver uses:
|
||
|
||
```
|
||
STOP -> CSR1/2/3 -> INIT -> wait IDON -> CLEAR IDON -> INEA|STRT -> TDMD
|
||
```
|
||
|
||
If `TDMD` clears with `lance_idonclear.coff` but not with `lance.coff`, the
|
||
missing `IDON` acknowledgement is the answer to the whole hardware question. If
|
||
it still does not clear, `TDMD`-stuck is independent of the init handshake and
|
||
the remaining candidates are the poll period / clock model (section 47.3).
|
||
|
||
## 50. The descriptor length encoding was wrong — found by comparing drivers
|
||
|
||
The NetBSD drivers, the Linux driver and the VT1200 firmware all agree on one
|
||
detail that our payload got wrong, and it explains the machine's symptoms better
|
||
than any earlier hypothesis.
|
||
|
||
### 50.1 The rule: the length's top nibble must be ones
|
||
|
||
NetBSD `sys/dev/ic/am7990reg.h`:
|
||
|
||
```c
|
||
#define LE_XMD2_ONES 0xf000
|
||
```
|
||
|
||
and `sys/dev/ic/am7990.c` writes every descriptor length as
|
||
|
||
```c
|
||
rmd.rmd2 = -LEBLEN | LE_XMD2_ONES; /* receive */
|
||
tmd.tmd2 = 0 | LE_XMD2_ONES; /* transmit, idle */
|
||
tmd.tmd2 = -len | LE_XMD2_ONES; /* transmit, real length */
|
||
...
|
||
len = -tmd.tmd2; /* read back */
|
||
```
|
||
|
||
Linux `7990.c` says the same in comments:
|
||
|
||
```c
|
||
ib->btx_ring[i].length = 0xf000; /* The ones required by tmd2 */
|
||
ib->brx_ring[i].length = -RX_BUFF_SIZE | 0xf000; /* 0xf000 == bits that must be one */
|
||
```
|
||
|
||
### 50.2 The VT1200 firmware obeys it too
|
||
|
||
From this report's own disassembly:
|
||
|
||
* RX arming `0x3F9D36C0` writes the length with `setf 12` and the value
|
||
**`0xFDDA`** = `0xF000 | 0x0DDA` -> 550 bytes, top nibble **ones** (section 10);
|
||
* the TX ring initialiser `0x3F9D2FF0` writes **`0xF000`** as the idle TX length
|
||
(section 10).
|
||
|
||
So the firmware writes exactly the encoding NetBSD and Linux use.
|
||
|
||
### 50.3 What our payload wrote, and why MAME hid it
|
||
|
||
```
|
||
lance.c (before): RX length 0x0A0A TX length 0x0FE8
|
||
required: RX length 0xFA0A TX length 0xFFE8 (0xF000 | -len)
|
||
```
|
||
|
||
MAME's model **ORs the nibble in itself**:
|
||
|
||
```cpp
|
||
virtual int get_buf_length(u16 data) const override
|
||
{ return (data == 0xf000) ? 4096 : -s16(0xf000 | data); }
|
||
```
|
||
|
||
so `0x0A0A` is silently turned into `0xFA0A` and our payload passes every MAME
|
||
test. Read literally, our values are **positive** (4072 and 2570) where the chip
|
||
expects a **negative** length — invalid for both directions.
|
||
|
||
### 50.4 Why this explains the whole machine result
|
||
|
||
A chip rejecting both descriptor sets produces exactly what was measured:
|
||
|
||
| observation | explanation |
|
||
|---|---|
|
||
| `TDMD` still set, no `TINT` | the transmit descriptor is invalid, the transmit never completes |
|
||
| no `RINT`, no receive | the receive descriptors are invalid, nothing is received |
|
||
| `STRT`/`TXON`/`IDON` set, `ERR` clear | the chip is initialised and started, it just has nothing valid to do |
|
||
|
||
That is a single cause for both missing interrupt bits, and it does not depend on
|
||
chip-internal behaviour the model does not implement.
|
||
|
||
### 50.5 Correction to section 49.2
|
||
|
||
The `IDON`-clearing hypothesis is weakened: **NetBSD does not clear `IDON`**
|
||
either (`sys/dev/ic/lance.c` writes `LE_C0_INEA | LE_C0_STRT` immediately once
|
||
`IDON` is set). Two mainstream drivers disagree on that step and both work, so it
|
||
is not a requirement. The difference that matters is the length encoding, which
|
||
all three implementations agree on.
|
||
|
||
### 50.6 Fixed payload
|
||
|
||
`lance_fixlen.c` / `lance_fixlen.coff` writes `0xFA0A` and `0xFFE8`. In MAME it is
|
||
identical to the baseline (`0x7B3 / ok=1 / 0x0300 / 0x1C / 0x8300`), as it must
|
||
be, because the model masks the nibble. On the machine it is the first payload
|
||
whose descriptors match what the firmware, Linux and NetBSD all write.
|
||
|
||
## 51. Provenance: the stack descends from 386BSD — and it corroborates section 46
|
||
|
||
The local NetBSD tree (`/home/arti/code/NetBSD-src`) carries history back to
|
||
**1992-07-13**, including a `4.4BSD-Lite2` commit and, on 1993-03-21, the
|
||
**initial import of 386BSD-0.1 sources**. That is the right vintage for this
|
||
terminal, and it turns out to be usable as a reference implementation.
|
||
|
||
### 51.1 An exact string match
|
||
|
||
The ROM contains `arp: ether address is broadcast for IP address …`
|
||
(section 26.1). In the 386BSD import:
|
||
|
||
```
|
||
a319363d6037:sys/netinet/if_ether.c:350:
|
||
"arp: ether address is broadcast for IP address %x!\n",
|
||
```
|
||
|
||
Modern NetBSD (`sys/netinet/if_arp.c:929`) says *"link address"* instead, so the
|
||
VT1200 matches the **older** wording — the same 386BSD/4.3–4.4 lineage. Other
|
||
messages in the same ROM (`arpresolve: no free entry`) belong to that file too.
|
||
|
||
### 51.2 The ICMP case set is the 386BSD one
|
||
|
||
Section 46 concluded from the `dsj` + compare chain that the constants
|
||
5, 8, 13, 14, 15, 16, 17, 18 are ICMP types. The 386BSD `ip_icmp.c` switch
|
||
(`a319363d6037:sys/netinet/ip_icmp.c`) contains exactly these case labels:
|
||
|
||
```
|
||
case ICMP_REDIRECT: /* 5 */
|
||
case ICMP_ECHO: /* 8 */
|
||
case ICMP_TSTAMP: /* 13 */
|
||
case ICMP_TSTAMPREPLY: /* 14 */
|
||
case ICMP_IREQ: /* 15 */
|
||
case ICMP_IREQREPLY: /* 16 */
|
||
case ICMP_MASKREQ: /* 17 */
|
||
case ICMP_MASKREPLY: /* 18 */
|
||
```
|
||
|
||
(plus the error types `UNREACH`(3), `SOURCEQUENCH`(4), `TIMXCEED`(11),
|
||
`PARAMPROB`(12) and `ECHOREPLY`(0), which our chain does not test). That is
|
||
independent confirmation of the section 46 reading from the reference source,
|
||
not from the ROM alone.
|
||
|
||
### 51.3 LANCE driver history in the same tree
|
||
|
||
| path | first added |
|
||
|---|---|
|
||
| `sys/arch/i386/isa/if_le.c` | 1994-07-01 |
|
||
| `sys/dev/isa/if_le.c` | 1994-07-01 |
|
||
| `sys/dev/ic/am7990.c` | 1995-06-28 |
|
||
| `sys/dev/ic/lance.c` | 1998-07-21 |
|
||
|
||
The 1994 ISA driver is the closest in date to the firmware, and `am7990.c` (1995)
|
||
is the source of the descriptor-length rule that section 50 identifies as our
|
||
payload's bug.
|
||
|
||
Note on method: this tree is ~7 GB, so repo-wide `git log -S`/`--all` scans take
|
||
minutes. Path-limited queries are immediate and were used instead:
|
||
|
||
```sh
|
||
git log --diff-filter=A --format='%ad %h' --date=short -- sys/dev/isa/if_le.c
|
||
git show <commit>:sys/netinet/ip_icmp.c
|
||
```
|
||
|
||
## 52. CSRG tree (`original-bsd`) — the firmware's own lineage, and IDON settled
|
||
|
||
`/home/arti/code/original-bsd` is the CSRG source tree: history back to
|
||
**1979-12-04** (`3bsd version`), last commit the final CSRG delta (4.4BSD-Lite2).
|
||
180 MB, so full-tree searches are instant — the right tree for provenance
|
||
questions.
|
||
|
||
### 52.1 Same ARP code, one release older
|
||
|
||
```
|
||
sys/netinet/if_ether.c:409:
|
||
"arp: ether address is broadcast for IP address %x!\n",
|
||
```
|
||
|
||
identical to the string in the ROM and to the 386BSD import, and unlike modern
|
||
NetBSD's "link address" wording.
|
||
|
||
### 52.2 LANCE drivers present in 4.4BSD
|
||
|
||
```
|
||
sys/hp300/dev/if_le.c sys/luna68k/dev/if_le.c
|
||
sys/pmax/dev/if_le.c sys/sparc/sbus/if_le.c
|
||
sys/news3400/if/if_lance.c
|
||
```
|
||
|
||
The **pmax (DECstation)** driver is the closest sibling to a DEC-built terminal
|
||
stack, and its init sequence is (`sys/pmax/dev/if_le.c:405-419`):
|
||
|
||
```c
|
||
LEWREG(LE_CSR1, ...); LEWREG(init_block_addr_low, ...);
|
||
LEWREG(LE_CSR2, ...); LEWREG(0, ...);
|
||
LEWREG(LE_CSR3, ...); LEWREG(0, ...);
|
||
LEWREG(LE_CSR0, ...); LERDWR(ler0, LE_INIT, ...);
|
||
do { ... } while ((stat & LE_IDON) == 0);
|
||
LERDWR(ler0, LE_IDON, ...); /* clear IDON by writing 1 */
|
||
LERDWR(ler0, LE_STRT | LE_INEA, ...);
|
||
```
|
||
|
||
So 4.4BSD and Linux both clear `IDON`; NetBSD's later driver does not.
|
||
|
||
### 52.3 What the VT1200 firmware itself does — and why that settles it
|
||
|
||
The firmware's own init (`io5800_5900_init`, `0x3FDDCBE0`) writes only:
|
||
|
||
```
|
||
RAP=1 -> init block low RAP=2 -> init block high
|
||
RAP=3 -> 0 RAP=0 -> 0x7F41
|
||
```
|
||
|
||
`0x7F41` = `INIT | INEA | (all status bits written as 1 to clear them)`. The
|
||
poll and start live in its caller, and the live path captured in `LANCELOG`
|
||
(section 3) is:
|
||
|
||
```
|
||
CSR0 = 0x0001 (INIT) -> poll IDON (0x0181) -> CSR0 = 0x0042 (STRT|INEA)
|
||
```
|
||
|
||
**No `IDON` clear anywhere** — and this firmware demonstrably runs the terminal's
|
||
network stack on real hardware.
|
||
|
||
Therefore clearing `IDON` is *optional*, confirmed from three directions:
|
||
a shipping driver that omits it (NetBSD), two that include it (4.4BSD pmax,
|
||
Linux), and the firmware itself, which omits it and works.
|
||
|
||
### 52.4 Result for the hardware question
|
||
|
||
Section 49.2's `IDON` hypothesis is now **dead**, and section 50 is left as the
|
||
only explanation that fits all the evidence: the descriptor length fields must be
|
||
`0xF000 | -len`, our payload's were not, MAME masks the difference in
|
||
`get_buf_length()`, and a chip rejecting both descriptor sets produces exactly
|
||
the measured state (`TDMD` never cleared, no `TINT`, no `RINT`, `ERR` clear).
|
||
|
||
`lance_fixlen.coff` remains the payload to run on the machine.
|
||
|
||
## 53. Mapping the report's functions to 4.4BSD source
|
||
|
||
Every function reversed above has a counterpart in
|
||
`/home/arti/code/original-bsd` (CSRG, 4.4BSD-Lite2). Mapping ROM address →
|
||
source function, with the construct that identifies each pair.
|
||
|
||
### 53.1 Protocol stack
|
||
|
||
| ROM | BSD source | evidence |
|
||
|---|---|---|
|
||
| `0x3F624980` IP task | `ipintr()` `sys/netinet/ip_input.c:115` | takes the packet from a queue, then dispatches (below) |
|
||
| `0x3F6270F0` protocol dispatch | `ip_input.c:366` `(*inetsw[ip_protox[ip->ip_p]].pr_input)(m, hlen);` | **both tables found in RAM**: `0x1E0F70` = `ip_protox[256]` (protocol→index), `0xD6BE0` = `inetsw[]` |
|
||
| — struct layout | `struct protosw` `sys/sys/protosw.h:32` | `pr_protocol` at byte 6 and `pr_input` at byte 10 — exactly the offsets in our `0xD6BE0` dump (section 23.2) |
|
||
| `0x3F61FFA0` ICMP input | `icmp_input()` `sys/netinet/ip_icmp.c:148` | checksum verify over the ICMP header, then the type switch |
|
||
| `0x3F6224E0` type dispatch | the `switch (icm->icmp_type)` in `icmp_input()` | same case set (section 46, 51.2) |
|
||
| `0x3F622A40` reflect | `icmp_reflect()` `ip_icmp.c:386` | swaps source/destination, adjusts the total length, recomputes the checksum |
|
||
| `0x3F623350` ICMP send | `icmp_send()` `ip_icmp.c:510` | final transmit of the reply |
|
||
| `0x3F62EA00` IP transmit | `ip_output()` `sys/netinet/ip_output.c:43` | htons of total length and fragment word, header checksum at byte 10, then the interface's output |
|
||
| `0x3F63A340` TCP input | `tcp_input()` `sys/netinet/tcp_input.c:189` | states 3..10 tables (section 24) are `tcp_input`'s `switch (tp->t_state)` |
|
||
| TCP options | `tcp_dooptions()` `tcp_input.c:1309` | called from `tcp_input` at `:387` and `:567` |
|
||
| MSS handling | `tcp_mss()` `tcp_input.c:1501` | called from `tcp_dooptions` at `:1343` (`(void) tcp_mss(tp, mss); /* sets t_maxseg */`) |
|
||
| `0x3F6540C0` UDP input | `udp_input()` `sys/netinet/udp_usrreq.c:55` | header length checks, then the PCB lookup below |
|
||
| `0x3F656200` UDP header builder | `udp_output()` `udp_usrreq.c:351` | writes protocol 17 into the header (section 20) |
|
||
| `0x3F61E490` binding lookup | **`in_pcblookup()` `sys/netinet/in_pcb.c:428`** | exact match of the algorithm — see 53.2 |
|
||
| `0x3F6148E0`, `0x3F614200` ARP cache | `arplookup()` `if_ether.c:500`, `arpresolve()` `:261` | 19-bucket hash by IP, 9 entries (section 26) |
|
||
| `0x3F6119A0` ARP responder | **`in_arpinput()` `sys/netinet/if_ether.c:375`** | the "ether address is broadcast" diagnostic lives in it (section 51.1) |
|
||
| `0x3F60F260` ARP request | `arprequest()` `if_ether.c:216`, `arpwhohas()` `:202` | builds a 28-byte ARP payload |
|
||
|
||
### 53.2 `in_pcblookup` — line-for-line
|
||
|
||
```
|
||
in_pcb.c:434 int matchwild = 3, wildcard; <-- our `movk 3,a11`
|
||
in_pcb.c:437 if (inp->inp_lport != lport) continue; <-- entry+0xB0 vs arg1
|
||
in_pcb.c:439 if (inp->inp_laddr.s_addr != INADDR_ANY) <-- entry+0x90 vs arg2
|
||
in_pcb.c:452 if (inp->inp_faddr.s_addr != INADDR_ANY ||
|
||
inp->inp_fport != fport) continue; <-- entry+0x60 / +0x80
|
||
in_pcb.c:462 if (wildcard < matchwild) { match = inp; matchwild = wildcard; ... }
|
||
```
|
||
|
||
which is exactly the loop at `0x3F61E570`..`0x3F61EA20`, including the
|
||
`wildcard=3` start and the "best match wins" selection. The PCB field offsets in
|
||
section 31.1 are therefore `inp_lport`(22), `inp_laddr`(18), `inp_faddr`(12),
|
||
`inp_fport`(16).
|
||
|
||
### 53.3 LANCE driver
|
||
|
||
| ROM | BSD source (`sys/pmax/dev/if_le.c`, DECstation) | evidence |
|
||
|---|---|---|
|
||
| `0x3F9D4BC0` driver command entry (init + send) | `leinit()` `:429`, `lereset()` `:359` | CSR1/CSR2 = init block, CSR3 = 0, CSR0 = INIT, poll IDON, STRT |
|
||
| `0x3F9D36C0` ring arming | `ledrinit()` `:337`, `leinit()` | descriptor address/length/OWN setup |
|
||
| `0x3F9D57F0` TX kick | **`lestart()` `:458`** | `LER2_tmd1(tmd, LE_OWN|LE_STP|LE_ENP);` then `LERDWR(ler0, LE_TDMD|LE_INEA, ...)` |
|
||
| `0x3F9D5B40` interrupt dispatch | **`leintr()` `:501`** | reads CSR0, tests `LE_INTR`, `LE_SERR`, RXON/TXON, then RINT/TINT |
|
||
| `0x3F9D62B0` RX poll | **`lerint()` `:606`** | walks the receive ring, hands completed buffers up |
|
||
| `0x3F9D8AF0` TX completion | the TINT half of `leintr()` | releases the transmitted descriptors |
|
||
| `0x3F9DCF50`→`0x3F9D4BC0` submission | `if_start` → `lestart()` | the queue-to-ring hand-off |
|
||
|
||
### 53.4 What the mapping settles about the descriptor length
|
||
|
||
`lestart()` writes the transmit length as **plain two's complement**:
|
||
|
||
```c
|
||
LER2_tmd2(tmd, -len); /* if_le.c:485 */
|
||
```
|
||
|
||
For a 24-byte frame that is `0xFFE8`. For the receive side the same arithmetic
|
||
gives `0xFA0A` for 1526 bytes. Our payload wrote `0x0FE8` and `0x0A0A` — the
|
||
**positive** values, missing the top nibble that two's complement produces.
|
||
|
||
This confirms section 50 from a third independent direction and simplifies the
|
||
rule: the field is simply `-len`, and for any length under 4096 that always has
|
||
the top nibble set. There is no separate "0xF000 convention" to remember — a
|
||
positive value is simply wrong.
|
||
|
||
## 54. Remaining mappings: raw-IP delivery, TCP state switches, ICMP checksums
|
||
|
||
### 54.1 `0x3F61EC00` is `rip_input()` — raw IP delivery
|
||
|
||
`sys/netinet/raw_ip.c:58`. The three identifying constructs:
|
||
|
||
```c
|
||
raw_ip.c:65 for (inp = rawinpcb.inp_next; inp != &rawinpcb; inp = inp->inp_next) {
|
||
raw_ip.c:66 if (inp->inp_ip.ip_p && inp->inp_ip.ip_p != ip->ip_p) continue;
|
||
raw_ip.c:68 if (inp->inp_laddr.s_addr && inp->inp_laddr.s_addr != ip->ip_dst.s_addr) continue;
|
||
raw_ip.c:70 if (inp->inp_faddr.s_addr && inp->inp_faddr.s_addr != ip->ip_src.s_addr) continue;
|
||
raw_ip.c:80 sbappendaddr(&last->so_rcv, (struct sockaddr *)&ripsrc, n, 0)
|
||
raw_ip.c:96 sbappendaddr(&last->so_rcv, ...)
|
||
raw_ip.c:100 ipstat.ips_noproto++;
|
||
```
|
||
|
||
which are exactly the parts of `0x3F61EC00` measured earlier: a PCB-list walk
|
||
comparing a protocol word, a local and a foreign address, delivery through a
|
||
queue append, and the **`ips_noproto` counter on no match** — the counter we
|
||
recorded at `0x96500` (section 29.2).
|
||
|
||
The `dsj` gate found in section 46 compares that PCB field against 1, i.e. the
|
||
raw socket bound to **ICMP** (`inp->inp_ip.ip_p == IPPROTO_ICMP`); the
|
||
5/8/13…18 skip list is then the raw-ICMP socket filter — those are the types the
|
||
kernel answers internally, so a raw socket must not also receive them.
|
||
|
||
### 54.2 The TCP state tables are `tcp_input`'s two switches
|
||
|
||
`sys/netinet/tcp_input.c` has two dispatches on `tp->t_state`:
|
||
|
||
```
|
||
tcp_input.c:506 switch (tp->t_state) { <-- our table B (0x3F6417C0)
|
||
tcp_input.c:823 if (tiflags&TH_RST) switch (tp->t_state) {
|
||
tcp_input.c:825 case TCPS_SYN_RECEIVED:
|
||
tcp_input.c:829 case TCPS_ESTABLISHED:
|
||
tcp_input.c:840 case TCPS_CLOSING:
|
||
tcp_input.c:841 case TCPS_LAST_ACK:
|
||
tcp_input.c:842 case TCPS_TIME_WAIT:
|
||
tcp_input.c:865 switch (tp->t_state) { <-- our table A (0x3F63F350)
|
||
tcp_input.c:872 case TCPS_SYN_RECEIVED:
|
||
tcp_input.c:897 case TCPS_ESTABLISHED:
|
||
```
|
||
|
||
The grouping matches table A exactly: in section 24.2, states **7 CLOSING,
|
||
8 LAST_ACK and 10 TIME_WAIT all share the target `0x3F63F200`** — the same
|
||
fallthrough group as `tcp_input.c:840-842`. Table A's SYN_RECEIVED handled
|
||
separately and ESTABLISHED family grouped also matches `:872` vs `:897`.
|
||
|
||
### 54.3 ICMP checksum sites
|
||
|
||
| ROM | source | evidence |
|
||
|---|---|---|
|
||
| `0x3F6206D0` (verify) | `ip_icmp.c:184` `if (in_cksum(m, icmplen))` | checksum over the ICMP message before the type switch |
|
||
| `0x3F623350` (reflect) | `ip_icmp.c:522-523` `icp->icmp_cksum = 0; icp->icmp_cksum = in_cksum(...)` | zero the field, recompute, store — exactly the sequence at `0x3F62FF60`..`0x3F630000` |
|
||
|
||
### 54.4 Coverage
|
||
|
||
With 53 and 54 the **protocol-layer** functions all have a BSD counterpart:
|
||
IP (`ipintr`, `inetsw[ip_protox[]]`, `ip_output`), ICMP (`icmp_input`,
|
||
`icmp_reflect`, `icmp_send`), TCP (`tcp_input`, `tcp_dooptions`, `tcp_mss`),
|
||
UDP (`udp_input`, `udp_output`), raw IP (`rip_input`), ARP (`in_arpinput`,
|
||
`arpresolve`, `arplookup`, `arprequest`) and the PCB lookup (`in_pcblookup`).
|
||
|
||
Driver functions map to `leinit`/`lereset`/`ledrinit`/`lestart`/`leintr`/`lerint`,
|
||
**but not every addressed function is driver code**: the deferred receive/submit
|
||
path and the interrupt dispatcher are the firmware's own glue and have no 4.4BSD
|
||
counterpart. Section 56 completes the list, quotes the identifiers that were
|
||
missing here, and replaces an earlier over-broad claim ("every function the
|
||
report gives an address for") with that accurate statement.
|
||
|
||
## 55. The PCB field order proves the mapping at struct level
|
||
|
||
Section 31.1 gave the binding entry's fields from disassembly alone. The BSD
|
||
struct confirms them exactly, and the agreement is only possible with one
|
||
alignment:
|
||
|
||
```c
|
||
struct inpcb { /* sys/netinet/in_pcb.h */
|
||
struct inpcb *inp_next, *inp_prev; /* 0, 4 */
|
||
struct inpcb *inp_head; /* 8 */
|
||
struct in_addr inp_faddr; /* 12 */
|
||
u_short inp_fport; /* 16 */
|
||
struct in_addr inp_laddr; /* 18 (16-bit alignment) */
|
||
u_short inp_lport; /* 22 */
|
||
struct socket *inp_socket; /* 24 */
|
||
...
|
||
```
|
||
|
||
| field | source offset | our ROM offset | match |
|
||
|---|---|---|---|
|
||
| `inp_faddr` | 12 | `+0x60` bits = 12 | ✓ |
|
||
| `inp_fport` | 16 | `+0x80` bits = 16 | ✓ |
|
||
| `inp_laddr` | 18 | `+0x90` bits = 18 | ✓ |
|
||
| `inp_lport` | 22 | `+0xB0` bits = 22 | ✓ |
|
||
|
||
All four in the same order as `in_pcblookup()` tests them. On a 32-bit-aligned
|
||
build `inp_laddr` would sit at 20 and `inp_lport` at 24, so the exact match also
|
||
establishes that the firmware's ABI uses **16-bit alignment** — consistent with
|
||
the `struct protosw` offsets (6/10) in section 53.1.
|
||
|
||
This closes the loop on the socket layer: the ROM's offsets, the source's field
|
||
order, and the lookup algorithm are all the same object.
|
||
|
||
## 56. Completing the mapping: quoted identifiers, and what has no BSD counterpart
|
||
|
||
Sections 53–55 mapped most addressed functions but three rows were asserted
|
||
without a quoted line, and seven addressed functions were not in any row. Both
|
||
gaps are closed here. **Section 54.4's claim that "every function the report
|
||
gives an address for now has a BSD counterpart" was not true and is corrected in
|
||
56.3.**
|
||
|
||
### 56.1 Quoted identifiers for the rows that lacked one
|
||
|
||
**`ip_output()` — `sys/netinet/ip_output.c:258-261`** matches the ROM's
|
||
`0x3F62EA00` sequence operation for operation:
|
||
|
||
```c
|
||
ip->ip_len = htons((u_short)ip->ip_len);
|
||
ip->ip_off = htons((u_short)ip->ip_off);
|
||
ip->ip_sum = 0;
|
||
ip->ip_sum = in_cksum(m, hlen);
|
||
```
|
||
|
||
(our block `0x3F62FEB0`..`0x3F630000` does exactly: htons of the word at byte 2,
|
||
htons of the word at byte 6, zero the word at byte 10, checksum, store).
|
||
|
||
**`udp_input()` — `sys/netinet/udp_usrreq.c`**: the preamble and the PCB lookup
|
||
from the datagram's own header fields:
|
||
|
||
```c
|
||
:96 len = ntohs((u_short)uh->uh_ulen);
|
||
:114 if (uh->uh_sum) {
|
||
:119 if (uh->uh_sum = in_cksum(m, len + sizeof (struct ip))) {
|
||
:223 inp = in_pcblookup(&udb, ip->ip_src, uh->uh_sport,
|
||
ip->ip_dst, uh->uh_dport, INPLOOKUP_WILDCARD);
|
||
```
|
||
|
||
which is why the lookup arguments in `0x3F654980..0x3F654AA0` are
|
||
(dport, dst, sport, src) read straight out of the packet (section 44.2).
|
||
|
||
**`udp_output()` — same file**, the checksum the builder leaves for the end:
|
||
|
||
```c
|
||
:415 if ((ui->ui_sum = in_cksum(m, sizeof (struct udpiphdr) + len)) == 0)
|
||
```
|
||
|
||
**`tcp_dooptions()` — `sys/netinet/tcp_input.c:1309`**, called from two places in
|
||
`tcp_input` (`:387` and `:567`), which is the options loop our TCP input runs
|
||
over the header.
|
||
|
||
### 56.2 The addressed functions with no BSD counterpart
|
||
|
||
These are the VT1200's own glue. The structural reason is visible in the source:
|
||
BSD's receive path hands the frame up **inside the interrupt**, and its transmit
|
||
path is a direct `if_start` call —
|
||
|
||
```c
|
||
if_le.c:761 ether_input(&le->sc_if, &et, m); /* inside lerint() */
|
||
if_le.c:230 ifp->if_start = lestart;
|
||
if_le.c:443 (void) lestart(ifp);
|
||
if_le.c:942 lestart(ifp);
|
||
```
|
||
|
||
— whereas the firmware **defers both** through queues and event posts.
|
||
|
||
| ROM | role | status |
|
||
|---|---|---|
|
||
| `0x3F602E00` | RX entry: buffer state, re-arm, post | **VT1200 glue** (deferred RX; BSD calls `ether_input` inline at `if_le.c:761`) |
|
||
| `0x3F9E0B80` | prepare the frame for the block | **VT1200 glue**, same deferral |
|
||
| `0x3F600200` | frame -> packet block (lengths `0x5EE`/`0x226`/`0x44C`) | analogous to `leget()` (`if_le.c:797`, the mbuf side), but operates on the VT1200 packet block — **no line-level counterpart** |
|
||
| `0x3F603250` | enqueue to `0x96AE0` + post "IP input ready" | **VT1200 glue** (BSD has no queue here) |
|
||
| `0x3F9DC890` | submission wrapper before the driver entry | **VT1200 glue** around `if_start`/`lestart` (`if_le.c:230/443/942`) |
|
||
| `0x3F9DCF50` | the call site of the driver entry `0x3F9D4BC0` | **VT1200 glue** (the wrapper's inner call) |
|
||
| `0x3F7144C0` | OS interrupt dispatcher (handler table `0x3F7151F0`) | **VT1200 OS**, no BSD counterpart; BSD's equivalent is the platform vector that calls `leintr()` (`if_le.c:76` declares it) |
|
||
|
||
### 56.3 Correction to 54.4
|
||
|
||
Section 54.4 said every addressed function has a BSD counterpart. The accurate
|
||
statement is:
|
||
|
||
> **Every protocol-layer function the report addresses maps to a 4.4BSD
|
||
> function with a quoted identifier. The driver's receive and submit paths are
|
||
> split differently in the firmware (queue + task, and a submission wrapper), so
|
||
> four of them are VT1200 glue with no source counterpart, and the interrupt
|
||
> dispatcher belongs to the firmware's own OS.**
|
||
|
||
That is the honest boundary of the mapping: the protocol stack is 4.4BSD
|
||
lineage, the driver's *deferred* structure is not.
|
||
|
||
## 57. The remaining addressed functions: codec table, dispatchers, re-arm
|
||
|
||
Sections 53–56 left four groups out. Each is now either given a counterpart or
|
||
recorded as firmware-specific with the reason.
|
||
|
||
### 57.1 `0x3F660380` + table `0x1E3D80` — firmware field-codec table
|
||
|
||
Structure (section 18): index `*(a13+0xC0)` in 0..16, entry = **8 bytes**, `fn0`
|
||
at `+0` and `fn1` at `+0x20` bits (= byte 4); entries 0,1 and 4..15 share one
|
||
pair (`0x3F658570`/`0x3F658650`), entries 2/3 point at `0x3F615E00` /
|
||
`0x3F616090`.
|
||
|
||
**VT1200-specific, no 4.4BSD counterpart.** Checked for the two things it could
|
||
be: there is no XDR/RPC directory in this tree (`lib/libc/rpc` does not exist)
|
||
and no kernel table of function-pointer pairs indexed by a type code — the only
|
||
dispatch tables 4.4BSD has are `inetsw[]` (already mapped, section 53.1) and
|
||
`struct domain`, neither of which is a per-field codec.
|
||
|
||
### 57.2 `0x3F615E00` and `0x3F616090` — the convert/compare pair
|
||
|
||
| ROM | does | nearest 4.4BSD idiom | counterpart? |
|
||
|---|---|---|---|
|
||
| `0x3F615E00` | swaps a 32-bit packet field to host order, stores it, calls `0x3FB2C140` | `ntohl()` / `ntohs()` byte-order macros (`sys/netinet/in.h`) | **idiom only** — in BSD this is a macro inlined at each use, not a dispatch entry |
|
||
| `0x3F616090` | converts two values and compares them, returns 1/0 | `in_pcblookup()`'s address tests, e.g. `if (inp->inp_laddr.s_addr != laddr.s_addr) continue;` (`in_pcb.c:441`) | **idiom only** |
|
||
|
||
So the *operations* are ordinary BSD operations, but the *table-driven
|
||
{convert, compare} per field type* is the firmware's own design. Neither has a
|
||
line-level counterpart.
|
||
|
||
### 57.3 `0x3F65A570` and its caller `0x3F6A86D0`
|
||
|
||
| ROM | what was established | status |
|
||
|---|---|---|
|
||
| `0x3F65A570` | four-case compare chain on a type word; reaches the ARP responder; **not reachable from any EtherType** (section 38); zero executions in every trace | **VT1200-specific dispatcher**, no BSD counterpart |
|
||
| `0x3F6A86D0` | its caller: extracts byte 1 of the argument and tests it against `0x72` (`cmpi 0x72`), i.e. a character-coded protocol id | **VT1200-specific**, no BSD counterpart |
|
||
|
||
### 57.4 The receive re-arm — `0x3F9D17E0`
|
||
|
||
Section 30.1 recorded that `0x3F602E00` re-arms the receive buffer, but omitted
|
||
the address. It is `0x3F9D17E0`, installed by the driver as the RX re-arm/post
|
||
routine (it appears as `movi 0x3f9d17e0` at `0x3F602EC0` and `0x3F603100`).
|
||
|
||
| ROM | 4.4BSD counterpart | evidence |
|
||
|---|---|---|
|
||
| `0x3F9D17E0` | `lereset()` `if_le.c:359` / `ledrinit()` `:337` — the driver's own re-initialisation of the ring | both rebuild the descriptor ring and hand buffers back to the chip; the firmware additionally posts it as a callback, which is the deferred structure noted in 56.2 |
|
||
|
||
(Note: an earlier review note referred to this as `0x3F9817E0`; the address in
|
||
the ROM is `3F9D17E0`.)
|
||
|
||
### 57.5 Coverage, final form
|
||
|
||
The mapping now covers, with a cited line or an explicit firmware-specific
|
||
entry, every function the report gives an address for:
|
||
|
||
| group | mapped to |
|
||
|---|---|
|
||
| protocol layer | 4.4BSD with quoted lines (53.1, 54, 56.1) |
|
||
| driver | `leinit`/`lereset`/`ledrinit`/`lestart`/`leintr`/`lerint` (53.3, 57.4) |
|
||
| deferred receive / submit / IRQ glue | firmware-specific (56.2) |
|
||
| codec table, protocol-id dispatcher | firmware-specific (57.1–57.3) |
|
||
|
||
Section 56.3's statement is therefore extended: *the protocol stack is 4.4BSD
|
||
lineage with function-for-function counterparts; the driver maps onto the pmax
|
||
`le*` routines; and the deferred I/O structure, the field-codec table and the
|
||
protocol-id dispatcher are the firmware's own, with no source counterpart.*
|
||
|
||
## 58. TCP helpers, socket/queue helpers and utilities — mapped
|
||
|
||
### 58.1 TCP helpers
|
||
|
||
| ROM | 4.4BSD counterpart | quoted evidence |
|
||
|---|---|---|
|
||
| `0x3F645170` "tcp output" | **`tcp_output()` `sys/netinet/tcp_output.c:46`** | `register struct socket *so = tp->t_inpcb->inp_socket;` then the length/window computation — the same field reads (`+0x280`, `+0x120`, `+0x140`, `+0x1E0`, `+0x2A0`) our function performs |
|
||
| `0x3F635910` "alloc/copy" | **`tcp_template()` `sys/netinet/tcp_subr.c:65`** | copies the segment header template into the connection block; the *allocator* (160-byte records from a pool, section 24.6) is the firmware's own |
|
||
| `0x3F64C730` close helper | **`tcp_close()` `sys/netinet/tcp_subr.c:244`** | the teardown called from the CLOSING/LAST_ACK/TIME_WAIT handler `0x3F63F200` (section 30.3) |
|
||
| `0x3F64E960` (timeout branch) | **`tcp_timers()` `sys/netinet/tcp_timer.c:144`, `case TCPT_2MSL:` `:158`** | the timer expiry path |
|
||
| `0x3F6BE2F0` (schedule it) | **`timeout()` `sys/kern/kern_clock.c:232`** | generic kernel timer scheduling |
|
||
|
||
### 58.2 The 120 in TIME_WAIT is `2 * TCPTV_MSL`
|
||
|
||
Section 32.2 confirmed on live traffic that entering TIME_WAIT stores **120** at
|
||
TCB `+0x80`. In the source that constant is:
|
||
|
||
```c
|
||
tcp_timer.h:60 #define TCPTV_MSL ( 30*PR_SLOWHZ) /* max seg lifetime */
|
||
tcp_input.c:1068 tp->t_timer[TCPT_2MSL] = 2 * TCPTV_MSL;
|
||
```
|
||
|
||
with `PR_SLOWHZ == 2` that is `2 * 60 = 120` — exactly the value measured. So
|
||
TCB `+0x80` is `t_timer[TCPT_2MSL]`, not a generic timeout field.
|
||
|
||
### 58.3 Socket/interface helper and the queue post
|
||
|
||
| ROM | counterpart | evidence |
|
||
|---|---|---|
|
||
| `0x3F61AB30` (walks the interface list, compares an address) | **`ifa_ifwithaddr()` `sys/net/if.c:141`** | `for (ifp = ifnet; ifp; ifp = ifp->if_next) for (ifa = ifp->if_addrlist; ...) if (equal(addr, ifa->ifa_addr))` — our function walks `0x96AC0` and compares through `0x3FB2C140` |
|
||
| `0x3F61B000` | **firmware-specific** | a lock wrapper around `0x3F6AC880`; BSD has no equivalent call shape here |
|
||
| `0x3F61BFC0` | **firmware-specific** | packet-field access after the header-pointer computation, no BSD counterpart |
|
||
| `0x3F60BD40` (queue post after enqueue) | **firmware-specific** | BSD calls `ether_input`/`ipintr` directly (section 56.2) |
|
||
|
||
### 58.4 Utilities
|
||
|
||
| ROM | counterpart | quoted evidence |
|
||
|---|---|---|
|
||
| `0x3FB2C290` (ones-complement sum) | **`in_cksum()` `sys/netinet/in_cksum.c:24`** | `#define ADDCARRY(x) (x > 65535 ? x -= 65535 : x)` `:20` — the carry folding our `ADDC` loop implements |
|
||
| `0x3F942230` (byte compare with count) | **`bcmp()` `lib/libc/string/bcmp.c:18`** | `bcmp(b1, b2, length)` — our function takes two pointers and a count and compares bytes |
|
||
| `0x3FB2C000` (16/32-bit swap) | **`ntohl()`/`ntohs()` macros**, `sys/pmax/include/endian.h:48` | `#define ntohl(x) (x)` on the big-endian pmax; the firmware implements the swap as a function because its build needs real byte swapping |
|
||
| `0x3FB2C140` (compare helper) | **firmware-specific** | a two-value compare used by `ifa_ifwithaddr`-style scans and by the codec pair; in BSD the equivalent is the inline `equal()` macro (`if.c:147`), not a callable |
|
||
|
||
### 58.5 Coverage after 58
|
||
|
||
Every function the report gives an address for now has either a quoted
|
||
counterpart or an explicit firmware-specific entry. The only rows marked
|
||
firmware-specific are the deferred I/O glue (56.2), the field-codec table and
|
||
protocol-id dispatcher (57.1–57.3), the lock wrapper `0x3F61B000`, the packet
|
||
accessor `0x3F61BFC0`, the queue post `0x3F60BD40`, and the compare helper
|
||
`0x3FB2C140`.
|
||
|
||
## 59. Coverage by function entry, and the remaining entry points
|
||
|
||
A raw grep of the report finds 225 distinct `0x3F…` addresses; only 66 appear in
|
||
sections 53–58. That is expected and not a coverage gap: most of the 225 are
|
||
**internal labels and call sites** (the ICMP type handlers inside `icmp_input`,
|
||
the `tcp_input` switch targets, instruction addresses inside a mapped function).
|
||
Mapping is per *function entry*. This section names the entry points that were
|
||
still missing.
|
||
|
||
### 59.1 Entry points mapped here
|
||
|
||
| ROM | counterpart | evidence |
|
||
|---|---|---|
|
||
| `0x3F604780` | **`tcp_slowtimo()` `sys/netinet/tcp_timer.c:76`** | walks the connection list and decrements timers (cells `0x1DA4D0`/`0x1DA4E0` in our reading) |
|
||
| `0x3F606210` | **`tcp_fasttimo()` `tcp_timer.c:51`** | the fast (delayed-ACK) timer, counter `0x1DA4F0` |
|
||
| `0x3F607C90` (device transmit method, `*(dev+0x160)`) | **`ether_output()` `sys/net/if_ethersubr.c:69`** | validates the destination/route and calls the interface output |
|
||
| `0x3F6084E0` (send, 0x32 clamp) | `ether_output()` framing; BSD pads to the Ethernet minimum | the firmware clamps explicitly to `0x32` (50) rather than padding an mbuf |
|
||
| `0x3F6105E0` (arpresolve caller) | **`arpresolve()`/`arpwhohas()` `if_ether.c:261`/`:202`** | emits the `arpresolve: no free entry` diagnostic |
|
||
| `0x3F6B0160` (trim / pull the header up) | **`m_pullup()` `sys/kern/uipc_mbuf.c:440`** | ensures `len` bytes are contiguous before parsing |
|
||
| `0x3FDDCBE0` (`io5800_5900_init`) | **`leinit()`/`lereset()` `if_le.c:429`/`:359`** | CSR1/CSR2 = init block, CSR3 = 0, CSR0 = INIT (section 52.3) |
|
||
| `0x3FDD8E80` (second CSR0 handler, diagnostics) | **`leintr()` `if_le.c:501`** | same role as `0x3F9D5B40`, registered for the diagnostics path (section 6) |
|
||
| `0x3FDD74B0` (Ethernet address ROM reader) | the MAC read at probe time, `leprobe()` `if_le.c:146` | reads the board's address ROM |
|
||
|
||
### 59.2 Entry points that are firmware-specific
|
||
|
||
| ROM | reason |
|
||
|---|---|
|
||
| `0x3F615610` (MAC formatter, `xx:xx:xx:xx:xx:xx`) | BSD prints link addresses inline in log messages; there is no formatter function to map to |
|
||
| `0x3F62BF30`, `0x3F62C4B0` | helpers that prepare the output block inside the IP transmit path; no line-level counterpart |
|
||
| `0x3F6AC3C0`, `0x3F6AC880`, `0x3F6ACE40`, `0x3F6FC200` | the firmware's own OS primitives (lock/unlock, deferred work) |
|
||
| `0x3FB33870`, `0x3FB338D0` | lock/unlock; the BSD analogue is `splnet()`/`splx()`, inlined there rather than called |
|
||
| `0x3FB2E040`, `0x3FB44400` | buffer helper / memcpy-style primitive |
|
||
| `0x3F61B000`, `0x3F61BFC0`, `0x3F60BD40`, `0x3FB2C140` | as listed in 56.2 and 58.3/58.4 |
|
||
|
||
### 59.3 Verification method
|
||
|
||
Coverage was checked mechanically against the report text:
|
||
|
||
```sh
|
||
python3 - <<'PY'
|
||
import re
|
||
s = open('harness/mame/NETWORK_REPORT.md').read()
|
||
sec = s[s.index('## 53.'):]
|
||
a = re.compile(r'0x3F[0-9A-F]{6}')
|
||
print(len(set(a.findall(s))), len(set(a.findall(s)) & set(a.findall(sec))))
|
||
PY
|
||
```
|
||
|
||
which reports 225 addresses in the report and 66 inside the mapping sections.
|
||
The difference is dominated by internal labels; every one of those belongs to an
|
||
entry point that *is* in a row. The entry points themselves are enumerated above
|
||
and in sections 53–58.
|
||
|
||
## 60. Machine-generated coverage: every entry point, not a spot check
|
||
|
||
Section 59.3's 30-address check was a spot check and 59.1's "internal labels"
|
||
claim was an assertion. This section replaces both with a mechanical
|
||
classification of **every** address the report mentions.
|
||
|
||
### 60.1 Method
|
||
|
||
```python
|
||
unmapped = all_report_addresses - addresses_in_sections_53_59
|
||
entries = sites whose first instruction is MMTM (0x098f)
|
||
covered = unmapped addresses whose enclosing entry is in a mapping row
|
||
orphans = the rest -> their enclosing entry needs a row
|
||
```
|
||
|
||
Result:
|
||
|
||
```
|
||
report addresses 225
|
||
in a mapping row 82
|
||
unmapped 143
|
||
of which covered by a mapped
|
||
enclosing function 102
|
||
in an unmapped entry 41 -> 26 distinct entry points
|
||
```
|
||
|
||
So the honest answer to "are all found addresses mapped?" was **no** — 26
|
||
function entries were missing. They are listed and mapped below.
|
||
|
||
### 60.2 The 26 entries
|
||
|
||
| ROM | status |
|
||
|---|---|
|
||
| `0x3F60FB60` | **ARP resolve** — contains the `arpresolve: no free entry` log site `0x3F610660` → `arpresolve()` `if_ether.c:261` |
|
||
| `0x3F623180` | **interface lookup by network** → `ifa_ifwithnet()` `sys/net/if.c:189` |
|
||
| `0x3F60EE00` | deferred ARP callback (registered at `0x3F614340`) — firmware-specific deferred work |
|
||
| `0x3F6BD9F0` | **`tcp_respond()` `sys/netinet/tcp_subr.c:112`** — builds the ACK from the SYN_RECEIVED handler |
|
||
| `0x3F9DAFF0` | **`leerror()` `sys/pmax/dev/if_le.c:972`** — called with `(5)` from the ISR on `ERR` |
|
||
| `0x3F9D8A50` | the TINT half of **`leintr()` `if_le.c:501`** (TX completion) |
|
||
| `0x3F9D27D0` | the driver's **`leinit()`/`lereset()`** path (contains the CSR/init-block writes) |
|
||
| `0x3F602DB0` | the receive function — **entry of the deferred-RX glue** (see 60.3) |
|
||
| `0x3F604C30` | contains `0x3F606010`/`0x3F606210` → the **`tcp_fasttimo()`/`tcp_slowtimo()`** pair (`tcp_timer.c:51`/`:76`) |
|
||
| `0x3F5FFD90`, `0x3F60C100`, `0x3F6166D0`, `0x3F623940`, `0x3F6448E0`, `0x3F64FE90`, `0x3F653610`, `0x3F65A0C0`, `0x3F660000`, `0x3F662410`, `0x3F675C40`, `0x3F9E14C0`, `0x3FA7C050`, `0x3FE21120` | **firmware-specific** — helpers inside the protocol/OS layers with no 4.4BSD counterpart found (each was checked for a prologue and for call sites; the codec dispatcher `0x3F660000` and the protocol-id dispatcher's default `0x3F65A0C0` are covered by 57.1–57.3) |
|
||
| `0x3F634600` | **firmware-specific** — the default IP-protocol handler that dumps the packet (BSD's equivalent is the silent `bad:` drop at `ip_input.c:368`, which has no body to map to) |
|
||
| `0x3FA932D0`, `0x3FA935D0` | **firmware OS interrupt stubs** (`0x3FA935D0` calls the diagnostics handler `0x3FDD8E80`) |
|
||
|
||
### 60.3 Correction: the RX function entry is `0x3F602DB0`
|
||
|
||
Sections 30.1 and 56.2 call `0x3F602E00` the receive entry. The prologue scan
|
||
shows the function starts at **`0x3F602DB0`** (`mmtm sp,a9,a11,a13`); `0x3F602E00`
|
||
is its third instruction. The mapping row should therefore be
|
||
`0x3F602DB0` (deferred-RX glue), with `0x3F602E00` as an interior label.
|
||
|
||
### 60.4 Coverage statement (replaces 56.3/57.5/58.5/59.3)
|
||
|
||
> Every function **entry point** the report mentions now has a mapping row:
|
||
> a quoted 4.4BSD counterpart where one exists, otherwise an explicit
|
||
> firmware-specific entry naming why. Interior labels and call sites are
|
||
> covered by their enclosing entry, and that is checked mechanically by the
|
||
> script in 60.1 rather than by inspection.
|
||
|
||
The remaining honest caveat: for the fourteen entries marked firmware-specific
|
||
in 60.2 I did not find a counterpart; that is a statement about my search, not a
|
||
proof that none exists.
|
||
|
||
## 61. CONFIRMED ON REAL SILICON: the corrected payload works
|
||
|
||
`harness/udpstream/build/lance_fixlen.coff` was loaded on the VT1200 (via
|
||
`dwt/loader/hostload.py` over rdebug) and its panel read off the screen. The
|
||
panel encoding is `bits16()`: 16 blocks, left to right = bit 0..15, white = 1.
|
||
|
||
| panel row | y | white blocks | decoded | meaning |
|
||
|---|---|---|---|---|
|
||
| 1 | 670 | 1,2,5,6,8,9,10,11 | `0x7B3` | CSR0 after the TDMD kick |
|
||
| 2 | 700 | same | `0x7B3` | CSR0 sampled later |
|
||
| 3 | 730 | 9,10,16 | `0x8300` | TMD1 (OWN still set at the sample) |
|
||
| 4 | 760 | 9,10 | **`0x0300`** | **RMD1: OWN clear, STP\|ENP** |
|
||
| 5 | 790 | 3,4,5 | **`0x001C`** | **MCNT = 28 bytes** |
|
||
| 6 | 850 | (none) | — | TX-done indicator dark (TMD1.OWN still set) |
|
||
| 7 | 850 | 1st | **ok = 1** | payload bytes intact at the RX buffer +12 |
|
||
|
||
The five boxes down the left edge are `dot(0..4)`, the payload's progress marks;
|
||
all five lit means it reached the RX-completion call.
|
||
|
||
### 61.1 The result equals the MAME reference
|
||
|
||
```
|
||
MAME lance.coff: csr0=0x7B3 ok=1 rmd1=0x0300 mcnt=0x1C txmd1=0x8300
|
||
real lance_fixlen: csr0=0x7B3 ok=1 rmd1=0x0300 mcnt=0x1C txmd1=0x8300
|
||
```
|
||
|
||
`ok` is computed only if the received buffer still holds the transmitted pattern
|
||
(`0x88 0xB5 0xDE` at offset 12), so this is a **complete loopback through the
|
||
chip** on the machine, not merely a descriptor status.
|
||
|
||
### 61.2 What that settles
|
||
|
||
* The firmware's hardware-driving sequence is confirmed on real silicon: init
|
||
block, CSR1/CSR2/CSR3, `INIT`, `IDON`, `STRT`, ring arming, descriptor format,
|
||
`TDMD` — all of it behaves as the model and the disassembly said.
|
||
* The earlier on-machine failure (`CSR0 = 0x1BB`, `TMD1 = 0x8338`, no receive)
|
||
was **the payload's descriptor-length encoding**, exactly as section 50
|
||
diagnosed: the field must be the two's complement (`-len`, i.e. `0xFFE8` /
|
||
`0xFA0A`), and MAME hid the bug by ORing `0xF000` in `get_buf_length()`.
|
||
* `TMD1 = 0x8300` (OWN set) in both MAME and on the machine is a sampling
|
||
artefact of this payload, not a failure: transmit completion is proven by
|
||
`RMD1` (OWN clear), `MCNT = 28` and `ok = 1`.
|
||
|
||
### 61.3 Operational note
|
||
|
||
The payload re-initialises the LANCE, so the terminal's network stack dies while
|
||
it runs — the rdebug connection used to load it is lost, and the results are only
|
||
observable on the panel (or in memory if the machine is still up). The
|
||
`lance_restore.c` variant adds a "give the chip back to the OS" block (re-program
|
||
CSR1/CSR2 from `0x23E7A0`, `INIT`, poll `IDON`, `STRT|INEA`), but on this run it
|
||
did not bring the network back; the unit needed a power cycle. Fixing that is
|
||
open work, not a blocker for the result above.
|
||
|
||
### 61.4 The panel is now text, and the restore is instrumented
|
||
|
||
Reading 16 white boxes off a photograph of the screen is error-prone, so the
|
||
payload now draws **labelled hex text** instead:
|
||
|
||
```
|
||
CSR0 7B3 TMD1 8300 MCNT 001C REST 0042
|
||
CSR1 7B3 RMD1 0300 OK YES
|
||
```
|
||
|
||
A 5x7 glyph table (`0-9`, `A-F`, and the letters the labels need) is built **at
|
||
runtime** — this toolchain never runs `.cinit`, so a statically initialised table
|
||
would arrive as zeros, the same reason `harness/c-hello` builds its font in code.
|
||
Glyph pixels are 8 px wide (the VRAM write granularity) and 3 px tall, on a
|
||
32-pixel row pitch.
|
||
|
||
`REST` is new: the CSR0 read back **after** handing the chip to the OS, so a
|
||
failed restore is visible rather than inferred.
|
||
|
||
Note on verification: the panel coordinates and the 2048-byte VRAM stride come
|
||
from the real unit (they are the ones that displayed correctly there). MAME's
|
||
frame dump does **not** reproduce the panel — the OS repaints the screen
|
||
continuously there, and the real unit's VRAM layout differs slightly — so the
|
||
panel is verified on the machine, not from an emulator screenshot.
|
||
|
||
## 62. Correction: `0x23CC50` is the RDP pointer, not a CSR0 shadow
|
||
|
||
Section 10 (and the restore code written from it) treated cell `0x23CC50` as a
|
||
software shadow of CSR0 and wrote `0x0042` into it. That is wrong and it
|
||
**crashed the terminal with an illegal opcode** on the machine.
|
||
|
||
The disassembly is unambiguous:
|
||
|
||
```
|
||
3F9D5960 move @0x23CC50,a3,1 ; a3 = value of the cell = 0x05800000
|
||
3F9D5990 move *a3,a5,0 ; a5 = *(0x05800000) = *RDP = current CSR0
|
||
3F9D59B0 or a3=8,a5 ; a5 |= TDMD
|
||
3F9D59D0 move @0x23CC30,a5,1 ; a5 = second RDP alias (also 0x05800000)
|
||
3F9D5A00 move *a13,*a5,0 ; *RDP = a5
|
||
```
|
||
|
||
`0x23CC50` holds a **pointer** (`0x05800000`, matching the RAM dump in section 2),
|
||
and the driver reads the *live* CSR0 through it before OR-ing `TDMD`. There is no
|
||
shadow: the "preserved" bits are simply whatever the chip currently reports.
|
||
|
||
Consequences, all now fixed:
|
||
|
||
* writing `0x0042` into `0x23CC50` overwrites the RDP pointer, so the driver's
|
||
next CSR access targets address `0x0042` — the fault observed on the unit;
|
||
* the restore must write CSR0 through `RAP`/`RDP`, never through that cell;
|
||
* section 10's phrase "CSR0 = shadow | TDMD" should read "CSR0 = read-back | TDMD".
|
||
|
||
The restore path in `harness/udpstream/lance.c` now re-programs the chip with
|
||
`0x7F41` (the firmware's own init value: `INIT | INEA | all status bits`), polls
|
||
`IDON`, writes `0x0042`, and leaves the driver's cells untouched.
|
||
|
||
## 63. Crash cause: the marker cells ran into `.text`
|
||
|
||
Adding the text panel grew `.text` from `0x018C6140` to `0x018CACD0`, which
|
||
**swallows the marker block** at `0x018C8000`. The payload then wrote
|
||
`cell(0x160, …)` and friends straight into its own code, and the CPU executed
|
||
the overwritten bytes — an illegal opcode before any text was drawn (four
|
||
progress dots, blank panel).
|
||
|
||
That is a general hazard of this payload layout: `.text` grows upward from
|
||
`0x018C0000` while the markers were hard-coded at `0x018C8000`. They are now at
|
||
**`0x018D0000`**, clear of the end of `.text` by ~2.6 KB.
|
||
|
||
Any future growth should re-check `build/lance.map` against `MARK`.
|
||
|
||
## 64. Text panel on the unit: loopback reproduced, chip hand-back works, OS does not
|
||
|
||
Photo: `images/20261030_155855.JPG`. Panel as drawn by
|
||
`lance_fixlen.coff` (text build, `MARK` moved to `0x018D0000`):
|
||
|
||
```
|
||
CSR0 07B3 TMD1 8300 MCNT 001C REST 0073
|
||
CSR1 07B3 RMD1 0300 OK YES
|
||
```
|
||
|
||
and five progress dots down the left edge (the payload ran to the RX completion).
|
||
|
||
### 64.1 The loopback is reproduced
|
||
|
||
`CSR0 = CSR1 = 0x07B3`, `TMD1 = 0x8300`, `RMD1 = 0x0300` (OWN clear, STP|ENP),
|
||
`MCNT = 0x001C` (28 B = 24 + FCS), `OK = YES`. Identical to the earlier
|
||
white-box run (section 61) and to the MAME reference — the text build changes
|
||
nothing in the test itself.
|
||
|
||
### 64.2 `REST = 0x0073` — the chip *was* handed back correctly
|
||
|
||
```
|
||
0x0073 = INIT | STRT | TXON | RXON | INEA
|
||
(STOP clear, TDMD clear, INTR clear, IDON clear)
|
||
```
|
||
|
||
So after the test the payload re-programmed the chip from the firmware's own
|
||
init block (`0x7F41`), polled `IDON`, wrote `0x0042`, and the chip responded:
|
||
transmitter on, receiver on, interrupts enabled. **The `INIT` bit still being set
|
||
is normal** — MAME's own `0x7B3` has it too (section 47.2).
|
||
|
||
In other words the "give the chip back" step works at the chip level. It has
|
||
never been the chip.
|
||
|
||
### 64.3 What actually stays broken: the OS's Ethernet driver
|
||
|
||
The screen behind the panel shows the Terminal Manager with
|
||
**`Ethernet Port Error - 24`** at the bottom, and the network is unreachable
|
||
afterwards (ping fails, rdebug port 771 closed).
|
||
|
||
So the recovery failure is in the **firmware's own network stack**, not in the
|
||
LANCE: the driver noticed the chip disappear during the takeover, marked the port
|
||
failed, and does not re-initialise on its own even when the chip comes back
|
||
enabled.
|
||
|
||
Practical consequence, recorded so it is not re-litigated: **a LANCE takeover run
|
||
on this machine costs a power cycle.** The restore code stays (it is correct and
|
||
harmless), but the unit will not return to service by itself.
|
||
|
||
### 64.4 Where that leaves the hardware question
|
||
|
||
It is answered:
|
||
|
||
| question | result |
|
||
|---|---|
|
||
| does the firmware's init/ring/descriptor/TDMD sequence work on real silicon | **yes** (sections 61, 64.1) |
|
||
| was the old failure our payload or the chip | **our payload**: descriptor length must be `-len` (`0xFFE8`/`0xFA0A`) |
|
||
| can the chip be handed back to the OS from a payload | **yes at the chip level** (`REST = 0x73`), not at the driver level |
|
||
|
||
### 64.5 The stray white box in the photo
|
||
|
||
The photo shows one white box half over the `REST` row. That is the **old `ok`
|
||
indicator** (`rect(120, 850, 24, 24, 0xFF)`), left over from the white-box
|
||
layout: the text row `REST` sits at y 846-867 and the box at y 850-874, so they
|
||
overlap. The text panel already reports `OK YES/NO`, so both indicator boxes
|
||
(`ok` at x 120 and TX-done at x 160) have been removed. Cosmetic only — the
|
||
values in 64.1/64.2 are unaffected.
|
||
|
||
## 65. Correction: "Ethernet Port Error - 24" is a clean-boot condition
|
||
|
||
Section 64.3 used the banner `Ethernet Port Error - 24` on the screen behind the
|
||
panel as evidence that the firmware's network driver had been left broken by the
|
||
payload. **That is wrong**: the terminal shows this banner after a **clean boot**
|
||
as well, while its Ethernet port works normally (the unit answers ping and the
|
||
rdebug service on port 771).
|
||
|
||
So the banner is the firmware's own **Ethernet self-test result**, not a runtime
|
||
fault. It is worth noting for the LANCE work that this matches the emulator: the
|
||
firmware's `C - LANCE TEST` also fails in MAME (`LANCE_LOOPBACK_REPORT.md`, the
|
||
`RMD1_FRAM` mismatch), while ordinary traffic passes. In other words the
|
||
self-test's expectation is not met by this hardware either — pre-existing, and
|
||
unrelated to the takeover payload.
|
||
|
||
### 65.1 What that changes
|
||
|
||
* The banner is **not** evidence about the payload. Section 64.3's conclusion
|
||
("the driver noticed the chip disappear and marked the port failed") is
|
||
withdrawn.
|
||
* The claim "a takeover run costs a power cycle" therefore rests only on the
|
||
runs that **crashed** (the `0x23CC50` write and the marker/`.text` overlap).
|
||
For the current build — the one that produced `REST = 0x0073` — the unit was
|
||
never pinged after the run, so **end-to-end recovery is unverified**, not
|
||
disproved.
|
||
* The check is one command, after a successful (non-crashing) run:
|
||
|
||
```sh
|
||
ping -c3 -W2 100.103.100.103
|
||
```
|
||
|
||
## 66. The payload sends and receives on the wire — with its own driver code
|
||
|
||
A standalone payload (`harness/udpstream/lance_bcast.c`) now drives the LANCE
|
||
with **no ROM calls at all**: it writes its own init block (PADR, LADRF,
|
||
rings), programs `CSR1/CSR2/CSR3`, runs `INIT`/`IDON`/`STRT`, owns eight TX and
|
||
eight RX descriptors, kicks `TDMD`, and reads completions itself.
|
||
|
||
### 66.1 The test
|
||
|
||
`harness/udpstream/test-bcast.sh` runs it in MAME and reports the result:
|
||
|
||
```
|
||
== results ==
|
||
TXD (frames transmitted) = 6
|
||
RXF (frames received) = 5
|
||
last ethertype = 0x88B5
|
||
last source MAC hi word = 0xC6FC
|
||
ok = 1
|
||
payload broadcasts seen on tap-mess-1000-0: 55
|
||
== PASS ==
|
||
```
|
||
|
||
and the tap capture separates the two directions:
|
||
|
||
```
|
||
6 08:00:2b:16:4b:5a > ff:ff:ff:ff:ff:ff, ethertype 0x88b5, length 60 <- our payload
|
||
49 c6:fc:cc:16:f5:66 > ff:ff:ff:ff:ff:ff, ethertype 0x88b5, length 60 <- the host
|
||
```
|
||
|
||
`TXD = 6` is the payload's own counter for descriptors it armed and saw
|
||
complete; `RXF = 5` counts descriptors the chip filled with host frames (the
|
||
listen loop stops early at five), and the recorded source MAC `0xC6FC…` is the
|
||
host's, not ours — so it really consumed frames from the wire rather than its own
|
||
transmissions.
|
||
|
||
### 66.2 What it took (all of these were my bugs, not the chip's)
|
||
|
||
| symptom | cause |
|
||
|---|---|
|
||
| `TMD3 = LCAR`, no carrier | MAME's `-networkprovider taptun` only *permits* the module; the interface choice lives in `harness/mame/cfg-net/vt1200.cfg`. An empty cfg dir means no tap is ever opened |
|
||
| `TXD = 1` then timeouts | the payload always armed **descriptor 0**; the chip advances its own ring position after each completion, so frames 2..n were kicked at descriptors it had already passed |
|
||
| test killed instantly | `pkill -f mame0289/vt1200` matched the wrapper shell's own command line; `pkill -x vt1200` is the safe form |
|
||
| markers read as `0x55555555` | two bit-vs-byte addressing mistakes: the payload called `rb(buf + 12)` with a *byte* address, and the script read `cell()`'s *bit* offsets as byte offsets |
|
||
| payload finished before the host frames | its listen window was too short and it returned, letting the OS reuse the marker memory; it now listens for 400M iterations, stops early at five frames, and holds after drawing the panel |
|
||
|
||
### 66.3 What this establishes
|
||
|
||
* The register, ring, descriptor and DMA behaviour documented in this report is
|
||
sufficient to build a working Ethernet driver: our code transmits frames that
|
||
appear on the wire with the terminal's MAC, and receives frames sent to it.
|
||
* No firmware routine is involved — the payload is loaded by the COFF loader and
|
||
runs from RAM.
|
||
* Practical consequence: a payload can now speak any Ethernet protocol it wants
|
||
(ARP, IP, a custom one) without the OS's stack, which is the foundation for a
|
||
wire-based loader or a faster frame path than the OS's ~20 datagrams/s.
|
||
|
||
## 67. Same payload on the real VT1200: send and receive both work
|
||
|
||
`harness/udpstream/lance_bcast.coff` was loaded on the unit with
|
||
`dwt/loader/hostload.py`, while the host sent `0x88B5` broadcasts at it from its
|
||
own NIC. The panel reads (some glyphs were missing at the time, see 67.2):
|
||
|
||
```
|
||
T D 0005 -> TXD = 5 frames transmitted
|
||
R F 0005 -> RXF = 5 frames received
|
||
ET 88B4/88B5 -> last ethertype: our own 0x88B5 broadcasts
|
||
MAC … -> last source MAC (row overlapped, see 67.2)
|
||
CSR0 0073 -> INIT|STRT|TXON|RXON|INEA: the chip is running
|
||
REST 0073 -> hand-back unchanged
|
||
```
|
||
plus five progress dots, i.e. the payload ran to the end.
|
||
|
||
### 67.1 What it shows
|
||
|
||
* **Transmit works on real silicon**: five descriptors armed by our code
|
||
completed. (`TXD` should be 6; one frame did not complete inside the payload's
|
||
wait window on the real chip, so the count is 5 — the same wait works in MAME
|
||
because the model services the demand immediately.)
|
||
* **Receive works on real silicon**: `RXF = 5` — five frames sent by the host
|
||
were delivered into our RX ring, and the early-stop threshold is exactly five,
|
||
which is why the number is 5 rather than "many". The recorded ethertype is
|
||
`0x88B5`, i.e. the frames the host was sending.
|
||
* `CSR0 = 0x0073` is a healthy chip (`STRT`, `TXON`, `RXON`, `INEA`), so the test
|
||
left it running.
|
||
|
||
This is the first time the payload has exchanged packets with a real peer, and it
|
||
does so with no ROM code: our init block, our rings, our descriptors, our
|
||
`TDMD` kicks.
|
||
|
||
### 67.2 Two display bugs found by reading the real panel
|
||
|
||
| symptom | cause |
|
||
|---|---|
|
||
| `T D` / `R F` instead of `TXD` / `RXF` | the glyph table had no **`X`** — `gi()` mapped it to an empty slot, so the letter simply did not draw |
|
||
| MAC row a smear of overlapping text | the three 4-digit MAC groups were drawn at x=312/384/456, but each group is 192 px wide, so they were painted on top of each other |
|
||
|
||
Both are fixed: `X` added, and the MAC is now one word per row (`MAC0`/`MAC1`/
|
||
`MAC2`) with `REST` moved to y 878.
|
||
|
||
## 68. Ring overlap: the reply was discarded by the chip, not by the wire
|
||
|
||
`lance_cmd.c` (the command/benchmark payload) received commands correctly —
|
||
`cmds` incremented and `laststat` was `0` — but no reply ever reached the host.
|
||
The TX descriptor read back as `TMD1 = 0x8000` (OWN set, STP and ENP clear)
|
||
although the code had written `0x8300`.
|
||
|
||
The DMA log (`VT1200_LANCE_DMA_LOG`) showed the chip doing this on **all eight**
|
||
TX descriptors, round-robin, forever:
|
||
|
||
```
|
||
r 00309A 8000 FFFF pc=018C9B00 cyc=275084712
|
||
w 00309A 0000 FFFF pc=018C9B00 cyc=275084712
|
||
```
|
||
|
||
Read `0x8000`, write back `0x0000` — that is `transmit_poll()`'s "owned but not
|
||
start of packet" path (`am79c90.cpp:329`), which clears ownership and advances
|
||
the ring without transmitting anything.
|
||
|
||
The cause was an address overlap, and it was arithmetic, not the chip:
|
||
|
||
```
|
||
RX ring: 0x3020 .. 0x309f (16 entries x 8 B)
|
||
TX ring: 0x3060 .. 0x309f
|
||
```
|
||
|
||
The RX ring had been grown from 8 entries to 16 (to survive bursts) but `TXR`
|
||
was left at `0x3060`, so **all eight TX descriptors lay inside the RX ring**.
|
||
The chip's RX write-back set `OWN` in what the payload believed were TX
|
||
descriptors, so every reply was rejected as a continuation fragment.
|
||
|
||
Fixed by moving the TX ring to `0x30C0`, plus a compile-time guard so the same
|
||
mistake cannot compile again:
|
||
|
||
```c
|
||
typedef char rx_tx_rings_do_not_overlap[(RXR + NRX * 64 <= TXR) ? 1 : -1];
|
||
```
|
||
|
||
This also explains the earlier "reply buffer has the broadcast address as its
|
||
source MAC": that was a second, independent bug (the reply copied the request's
|
||
destination MAC into its own source field), fixed separately. A frame whose
|
||
source is multicast is dropped by the tap, so it would have failed even with
|
||
correct descriptors.
|
||
|
||
## 69. Packet handling cross-checked against the four drivers
|
||
|
||
Four implementations are available locally and were read line by line:
|
||
`original-bsd/sys/pmax/dev/if_le.c` (the firmware's ancestor, section 52),
|
||
`NetBSD-src/sys/dev/ic/am7990.c`, `linux/drivers/net/ethernet/amd/7990.c`, and
|
||
`linux/drivers/net/ethernet/amd/7990.h`.
|
||
|
||
| | 4.4BSD `if_le.c` | NetBSD `am7990.c` | Linux `7990.c` | this payload |
|
||
|---|---|---|---|---|
|
||
| RX buffer | `LEMTU` **1518** (`if_lereg.h:13`) | `LEBLEN` **1536** (`lancereg.h:116`) | `PKT_BUFF_SIZE` **1544** (`7990.h:45`) | 1526 |
|
||
| ring | `LERBUF` | `sc_nrbuf` | 8 RX / 2 TX (`7990.h:35-36`) | 16 RX / 8 TX |
|
||
| RX length word | `-LEMTU` (`:344`) | `-LEBLEN \| 0xf000` (`:186`) | `-RX_BUFF_SIZE \| 0xf000` (`:200`) | `0xFA0A` |
|
||
| complete frame | `(rmd1 & (STP\|ENP)) == (STP\|ENP)` (`:630`) | same, `"dropping chained buffer"` (`:247`) | `(bits & POK) != POK` (`:303`) | now both bits |
|
||
| FCS | `len - 14 - 4` (`:691`) | `rmd3 - 4` (`:256`) | `(mblength & 0xfff) - 4` (`:322`) | now `- 4` |
|
||
| re-arm | clear RMD3, then OWN (`:661`) | `OWN`, `rmd2 = -LEBLEN\|ONES`, `rmd3 = 0` (`:259`) | clear mblength, then OWN (`:346`) | now all three |
|
||
| TX descriptor | `OWN\|STP\|ENP`, `tmd2=-len`, `tmd3=0` (`:485`) | same (`:513`) | `POK\|OWN`, `(-len)\|0xf000` (`:583`) | same |
|
||
| TX kick | `TDMD\|INEA` (`:491`) | `INEA\|TDMD` (`:524`) | `INEA\|TDMD` (`:583`) | now `INEA\|TDMD` |
|
||
| slot still owned | `panic("lestart")` (`:471`) | `"missing buffer"`, break (`:482`) | stop queue | skip, count `txfull` |
|
||
|
||
### 69.1 Why the buffer must be ≥ 1518
|
||
|
||
All three size the RX buffer at `ETHERMTU + 14 + 4`: the chip writes the
|
||
**FCS** into the buffer and includes it in `RMD3`. The payload's counter showed
|
||
this before it was fixed — a 60-byte frame was counted as 64 bytes. The earlier
|
||
1526 was therefore already legal; a 1500-byte buffer would not have been, and
|
||
`RMD1_ENP` handling alone would have miscounted the tail of a chained frame as a
|
||
whole frame.
|
||
|
||
### 69.2 What changed in the payload because of this
|
||
|
||
1. a frame must have **STP and ENP** on the same descriptor, or it is counted as
|
||
`chained_dropped` and re-armed without being parsed;
|
||
2. the byte counter subtracts the 4-byte FCS;
|
||
3. re-arm restores RMD2 and clears RMD3, with ownership written last;
|
||
4. the TX kick is `INEA|TDMD`, not `STRT|TDMD` — no reference driver re-writes
|
||
`STRT`, and 4.4BSD and NetBSD both write exactly `TDMD|INEA`;
|
||
5. a TX slot that is still owned is skipped and counted in `txfull`.
|
||
|
||
## 70. The command protocol: read and stats over the wire
|
||
|
||
The payload (ethertype `0x88B6`, magic `'K' 'S'`) answers two commands, which is
|
||
what makes the machine debuggable while the OS is down:
|
||
|
||
| cmd | request | reply |
|
||
|---|---|---|
|
||
| 1 `read` | 32-bit big-endian **bit** address, 16-bit length (≤ 64) | status, then the bytes |
|
||
| 2 `stats` | — | `RXF`, `bytes`, `cmds`, `chained_dropped`, `txfull`, `csr0_err` |
|
||
| other | — | status `0xE002` |
|
||
|
||
`harness/udpstream/vtcmd.py` drives it: `stats`, `read --addr --len`, and
|
||
`bench`. The first frame after takeover is routinely lost (the ring is armed
|
||
before the receive loop runs), so requests are retried three times.
|
||
|
||
Confirmed in MAME and on the unit: `read 0x018D0000 --len 16` returned
|
||
`01 00 00 00 55 55 …`, the payload's own MARK block — arbitrary memory over
|
||
Ethernet, with no ROM code and no OS.
|
||
|
||
## 71. Receive throughput on the real VT1200
|
||
|
||
The unit and the host are joined by a **10 Mbit half-duplex hub** (the VT1200 has
|
||
coax Ethernet). That fact governs every number below: above the receiver's
|
||
capacity the *delivered* rate does not plateau, it collapses, which is what a
|
||
half-duplex collision domain does under overload.
|
||
|
||
`bench` sends 60-byte and 1514-byte frames at a paced or unpaced rate, then reads
|
||
the payload's counters. Background traffic is subtracted by taking a `before`
|
||
reading; the payload's own control frames are subtracted explicitly
|
||
(`frames = ΔRXF − Δcmds`).
|
||
|
||
| frame | offered | delivered | rate | `csr0_err` |
|
||
|---|---|---|---|---|
|
||
| 1514 B | 1717–1723 fps, unpaced | **99.9–100 %** | **856–858 fps, 10.37–10.39 Mbit/s** | `0x9000` |
|
||
| 60 B | 3001 frames @ 1500 fps | **99.8–99.9 %** (5 runs) | 1498–1499 fps, 88 KiB/s | `0x9000` |
|
||
| 60 B | 6001 frames @ 2000 fps | 58 % | 1165 fps | `0x9000` |
|
||
| 60 B | 12001 frames @ 4000 fps | 0.3 % | 12 fps | `0x9000` |
|
||
|
||
Readings and their limits:
|
||
|
||
* **Full-MTU reception is wire-limited, not code-limited.** 856–858 fps of
|
||
1514-byte frames is 10.37–10.39 Mbit/s of frame bytes; with preamble, FCS and
|
||
inter-frame gap that is at or slightly above the nominal 10 Mbit rate
|
||
(theoretical maximum for this frame size is ~813 fps at exactly 10.000 Mbit/s,
|
||
so the link clock is a few percent fast or the sender's pacing is). Nothing was
|
||
dropped.
|
||
* **Minimum-size frames sustain ~1500 fps with ~100 % delivery.** Above that the
|
||
ring overflows and the hub also collides, and the two are not separable from
|
||
the outside.
|
||
* `csr0_err = 0x9000` is `ERR|MISS`: **the chip did run out of descriptors**, but
|
||
the bit is **sticky** in this build (it ORs CSR0 on every pass), so it records
|
||
"at some point", not "how many". It was clear before the 2000/4000 fps runs and
|
||
set afterwards, which is consistent with those runs overrunning a 16-deep ring.
|
||
A per-event counter is the next refinement; the sticky bit cannot attribute
|
||
the misses to a particular run.
|
||
* The payload's `stats`-command handling stalls the receive loop for the length
|
||
of one reply transmit, so a burst that coincides with a command can produce
|
||
misses that a pure data burst would not.
|
||
|
||
### 71.1 What is proven and what is inferred
|
||
|
||
**Proven:** the payload receives every frame offered up to the wire limit at full
|
||
MTU; the chip reports no error bits at that rate; 1500 fps of 60-byte frames is
|
||
reproducible at 99.8–99.9 % over five runs; the reply path works (`stats` and
|
||
`read` both answer on the unit).
|
||
|
||
**Inferred, not proven:** that the collapse above ~2000 fps of 60-byte frames is
|
||
the half-duplex hub rather than the LANCE. `ERR|MISS` proves the ring overflowed
|
||
at 2000–4000 fps, which is sufficient to explain *loss*; it does not explain why
|
||
the *delivered* rate falls from 1165 fps to 12 fps as the offered rate rises —
|
||
that shape (rather than a plateau) is characteristic of collision collapse, but
|
||
the unit's side of the hub is not instrumented.
|
||
|
||
### 71.2 A 64-deep ring crashed the unit
|
||
|
||
Raising `NRX` to 64 (ring at `0x3020`–`0x321F`, TX ring and buffer moved to
|
||
`0x3240`/`0x3300`) ran correctly in MAME but died on the unit with an **illegal
|
||
opcode** during the first 60-byte burst. The 64 buffers grow the RX buffer pool
|
||
from 24 KB (`0x4000`–`0x9F78`) to 97 KB (`0x4000`–`0x1B78A`) of low RAM, and the
|
||
firmware's OS is still resident while the payload holds the chip. The payload was
|
||
reverted to the proven 16-deep layout; the illegal opcode is **not reproduced in
|
||
MAME**, so the memory hypothesis is an inference, not a demonstrated cause.
|
||
|
||
## 72. peek / poke / jump / reboot, so a taken-over unit can be driven
|
||
|
||
The command byte selects the operation; addresses are **bit** addresses
|
||
throughout, exactly like every pointer the TMS34010 uses.
|
||
|
||
| cmd | name | request | reply |
|
||
|---|---|---|---|
|
||
| 1 | `read` / peek | `[17..20]` address, `[21..22]` length (≤ 64) | status, then the bytes |
|
||
| 2 | `stats` | — | `RXF`, `bytes`, `cmds`, `chained_dropped`, `txfull`, `csr0_err` |
|
||
| 3 | `poke` | `[17..20]` address, `[21..22]` length (≤ 1024), `[23..]` bytes | status, 2-byte count written |
|
||
| 4 | `jump` | `[17..20]` address | status, 4-byte target, **then control is transferred** |
|
||
| 5 | `reboot` | — | status, 4-byte target, **then control is transferred** |
|
||
| — | — | — | `0xE002` unknown, `0xE001` read too long, `0xE003` poke truncated |
|
||
|
||
The reply is transmitted and its descriptor completion waited on **before** the
|
||
transfer, so the host always gets an acknowledgement.
|
||
|
||
### 72.1 `jump` needs the entry point, not `.text`
|
||
|
||
The COFF entry is **`0x018CBEB0`**; `0x018C0000` is only the start of `.text`.
|
||
Measured in MAME:
|
||
|
||
* `jump 0x018C0000` → payload stops answering and does not come back;
|
||
* `jump 0x018CBEB0` → the payload restarts its own `crt` and answers again with
|
||
counters back to zero (`RXF=1`, i.e. just the `stats` request itself).
|
||
|
||
### 72.2 `reboot` jumps to the ROM reset vector
|
||
|
||
`reboot` reads a 32-bit word from bit `0xFFFFFFE0` and jumps there. On this ROM
|
||
that word is `0x3F200040`, the same entry the CPU takes at power-on. The first
|
||
instructions there are (`harness/mame/reset.dasm`):
|
||
|
||
```
|
||
3F200040: 0360 DINT
|
||
3F200050: 0550 SETF 10h,0,0
|
||
3F200060: 0740 SETF 20h,0,1
|
||
3F200070: 5600 CLR A0
|
||
3F200080: 0580 00D0 0640 MOVE A0,@64000D0h,0
|
||
...
|
||
```
|
||
|
||
`DINT` first, then the DUART control registers — so the entry does not depend on
|
||
the state a payload leaves behind, and at real power-on `SP` is undefined
|
||
anyway, which is the condition we reproduce.
|
||
|
||
MAME evidence: after the reply, the PC leaves the payload (`0x018Cxxxx`) for ROM
|
||
(`0x3FB43E40` at the next sample) and the payload stops answering. What is **not**
|
||
shown yet is that the unit comes back *without a power cycle* — in MAME the OS
|
||
was never down, so a warm restart is indistinguishable from the OS continuing.
|
||
That test needs the real machine and is the reason the command exists.
|