Three blocks move and nothing else changes. Branches take 0x60, subroutines take 0x70, and the ALU moves up into the 0x10 block the two of them used to share. Order within each block is preserved exactly - this relocates them, it does not rethink them. WHAT IT BUYS IS AN EMPTY 0x00 TO 0x0F. Program Memory that was never written, or a load that stopped part way and left zeroes in its tail, used to read as a long run of ADDs: the machine carried on through them, arrived somewhere unpredictable, and whatever broke there was a long way from the byte that caused it. Now it faults where it is met: Fault: 0x00 at Program Address 0x0004 is not an instruction. That is the address of the byte after the last real instruction, which is the difference between a diagnosis and a search. Reserving the whole nibble rather than just 0x00 means a run into blank memory faults wherever it starts rather than only when it lands on the right byte. runOffTest records it, and the block is left empty for whatever turns out to want it. The other half is room: branches and subroutines had filled 0x10 to 0x1F between them, so a service return that keeps Q and DP3 had nowhere to sit next to its family. It has 0x76 waiting now. Five places wrote an opcode down that the scripted remap did not reach, and four of them were found by tests rather than by looking: - secondPass.c lists which opcodes take an address, and firstPass.c knows SWI by number. Missing those made XOR read as a branch. - Asm.asm knows SWI by number too, being the other assembler. Missing it made the native and host assemblers disagree byte for byte, which is exactly the check that exists to catch a thing known in two places. - loaderTest.asm carries a hand written payload, and its RETI was 0x19. To the assembler those are numbers and to the program they are data, so nothing but running it could notice. It says so in a comment now. - The Assembler Manual prints the bytes hello.asm assembles to, and two of them were branches. The monitor's recorded disassembly moved by exactly the bytes it should: 18 became 72 wherever SWI appears, with SETD and INIB untouched and every disassembled line still reading the same.
132 lines
3.5 KiB
NASM
132 lines
3.5 KiB
NASM
; A program that loads a program.
|
|
;
|
|
; This is what the memory controller was built for. The bytes of a small routine sit in
|
|
; the Data Segment the way a program read off a disk would. The loader blits them into
|
|
; Program Memory, writes their address into a software vector, and calls it. Nothing
|
|
; about that routine was known to the assembler as code: to the loader it is data, and it
|
|
; becomes code only because it was put somewhere the CPU fetches from.
|
|
;
|
|
; Installing the vector at run time is the part worth watching. The argument that started
|
|
; the whole controller design was that a vector table nothing can write is not really a
|
|
; vector table. Vector 20 is empty when this program starts and holds a real handler by
|
|
; the time it is called, and nothing but the controller could have put it there.
|
|
;
|
|
; WHAT THIS DOES NOT SOLVE:
|
|
;
|
|
; The payload works at whatever address it lands at only because it contains no
|
|
; addresses. It is INIA and OUTA and a RETI, and none of those name a place. The moment a
|
|
; loaded routine contains a branch, a CALL, or a SETD, it holds an address that was fixed
|
|
; when it was assembled, and it will be wrong everywhere except where it was assembled
|
|
; for. SplitBit has no answer to that yet. Loading works; relocating does not exist.
|
|
;
|
|
; Correct output is:
|
|
; before the vector is still empty here
|
|
; loaded printed by code that was data a moment ago
|
|
; back and RETI came home
|
|
|
|
#Include print.asm
|
|
|
|
#Program
|
|
|
|
start:
|
|
SETD.0 Before
|
|
CALL say
|
|
|
|
; ---- Put the payload into Program Memory. ----
|
|
SETD.0 Payload
|
|
CALL aimSourceData
|
|
SETD.0 LandingZone
|
|
CALL aimDestProgram
|
|
INIA 0d29
|
|
CALL setLength
|
|
INIA 0x01
|
|
OUTA 0xE8 ; Blit: Data Memory into Program Memory.
|
|
|
|
; ---- Write its address into software vector 20. ----
|
|
; The table is at 0xFC00 and entries are two bytes, so vector 20 is at 0xFC28.
|
|
RSTA
|
|
OUTA 0xE3 ; DestBank = 0, Program Memory.
|
|
INIA 0xFC
|
|
OUTA 0xE4
|
|
INIA 0x28
|
|
OUTA 0xE5
|
|
SETD.0 LandingZone
|
|
PSHD.0
|
|
POPA
|
|
POPB
|
|
OUTB 0xE9 ; High byte of the handler's address.
|
|
OUTA 0xE9 ; Low byte. The Data port steps on by itself.
|
|
|
|
; ---- Call it. ----
|
|
SWI 0d20
|
|
|
|
SETD.0 Back
|
|
CALL say
|
|
HALT
|
|
|
|
; DP0 holds the address to aim at.
|
|
aimSourceData:
|
|
INIA 0d1
|
|
OUTA 0xE0
|
|
PSHD.0
|
|
POPA
|
|
POPB
|
|
OUTB 0xE1
|
|
OUTA 0xE2
|
|
RET
|
|
|
|
aimDestProgram:
|
|
RSTA
|
|
OUTA 0xE3
|
|
PSHD.0
|
|
POPA
|
|
POPB
|
|
OUTB 0xE4
|
|
OUTA 0xE5
|
|
RET
|
|
|
|
setLength:
|
|
PSHA
|
|
RSTA
|
|
OUTA 0xE6
|
|
POPA
|
|
OUTA 0xE7
|
|
RET
|
|
|
|
say:
|
|
CALL printString
|
|
CALL lineFeed
|
|
RET
|
|
|
|
; Where the payload lands. Reserving it means the assembler knows the room is spoken for,
|
|
; so nothing else is ever placed here.
|
|
LandingZone:
|
|
#Reserve 0d48
|
|
|
|
#Data
|
|
|
|
Before:
|
|
"before"
|
|
Back:
|
|
"back"
|
|
|
|
; The payload, as bytes rather than as code. It prints "loaded" and returns. Written out
|
|
; by hand because SplitBit has no linker: to this program it is just so much data.
|
|
;
|
|
; INIA 'l' OUTA 0 INIA 'o' OUTA 0 ... and so on, then RETI
|
|
;
|
|
; THESE ARE OPCODES AND THEY MOVE WHEN THE MAP MOVES. Nothing checks them - to the
|
|
; assembler they are numbers, and to this program they are data - so an opcode
|
|
; reorganisation has to come here by hand. It has happened once: RETI was 0x19 before the
|
|
; branch and subroutine blocks were split apart, and this was the only place in the corpus
|
|
; that noticed, by faulting on a byte that had stopped being an instruction.
|
|
Payload:
|
|
0x26 0x6C 0xD1 0x00
|
|
0x26 0x6F 0xD1 0x00
|
|
0x26 0x61 0xD1 0x00
|
|
0x26 0x64 0xD1 0x00
|
|
0x26 0x65 0xD1 0x00
|
|
0x26 0x64 0xD1 0x00
|
|
0x26 0x0A 0xD1 0x00
|
|
0x73 ; RETI
|