diff --git a/Programs/CosmOS/Source/cosmos.asm b/Programs/CosmOS/Source/cosmos.asm index ba6609a..825adc6 100644 --- a/Programs/CosmOS/Source/cosmos.asm +++ b/Programs/CosmOS/Source/cosmos.asm @@ -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 diff --git a/Programs/testPrograms/serviceReturnTest.asm b/Programs/testPrograms/serviceReturnTest.asm new file mode 100644 index 0000000..4b4f64f --- /dev/null +++ b/Programs/testPrograms/serviceReturnTest.asm @@ -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 diff --git a/SplitBit Programming Manual.md b/SplitBit Programming Manual.md index b816c6a..2c25f95 100644 --- a/SplitBit Programming Manual.md +++ b/SplitBit Programming Manual.md @@ -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. diff --git a/Tests/expected/serviceReturnTest.out b/Tests/expected/serviceReturnTest.out new file mode 100644 index 0000000..f9f029e --- /dev/null +++ b/Tests/expected/serviceReturnTest.out @@ -0,0 +1,5 @@ +quiet: 7 +answer: 42 +pointer: ABC +Execution halted. +[exit 0] diff --git a/Tests/manifest b/Tests/manifest index f5d3d22..0a4faa0 100644 --- a/Tests/manifest +++ b/Tests/manifest @@ -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.