SandBox Ultra

An Operating System Development Environment


SandBox is a relatively safe and simplified virtual machine designed to minimize the usual hasssle in getting an operating system working. Much of the complexity of process management has been automated in architecturally supported semaphors, so that a full context switch, including memory manager retargeting, can be done in less than 100 machine instructions. Input and output has also been largely idealized and simplified to make writing device drivers as painless as possible. The virtual machine also includes a debugging mode with single-step capabilities.

Download Sandbox
 

Known Bugs

FIXED The source debugger window is now a little less easily confused. Also, when the print is on, it trims the accumulated text more severely, so to cope with smaller host systems.

FIXED SandBox now runs continuously without crashing!

FIXED The keyboard interrupt now really works OK, and the return key now (really) makes it in.

FIXED The virtual disk hardware is working correctly now.

FIXED The MMU hardware is now known to work. (2004 March 24, fixed a couple more MMU bugs)

The Pause button, if you click it to stop execution, turns back into the Run button and immediately clicks itself. Click on the Step button to stop execution.

Not very many parts are still untested.
 

Operator Interface

SandBox is a standard Windows program interface window with a 640x480 virtual display screen and some buttons and fields along the top for opening a virtual "floppy disk" and a virtual "printer" (output text file), and for starting up executing, as shown in Figure 1.

Figure 1. The normal execution interface.

Clicking on the Debug button toggles between this form and the debugging mode shown in Figure 2, which includes all the interface elements of the normal execution mode, plus several additional debugging elements. These are described below. It also opens a second window for source level debugging. For more detail on how things work, see Understanding SandBox Code.

         PrintPrintFileText->FlopMountFloppyFileBootRestartDebugRunStepPreMo

 

  ClockInts
  Registers
 
 
 
 
 
 

Breakpoints
 
 

 Trace
 
 
 
 
 
 
 

MemorySelectors    Memory Display

                        Figure 2. The debugging mode interface.

Video Screen -- The large panel on the left is the simulated video screen. It is initially a light gray when SandBox is initially started, and changes to a dark gray as shown when you first roll the mouse over it, thus showing that the system is ready to begin.

Print Button -- Clicking this button activates the printer by opening an output text file to receive the printed output, or deactivates it again by closing the file.

Print File -- This is the name of the currently open print file. It is blank if no file is open.

Text -> Floppy -- Click this button to convert a specially formatted text file such as produced by the compiler, into a virtual floppy disk and mount it.

Mount -- Click this button to mount a virtual floppy disk, or to "eject" the the currently mounted floppy. It's a little flakey and often takes several attempts, but you can click on this button while in Run mode to change out a floppy without stopping execution.

Floppy File -- This is the name of the currently mounted virtual floppy disk file, or blank if none.

Boot -- Click this button to load a program from the currently mounted floppy disk into memory and start it. In Debug mode the program is paused, ready to step or run.

Restart -- Click this button to stop current execution and restart the program last booted. In Debug mode the program is paused, ready to step or run. Note that you can use this button to start up your boot program with a different floppy in the drive.

Debug -- Click this button to toggle the debug mode and enlarge or shrink the window to match.

Run/Stop -- Click this button to run continuously. To pause execution you should click some other button, such as Step.

Step -- Click this button to step one virtual machine instruction. If the source code is available, you can trace one line of source in the Source Code Debugger window. The effects will be shown on the trace panel.

PreMo -- Click this button after you have stopped in a repeatable error or a breakpoint, to restart execution and trace the last 1000 cycles before the stop. This is useful when you are deeply in your program when the error condition occurs, and you don't want to trace a million irrelevant lines just to find out what happened going into the error condition.

ClockInts -- If this checkbox is not clicked, you will not get clock interrupts in debug mode. This is helpful when single-stepping, because they come all the time otherwise.

Registers -- This is the current values of the four CPU registers, plus the MMU address register. If the MMU is active, it also shows the real addresses the four CPU registers map to. The checkbox indicates whether the MMU (and interrupts) are enabled.

Breakpoints -- (Not in this screen shot) You can type in up to 9 memory addresses, which will stop execution when the program touches that address, either as instruction, or else load or store. Delete the currently visible breakpoint by clicking the Delete button.

Trace -- This panel traces the stepped execution. If the Trace All checkbox is checked, it shows all instructions in Run mode; otherwise it shows only the first and last. Normally you would leave this unchecked when using the Source Code Debugger.

Memory Selectors -- These four combo popups make it easy to select memory to view based on a register value, or the I/O register area. You can also type in the hex address to be displayed. Choose +0020 to display the next 32 bytes after the line above it.

Memory Display -- These four lines of text display the current memory contents as selected by the memory selectors.
 

Source Level Debugging

In the debug mode, you have the option of providing in the same directory as the ".dsk" disk image file, the ".osl" source and ".ftx" object file used by the OSL Compiler, which the source debugger reads for debugging information. This window has a larger text panel for viewing the trace, a smaller panel for viewing the open stack frames, and some buttons to control stepping. The source file debugging is activated when you mount a virtual floppy disk with the debugging panel open and the source files are present. If you are debugging several compiled programs at once, you will need to mount and unmount each virtual disk as its program is activated and deactivated. Sorry about that.

The Stack Frames panel is divided into active function groups, each headed by the function name and the current frame pointer. Note that in the case of recursion, there may be several copies of a function, each with its different frame pointer address. The addresses are all 17-bit real memory addresses.

All declared local variables in each stack frame are listed below the function name, as name, type, relative offset, absolute (real) address, and current value. Arrays and other structures larger than two bytes show only the first two bytes.

The Trace panel displays the most recent executed code. If the Trace Disasm checkbox is checked, the listing shows both source code and interleaved disassembled machine code, as shown. If it is not checked, only source code is shown. In any case this trace always executes complete lines before pausing. If you have the Printer enabled, it also writes the trace listing to that file.

The Step button executes machine code until the next embedded line number, which typically is one line of source code. It will stop with the line about to be executed last on the listing. In the screen shot here, line 164 is ready to be executed.

The Over button is like Step, but in the case of subroutine calls or context switches it continues without tracing until the subroutine or interrupt returns. If your memory manager moves the real address of the code or stack while this button is awaiting return, it probably will not recognize the return. Click the Pause button to regain control if this happens.

The StepOut button is like Over, but it continues until the current function exits. This can be useful if you step into a function then realize it will take a long time to process and its operation is not interesting at the moment. Note that StepOut has no way of knowing if it is activated in an interrupt handler, so it only applies to the current function in the current process. If the context switches while waiting, it will continue to wait until the context switches back.

The Disassemble button is used to create a complete, source-order listing of your program file, including the disassembled machine code. If you have the Printer enabled, it also writes the disassembly listing to that file. WARNING: There is a bug in VisualBasic that limits the scroll capability of large text fields. It will look like the disassembler has stopped, but it's still running; just wait, and when the listing panel returns to the top line, it will have finished.

The stack frame panel is continuously and fully refreshed during execution; if you need a reference copy of the current state, you can click the Stack --> Print button to get it added to the current open print file.

Note that gathering and formatting the debugging information takes considerably longer than execution in the non-debug mode. With full trace it probably runs three or more orders of magnitude slower than with the dibugger closed; even if you are not currently tracing, it is likely to run two orders of magnitude slower. For more detail on how things work, see Understanding SandBox Code.
 

Architecture

The virtual machine emulated by SandBox has a 16-bit stack-based architecture with an 8-bit system bus. With the memory manager unit (MMU) turned on, it is capable of addressing 131072 bytes of main memory. In addition it features a virtual floppy drive which mounts and/or ejects a specially formatted file as if it were a floppy disk, a virtual hard disk drive which shares most of the same "hardware" as the floppy drive, but is much larger and faster, and cannot be unmounted. A virtual keyboard accepts keystrokes from the host computer when the SandBox window has focus, and a virtual mouse that tracks the host computer mouse when it is over the virtual video display screen. The virtual display screen is supported by a simplified graphics accellerator "chip" capable of filling regions with a solid color or a picture, and drawing text from the pixels of a user-supplied font.

Memory is addressed by byte from 00000 to 1FFFF, which is divided into 1K pages by the MMU, the last page at address 1FC00 is reserved for Input/Ouput registers, but at this time all I/O registers are in the last 36 bytes. The penultimate memory page at 1F800 does not exist and always generates a memory fault on access. Except for instruction fetch, all memory access is in 2-byte words; there is no penalty for odd address access, but it should not normally occur in normal operation except for byte data. Word addressing is "Big-Endian" so that the addressed byte represents the most significant byte of the 16-bit word, and the least significant byte is in the next sequentially higher address.

The MMU and the semaphore mechanism require their data to be in the first 64K or 32K of memory, that is to have a 2-byte positive address.

There are four programmable 16-bit CPU registers:

SP -- The stack pointer always points to the top two bytes of the stack, which grows in a positive direction. All operations work on 2-byte data at the top of the stack, pushing or popping as necessary.

FP -- The frame pointer is the basis for local variable management. It is advanced to the current SP position on subroutine entry, and restored to its previous value on exit. Local variables are at positive or negative offsets from the FP.

PC -- The program counter always points to the next instruction to be executed, which unlike the other three registers, is often at an odd byte address. Most instructions are one byte, but the FP-based load and store instructions, and the branch and call instructions are generally two bytes. Two instructions occupy three bytes.

CB -- The Code Base register points to the beginning of a program's executable code, which starts with a table of 2-byte subroutine offsets, relative to CB. A program is thus potentially 64K bytes long and completely self-relative.

During context switch, all registers except the SP are pushed onto the stack, then the SP is exchanged with another program's SP in lower memory, then the three registers from that program's stack are popped, leaving execution to resume in the new context. The MMU (and with it, interrupts) is selectively disabled by context switched into the kernel, and re-enabled on resuming user mode. Thus the kernel is expected to run largely with the MMU turned off. The MMU provides all the system protection; there are no priviledged instructions.
 

Input/Output

The following addresses are defined in the I/O page (the names in parentheses are defined in the compiler); these are further described in their respective sections:
1FFFE -- (MMU.address) MMU page table address
1FFFC -- (MMU.fault) MMU fault address
1FFFA -- (SignalWait.address) Semaphore address
1FFF8 -- (Enables.datum) Interrupt enable, Printer online, Floppy mounted, Disk interrupt pending
1FFF6 -- (Print.datum) Printer/Keyboard data
1FFF6 -- (KeyIn.datum) Printer/Keyboard data
1FFF6 -- (Mouse.datum) Mouse btn & modifier keys
1FFF4 -- (Disk.count) Disk byte count
1FFF2 -- (Disk.address) Disk DMA address
1FFF0 -- (Disk.sector) Disk sector number
1FFEE -- (Mouse.H) Mouse horizontal position
1FFEC -- (Mouse.V) Mouse vertical position
1FFEA -- (Video.address) Video DMA address
1FFE8 -- (Video.datum) Video color/operation
1FFE6 -- (Video.right) Video rectangle right
1FFE4 -- (Video.bottom) Video rectangle bottom
1FFE2 -- (Video.left) Video rectangle left
1FFE0 -- (Video.top) Video rectangle top
1FFDE -- (Date.seconds) Current time, in seconds in this hour (0..3599)
1FFDC -- (Date.hours) Date & time, in hours from midnight 2000 Jan 1
1FFD0 -- (MMU.CB, MMU.FP, MMU.PC) MMU double-fault register save
1FF00 -- MMU odometers
There is also a 16-byte Interrupt Vector at address 00000.
 

MMU

The Memory Management Unit has two registers to control and monitor its operation. The address register points to a 64-byte page address table in lower memory, where each byte defines one of 128 1024-byte pages, with a single bit indicating that the page is writeable. When the MMU is active, every memory access address is split into a 6-bit page number, and a 10-bit byte offset in that page. The page number indexes into the page address table to select one of the 128 1K pages in the SandBox address space, then the offset is used to select a byte in that page. This is done separately for both bytes of a normal memory access, so that an odd address split over a page boundary may potentially access two very different parts of memory.

In normal usage, the operating system would normally allocate page 0 in lower memory for semaphore and process ID block usage, then the remainder of the program's required memory sequentially as needed from upper memory, then fill out the rest of the 64-byte table with 126, meaning that no memory is allocated for that page. The stack grows upward into that unallocated space, so additional pages can be allocated on demand. For dynamic memory requirements, the pages can be allocated downward from the top of the table, until 64K has been allocated. No single process can simultaneously access more than 64K, although a system might choose to offer paging facilities for swapping additional memory in and out of visibility.

In case of a page fault -- either attempting to write into a read-only page, or else any access to page 126 -- the MMU Page Fault interrupt is invoked. The (virtual) address being attempted is stored into the fault address register, where it is easily accessed by the interrupt driver for swapping or other management chores. If the cause of the fault was the stack growing into an unmapped or read-only page, then of course the processor state cannot be saved on the stack. In that case only, three registers in the I/O area (MMU.CB, MMU.FP, and MMU.PC) get the processor state. You can tell this happened if MMU.PC is non-zero, and you should copy all three of them into the appropriate places on the user stack after enabling a writable page there.

Note that the MMU register is only 16 bits, so the page table must always be in the first 64K of real memory; since you cannot even touch the second 32K without the MMU turned on, it is practically limited to the first 32K. The MMU is disabled during interrupt servicing, but you can still use the current MMU by invoking the mmx instruction just before the memory load or store inside the interrupt handler. The compiler gives you access to this operation in the mmPEEK() and mmPOKE() functions.

Implementing a virtual memory strategy requires some additional information on page usage, so you can know which pages to swap out. Every time a mapped page is touched two bytes in the I/O space are incremented (up to a maximum of 255). At address 1FF00 are 128 bytes representing real memory pages. At 1FF80 are 64 more bytes representing the pages visible under the current memory map table. Depending on your aging strategy, you can use either or both tables. Reset them to zero, then look at them from time to time to see what has been used since they were reset; look at their values to see how many times they were used (up to 255). These bytes are not updated for memory access when the MMU is turned off or disabled (as in an interrupt driver), except for mmx usage.
 

Semaphore

The StandBox architecture supports an integral counting semaphore mechanism. When a process waits on a semaphore with no available counts, or signals a semaphore with a waiting process, the respective interrupt is taken and the semaphore address is stored in the address register, where the interrupt driver can take appropriate action. Signalling a semaphore that has no waiting process merely adds to the count on the semaphore without interrupt; similarly waiting on a semaphore with an outstanding count merely takes one of the counts and continues execution.

Note that the interrupted process SP is exchanged with the respective interrupt handler's SP; the semaphore itself is left unchanged. It is the process manager's responsibility (in the case of wait) to remove the interrupted process SP from the interrupt control pointer (SemSP in the MiniOS) and link it into the semaphore; or (in the case of signal) to unlink a process from the semaphore and add it to the ready list before returning to the interrupted process.
 

Printer

The simulated printer is buffered by lines. The running program may send up to 120 characters out to the printer by storing them in the printer output register, followed by a control character such a null or return to start the print (write the line out to the open text file, if any). After a suitable delay (about one second), the printer will issue an interrupt to signal that it is ready for another line. Data sent to the printer before it is ready is simply lost.
 

Keyboard

The keyboard input register shares the same memory location as the printer output. When the user presses a key on the keyboard, the key becomes available in the keyboard input register and an interrupt is signalled. When the user releases the key, it disappears from the register, whether it was read or not, and is replaced by a null. The current state of the shift and control keys are continuously monitored in the low two bits of the upper byte of the keyboard input register.

Note that the keyboard and mouse share this input register, so keyboard data also has a sign bit reflecting the current state of the mouse button.
 

Mouse

There is no interrupt associated with the mouse. The mouse horizontal and vertical coordinates are constantly updated from the host system mouse position, relative to the virtual screen, and stored in their respective registers. The current mouse button state is stored in the high (sign) bit of the same register as the keyboard input data. The SandBox virtual machine assumes a one-button mouse, which normally tracks the left mouse button of the host computer.
 

Disk

The virtual floppy disk and hard drive of the SandBox computer is divided into sequentially numbered 256-byte sectors. There are 32 sectors per track, but the driver does not need to be concerned about that, since there is a unified track-sector number to access it, unless it is for the purpose of improving performance by disk scheduling. Like normal (hardware) disk drives, there is a small delay for reading continuous sectors, and a much larger delay for track-to-track seeking. The floppy disk has even larger delays.

Disk input and output is controlled by a Direct Memory Access (DMA) controller: the program stores a memory address into the address register, a sector number into the sector register, and then stores a byte count to read or write into the count register. Once the count register has been set, the disk controller begins a seek to the sector address, then starts DMA transfer of the data bytes into or from memory, until the count is used up or the sector has been fully transferred. If the count is not yet zero, it continues to wait for the next sector to become available, then transfers another 256 bytes (or less), and so on. When the count reaches 0, the disk interrupt is invoked. A seek with no transfer can be triggered by issuing a count of 0.

If the count stored is positive, the disk data will be read; if negative, the data is written. Note that a whole sector is always written, regardless of the count value.

The floppy drive is accessed with negative sector numbers (sector 0 is -1), but otherwise works the same. If no floppy disk is mounted, it interrupts immediately with no transfer.

One of the user buttons on the SandBox window is labelled "Boot". If there is a mounted floppy disk image, this button loads it into memory at address 18000 and starts executing the program loaded. If there is no image mounted, it requests a mount, then continues. The floppy disk is read beginning from sector 4, which is where the "->Floppy" button puts the compiled memory image from the OSL compiler. If your programs write their own boot floppy disk, you need to be sure the code is in the right place. If you attempt to boot a disk with other data in that sector, it will still try to load and execute it, probably catastrophically.
 

Video

The virtual video screen is 640x480 like the older VGA screens. This is large enough to handle most operating system requirements, but small enough to fit on most modern host computer screens with space left over for debugging information.

The virtual screen has a "graphics accelerator" that does much of the video display work needed by a simple operating system. It has five commands, defined by the upper bits of the Video.datum register. All operations are immediate and synchronous; there is no need for an interrupt. (Actually, the operations take some real time, but the CPU is stalled for the duration).

To operate the video accelerator, the program first sets the boundary rectangle coordinates, top, left, bottom, and right. Depending on the operation, it may also set a memory address for the data. If there is color information, it is stored in the datum register with the display command. These are the defined commands:

0:  Picture Copy. Use this to copy an image from memory to the screen. Each pixel of the image must be one of the 216 colors in a byte (the other values are not displayed); the low order bits of the command word are not a color (because the color information is in the data), but rather the number of bytes each line occupies (which could be different from the displayed image width). The image data is read from sequential bytes in memory in left-to-right row-major order.
1:  Fill rectangle with color. Use this also for horizontal and vertical lines by setting the rectangle with to one pixel.
2:  Generate a character by setting the pixels of the top 16 bits of the rectangle to the specified color where the bits of the memory data is 1, and leaving the previous colors unchanged where the bits are 0.
3:  Define a clipping region to draw in. There are two sub-commands, ClipRect to set an initially rectangular region to draw in, and HideRect to punch out rectangular holes in that region.
Each two bytes of font data represents one vertical column of the generated text, with the least significant bit at the top. Think of the bits in the character rotated 90 degrees to the left for storage, thus:

The first column is two bytes, the first byte is (bottom to top, or left to right as rotated) 1000=8, and the second byte is 00000110=6, so the stored word in the table would be 8*256+6 = 2054. The next column is 4*256+9 = 1033, then 961, 62, 36, and so on. The character ends when the rectangle right edge is reached. In this script R case, the width would be given as 13 pixels, or maybe less to accomodate kerning. Fonts taller than 16 pixels can be accomodated by multiple passes.

The color in each case is a single byte encoding one of six values each for red, green, and blue, thus:
 

Blue Green Red
36 
72  12 
108  18 
144  24
180  30 

Summing the color values for each of the primaries gives a reasonable distribution of colors. Black is 0, white is 215, and gray is any multiple of 37. Using color values greater than 215 may have unpredictable results.

The clipping region is initially set unbounded. For a non-rectangular region, you should first set the outside bounding rectangle using ClipRect, then delete unwanted parts using HideRect. For example, in the region here, you would first set a ClipRect 10,20,50,90, then HideRect 10,65,25,90, and again HideRect 35,20,50,70.  You are limited to somewhere between four to six holes (15 rectangular subregions), which should be sufficient for a simple windowing system with not too many overlapping windows.
 

Clock

The real-time clock has two independent functions in SandBox. Once every 16 milliseconds (64 times per second) it generates a clock interrupt, which the operating system can use for process management and for watching for mouse activity. The clock also maintains the current date and time in two 16-bit words: Date.hours is the number of hours in this millennium (since midnight January 1, 2000), and Date.seconds is the number of seconds so far in this hour. These registers are maintained continuously without interrupting the CPU.
 

Interrupt Vector

At memory location 00000 there is a 16-byte table iVector, eight ints, each pointing to the SP exchange location for one of the following interrupts.  You need to store in each item here the address of the context switch variable for its respective interrupt. See the example code:
int[8] iVector
  // iVector[0] is MMU
  // iVector[1] is Clock
  // iVector[2] is Disk
  // iVector[3] is KeyIn
  // iVector[4] is Print
  // iVector[5] is SysCall
  // iVector[6] is Signal
  // iVector[7] is Wait

Machine Instructions

SandBox has 39 defined machine language instructions, which can be grouped into five categories. If you program in a high-level language and use the source-code debugger facility, it will probably not be necessary to become extremely familiar with these instructions. However, understanding them might be helpful from time to time.
 

Load/Store

01,xx,xx push Large constant
2x/3x push Small constant
4x,xx ldf Load frame
5x,xx stf Store frame
0C ldx Load indexed
0D stx Store indexed
07 stb Store Byte

The two push instructions push respectively a full 16-bit constant onto the stack, or else a small number between 0 and 31. The Load and Store frame instructions, ldf and stf, have a signed word number offset, which is doubled and used to access that word offset from the FP. Load copies that word and pushed it onto the top of stack, while store pops the top of stack and stores it into that word. The two indexed instructions ldx and stx pop a 16-bit address off the stack, and then either push the addressed word, or else pop another word to store at that address.

Everything in the SandBox virtual machine is (2-byte) word oriented except the machine instructions. However, it is possible to access individual bytes computationally. To load a byte, one simply loads the 2-byte value ending with that byte, then masks off the unwanted upper byte using the and instruction. Storing a byte into memory can also be done by masking, but it is quite complex, so the StoreByte stb instruction provides a somewhat simpler way to accomplish the same end. This has the same operation code as the aix instruction, but the top word on the stack is 0; the two words under it are the same as for the stx instruction, except that stb stores only the low byte of the lower word.
 

Arithmetic/Logical

10  neg Negative
11 isz Is Zero
12 equ Equal
13 neq Unequal
14 less Less than
15 grtr Greater than
16 leq Less or equal
17 geq Greater or equal
18 shft Shift
19 and Logical AND
1A or Logical OR
1B xor Exclusive Or
1C add Add
1D sub Subtract
1E mpy Multiply
1F div Divide with remainder

Negative neg replaces the top of stack with its two's complement negative. IsZero isz replaces the top of stack with 1 or 0 depending respectively on whether the previous value is zero or not. It can also be used as a logical NOT operator on boolean values.

The six comparison operators pop two numbers and push 1 (true) or 0 (false) as the two values are equal, less, greater, or not; the right operand is popped first, that is they are pushed onto the stack in left-to-right order. The three bitwise logical operators pop two numbers, perform the operation, and push the 16-bit result; they can also be used on boolean values. To obtain a bitwise complement, use xor with a second operand -1.

The arithmetic operators similarly pop their two operands, do the arithmetic, and push the result. The subtract instruction sub subtracts the top number from the second, again assuming they were pushed in normal left-to-right order. The integer divide instruction div pushes two results, first the quotient, then the remainder. Thus to obtain just the integer quotient, push the dividend, then the divisor, then do the divide, then pop (discard) the remainder. To obtain just the remainder, proceed as above, but swap (exchange) the results before popping. If the divisor (the first value to be popped) is zero, the divide operation does nothing, leaving both original operands on the stack unchanged.

The shift instruction shft is a little more complex: the top value on the stack is taken as a signed value, the direction and number of places to shift the second value. Positive shift values shift left, and negative values shift it right with zero fill. The 16-bit shifted result is pushed back on the stack; bits shifted off either end are lost.
 

Stack Operators

04  dupe Duplicate top of stack
05 pop Pop and discard top
06 swap Exchange top two words

These operations manipulate the top elements of the stack without altering their values. Duplicate pushes a second copy of the top element; pop removes and discards the top. The other two move the top word down one or two words and bring the others closer to the top. These can be used, for example, to duplicate the top two words and leave them in pairs: dupe, rot, swap, dupe, rot. This sequence leaves them reversed from the order they were originally in, but that can be corrected by a couple more swaps.
 

Flow Control

02 enter Enter subroutine
03 exit Return from subroutine
08 cocall Context switch
09 syscall Software interrupt
6x,xx call Call subroutine
8x,xx br Branch unconditional
Cx,xx bz Branch if zero
0F brx Branch indexed

There are seven instructions to effect programmed flow control. The three branch instructions add a signed offset (either from the instruction word, or else popped from the stack) to the PC to jump to some other part of the program, possibly nearby. The bz instruction first pops a value (typically the result of a compare) from the stack, then branches only if that value is zero; otherwise it continues with the next instruction in sequence. In an if-statement, the condition is evaluated, then bz branches to the else-part if the condition is false. In a while loop, the condition is evaluated, then bz branches out of the loop if the popped value is false. The indexed branch brx is normally used to calculate some number of bytes to jump over, such as into a switch table.

The call instruction assumes that the CB register points to the front of the program, and that there is a table of 2-byte offsets there; the low 13 bits of the call instruction select one of 8191 entries in the table, which is added to the CB to form the address of the subroutine entry. The previous value of the PC (the address of the instruction after the call) is pushed onto the stack, then the PC is replaced by the calculated subroutine address, and execution continues at that address. Subroutine #0 is not used; call#0 pushes the PC but continues with the next instruction. This can be used to calculate the real (virtual, if MMU is active) address of some other part of the program code, such as a constant table.

The enter and exit instructions set up and tear down the stack frame on subroutine entry and exit, respectively. Each operation takes a single operand which it pops off the stack, presumably placed there by a prior push instruction. In the case of enter, the operand represents the number of local variable words to allocate space for; in the case of exit, it is the number of argument words to remove from under the return address. Prior to executing the call instruction, the calling program should push first a placeholder for the return value, then any parameter values, then finally the address of the program globals (static variables). The call itself pushes the return address, then the entry sequence pushes a number representing the space to allocate for local varuables. At this point the enter instruction pops off that number and replaces it with the current (now previous) frame pointer, sets the FP to point to the globals address (just below the return address), and moves the SP up to accomodate the local variables requested, leaving the stack looking as at the left here. The parameters can be accessed with negative offsets on the FP, and the local variables with positive offsets. To access global or static variables, the program simply loads the globals address from offset 0, adds to it whatever offset is necessary to address the variable, then uses an indexed load or store.

This process is reversed in the case of an exit. The number pushed should be the number of words the caller pushed (not counting the result word, but including the globals pointer), and the exit instruction restores the old FP and PC from the stack, pops off the indicated number of argument words, leaving the stack with just the result on top, in the calling program, ready to store or otherwise use that result.

Giving a negative number of local variables to enter is nominally invalid, so it replaces it with the current FP and continues with the next instruction, without completing the entry protocol. This is useful for calculating an indexed address of a local variable.

The cocall and syscall operations are used for completely switching context between separate processes. The syscall essentially invokes a software interrupt; the interrupt handler can examine the stack contents for parameters, perform the requested function, and return with a cocall. These two operations have a further side effect: like all interrupts, the syscall instruction disables further (hardware) interrupts and the MMU; the cocall instruction always enables interrupts and the MMU (if it has a non-zero table address).

The interrupt vector in the first 16 bytes of memory gives memory addresses for eight interrupts, one of them (000A) being syscall. It is assumed that at the addressed memory location there is a stored SP for the interrupt handler, which the interrupt exchanges the current SP (of the interrupted program) for this memory value to effect a complete context switch; note that the other three CPU registers are saved on and reloaded from their respective stacks. When the handler completes its duties, it pushes the address of this same memory location, then executes a cocall to reverse the context switch and be ready for the next interrupt.
 

Other

00,xx,xx nop Line number
0E mmx MMU access
07 aix Array bounds check

The mmx instruction overrides the MMU disable for one instruction, so that an interrupt handler can access a memory location using the interrupted program's MMU table. Normally the interrupts and MMU are disabled by an interrupt (including syscall). However it may be necessary to access the interrupted program data space using addresses supplied by that program. Since the program has no way of knowing its own real addresses, the system must do that; mmx makes it substantially easier. Although the user program can also execute the mmx instruction, it has no effect because the MMU is already enabled.

The Array Index Bounds Check aix instruction takes the address of the front of an array, the calculated byte offset, and the number of bytes allocated to the array (pushed in that order) to determine if the calculated byte offset (which is the programmed index value multiplied by the element size) lies within the array bounds; the top of stack is replaced with a boolean true or false (1 or 0) exactly as the index value is within bounds or not. The base address and offset are also summed and left below the boolean result. For dynamic arrays whose size cannot be known at compile time, a negative size can be pushed, and this instruction looks at the value stored in the two words immediately before the array for a byte size number to use. An array size of zero must be checked by the dynamic method, because a zero compile-time size is reserved for the stb instruction, which uses the same opcode.

The nop instruction has no effect on the program state (other than advancing the PC; its sole purpose is to embed line numbers in the code, so to link to source code in the debugger.

Process management in SandBox is complex enough to deserve its own document.
 
 

Rev: 2004 April 6