Identity-modifying instructions (privileged)
Opcode | P/U | Category | Description |
NPCALL |
priv | identity modifying | call nonprivileged subroutine |
NPRIV |
priv | identity modifying | nonprivileged user |
PCALL |
priv | identity modifying | call privileged subroutine |
PEEK |
priv | identity modifying | peek |
POKE |
priv | identity modifying | poke |
PRIV |
priv | identity modifying | privileged user (superuser) |
SETUP |
priv | identity modifying | setup |
USER |
priv | identity modifying | user |
As mentioned earlier, the Dauug|36 concept of privilege is simply the presence of a privileged instruction to execute. A related concept is identity, or which running program an instruction affects. A Dauug|36 identity, loosely called a user because it numerically identifies an actual user (executing program), is an eight-bit differentiator applied to the address bits of:
- both copies of the register file
- return address stack
- page table
In the netlist, the user is specified in 8-bit register named ff u
, and 1-bit flip-flops reguserflop
and ptsuserflop
can mask its content using AND gates (make the user appear to be user 0, which we call the superuser) on the way to the three destinations. Three electrical situations can result:
ptsuserflop |
reguserflop |
Outcome |
0 | 0 | superuser’s registers, page table, & stack |
0 | 1 | (electrically impossible to reach this state) |
1 | 0 | superuser’s registers, but user’s page table & stack |
1 | 1 | user’s registers, page table, & stack |
Of the eight identity-modifying instructions, half do nothing but change the values of these ten bits. These instructions are NPRIV
, PRIV
, SETUP
, and USER
. The other four instructions handle situations where the user and superuser swap roles during register transfers or subroutine calls. These instructions are NPCALL
, PCALL
, PEEK
, and POKE
.
The following table describes the state of the two single-bit flip-flops after each instruction described on this page:
Instruction | ptsuserflop |
reguserflop |
Comment |
NPCALL |
1 | 1 | NPRIV mode |
NPRIV |
1 | 1 | NPRIV mode |
PCALL |
0 | 0 | PRIV mode |
PEEK |
0 | 0 | PRIV mode |
POKE |
1 | 1 | NPRIV mode |
PRIV |
0 | 0 | PRIV mode |
SETUP |
1 | 0 | SETUP mode |
USER |
unchanged | unchanged | mode unchanged |
NPCALL
Call nonprivileged subroutine
Syntax |
npcall subr |
No registers used |
No flags changed |
NPCALL
is a variant of CALL
where after the return address and flags are pushed on the stack, ptsuserflop
and reguserflop
are both set to 1
, causing further code to run in user mode as opposed to superuser mode.
NPCALL
is used to move the CPU’s attention from the operating system to a user program. Here is how that works:
1. Because NPCALL
is being executed by the operating system, the user program is already frozen, and its flags and next instruction address are ready at the top of its stack.
2. Even though the purpose of NPCALL
is to switch to a user program, the code at subr
is actually part of the operating system instead. It’s just one instruction: REVERT
.
3. NPCALL
does three things, the first of which is save the return address (of the instruction immediately after NPCALL
) on the superuser’s stack.
4. Second, NPCALL
changes the instruction pointer to point to subr
, which should be the location of a REVERT
instruction within the operating system’s code.
5. Third, NPCALL
switches the system to user mode. The page table, stack, and registers now all belong to the user program.
4. Now the REVERT
in the operating system’s code is executed, except the user’s stack is now in effect, so the flags and instruction pointer recovered are that of the suspended user program.
5. The next instruction executed is a continuation of the user’s program.
6. At this point, the frozen operating system has its flags and return address from NPCALL
on top of its stack. These will be restored later by instruction decoder hijacking when either the multitasking timer expires or the user program executes a YIELD
instruction.
NPRIV
Nonprivileged user
Syntax |
npriv |
No registers used |
No flags changed |
NPRIV
sets the one-bit flip-flops ptsuserflop
and reguserflop
to both output 1
, forcing executing code to use the (nonprivileged) user’s registers, return address stack, and page table. This instruction is the opposite of PRIV
.
The multitasking timer is enabled by reguserflop
and doesn’t have a separate control, so going NPRIV
starts the multitasking countdown from whatever its configured initial setpoint is. Partial countdowns are not saved, so any NPRIV
restarts the clock at the beginning. See also TIMER
under Program initialization.
Simulator note: 11 July 2023
NPRIV
has been observed crashing the electrical simulation with the message:
glitch on D1 pin(s) a11
This apparently results from a multitasking timer bit not being initialized to high or low, but instead being left as uncertain. I was able to avoid this crash by initializing the timer prior to going NPRIV
. For instance:
unsigned timer.setting timer.setting = 10_0000_0000_0000_0000`b disable.timer: timer timer.setting jump >= disable.timer
Although I consider this a bug in that the simulation crashes, it’s not a high-priority one to fix, because code that makes this bug surface already places the CPU at the mercy of an unpredictable timer. So correcting the simulator would still leave buggy software in simulation.
PCALL
Call privileged subroutine
Syntax |
pcall subr |
No registers used |
No flags changed |
PCALL
is a candidate for removal from the architecture.
PCALL
is a variant of CALL subr
where after the return address and flags are pushed on the stack, ptsuserflop
and reguserflop
are both set to 0
, causing further code to run in superuser mode as opposed to user mode.
I wrote PCALL
at the same time I was writing NPCALL
, although PCALL
does not have an identified use as of 19 June 2023. Because the opposite process of what NPCALL
does is not done by PCALL
, but is done by hijacking the instruction decoder.
The HIJACK
Pseudo instruction is almost exactly a PCALL
.
PEEK
Peek
Syntax |
su_reg = peek u_reg |
Register | Signedness |
All | ignored |
1 opcode only |
Flag | Set if and only if |
N |
bit 35 of the result is set |
Z |
all result bits are zero |
T |
flag does not change |
R |
flag does not change |
PEEK
fetches the contents of register u_reg
into the arithmetic logic unit (ALU), forces the CPU into PRIV
mode regardless of its preceding mode, then writes the result to register su_reg
. The superuser’s flags are modified as if the destination is an unsigned register. No other user’s flags are modified.
PEEK
provides a mechanism for the superuser to read the contents of a user register. Ordinarily, registers belonging to different users cannot mingle, due to the 8-bit user differentiator’s contribution to the register file’s address bits. PEEK
overrides this limitation by clearing the single-bit flip-flop reguserflop
while the ALU is actively copying a register. For consistency, ptsuserflop
is also cleared.
Ideally PEEK
would keep the system in superuser (PRIV
) mode, but there is no electrical means to do this. The second choice would be to have PEEK
temporarily enter NPRIV
mode and return to PRIV
mode upon completion, but there isn’t enough time in the instruction cycle to do this either. So what actually happens is:
- The CPU must already be in
NPRIV
mode whenPEEK
is executed. PEEK
will leave the CPU inPRIV
mode, which is as we would want it.
Be warned that the multitasking timer will be active while the system is NPRIV
. This won’t affect most operating systems, but an uncharacteristically short timer setting in combination with an uncharacteristically long set of instructions while in NPRIV
mode could produce a surprising result. See POKE
for an example of how to use NPRIV
before PEEK
.
When using PEEK
and POKE
, keep in mind that the user and superuser need to coordinate numbering of any user registers that exchange data. The KEEP
assembler keyword can be used to force consistent register numbering.
POKE
Poke
Syntax |
poke u_reg = su_reg |
Register | Signedness |
All | ignored |
1 opcode only |
No flags changed |
POKE
fetches the contents of register su_reg
into the arithmetic logic unit (ALU), forces the CPU into NPRIV
mode regardless of its preceding mode, then writes the result to register u_reg
. No flags are modified.
POKE
provides a mechanism for the superuser to set the contents of a user register. Ordinarily, registers belonging to different users cannot mingle, due to the 8-bit user differentiator’s contribution to the register file’s address bits. POKE
overrides this limitation by setting the single-bit flip-flop reguserflop
while the ALU is actively copying a register. For consistency, ptsuserflop
is also set.
Ideally POKE
would keep the system in superuser (PRIV
) mode, but there is no electrical means to do this. The second choice would be to have POKE
temporarily enter NPRIV
mode and return to PRIV
mode upon completion, but there isn’t enough time in the instruction cycle to do this either. So what actually happens is:
- The CPU must be in
PRIV
orSETUP
mode whenPOKE
is executed. POKE
will leave the CPU inNPRIV
mode as an unwanted side-effect.
Things will go very wrong if you forget to restore PRIV
mode after using POKE
, because (i) the multitasking timer will have started, (ii) your program will have the wrong page table, (iii) the wrong stack, and (iv) the wrong registers. The silver lining is that because PEEK
leaves the system in PRIV
mode, you can “collapse” mode changes if data is being exchanged in two directions. In the code:
; Here is an example of how to increment `u_reg`, ; which the superuser cannot directly do. npriv su_reg = peek u_reg priv ; can't have NPRIV mode su_reg = su_reg +1 poke u_reg = su_reg priv
the first PRIV
is extraneous, because the PEEK
already took us there. The remaining NPRIV
and PRIV
are necessary, however. The code should be one instruction shorter like this:
; Here is an example of how to increment `u_reg`, ; which the superuser cannot directly do. npriv ; PEEK requires CPU in NPRIV mode su_reg = peek u_reg ; PEEK leaves CPU in PRIV mode su_reg = su_reg +1 poke u_reg = su_reg ; POKE leaves CPU in NPRIV mode priv
On the other hand, for one-directional transfers of data into multiple registers, you need to restore the correct mode at the start of each transfer like this:
; This example zeros three user registers from within a superuser program. poke u_reg1 = 0 priv ; POKE gave up the CPU's PRIV mode poke u_reg2 = 0 priv ; POKE gave up the CPU's PRIV mode poke u_reg3 = 0 priv ; POKE gave up the CPU's PRIV mode
When using PEEK
and POKE
, keep in mind that the user and superuser need to coordinate numbering of any user registers that exchange data. The KEEP
assembler keyword can be used to force consistent register numbering.
PRIV
Privileged user
Syntax |
priv |
No registers used |
No flags changed |
PRIV
sets the one-bit flip-flops ptsuserflop
and reguserflop
to both output 0
, forcing code to execute with the superuser’s registers, return address stack, and page table. This instruction is the opposite of NPRIV
.
The multitasking timer is enabled by reguserflop
and doesn’t have a separate control, so the superuser is not subject to preemption. You could hack around that by:
user 0 npriv
which gives you a superuser that can be interrupted by the multitasking timer. Control would be transferred from the superuser to itself. Maybe even successfully, but I haven’t tried it.
SETUP
Setup
Syntax |
setup |
No registers used |
No flags changed |
SETUP
is a halfway state between NPRIV
and PRIV
, where the one-bit flip-flop ptsuserflop
outputs 1
, enabling access to the (nonprivileged) user’s call stack and page table, but reguserflop
outputs 0
, preserving the running superuser program’s access to its own registers.
Were it not for having a SETUP
instruction, a superuser would go NPRIV
to set up a user’s page table. But without its own registers, the program would have forgotten what it wanted to do with the page table. So it would go PRIV
to try to remember, but then the user’s page table wouldn’t be accessible. It would be a hot mess.
SETUP
is also used to initially set the instruction pointer for a user program that hasn’t run at all yet. This is achieved via some shenanigans that temporarily inserts a function call at the instruction immediately prior to the user program’s start address, and then switches call stacks while the function is called. You only need to understand the whole story if you’re writing an operating system, but there are examples under Preemptive multitasking and Cooperative multitasking to help you.
The multitasking timer is enabled by reguserflop
and doesn’t have a separate control, so SETUP
mode is not subject to preemption.
USER
User
Syntax |
user uid |
Register | Signedness |
All | ignored |
1 opcode only |
No flags changed |
USER
sets the 8-bit user differentiator within ff u
, thereby associating a running program with one of the 256 independent return address stacks, 256 independent page tables, and 256 independent sets of 512 registers. Bits 8–35 of uid
are ignored.
Although USER
is the principal component of selecting which program is running, it does not modify that program’s instruction pointer or flags. These are modified using the REVERT
instruction to switch from operating system to user code, and by control decoder hijacking initiated by a YIELD
instruction or the multitasking timer to switch from user code to the operating system.
USER
may or may not take immediate effect, depending on the condition of the one-bit flip-flops ptsuserflop
and reguserflop
. The condition of these flip-flops depend on every other instruction on this page—but not on USER
.