Intel Segmented Memory

When the Intel 8086 and 286 were designed, it was widely believed that a two-dimensional, "segmented" memory space would lead to more reliable code. The basic idea of memory segmentation is that instead of a virtual address being simply a number, it should be expressed as an object identifier and an offset within the object. For instance, you might have an object for an array; by placing a limit on the size of the object, you could get automatic array bounds checking.

They had "segment registers", which were used to define the segments currently in use. So the next instruction to be executed wasn't just the instruction being pointed to by IP, it was the pair CS:IP. Likewise, there was a stack segment register SS, a data segment register DS, and an extra segment register that was used for a few data movement operations called ES.

So... if you wanted to do a jump, you had two forms: a "near jump", which was a jump within the current code segment, and a "far jump", which would jump to another segment. Incidentally, this caused almost unbelievable problems with trying to write libraries. If you called a library function with a "far call", you needed to perform a "far return" to get back to your original segment. If you called it with a "near call", you had to do a "near return". And which one you did wasn't on the stack, so anything that might ever be called from outside the segment had to have a far call... a mess.

When the 8086 was designed, there wasn't enough room on a chip to do real segmentation. Consequently, they used a bizarre pseudo-segmentation in which a physical address was simply (segment number)*16 + (offset). With the 286, they had enough room to do real segmentation. A segment selector now actually indexed a segment table, to do logical/physical translations.

Here's the format of a segment selector:

The "table indicator" bit distinguished two segment descriptor tables: the local descriptor table and the global descriptor table. The idea here is that each process was to have its own LDT, while they would all share the GDT. So the segments in the LDT defined the process, while the GDT entries would typically be for the kernel (of course, you could put library code in there, too).

The index determines which entry in the descriptor table you're going to look up.

Finally, the RPL field defines what privilege level you want to run at. Each segment has a privilege level; the value of the RPL in the CS segment defines which privilege level you are running at (0 is system 3 is user).

The segment descriptor, as its name implies, describes the segment: where it is in physical memory, how large it is, what type it is (code, data, and many, many more with many possibilities on whether you can read, write, or execute the segment. There are eight types of "application" segments and eight types of "system" segment).

Among code segments, we have "conforming" and "non-conforming" segments. If you try to make a call into a "conforming" segment, you are allowed to do it, but the privilege level you actually run at is unchanged. The idea here is that trusted code like a math library might be a conforming segment with a DPL of 0; if you call a routine in the library as a user you keep executing at the user privilege level of 3. But the OS can also use the same math library, and if it does so, it happens at a privilege level of 0. If you try to call into a "non-conforming" segment that's at a higher privilege level than your own an exception occurs.

Might as well talk about system segments here, while we're at it. Just as there are eight types of application segment, there are eight types of system segment. Our text mentions a couple of them: a task state segment and a local descriptor table descriptor.

What I regard as the most interesting of the system segment descriptors are the various gates....


Last modified: Fri Aug 26 09:23:41 MDT 2005