The Dauug House Wright State University logo
Dauug|36 minicomputer documentation

Osmin kept registers

Scope api::

This data-only scope connects user programs to the Osmin kernel via consistently-numbered registers. A user program places an API request in its own registers and then YIELDs to the kernel. The kernel fetches the user’s registers by using the PEEK instruction, processes the API request, and writes results back to the user’s registers using the POKE instruction. The user program is then scheduled to continue executing. See also API.

api::request is ordinarily zero. The user initiates API requests by setting its own copy of this register to a specified nonzero constant before it YIELDs the CPU.

api::result is set by the kernel (in the user’s copy) to indicate the outcome of an API request.

Scope code::

This data-only scope is used solely for unit conversions. Their use looks like:

code::asl is a control word for the ASL (arithmetic shift left) instruction. It can convert a number of code pages to the total number of words that represents. This register is not used and is a candidate for removal. Example:

n.code.words = n.code.pages asl code.asl

code::asr is a control word for the ASR (arithmetic shift right) instruction. It can convert a number of code words, which must be a multiple of the code page size, to the total number of pages that represents. Example:

n.code.pages = n.code.words asr code.asr

Scope count::

This data-only scope contains counts of how many of various things exist in the system.

count::code.page.mask is one off of an actual count, as indicated by the .mask part of its name. It’s one less than count::code.words.per.page, and it’s used both to compute which page a code address is at, as well as what offset within a page a code address is at.

count::code.pages.free is the number of code pages that do not contain program text segments (code segments) or operating system code. To compute the canonical amount of free code memory in words, multiply this register by count::code.words.per.page.

count::code.words.per.page is the number of words in one page of code memory. This must be a power of two, but is otherwise configurable. I suggest 512. See also Performance tuning.

count::C.words is the size of the installed code RAM in words, as measured by Osmin.

count::M0.words is the size of installed data RAM M0 in words, as measured by Osmin. M0 is optional as long as M1 is installed, and it is not required for M0 and M1 to be the same size. If M0 is not installed, this count is zero. If the page table RAM is not wide enough to use all of M0, this count is clamped to the usable data RAM size as configured.

count::M1.words is the size of installed data RAM M1 in words, as measured by Osmin. M1 is optional as long as M0 is installed, and it is not required for M0 and M1 to be the same size. If M1 is not installed, this count is zero. If the page table RAM is not wide enough to use all of M0, this count is clamped to the usable data RAM size as configured.

count::max.users is the largest number of users (programs) that Osmin can have in its schedule, including users belonging to the kernel such as the superuser. There is an architectural maximum of 256 users, although a custom build of Osmin may opt for a lower number to reduce superuser memory use and/or speed up electrical simulations.

count::phys.pages.free is the number of 4096-word pages of physical data memory that remain available for allocation. To compute the canonical amount of free data memory in words, multiply this register by 4096.

count::phys.pages.installed is the total number of 4096-word pages of physical data memory in the machine, and is (count::M0.words + count::M1.words) รท 4096.

count::P.slots.per.user is the maximum number of 4096-word virtual data memory pages that a user can allocate, as measured by Osmin. On many machines, this number will more than cover the actual physical memory installed. (Note that neither Osmin nor Dauug|36 offer page fault detection, so neither is able to overcommit memory by swapping.)

count::P.words.per.user is the maximum number of words of virtual data memory that a user can allocate, as measured by Osmin. This is simply count::P.slots.per.user multiplied by 4096. This register has little use and is a candidate for removal.

Scope dict::

This data-only scope holds registers used for input, output, and state for the dict.* scopes that implement collections.

The dict.* scopes aren’t reentrant, so it may be necessary to spill and restore these registers when collection operations are nested. Such nesting may not be obvious, such as when a diagnostic routine to display a memory pool is called within a loop that does something else.

dict::addr is a pointer to a collection. This pointer is always the same for a given collection, because the collection’s maximum size is determined when the collection is created and cannot be changed later. The dict.* scopes use this register for both input and output.

dict::el is a pointer to an element (also called a member) within a collection. The dict.* scopes use this register for both input and output.

dict::el.max is an input parameter to set the maximum number of elements in a collection dict::new will create. This parameter may be as large as virtual memory permits.

dict::el.size is an input parameter to set the number of words in each element in a collection dict::new will create. This parameter cannot exceed 63 words, because short multiplication is used hereafter to compute the address of elements within the collection.

dict::key is used as both a one-word key to an element in a key-value collection, and as an index to a collection that is being treated as an array. The dict.* scopes use this register for both input and output.

Scope dump.code.page.chain::

dump.code.page.chain::p is an input parameter to identify the first of a chain of code pages to print for testing.

Scope hold.or.retain.user.program::

hold.or.retain.user.program::hold.or.retain is an input parameter to hold.or.retain.user.program:: that determines whether a program is being held (kept in code memory indefinitely) or retained (kept in code memory until a specific running instance terminates). Its value is either 235 for retain or 20 for hold.

Scope ig::

ig::nore is a generally-unused register that is safe to write to. Its purpose is for instructions where side effects matter, but the result will not be used. Designating a single register for this use improves documentation and lessens register pressure.

Scope index::

This data-only scope holds registers that could be regarded as element indices.

index::next.free.phys.page is an index within the v.addr::phys.page.pool where a free page of physical data memory is indicated. It refers to the page that will be allocated if nothing more is freed before the next request for a new page. This register will be zero if no physical data memory remains to be allocated—although zero is in fact a valid physical page index, it’s a page that is never free, so there is no ambiguity as to what zero means.

index::nop.code is 36-bit code for the NOP (no operation) instruction. This register falls under the index:: scope, because bits 27–35 of this register refer to a four-doubleword location in the control decoder RAMs. Bits 0–26 are zeros.

Scope internal.error::

internal.error::code is an input parameter to internal.error:: that identifies one of several invariants that, if violated, require an immediate shutdown of the operating system.

Scope p.addr::

This data-only scope is for registers that hold pointers to into physical memory. There won’t be many such pointers, because good design practice will prefer virtual memory, even for the kernel.

p.addr::zero.page is the address of a write-protected page of physical memory that contains only zeros. This address is copied into every unused page table entry for every user so that no program can bypass memory protection. Because the physical address format uses bits 34 and 35 for chip select and write protect respectively, p.addr::zero.page will be either 400_000_000_000`o or 600_000_000_000`o depending on whether data memory M0 is installed or not.

Scope page::

This data-only scope is for passing parameters to and from a family of related callable scopes, all of which are listed in this table.

Scope page::i page::p
alloc.and.retain.phys.page written written
further.retain.phys.page written read
index.to.phys.page read written
phys.page.to.index written read
release.phys.page written read
wipe.phys.page read
wipe.data.ram read & written

It is safe to use “read” registers (but not “read & written”) to set up parameters, including as caller loop registers. But don’t use this scope for ad-hoc temporary variables. Name and allocate your own temporary registers, so there is no unexpected crosstalk between uses.

page::i is an index within the pool of physical pages (of data memory).

page::p is the address of a physical page (of data memory).

There is a one-to-one correspondence between valid page::i and page::p values. Moreover, the distribution of page::i indices is contiguous, but the page::p pointers will not be contiguous if two data RAMs are present due to the chip select bit’s presence in the physical address format.

Scope text.seg::

This data-only scope is where parameters go when text segments (code segments) are manipulated. The following code scopes use these parameters:

text.seg::allow.priv determines whether a text segment operation will be forbidden if the segment contains privileged instructions. If text.seg::allow.priv is nonzero, any text segment is permitted. If text.seg::allow.priv is zero, the operation will only succeed if all instructions in the text segment are nonprivileged.

text.seg::filename is the location of an executable program within the paravirtualized file system. Because the entire namespace is only 36 bits so that a single register is adequate, filenames are ordinarily written as tetrasexagesimal constants such as MyProg`t.

text.seg::result is the outcome of a text segment operation. A zero text.seg::result indicates success. A nonzero result describes why the operation failed, and will always be a constant that a single IMH (immediate high) can load. For example, text.seg::result will be ...PRV`t whenever a text segment contains privileged instructions but text.seg::allow.priv is zero.

text.seg::start.jump is a JUMP instruction to the start of a text segment. This means bits 0–26 are a code address, and bits 27–35 are the JUMP opcode. The reason a JUMP is bundled with the address is that the text segment is divided into pages that may not be contiguous, and the last instruction of each page—effectively a pointer to the next page—is a JUMP instruction to the next page. Because text.seg::start.jump is a register pointer to the first page, the same format is used for consistency.

Scope trace::

This code-with-data scope is an early effort to support printing from the kernel for testing. It’s cumbersome. Typical usage looks like:

; AT THE BEGINNING OF THE PROGRAM:
trace::outfile = pts`t      ; file or link in the paravirtualized I/O namespace
ig::nore = trace::outfile pvio out8`t

; HOW TO PRINT THE VALUE OF some.register:
trace::radix = hex`t        ; or dec`t, oct`t, tet`t, etc.
trace::n = some.register    ; whatever register we'd like to know
trace::label = stuff`t      ; whatever label goes with this
call trace

This would output (assuming some.register contains the below constant):

0stuff`t 34fc9007a`h

Assuming the radix doesn’t need to between successive calls, then three lines of assembly code can print a register’s value with a label.

trace::label is used to give the output a name. It will be printed as a tetrasexagesimal constant. Leading zeros and the trailing `t will be included. A space will follow the label.

trace::n is the quantity that will be printed. A newline will follow the quantity.

trace::outfile is the register that trace output has been mapped to. There are two quantities involved: the contents of trace::outfile at the time a file (usually a link to a virtual terminal) is opened to receive trace output, and the register number of trace::outfile that is used as a file handle.

trace::radix is a paravirtualized I/O command that indicates the radix trace::n will be printed in. Options are dec`t, hex`t, oct`t, sdec`t, and tet`t.

Scope user.pool::

This data-only scope manages a FIFO (first-in, first-out) queue of user ids that will be assigned to running programs. They are analogous to process ids in other operating systems.

This scope violates feng shui, because most of its registers contain virtual addresses. It can be argued they should therefore be in the v.addr:: scope, not user.pool::. Counterbalancing this argument are the considerations that (i) user.pool:: is self-contained, and (ii) v.addr:: mostly does not dive into data structures like user.pool:: does.

user.pool::a is the virtual memory address of the lower (inclusive) boundary of the queue. This address is determined when the queue is allocated and will not change.

user.pool::b is the virtual memory address of the upper (exclusive) boundary of the queue. This address is determined when the queue is allocated and will not change.

user.pool::r is the virtual memory address of the next queue element to be read.

user.pool::uid is a user id that is being obtained from or returned to the pool by the get.from.user.pool:: and return.to.user.pool:: scopes.

user.pool::w is the virtual memory address of the next queue element to be written.

Scope v.addr::

This data-only scope is for registers that hold virtual addresses, of which there are two kinds. The first (and more common) kind is virtual addresses where various data structures are kept. The second kind is virtual addresses that help manage the growth in use of virtual memory as the operating system initializes. Only v.addr::backed.to and v.addr::used.to are of the second kind.

v.addr::backed.to is the virtual address of the lowest virtual page that does not have a physical page mapped. It’s effectively a “high-water mark” for virtual memory that the superuser has allocated. This address will always be a page boundary.

v.addr::code.pool is a pointer to the first page of a linked list of code memory pages that are not in use. The last word of each code memory page is a pointer to the next page. An all-zero “pointer” is used as an end-of-list marker. Because the CPU must natively follow these pointers when a program’s pages are not contiguous in code memory, all pointers are represented as JUMP instructions to the next page. This means that bits 0–26 are the address, and bits 27–35 are the JUMP opcode. (The JUMP opcode must not be zero, or an ambiguity with the end-of-list marker can occur.)

BUG. v.addr::code.pool should be in a different scope, because code memory is not virtualized. 2 April 2024.

v.addr::phys.page.pool is a pointer to an array of words that track the use of physical memory pages. The interpretation of these words depend on whether they are negative (indicating the negated retention count of a page that is in use), zero (indicating a page is free, but no more follow), or positive (indicating a page is free, as well as the index of the next free page).

v.addr::schedule is a pointer to a list of running program instances. Because Osmin uses round-robin scheduling, this list is the same as the schedule for assigning time slices to programs, so this pointer has two uses.

v.addr::text.pool is a pointer to a list of text segments (code segments) of programs that are loaded into memory.

v.addr::used.to is the lowest virtual address that is not in use by the superuser. It’s effectively a “high-water mark” for virtual memory that the superuser is actually using. This address will not necessarily be a page boundary.

Scope wipe.code::

This code-and-data scope is used to clear code memory at system startup and shutdown. Wiped code memory is filled with NOP (no operation) instructions, not zeros. This scope is not involved in the more complex operation of freeing code memory when a program terminates.

wipe.code::from is the inclusive lower bound of a range of code memory to wipe.

wipe.code::to is the exclusive upper bound of a range of code memory to wipe.

Scope wipe.user.stack::

This code-and-data scope is used to clear the stack of the currently active user. The entire stack of 255 positions is cleared, not simply a single protective boundary in the manner of the CALI (call stack initialize) instruction. wipe.user.stack:: is not needed to assure separation of running programs, because CALI is sufficient. The value of wipe.user.stack:: is to prevent a future cold boot attack from extracting stack information.

wipe.user.stack::jump.addr is zero when user stacks are wiped, and the address to return to after the superuser’s stack is wiped. The register is necessary, because the wipe operation will necessarily overwrite the superuser’s return address.


Marc W. Abel
Computer Science and Engineering
College of Engineering and Computer Science
marc.abel@wright.edu
Without secure hardware, there is no secure software.
937-775-3016