CALL saves A, B and Data Pointers 0 to 2 and nothing else, which is exactly why Q and DP3 are how a subroutine hands something back. An interrupt saves all of it, so a service with an answer had to reach into its own frame and un-save two fields by hand: MVSD.2 DPUP.2 0d02 ; the saved Q, by an offset it had to know STA.2 RETI Thirty places in CosmOS did that. Every one knew the frame's layout by heart, and all thirty would have gone quietly wrong the day the frame gained a field - the same duplicated fact this project keeps being bitten by, except duplicated into thirty places AND into the CPU. SRET is 0x76, in the seat the block split left for it. It is RETI's frame with RET's rule applied: A, B and DP0 to DP2 come back, the saved Q and DP3 are dropped, and the Interrupt Flag is restored from the frame - only that bit, so carry survives a service the way it survives a call, and there is one rule rather than two. RETI stays exactly as it was: a hardware handler has nothing to say and must leave no trace. CosmOS is 10,969 bytes against 11,122, and no handler knows a frame offset. TWO MISTAKES WORTH RECORDING, both mine, both caught by tests. The first conversion matched STA.2 with a regular expression that did not allow a trailing comment, so it ran past the end of one handler and into the next. The second understood the pattern and still got it wrong: the old frame write carried the answer from A into the saved Q slot, so simply deleting the write left Q holding whatever it happened to hold. Services that answer by calling something were fine - Q already had it - and services that set A directly silently reported success for every failure. cosmosCwd is what noticed, by saying "cannot go there" about a directory that was there. Sixteen handlers move the answer into Q now. Seven MVQA went with it. They copied Q into A so the frame write could carry it; SRET puts A back, so they moved a value nobody would ever read.
190 lines
5.3 KiB
NASM
190 lines
5.3 KiB
NASM
; 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
|
|
; sret: 42 and SRET says the same thing without touching the frame
|
|
; sretptr: ABC for a pointer too
|
|
; sretkept: 7 while everything a RET would restore still comes back
|
|
;
|
|
; ---- The frame editing above is what SRET exists to replace ----
|
|
;
|
|
; Everything between MVSD and RETI in "answer" and "pointer" is a routine reaching into its
|
|
; own frame to un-save two fields, using offsets it has to know by heart. Thirty places in
|
|
; CosmOS did that, and all thirty would have gone quietly wrong the day the frame gained a
|
|
; field. SRET is the same instruction sequence as RETI with the saved Q and DP3 stepped
|
|
; over instead of restored, so a handler answers the way a subroutine does and nothing
|
|
; below has to know what a frame looks like.
|
|
;
|
|
; Both are kept here on purpose. RETI is still how a HARDWARE handler says it was never
|
|
; there, and a service that has nothing to say should still use it.
|
|
|
|
#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
|
|
|
|
; ---- And the same three answers, given with SRET instead ----
|
|
SETD.0 SretText
|
|
CALL printString
|
|
SWI sretAnswer
|
|
MVQA
|
|
CALL printByteDecimal
|
|
CALL newLine
|
|
|
|
SETD.0 SretPtrText
|
|
CALL printString
|
|
SWI sretPointer
|
|
PSHD.3
|
|
POPD.0
|
|
CALL printString
|
|
CALL newLine
|
|
|
|
; What SRET must still put back. A handler that trampled A, B and DP0 to DP2 would break
|
|
; every caller, which is exactly why RET restores them - so SRET does too, and the number
|
|
; printed here is the caller's own, loaded before the service was asked for anything.
|
|
INIA 0d7
|
|
RSTB
|
|
CCF
|
|
ADD ; Q is the caller's seven,
|
|
SETD.0 SretKeptText ; and DP0 names the label, BEFORE the service is asked.
|
|
SWI sretTrample
|
|
; Printed after the call rather than before it, which is the whole of the check: the
|
|
; handler pointed DP0 somewhere else, so this says "sretkept" only if SRET brought the
|
|
; caller's own pointer back. Printing first would have tested nothing at all.
|
|
CALL printString
|
|
MVQA
|
|
CALL printByteDecimal
|
|
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
|
|
|
|
; ---- The same answers, without the frame ----
|
|
;
|
|
; Q and DP3 are set the way any subroutine sets them, and SRET leaves them alone. There is
|
|
; no MVSD, no offset, and nothing here that would need revisiting if the frame changed.
|
|
sretAnswer:
|
|
INIA 0d42
|
|
RSTB
|
|
CCF
|
|
ADD
|
|
SRET
|
|
|
|
sretPointer:
|
|
SETD.3 Letters
|
|
SRET
|
|
|
|
; Sets everything a RET would restore to something wrong, to show that SRET restores it.
|
|
; If any of these came back, the caller's 7 would not.
|
|
sretTrample:
|
|
INIA 0d99
|
|
INIB 0d98
|
|
SETD.0 Letters
|
|
SETD.1 Letters
|
|
SETD.2 Letters
|
|
SRET
|
|
|
|
#Data
|
|
|
|
QuietText:
|
|
"quiet: "
|
|
AnswerText:
|
|
"answer: "
|
|
PointerText:
|
|
"pointer: "
|
|
SretText:
|
|
"sret: "
|
|
SretPtrText:
|
|
"sretptr: "
|
|
SretKeptText:
|
|
"sretkept: "
|
|
Letters:
|
|
"ABC"
|
|
|
|
#Vectors
|
|
|
|
Boot start
|
|
quiet quiet
|
|
answer answer
|
|
pointer pointer
|
|
sretAnswer sretAnswer
|
|
sretPointer sretPointer
|
|
sretTrample sretTrample
|