A Different Memory Model for Every Day of the Week
The original 8086 had used 16-bit pointers. This meant that it could access 2^16 bytes of RAM, or 64KB. Obviously, this wasn't enough for many things, and so it employed a segmented architecture. Four segment registers are present in x86 chips: the code, data, stack and extra segment registers (CS, DS, SS, and ES, respectively).
Now, because each segment register is 16 bits wide, this obviously lets you access 4GB of RAM; 2^16 segments of 2^16 bytes gives 2^32. Right? Well, no, actually. The 8086 could only address 1MB of RAM.
Now, at this point, you are probably wondering how, exactly, you could design a system like this. More astute readers will be wondering "why?" The reason is simple; they wanted segments to be able to overlap. When you specify a memory address for an 8086 instruction, the relevant segment register is left-shifted by four (giving a 20-bit number) which is then added to the specified address. The first 3 bits of the final address are dependent on the value of the segment register, the last 4 on the specified address, and the remaining 13 on both.
This mechanism allows a single physical address to have multiple logical addresses (quite a lot, actually, around 8,000). The following table shows some examples:
Segment Register: 200 206 300 Logical Address : 396 300 396 Physical Address: 3600 3596 5196
MS-DOS users may remember that some programs had a .com suffix, instead of the more conventional .exe. These programs did not ever modify the segment registers, and so could be placed anywhere in memory. Device drivers were often written using this mechanism, because it meant that they could run concurrently with other programs, being invoked as the result of an interrupt.
One megabyte of RAM sounded like a lot back in 1981, when a lot of computers still only had 64KB. It soon started to sound small, however. To get around this, the Expanded Memory Specification (EMS) was created. DOS applications were allowed to use 640KB of memory, leaving 384KB for the operating system and memory-mapped I/O. EMS allowed an application to use some of this memory, typically 64KB. It would have to request it from the operating system, however, which allowed some tricks to be performed. In a machine with an EMS board containing extra RAM, some extra hardware sitting on the address bus would map a different 64KB into the top bit of the address space on request. Applications could use all of the extra physical memory, but they had to use it 64KB at a time.
The 80286 didn't improve matters greatly. Twenty-four bit physical addressing was added, although this was still a segmented mode, where 16-bit pointers were taken as relative to a 24-bit segment. The 80286 booted in a fully 8086 compatible mode, known as "real mode." Unfortunately, the designers neglected to include a method of swapping back from protected to real mode, which meant that a system in protected mode could not run legacy code. For this reason, protected mode was rarely used. There were two ways later discovered to switch back into real mode:
- One involved going via the keyboard controller (which was very slow).
- The other involved setting up an invalid interrupt vector and raising an interrupt (which was very ugly).
The 80386 improved things slightly, but began to suffer from feature creep. The segmented addressing mechanism was extended to 32-bit segments and 32-bit offsets (still only giving 32-bits of address space), but paging was added. They fixed real mode, so you could switch back to it from protected mode, but also added a virtual 8086 mode, allowing 8086 operating systems to be virtualized.
Paging is quite useful. It's a nice way of implementing virtual memory, because every page is the same size, so you can easily re-order them or put them on disk. Segments are also nice sometimes; you can do a few nice tricks with them. Using segments, you can cleanly isolate segments of memory—for example, to do hardware bounds-checking on arrays, or giving each object in a system its own segment to stop it touching others. Most of the time, however, they are ignored. One group of people in Cambridge had just started using them for a little piece of software called Xen. They used the segmented addressing to protect the hypervisor. Minix 3 also makes use of segmented memory. Of course, now that someone is actually using a feature, it is declared obsolete, and AMD removed it from x86-64.
Now we've gone from 16-bit to 64-bit, but we missed a little detour along the way. Starting with the Pentium Pro, Intel introduced the Physical Address Extension (PAE) mode. This gave a 36-bit addressing mode. Somewhat unusually, this was done via the paging system, rather than once again modifying the segmentation system.
PAE provides a few things, such as 2MB or even 4MB pages. For systems where segments are large, this can be beneficial, because more of the page table can be kept in cache. This is most apparent when moving to a 36-bit address space, where PAE requires an extra level of indirection for 4MB pages, but not for 2MB.
You may have read in recent years about the NX bit being added to x86 chips. You have always been able to set memory as non-executable in a protected mode x86 chip, but this used to only be at the segment level. If you look at the memory layout diagrams for OpenBSD, you will see quite how much effort this is to use for everyday applications (such as implementing the mprotect function specified by POSIX). The newer chips allow read-but-not-execute permission to be granted on a page granularity. This is slightly more expensive to test, but it is easier to use. x86-64 chips get rid of the segmented protections entirely, so you need to use page level protections for everything while in 64-bit mode. This extra cost is reduced on AMD chips by passing it off to the on-die memory controller.