Your CPU designer friends are right, and your "emulator" is pretty much how real mode and v8086 protected mode addressing were actually architected. The descriptor parts of segment registers effectively had fixed contents derived from what was loaded into the selector parts, rather than from descriptor tables. Yes, this indeed allowed the 80386 to run real mode programs. (-:
Even if you never wanted to go back to real mode, the 80286 was brain dead because of the way they laid out the bits in selectors. One small change to the layout would have prevented so much pain for programmers.
In protected mode, like in real mode, the 286 used a segmented addressing scheme.
In real mode, an address consists of two 16-bit parts:
The segment and the offset are 16 bits. The mapping from segment:offset to physical address is simple: physical = segment * 16 + offset.In protected mode, an address looks like this:
The mapping to physical address uses a lookup table, indexed by the selector. The table entry, which is called a "descriptor", contains the physical address of the base of the segment. The offset is added to that base to get the physical address. The descriptor also contains information about the segment, such as its length and access permissions.There are two lookup tables available to the program at any given instant. The T bit selects which is used. If T is 0, it uses a table called the Global Descriptor Table (GDT). If T is 1, it uses a table called the Local Descriptor Table (LDT).
The GDT, as the name suggests, is global. There is a single GDT shared by all code on the system. Typically it is used to map the segments containing the operating system code and data, and os protected so that user mode code cannot access it.
Each process has its own LDT.
The RL bits specify the Requested Privilege Level (RPL). The 286 has four privilege levels, 0-3, with lower numbers being higher privilege. To determine if access is allowed, the processor looks at RPL, the provilege level given for the segment in the descriptor (DPL), and the privilege level the processor is currently running at (CPL). The check is max(CPL, RPL) <= DPL. It's OK for user mode code to set RPL to whatever it wants, because it runs at CPL == 3, so RPL has no effect.
Let's look at this from the point of view of a user mode C program running on a protected mode operating system. The address space as seen by the C program, viewed as a 32-bit unsigned value, looks like this:
Note that if you are stepping through your address space, every 64k the address jumps by 0x70001 instead of the more usual 1. This makes pointer arithmetic take several more instructions than it would on a machine with a flat addresses space.To prevent making all programs incur the performance hit from that, the compiler writers gave us several different memory models. If you compiled a program with "small model" [1], the compiler would limit it to a single 64k code segment and a single 64k data segments. It could load the CS and DS segment registers once at launch, and treat thee address space as a 16-bit flat space. In "large model" it would do multiple code and data segments, and incur the overhead of dealing with pointer arithmetic crossing segment boundaries. There were mixed models, that had multiple code segments but only one data segment, or vice versa.
The C compilers for 286 protected mode added new keywords, "near", "far", and "huge" that could modify pointer declarations. If you declared a point "near", it was just an offset within a segment. It had no selector. A "far" pointer had a selector and offset, but the selector would NOT be changed by pointer arithmetic--incrementing past the end of a segment wrapped to the beginning of the segment. A "huge" pointer had a selector and offset, and would include the selector in pointer arithmetic, so that you could increment through to the next segment.
Now throw in needing to link to libraries that might have been compiled using a different model from the model your code is compiled with, and that might be expecting different kinds (near, far, huge) pointers than you are using, and all in all it was a big freaking pain in the ass.
They could have avoided all that pain with a simple change. Instead of addresses in protected mode looking like this:
they could have made them look like this: Then the address space would like like this, from the point of view of a user mode C program looking at addresses as 32-bit unsigned values: If they had done this and also flipped the meaning of the T bit, so 0 means LDT and 1 means GDT, we'd have the address space look like this: A C compiler might still have offered near, far, and huge pointers, but now that would just be an optimization programmers might turn to if they needed the last possible bit of speed or needed to squeeze their code into the smallest possible amount of memory, and were willing to go to the pain of arranging things so pointer arithmetic would never cross a 64k boundary in order to do so. 99% of programmers would be able to simply pick "flat" model and be done with it.Why didn't they do this?
I've heard one theory. The descriptors in the GDT and LDT were 8 bytes long, so the physical address of the descriptor is 8 * selector + base_of_descriptor_table. Note that the selector in the segment register:
is already shifted over 3 (e.g., multiplied by 8), so the descriptor lookup can be done as (segment_register & 0xFFF8) + base_of_descriptor_table. The theory is that they put the selector where they did so they could do it that way instead of having to shift it.My CPU designer friends tell me that this is unlikely. They tell me that when you have an input to some unit (e.g., the selector input to the MMU) that is always going to be shifted a fixed amount, you can build that in essentially for free.
Note also if they had done it this way:
and had gone with T == 0 for LDT, then a protected mode operating system could allocate up to 196592 bytes of consecutive physical memory, and set up consecutive descriptors to point into that memory offset from each other by 16 bytes, and the address space would then look like the first 196592 bytes of real mode address space. That might have allowed writing an emulator that could run many real mode binaries efficiently in protected mode.[1] I might be mixing up the names of the various memory models.