The GM-NAA I/O System (which was discussed in Part 1) introduced the basic concepts of a “supervisor” program, or operating system (OS), that allows operators to run their programs sequentially and efficiently. This new supervisor program was great, except that from time to time honest mistakes made by programmers caused data to be written to the wrong memory address, resulting in “memory corruption”. The memory corruption did not necessarily bring the computer to a grinding halt – imagine a single byte corrupted in the binary-to-decimal conversion subroutine that is a part of the OS’s code – this might have severe effects on programs that will run on that system (causing them to produce incorrect results), without giving the operators any clues about what exactly went wrong.
It very quickly became clear that the OS code must be protected from memory corruption by rogue program code.
The question is – how? The OS’s code consists of the same bits and bytes that make up regular program code, and it’s the CPU is entirely indifferent about the names we humans give to different pieces of software. In other words, the OS’s code is not inherently special in any way. Those who read the ITMC might be thinking about using ROM, but while ROM can protect the OS’s code, the OS still makes use of variables and variable data that must be stored in RAM (leaving it exposed to corruption).
Protection cannot be provided without privilege and access restriction. As long as programs can access code and data belonging to the OS, there is no real way to prevent memory corruption. Protection mechanisms must be provided and supported by hardware. Hardware can provide the means to prevent memory reads and/or writes on a physical level if certain conditions are met. As an example, think about an imaginary modified version of the 8228 system controller (discussed in part 8A of the ITMC) that blocks I/O or memory R/W control signals if the address lines contain a specific address range. But just blocking out an address range makes no sense in terms of protection, meaning there must be a way to program the hardware based protection mechanisms. It seems like we are back in square one – we need software to manage the hardware protection mechanisms, and software can be corrupted. back in the mid-60’s IBM’s engineers created a protection model that works by providing hardware privilege enforcement facilities that can be used by the first software, but in a very specific manner that will be described next.
One of the first (if not the first) computer system to support these concepts of protection was IBM’s System/360. At the heart of the system there’s the Process Status Word (PSW) as described on page 16 in this document:
The PSW contains all relevant controls and flags for a running program. Some fields, like the 24-bit instruction address (which serves the same purpose as the PC/IP register), change automatically depending on instructions that were executed and events that occurred while the system is running. In order for software to actively change the PSW, a LOAD PSW instruction must be used – but here’s where things get interesting – the LOAD PSW command is a privileged command that will be executed by the CPU only if the problem state bit in the current active PSW is set to ‘0’, meaning that the program currently executing is the supervisor program (i.e the OS). But how does the system get to that state in the first place? Since the OS is the first software that is executed after the computer system is powered on, it gets the highest level of privilege. Lets see how it works step by step – To load the OS an operator selects the load unit by turning a few knobs, powers on the system, and depresses the “load” key:
Depressing the load key makes the system perform a predefined set of actions (called Initial Program Load – IPL phase). First, the system reads 24 bytes from the load unit (starting from index 0) to address 0 in RAM. Address 0-8 will contain a PSW that is not yet loaded, following two Channel Command Words (CCW) that describe an I/O operation:
Bits 8-31 specify the location in RAM in which the data will be stored in case of a read I/O operation. The system then executes the input operation as described by the CCW to read the OS “nucleus” from the load unit to RAM. Once the input operation is complete, the system loads the PSW from address 0 to the CPU, the CPU kicks in and starts executing instructions from the given address with the problem state bit set to ‘0’ (supervisor mode). The IPL is synonymous to the “boot” operation of PCs today, with the nucleus being synonymous to today’s “bootloader“.
Since the nucleus is the first piece of code that runs under the constraints of IBM’s protection model, it must conform to the memory protection rules. The protection key is System/360’s method of memory protection – RAM is divided to blocks of 2,048 (0x800) bytes that are protected by a 4-bit protection key that is unreadable and unchangeable by memory read and write operations. The block’s protection key is compared to the PSW’s protection key for every memory access by a program. A key mismatch not only prevents the memory access, but also raises a specific interrupt (interrupts and interrupt handling are discussed in Part 8A and Part 8B of the ITMC). The IPL sets the key of the RAM block to which the nucleus is loaded to ‘0’ while bits 8-11 in the relevant PSW are also set to ‘0’. A RAM block’s key can be set by executing the SET STORAGE KEY command, which is a privileged command that can be executed only when the PSW’s bit 15 (problem state bit) is set to ‘0’ (supervisor mode).
The nucleus’ code than proceeds to set up the system:
- Collect information about available resources and peripherals and set the OS’s parameters accordingly.
- Copy (from disk/tape) the interrupt handling routines to RAM.
- Copy “supervisor call” handling routines to RAM (will be discussed next).
- Copy service routines (that will be used by programs) to RAM.
When the OS is running, it is basically interrupt driven. Input from the user is handled by the relevant interrupt handling subroutines (discussed in Part 8B of the ITMC), which can in turn call other subroutines to initiate different functionalities. The most basic usage of the OS is loading and executing a program from a given input device (punched-card reader/disk/tape). Job (a job might contain several programs) loading and execution is done by the following steps:
- Read job data from selected input device.
- Mark a data block as used (data block usage is managed by OS subroutines), assign it with a protection key and copy the program’s to it.
- Run the assembler to generate the program’s object code (copy the object code to another block if needed).
- Execute a LOAD PSW instruction to begin executing the program’s code (bits 8-11 are set with the key, bit 15 is set to ‘1’, and bits 40-63 point to the first instruction in the code).
Once the program’s code begins executing, the program is in control of the system, but with limited privileges. The program cannot access other RAM memory blocks and cannot execute privileged instructions, like LOAD PSW, SET STORAGE or any instruction that handles device I/O. Basically, an unprivileged program is free to make changes to data and code that reside in memory blocks that match the program’s protection key (that sits in the PSW during execution). If an unprivileged program needs to perform device I/O, it can do so by “asking” the supervisor program by triggering a software interrupt – this is achieved by executing a CALL SUPERVISOR instruction. System/360’s handles 5 different interrupt classes with handlers that are set by writing their addresses to permanently assigned locations in low RAM addresses:
Memory addresses 0-24 are used by the IPL and were discusses earlier in this post. Addresses 24-56 store the PSWs that should be loaded on each interruption respectively, while addresses 88-120 hold the saved PSW from the moment interruption occurred. When a supervisor call interruption occurs, the system automatically saves the interrupting program’s PSW to address 32, and loads the new PSW from address 96 to the CPU’s circuitry. This way, code the interrupt handler’s code will begin executing with supervisor privileges (since the supervisor call PSW will have bit 15 set to ‘0’), and it’s up to the OS programmer to read the interruption code from bits 16-31 of the old PSW and branch to the subroutine that can service the request coming from the unprivileged program.
It is now completely up to the OS developers to handle the request coming from the program. Naturally, there are standards set by the OS developers that the programmers follow. One of these standards is the calling convention that’s agreed upon when a program makes a supervisor call. In case a program wants access to more memory, it must must pass a size parameter (size of extra memory access requested) to the OS, and the OS in turn must return the address of the allocated memory back to the program (or return some kind of error value which is agreed upon as an error value). The DOS (IBM’s Disk Operating System, which is a slightly less powerful version of OS/360) calling convention makes use of specific registers:
It’s again the OS developers job to make sure the SVC (supervisor call) subroutines perform parameter checking, and return and error value to the requesting program if the parameters are invalid. If proper checks aren’t made by the supervisor, rogue parameters might cause the SVC subroutine (which is running with supervisor permissions) to corrupt data or crash the system, rendering all the hardware-enforced protection features useless.
- During system boot, the OS is loaded from the disk/tape to RAM and executes at supervisor privilege level.
- Before a program is executed, the OS assigns unused blocks of memory for the program’s usage.
- To begin program code execution, the OS executes the LOAD PSW instruction.
- The PSW that was loaded by the OS has bit 15 set to ‘1’, meaning that the program’s code will execute as unprivileged code.
- If the program tries to execute a privileged instruction (like an I/O operation instruction), or access a memory block with a key that doesn’t match, it’ll trigger a program exception interrupt which will be handled by the supervisor (which will probably terminate the program that caused the interruption).
- If a program needs to perform a privileged action, it must ask the supervisor to do it by setting up the requested parameters, and making a supervisor call.
This model of privilege and memory access control is implemented in a very similar way in modern operating systems running on modern hardware. Take the following C language code for example:
A program that was compiled while containing this line of code will at some point have to print a string to the selected output device, i.e it will have to perform a privileged operation. If this program is ran on a x64 CPU (which uses 64-bit words) under Windows 10 OS, the user program eventually executes the SYSCALL instruction (which is the supervisor call equivalent) to ask the OS to print the string:
Before the SYSCALL instruction is called, the relevant parameters are set in memory. Here are two of them:
At address 0xaa33b7bdd48 there are 8 bytes representing the address of the string to be printed, followed by 4 bytes representing the length of the string. The 8 byte address might look strange because it is displayed exactly the way it’s stored in memory – in little-endian form (which is a common way to store data in desktop computers). If we go to that address we’ll find our 0xf long character string:
In modern operating systems like Linux and Windows, the different privilege level execution states are dubbed “Kernel mode” (supervisor), and “User mode” (unprivileged). While the OS’s privilege control model remained basically similar to what it was half a decade ago, memory management and protection by the OS changed significantly, along with the addition of the virtual memory model, which will be discussed in the next parts of the series.
Hope you found this post informative. Feel free to leave comments, and ask questions.