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
+56
View File
@@ -0,0 +1,56 @@
; Stopping a program to look at it.
;
; SWI osBreak shows every register as this program had them, waits for a key, and carries
; on. It is two bytes and it fires every time it is reached.
;
; WHY IT IS AN INSTRUCTION RATHER THAN SOMETHING SET FROM OUTSIDE. A breakpoint that was
; poked into a running program would have to overwrite an instruction, and then 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 one instruction. Nothing is overwritten here,
; so there is nothing to restore and nothing to re-arm.
;
; The price is that it is part of the program. A build with breakpoints in it has different
; addresses from a build without, which is the same bargain every machine makes that has a
; break instruction.
;
; Correct output is two stops, showing A and B changing between them, and the addresses of
; the two SWIs.
#Include services.asm
#Program
#Base 0x2000
start:
SETD.0 Banner
SWI osPrintString
INIA 0d17
INIB 0d34
SETD.0 Marker
SWI osBreak
; Something for the second stop to show as different.
INIA 0d68
INIB 0d85
SETD.0 Banner
SWI osBreak
SETD.0 DoneText
SWI osPrintString
SWI osExit
#Data
#Base 0x1000
Banner:
"two stops, and what the registers were at each
"
Marker:
"marker"
DoneText:
"carried on to the end
"
+164
View File
@@ -985,6 +985,127 @@ serviceNoDisk:
STA.2 STA.2
RETI 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 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. ; a line number and a byte count both, so there is no need for one service each.
handlePrintNumber: handlePrintNumber:
@@ -1279,6 +1400,16 @@ dumpNoBank:
; The cursor is left alone. Somebody poking a byte is usually looking at something else, and ; 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. ; having the address they were reading move underneath them would be a poor reward.
doSet: 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 SETD.1 TextRest
LDD.0.1 LDD.0.1
CALL textHexWord CALL textHexWord
@@ -1309,6 +1440,12 @@ setByte:
POPD.0 POPD.0
BRI setByte BRI setByte
setReadOnly:
SETD.0 ReadOnlyText
CALL printString
CALL newLine
BRI prompt
setWhat: setWhat:
SETD.0 SetUsage SETD.0 SetUsage
CALL printString CALL printString
@@ -1694,6 +1831,8 @@ bankPresent:
LDA.0 LDA.0
OUTA 0xE2 OUTA 0xE2
INA 0xE9 ; The flags byte of that bank's record. 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 INIB 0x01
AND AND
RET RET
@@ -1788,6 +1927,26 @@ NothingLoaded:
Finished: Finished:
"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: MonitorPrompt:
"* " "* "
UnknownText: UnknownText:
@@ -1813,6 +1972,8 @@ BankIs:
"bank " "bank "
SetUsage: SetUsage:
"s <address> <byte> <byte> ..." "s <address> <byte> <byte> ..."
ReadOnlyText:
"that bank will not be written"
GoUsage: GoUsage:
"g <address>" "g <address>"
DirName: DirName:
@@ -1886,6 +2047,8 @@ DumpCount:
0x00 0x00
BankWas: BankWas:
0x00 0x00
BankFlags:
0x00
ShowAsCode: ShowAsCode:
0x00 0x00
DumpRecord: DumpRecord:
@@ -2006,4 +2169,5 @@ CommandLine:
osFileDelete handleFileDelete osFileDelete handleFileDelete
osFileRename handleFileRename osFileRename handleFileRename
osPrintNumber handlePrintNumber osPrintNumber handlePrintNumber
osBreak handleBreak
Device 0x20 diskDone 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 ; 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. ; a line number and a byte count and there is no need for two.
osPrintNumber 0d24 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
+19
View File
@@ -560,6 +560,7 @@ Those numbers are written down once, in `Programs/CosmOS/Source/services.asm`, w
| osFileDelete | DP0 names a file. Q is zero if it went. | | osFileDelete | DP0 names a file. Q is zero if it went. |
| osFileRename | DP0 is the name a file has, DP1 the name it should have. Q is zero if it moved. | | osFileRename | DP0 is the name a file has, DP1 the name it should have. Q is zero if it moved. |
| osPrintNumber | A and B together are a number. Prints it in decimal, without leading zeroes. | | osPrintNumber | A and B together are a number. Prints it in decimal, without leading zeroes. |
| osBreak | Stops the program, shows every register as it had them, waits for a key, and carries on. |
``` ```
#Include services.asm #Include services.asm
@@ -568,6 +569,23 @@ Those numbers are written down once, in `Programs/CosmOS/Source/services.asm`, w
SWI osPrintString SWI osPrintString
``` ```
### Stopping To Look:
`SWI osBreak` is a breakpoint. It shows every register as the program had them, waits for a key, and returns as though nothing happened.
```
break at 200E
A 11 B 22 Q 00 S 00
DP0 1030 DP1 05AE DP2 039A DP3 2000
press a key
```
Every value comes out of the interrupt frame rather than out of the registers, because by the time the handler runs the registers belong to the handler. The frame is what the program had and what RETI is about to give back, so what is shown is what will be resumed with. The address is two before where it resumes: the `SWI` and the vector it names.
**Nothing is overwritten, and that is the whole of why it is simple.** 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 has no way to step a single instruction. Two bytes of `SWI` cost a little space and fire for ever, because there was never anything to restore.
The price is that a breakpoint is part of the program. A build with breakpoints in it has different addresses from a build without — the same bargain every machine makes that has a break instruction.
### The Disk Without A Filesystem: ### The Disk Without A Filesystem:
A program that wants a file does not need to know what a filesystem is. Before these existed it had to include the whole of `sbfs.asm` — two and a half kilobytes of a private copy of code the system already had running — and then mount a disk that was already mounted. A program that wants a file does not need to know what a filesystem is. Before these existed it had to include the whole of `sbfs.asm` — two and a half kilobytes of a private copy of code the system already had running — and then mount a disk that was already mounted.
@@ -668,6 +686,7 @@ Whoever does the loading keeps its own code and data below the addresses the loa
| Keys | The console interrupting rather than being asked. The only one that brings a vector of its own, which is what the version two format exists for. | | Keys | The console interrupting rather than being asked. The only one that brings a vector of its own, which is what the version two format exists for. |
| Say | Prints whatever it was told, which is the shortest thing that shows osArgument working. | | Say | Prints whatever it was told, which is the shortest thing that shows osArgument working. |
| Files | Writes a file, reads it back, renames it and deletes it, in 645 bytes, including nothing but the service names. It is what says a program does not need a filesystem inside it. | | Files | Writes a file, reads it back, renames it and deletes it, in 645 bytes, including nothing but the service names. It is what says a program does not need a filesystem inside it. |
| Break | Stops itself twice with SWI osBreak, so that the registers can be seen changing between one stop and the next. |
| Edit | A line editor. | | Edit | A line editor. |
### The Monitor: ### The Monitor:
+16
View File
@@ -0,0 +1,16 @@
CosmOS
> loaded, starting at 2000
> two stops, and what the registers were at each
break at 200E
A 11 B 22 Q 00 S 00
DP0 1030 DP1 05CC DP2 039A DP3 2000
press a key
break at 2018
A 44 B 55 Q 00 S 00
DP0 1000 DP1 05CC DP2 039A DP3 2000
press a key
carried on to the end
finished
> halted
Execution halted.
[exit 0]
+31 -2
View File
@@ -26,6 +26,35 @@ CosmOS
0010 03 FF 08 00 00 00 00 00 01 20 01 00 00 00 00 00 ......... ...... 0010 03 FF 08 00 00 00 00 00 01 20 01 00 00 00 00 00 ......... ......
0020 00 FF 00 00 00 00 00 00 00 FF 00 00 00 00 00 00 ................ 0020 00 FF 00 00 00 00 00 00 00 FF 00 00 00 00 00 00 ................
0030 00 FF 00 00 00 00 00 00 00 FF 00 00 00 00 00 00 ................ 0030 00 FF 00 00 00 00 00 00 00 FF 00 00 00 00 00 00 ................
* Fault: The device on port 233 refused the access at Program Address 0x1359, and nothing is installed to deal with it. * that bank will not be written
* bank 00
* * 8000 26 48 INIA 48
8002 D1 00 OUTA 00
8004 26 0A INIA 0A
8006 D1 00 OUTA 00
8008 18 12 SWI 12
800A 00 ADD
800B 00 ADD
800C 00 ADD
* H
finished
* 8000 26 48 INIA 48
8002 D1 00 OUTA 00
8004 26 0A INIA 0A
8006 D1 00 OUTA 00
8008 18 12 SWI 12
800A 00 ADD
800B 00 ADD
800C 00 ADD
* > greet.sbx 210
hello.sbx 52
Life.sbx 1411
Snake.sbx 2175
Keys.sbx 663
Say.sbx 155
Break.sbx 128
notes.txt 21
8 files
> halted
Execution halted. Execution halted.
[exit 1] [exit 0]
+2 -1
View File
@@ -6,8 +6,9 @@ Life.sbx 1411
Snake.sbx 2175 Snake.sbx 2175
Keys.sbx 663 Keys.sbx 663
Say.sbx 155 Say.sbx 155
Break.sbx 128
notes.txt 21 notes.txt 21
7 files 8 files
> load what? > load what?
> no such file > no such file
> not a program > not a program
+5
View File
@@ -0,0 +1,5 @@
load Break.sbx
run
exit
+2
View File
@@ -9,6 +9,8 @@ b data
x 1000 x 1000
b 2 b 2
x 0 x 0
s 0 FF
b program
s 8000 26 48 D1 00 26 0A D1 00 18 12 s 8000 26 48 D1 00 26 0A D1 00 18 12
d 8000 d 8000
g 8000 g 8000
+5
View File
@@ -91,6 +91,11 @@ for i in 1 2 3 4 5 6 7 8; do "$TOOL" put "$DISKS/sbfs.img" "filler$i.txt" >/dev/
"$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" \ "$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" \
"$ROOT/Programs/CosmOS/Apps/Say.asm" -o "$WORK/Say.sbx" >/dev/null "$ROOT/Programs/CosmOS/Apps/Say.asm" -o "$WORK/Say.sbx" >/dev/null
"$TOOL" put "$DISKS/cosmos.img" "$WORK/Say.sbx" >/dev/null "$TOOL" put "$DISKS/cosmos.img" "$WORK/Say.sbx" >/dev/null
# Break.sbx stops itself twice and shows what the registers were each time. It needs no disk
# of its own: it only prints.
"$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" \
"$ROOT/Programs/CosmOS/Apps/Break.asm" -o "$WORK/Break.sbx" >/dev/null
"$TOOL" put "$DISKS/cosmos.img" "$WORK/Break.sbx" >/dev/null
printf 'this is not a program' > notes.txt printf 'this is not a program' > notes.txt
"$TOOL" put "$DISKS/cosmos.img" notes.txt >/dev/null "$TOOL" put "$DISKS/cosmos.img" notes.txt >/dev/null
+12
View File
@@ -221,6 +221,10 @@ cosmosRun | CosmOS/Source/cosmos.asm | run | cosmosRun
# The last part is what the mode is FOR: a program typed in as bytes, run with g, and the # The last part is what the mode is FOR: a program typed in as bytes, run with g, and the
# prompt that comes back is the monitor's own. A program giving the machine back lands where # prompt that comes back is the monitor's own. A program giving the machine back lands where
# it was started from, so looking at something and running it do not interrupt each other. # it was started from, so looking at something and running it do not interrupt each other.
#
# It also asks to write into bank 2, the controller's own table, which is published read
# only. That used to stop the machine, and the recorded output of this test contained the
# crash without anybody noticing, which is what blessing a result without reading it buys.
cosmosMonitor | CosmOS/Source/cosmos.asm | run | cosmosMonitor.in | - | disks/cosmos.img cosmosMonitor | CosmOS/Source/cosmos.asm | run | cosmosMonitor.in | - | disks/cosmos.img
# The original hello.asm, brought over as an application. It is not much of a program, # The original hello.asm, brought over as an application. It is not much of a program,
# but it is the one that talks to the hardware directly: it writes to port 0x00 instead # but it is the one that talks to the hardware directly: it writes to port 0x00 instead
@@ -270,6 +274,13 @@ cosmosFiles | CosmOS/Source/cosmos.asm | run | cosmosFil
# name, and with several words, since what arrives is the rest of the line rather than a # name, and with several words, since what arrives is the rest of the line rather than a
# list and it is the program's business what to make of it. # list and it is the program's business what to make of it.
cosmosSay | CosmOS/Source/cosmos.asm | run | cosmosSay.in | - | disks/cosmos.img cosmosSay | CosmOS/Source/cosmos.asm | run | cosmosSay.in | - | disks/cosmos.img
# A program stopping itself to be looked at. Two breakpoints, so what is checked is not only
# that one fires but that the SECOND one does - which is the whole difference between this
# design and one that overwrites an instruction, since an overwritten instruction has to be
# put back to continue and putting it back is the same act as disarming the breakpoint.
# Every value shown comes out of the interrupt frame rather than the registers, because by
# the time the handler runs the registers are the handler's.
cosmosBreak | CosmOS/Source/cosmos.asm | run | cosmosBreak.in | - | disks/cosmos.img
# The editor, which is the first program on this machine that makes a file a person typed: # The editor, which is the first program on this machine that makes a file a person typed:
# every byte on every other image here was put there by the host tool. It is run twice in # every byte on every other image here was put there by the host tool. It is run twice in
# one session, and that is the test rather than a flourish - the second run reads back what # one session, and that is the test rather than a flourish - the second run reads back what
@@ -291,6 +302,7 @@ app-Life | CosmOS/Apps/Life.asm | assemble | -
app-Snake | CosmOS/Apps/Snake.asm | assemble | - | - app-Snake | CosmOS/Apps/Snake.asm | assemble | - | -
app-Keys | CosmOS/Apps/Keys.asm | assemble | - | - app-Keys | CosmOS/Apps/Keys.asm | assemble | - | -
app-Say | CosmOS/Apps/Say.asm | assemble | - | - app-Say | CosmOS/Apps/Say.asm | assemble | - | -
app-Break | CosmOS/Apps/Break.asm | assemble | - | -
app-Edit | CosmOS/Apps/Edit.asm | assemble | - | - app-Edit | CosmOS/Apps/Edit.asm | assemble | - | -
app-Files | CosmOS/Apps/Files.asm | assemble | - | - app-Files | CosmOS/Apps/Files.asm | assemble | - | -