Infrastructure for system services through software interrupts.

This commit is contained in:
Anachronaut
2026-08-19 15:16:30 -04:00
parent e3100b4718
commit c4b59acc68
5 changed files with 152 additions and 1 deletions
+12
View File
@@ -759,6 +759,18 @@ handlePrintString:
handleReadLine:
CALL readLine
; readLine works out how long the line was, and RETI would throw that away: it restores
; every register from the frame, which is exactly what makes an interrupt safe to arrive
; unannounced and exactly what stops a service answering. So the answer is written into
; the frame, over the saved Q, and RETI puts it back as though the caller had computed it.
;
; This has to be here rather than in a routine, because the offset is from where the
; Stack Pointer is now and a CALL moves it by ten.
MVQA
MVSD.1
DPUP.1 0d02
STA.1
RETI
; What the program was asked to work on. DP0 says where to put it and B how much room
+107
View File
@@ -0,0 +1,107 @@
; A software interrupt handing something back.
;
; RETI restores every register from the frame, which is what makes an interrupt safe to
; arrive at an arbitrary moment: the interrupted code cannot tell it happened. A SERVICE is
; not arbitrary - it was asked for - and the same rule means it has no way to answer.
;
; So a service that has something to say writes it INTO ITS OWN FRAME, over the saved
; register, and lets RETI put it back. MVSD is what makes the frame reachable: it copies
; the Stack Pointer into a Data Pointer, and the frame sits just above it.
;
; +1 Status +5 DP3 high +9 DP1 high +13 resume high
; +2 Q +6 DP3 low +10 DP1 low +14 resume low
; +3 A +7 DP2 high +11 DP0 high
; +4 B +8 DP2 low +12 DP0 low
;
; WHICH REGISTERS A SERVICE MAY ANSWER IN is a convention rather than a rule, and it is the
; same one CALL already has: Q and DP3. A subroutine cannot hand back A, B or DP0 to DP2
; because RET puts them back; a service could write over any of them, and should not, for
; exactly the reason the first list exists. A caller expects what it kept to still be there.
;
; ONLY THE HANDLER ITSELF CAN DO THIS. The offsets are from where the Stack Pointer is, and
; a CALL moves it by ten. A routine called by a handler that tried this would be writing
; into its own return address.
;
; Correct output is:
; quiet: 7 a service that says nothing leaves Q as it found it
; answer: 42 one that does, does not
; pointer: ABC and DP3 comes back the same way
#Include console.asm
#Program
start:
; Something recognisable in Q, so that a service leaving it alone is visible.
INIA 0d7
RSTB
CCF
ADD
SETD.0 QuietText
CALL printString
SWI quiet
MVQA
CALL printByteDecimal
CALL newLine
SETD.0 AnswerText
CALL printString
SWI answer
MVQA
CALL printByteDecimal
CALL newLine
SETD.0 PointerText
CALL printString
SWI pointer
PSHD.3
POPD.0
CALL printString
CALL newLine
HALT
; Says nothing, so whatever the caller had in Q is still there afterwards.
quiet:
INIA 0d99
RSTB
CCF
ADD ; Q is 99 in here, and nobody outside will ever know.
RETI
answer:
INIA 0d42
MVSD.1
DPUP.1 0d02 ; The saved Q.
STA.1
RETI
pointer:
SETD.0 Letters
MVSD.1
DPUP.1 0d05 ; The saved DP3, high byte first the way everything is stored.
PSHD.0
POPA ; The low half comes off the Stack first.
POPB
STB.1
INCD.1
STA.1
RETI
#Data
QuietText:
"quiet: "
AnswerText:
"answer: "
PointerText:
"pointer: "
Letters:
"ABC"
#Vectors
Boot start
quiet quiet
answer answer
pointer pointer
+22 -1
View File
@@ -552,7 +552,7 @@ Those numbers are written down once, in `Programs/CosmOS/Source/services.asm`, w
| Service | Does |
| --- | --- |
| osPrintString | DP0 names a string ending in a zero byte. Prints it. |
| osReadLine | DP0 names somewhere to put a line, B says how much room there is. Reads one from the console. |
| osReadLine | DP0 names somewhere to put a line, B says how much room there is. Reads one from the console. Q comes back holding how long it was. |
| osExit | Gives the machine back. Does not return. |
| osArgument | DP0 names somewhere to put whatever followed the run command, B says how much room there is. |
@@ -567,6 +567,27 @@ Those numbers are written down once, in `Programs/CosmOS/Source/services.asm`, w
A handler is entered with the caller's registers exactly as they were, because an interrupt frame is pushed rather than cleared. That is why a service can be given a pointer in DP0 and a count in B without any of it being copied anywhere first.
### How A Service Answers:
The same thing that makes an interrupt safe makes a service mute. RETI restores every register from the frame, so whatever a handler worked out is thrown away on the way out — which is exactly right for a device interrupting at a moment nobody chose, and useless for a service that was asked a question.
A service answers by **writing into its own frame**, over the saved register, and letting RETI put it back. MVSD copies the Stack Pointer into a Data Pointer and the frame sits just above it, so returning a byte in Q is three instructions:
```
answer:
INIA 0d42
MVSD.1
DPUP.1 0d02 ; The saved Q. See the frame table under Interrupts.
STA.1
RETI
```
**Which registers a service may answer in is the convention CALL already has: Q and DP3.** A subroutine cannot hand back A, B or Data Pointers 0 to 2 because RET puts them back; a service *could* write over any of them and should not, for exactly the reason that list exists. A caller is entitled to find what it kept still there.
**Only the handler itself can do this.** The offsets are from wherever the Stack Pointer is, and a CALL moves it by ten — so a routine called by a handler that tried the same thing would be writing into its own return address. The poke belongs inline, next to the RETI.
A service that has nothing to say does nothing, and the caller's registers arrive back untouched. That is worth knowing from the other side too: a service cannot corrupt a register by accident, only by deciding to.
## Loading A Program From A Disk:
A program that was not the one the machine booted from carries sixteen bytes in front of it saying where it belongs.
+5
View File
@@ -0,0 +1,5 @@
quiet: 7
answer: 42
pointer: ABC
Execution halted.
[exit 0]
+6
View File
@@ -141,6 +141,12 @@ vectorTest | testPrograms/vectorTest.asm | run | -
# it. This pins both ends of that range and leaves a third to the assembler, then calls all
# three by name: a number going astray shows up as the wrong word rather than as silence.
pinnedVectorTest | testPrograms/pinnedVectorTest.asm | run | - | -
# A service handing something back, which RETI otherwise makes impossible: it restores every
# register from the frame, so a handler that worked something out has no way to say so. The
# answer is written into the frame over the saved register. All three cases are here - a
# service that says nothing and leaves Q exactly as it found it, one that answers in Q, and
# one that answers in DP3 - because the first is what makes the other two safe to rely on.
serviceReturnTest | testPrograms/serviceReturnTest.asm | run | - | -
deviceTest | testPrograms/deviceTest.asm | run | - | -
faultResumeTest | testPrograms/faultResumeTest.asm | run | - | -
# The worked example out of the Assembler Manual, so the manual cannot go stale.