Breakpoints: SWI osBreak, and s refuses a read only bank

A breakpoint that shows every register as the program had them, waits for a
key, and carries on.

NOTHING IS OVERWRITTEN, and that is the design rather than a shortcut. A
breakpoint poked into a running program has to replace an instruction, and
putting that instruction back in order to continue is the same act as
disarming the breakpoint; firing a second time would mean stepping over the
restored instruction and putting the breakpoint back behind it, and this
machine cannot step a single instruction. SWI is two bytes, dispatches
through a vector, and its frame already holds the address after it, so RETI
resumes at the next instruction with nothing to restore and nothing to
re-arm. It fires every time it is reached.

The price is that a breakpoint is part of the program: a build with them in
has different addresses from a build without. That is the bargain every
machine with a break instruction makes.

Every value shown comes out of the frame rather than the registers, because
by the time the handler runs the registers are the handler's. Apps/Break.asm
stops twice so that the second stop is checked as well as the first.

Also here, found by the test that came with it: the monitor's s wrote into
whichever bank was selected, and bank 2 is the controller's own table,
published read only. Writing to it was refused, and a refusal nobody catches
stops the machine - so selecting the bank table to look at it and then typing
s killed the session. bankPresent now keeps the whole flags byte and s
declines. The recorded output of cosmosMonitor had contained that crash,
having been blessed without being read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Anachronaut
2026-08-19 22:09:21 -04:00
co-authored by Claude Opus 5
parent b36d438132
commit 5fd995aa62
11 changed files with 326 additions and 3 deletions
+164
View File
@@ -985,6 +985,127 @@ serviceNoDisk:
STA.2
RETI
; A breakpoint. Shows every register as the interrupted program had them, waits for a key,
; and returns as though nothing happened.
;
; EVERY VALUE COMES OUT OF THE FRAME, not out of the registers, because by the time this
; runs the registers belong to the handler. The frame is what the program had, and RETI is
; going to give it all back, so what is shown is what will be resumed with.
;
; DP3 holds the frame throughout. It survives a CALL, and console.asm promises not to
; disturb it, which is what lets the printing routines be used between one field and the
; next. The Stack Pointer comes back to the same place after a balanced call, so the frame
; stays where it was found.
;
; +1 Status +2 Q +3 A +4 B +5 DP3 +7 DP2 +9 DP1 +11 DP0 +13 where it resumes
handleBreak:
MVSD.3
; Where it broke, which is two before where it resumes: the SWI and the vector it names.
SETD.0 BreakText
CALL printString
PSHD.3
POPD.0
DPUP.0 0d13
LDA.0
PSHA ; The high half, while the low one is worked on.
INCD.0
LDA.0
INIB 0d2
CCF
SUB ; Two back from where it resumes: the SWI and the vector it names.
MVQA
POPB
BRC breakBorrowed ; It borrowed, so the high half comes down by one.
BRI breakAddress
breakBorrowed:
DECB
breakAddress:
PSHA ; low
PSHB ; high
POPA
CALL printByteHex
POPA
CALL printByteHex
CALL newLine
SETD.0 ARegText
PSHD.3
POPD.1
DPUP.1 0d3
CALL breakByte
SETD.0 BRegText
PSHD.3
POPD.1
DPUP.1 0d4
CALL breakByte
SETD.0 QRegText
PSHD.3
POPD.1
DPUP.1 0d2
CALL breakByte
SETD.0 SRegText
PSHD.3
POPD.1
DPUP.1 0d1
CALL breakByte
CALL newLine
SETD.0 DP0Text
PSHD.3
POPD.1
DPUP.1 0d11
CALL breakWord
SETD.0 DP1Text
PSHD.3
POPD.1
DPUP.1 0d9
CALL breakWord
SETD.0 DP2Text
PSHD.3
POPD.1
DPUP.1 0d7
CALL breakWord
SETD.0 DP3Text
PSHD.3
POPD.1
DPUP.1 0d5
CALL breakWord
CALL newLine
; Anything typed carries on. Reading the data port waits however the console is set, which
; is one key if the program asked for key mode and a whole line if it did not - and either
; way it is the program's own console being borrowed for a moment.
SETD.0 ResumeText
CALL printString
INA 0x00
CALL newLine
RETI
; DP0 names a field and DP1 points at it in the frame. The caller does the stepping, with
; DPUP and a number written into the program, because a routine cannot hand a pointer back:
; CALL saves DP0 to DP2 and RET puts them back, so a walk done in here would be undone on
; the way out. Written that way first, and every field showed the frame's first byte.
breakByte:
CALL printString
LDA.1
CALL printByteHex
INIA 0x20
OUTA 0x00
RET
; The same for the two byte fields, most significant first the way the frame holds them.
breakWord:
CALL printString
LDA.1
CALL printByteHex
INCD.1
LDA.1
CALL printByteHex
INIA 0x20
OUTA 0x00
RET
; A and B together are a number. Prints it in decimal without leading zeroes, which covers
; a line number and a byte count both, so there is no need for one service each.
handlePrintNumber:
@@ -1279,6 +1400,16 @@ dumpNoBank:
; The cursor is left alone. Somebody poking a byte is usually looking at something else, and
; having the address they were reading move underneath them would be a poor reward.
doSet:
; A bank can be present and still refuse to be written: the controller's own table is
; published read only, and writing to it is refused. A refusal nobody catches stops the
; machine, which is a poor answer to somebody looking around with b and then typing s.
CALL bankPresent
SETD.0 BankFlags
LDA.0
INIB 0x02 ; The read only bit of that bank's record.
AND
BNQ setReadOnly
SETD.1 TextRest
LDD.0.1
CALL textHexWord
@@ -1309,6 +1440,12 @@ setByte:
POPD.0
BRI setByte
setReadOnly:
SETD.0 ReadOnlyText
CALL printString
CALL newLine
BRI prompt
setWhat:
SETD.0 SetUsage
CALL printString
@@ -1694,6 +1831,8 @@ bankPresent:
LDA.0
OUTA 0xE2
INA 0xE9 ; The flags byte of that bank's record.
SETD.0 BankFlags
STA.0 ; Kept whole, since present is not the only thing it says.
INIB 0x01
AND
RET
@@ -1788,6 +1927,26 @@ NothingLoaded:
Finished:
"finished"
BreakText:
"break at "
ARegText:
"A "
BRegText:
"B "
QRegText:
"Q "
SRegText:
"S "
DP0Text:
"DP0 "
DP1Text:
"DP1 "
DP2Text:
"DP2 "
DP3Text:
"DP3 "
ResumeText:
"press a key "
MonitorPrompt:
"* "
UnknownText:
@@ -1813,6 +1972,8 @@ BankIs:
"bank "
SetUsage:
"s <address> <byte> <byte> ..."
ReadOnlyText:
"that bank will not be written"
GoUsage:
"g <address>"
DirName:
@@ -1886,6 +2047,8 @@ DumpCount:
0x00
BankWas:
0x00
BankFlags:
0x00
ShowAsCode:
0x00
DumpRecord:
@@ -2006,4 +2169,5 @@ CommandLine:
osFileDelete handleFileDelete
osFileRename handleFileRename
osPrintNumber handlePrintNumber
osBreak handleBreak
Device 0x20 diskDone
+14
View File
@@ -51,3 +51,17 @@
; number, in decimal, without leading zeroes. A and B together, so one service covers both
; a line number and a byte count and there is no need for two.
osPrintNumber 0d24
; ---- Stopping to look ----
;
; A breakpoint. Put SWI osBreak anywhere in a program and the system shows every register as
; the program had them, waits for a key, and carries on.
;
; NOTHING IS OVERWRITTEN, which is what makes this simple. A breakpoint that replaced an
; instruction would have to put it back to continue, and putting it back disarms the
; breakpoint - so firing twice would need the instruction to be stepped over and the
; breakpoint replaced behind it, and this machine has no way to step one instruction. An SWI
; costs two bytes of the program and fires for ever, because there was never anything to
; restore. The price is that it is part of the program: a build with breakpoints in it has
; different addresses from one without.
osBreak 0d25