How It Works: Virtual Memory
Virtual memory is one of those things that we expect from every operating system, that works, and that we rarely think about (unless we’re operating systems designers). The basic idea of virtual memory is that every process gets its own address space and can’t trample over other programs’ data without explicit consent. Also somewhere in the concept is the idea that some bits of memory might (temporarily) be stored on disk.
The core of any virtual memory implementation is the CPU’s memory management unit (MMU). The reason why MS-DOS, and other operating systems of a similar vintage, didn’t support protected memory was that the hardware on which they were designed to run (in the case of DOS, the Intel 8088 CPU) didn’t contain a memory management unit.
In general terms, an MMU has a very simple function. It performs some permutation on addresses used as operands for load and store instructions. While the 8088 (and the 8086 on which it was based) didn’t have a true MMU, it did have segment-relative addressing. This allowed programs to use 16-bit pointers, which then were added to an address in a segment register. Executables with the .COM extension in DOS didn’t alter this register and only used 16-bit pointers, allowing them to be loaded into any address in memory. Unlike a true protected memory environment, this behavior required the cooperation of the program writer to work correctly.
Protection
The first part of virtual memory is protection. A simple MMU might have two options for mapping load or store instruction operands to real addresses:
- Use the real address.
- Cause the instruction to fail.
With this simple mechanism it becomes possible to run two programs at once and guarantee that a bug in one will not affect the other’s memory. When each process receives some CPU time, the MMU will be configured to allow access to its memory but not that of the other process.
MMUs this simple are not particularly common in general-purpose CPUs, with one notable exception: Modern AMD chips contain a mechanism known as a device exclusion vector (DEV), which works in exactly this way, either permitting or allowing access to memory. Unlike the MMUs described so far, this isn’t for controlling a process’s access to memory, but to lock the regions that a device may issue DMA requests to or from.
Most modern systems permit finer-grained access than simply "allow or deny." Typically, each region can be marked with some combination of read, write, and execute permissions. Secure operating systems only allow some subset of these, preventing pages from being both writable and executable at the same time. This is slightly tricky for just-in-time (JIT) compilers, because they have to mark the memory as writable and then toggle the permission to make it executable once they’ve finished code generation.