Overview
An x86 CPU operates in real mode, protected mode, virtual 8086 mode, system management mode, and so on.
An x86 CPU can only enter real mode at startup. Once it has switched to protected mode, it can never return to real mode.
Real Mode and Protected Mode
Put simply, real mode is the mode in which the 8086 uses the CPU. At the time, the term “real mode” did not yet exist; it was only after protected mode came along that the old mode acquired the name “real mode”.
A brief introduction to real mode:
- 16-bit registers
- 20-bit address bus, able to access 1MB of memory
- A 20-bit physical address is formed by left-shifting the CS/DS register by 4 bits and adding the IP register
Two issues here deserve attention:
- Theoretically, CS<<4 + IP can represent a maximum value of 0xFFFF0 + 0xFFFF = 0x10FFEF, i.e., approximately 1MB + 64KB – 16 Bytes. However, because there are only 20 address lines (A0~A19), the leading 1 in this address cannot be expressed. When CS = 0xFFFF, the actual access to address 0x10FFEF becomes 0xFFEF. That is, the address wraps around to the range from address 0 up to 64KB – 16 Bytes.
- Because a program can arbitrarily modify the current values of CS/DS, every program can use the entire 1MB of memory. As a result, the CPU has almost no way to effectively support multitasking, because when two programs run together they easily trample on each other’s memory. The usage pattern at the time was therefore to have only one application and the DOS operating system running in the system at the same time. The operating system and the application each specified the range of memory addresses they were allowed to use—for example, DOS used only the upper 64KB of memory, leaving the rest for the application. This way they would not interfere with each other. To run another application, the currently running application had to exit first.
The second issue above is in fact the source of the word “protected” in “protected mode”. The original intent of protected mode was to protect one process’s memory from being illegally accessed by another process.
The first issue above is mentioned here because it required special handling in the design of the 80286.
The 80286’s Protected Mode
Real mode, i.e., actual-address mode, means that the address formed by combining CS and IP in the user’s code is the actual physical memory address. This approach is very insecure and inconvenient for implementing multitasking systems. Solving this problem was the primary goal when designing the 80286.
In the 80286, segmented memory access was improved: the address originally obtained from segment register + IP is no longer the actual physical address, but is converted into a physical address through a translation layer. The value in CS is no longer the upper 16 bits of a 20-bit physical address; it has become an index into the segment descriptor table.

As shown in the figure:
The original 16-bit segment register was renamed the segment selector. Now, how does this work, and how does it control the range of memory a program can access?
Answer:
When the DOS system launches a user application, it first sets up two tables in memory: one called the Global Descriptor Table (GDT) and one called the Local Descriptor Table (LDT). Each entry in a table actually represents a region of memory, containing that region’s starting address and length, i.e., base and limit. The multiple entries of a table represent the multiple regions of memory that this user program can access.
The DOS system writes the base address of these two tables in memory into two registers, LDTR and GDTR. It also sets an initial index in the CS and DS registers.
When an application accesses memory, the CS:IP combination is called a logical address. The CPU uses bits 3 through 15 of CS as an index to find a descriptor in the LDT or GDT, then uses that descriptor’s base + IP as the actual physical memory address for the access. Only the DOS operating system can modify the LDT and GDT, so an application can only access the memory range that DOS has set up for it, and cannot arbitrarily access all of physical memory.
When an application runs out of memory, it must invoke a system call to have DOS allocate a region of memory, then take that region’s base and limit and create a descriptor for it to add to the GDT or LDT.
If an application carelessly modifies CS such that it can no longer index to a valid segment descriptor, a “segmentation fault” is raised.

As shown in the figure, in the 80286 the meaning of the CS/DS/ES/FS registers has changed. Bits 3 through 15 are the index into the descriptor table. TI indicates which table the index refers to: the GDT or the LDT. RPL is called the Requested Privilege Level. RPL corresponds to the DPL in the descriptor; access to this memory is permitted only when RPL is at a higher level than (numerically less than) DPL.
The DPL in the descriptor is shown in the figure below:


Multitasking on the 80286
With the aid of segment descriptor tables, the system can allocate memory to applications and restrict users’ access to memory. At this point, the multitasking approach is summarized in the figure below:

Based on this memory-management approach, user applications can implement dynamic linking. For example, a program may be divided into a code segment, data segment, zero-initialized segment, etc., and the libraries it depends on are likewise segmented. When the system loads the program, it only needs to allocate a region of memory for each segment and set up a descriptor for it.
The starting address of each segment can be modified at load time according to the actual situation.
Setting a code segment to read-only only requires setting the TYPE field of its descriptor.

A20 Gate and 16MB Memory
The 80286’s registers are still 16 bits wide, but its memory address bus is 24 bits wide, allowing it to access 16MB of memory. In protected mode, because the memory-access method has changed, accessing 16MB of memory only requires that the base field in the descriptor be up to 24 bits wide.
However, in real-address mode the memory-access method is CS<<4 + IP, which can express at most the address 0x10FFEF, i.e., 1MB + 64KB – 16 Bytes. This access method has no advantage whatsoever, but for compatibility with old programs, CPUs from the 80286 all the way up to the latest x86 architecture still support real mode at startup, allowing access only to the 1MB + 64KB – 16 Bytes memory region.
For the 8086, since there are only address lines A0~A19, it cannot access memory above 1MB. The 80286, however, has 24 address lines, so the leading 1 in 0x10FFEF can be expressed on the A20 address line, thereby accessing the memory between 1MB and 1MB + 64KB – 16 Bytes.
Whether to express this 1 on A20 was a very natural idea, and Intel did exactly that. However, this turned out to be a bug.
On the 8086, address accesses above 1MB wrap around to address 0. This is an accidental phenomenon, never intended as a feature, but more like an ill-considered bug. However, in actual program development many programs written for the 8086 took advantage of this property to access the low 64KB of memory starting at address 0. On the 80286, programs that relied on this bug stopped working. So this became a bug of the 80286. One can only sigh.
To make the 80286 behave like the 8086 in real mode, IBM came up with a workaround: add an AND gate on the A20 line. The other input of the AND gate is connected to the keyboard-controller chip 8042, and a new combination of keyboard-control keys was added to the 8042 to control the final output on the A20 line:

This AND gate is called the A20 Gate.
In today’s CPUs, the 8042 has been integrated into the southbridge, and the A20 gate has been made into a standard feature called Fast A20 Gate. Control of A20 has become a read/write of bit 1 on port 0x92. The code to open the A20 Gate is as follows:
openA20:
in al, 92h
or al, 00000010b
out 92h, al80286 Descriptor Tables
The 80286 has three types of descriptor tables: the Global Descriptor Table (GDT), the Local Descriptor Table (LDT), and the Interrupt Descriptor Table (IDT).
In the entire system, there is only one GDT and one IDT, but there can be multiple LDTs—one per task. When a task switch occurs, the LDT switches as well: the value in LDTR is modified to the base address of the LDT corresponding to the current task. During a task switch, the LDT switches but the GDT does not. The LDT allows each task’s private segments to be isolated from other tasks, achieving the goal of protection. The GDT allows segments shared by all tasks to be shared. Sharing memory among multiple tasks through the GDT is very convenient.
Each descriptor table itself forms a special data segment. Such a special data segment can contain at most 8K (8192) descriptors.
Descriptor Table Registers
A descriptor table can be placed anywhere in memory, but the CPU must know where it is. Therefore each descriptor table has a corresponding register, namely:
GDTR
LDTR
IDTR
A Detailed Look at the LDT
The LDTR register specifies the LDT used by the current task.
The LDTR is similar to a segment register, consisting of a 16-bit portion visible to the programmer and a hidden cache register invisible to the programmer. In practice, each task’s LDT, being a special system segment, is described by a descriptor. The descriptor that describes the LDT is stored in the GDT.
During initialization or task switching, the selector of the descriptor that describes the current task’s LDT is loaded into the LDTR. Based on the selector loaded into the visible portion of the LDTR, the processor fetches the corresponding descriptor from the GDT and stores the LDT’s base address, limit, attributes, and other information in the LDTR’s hidden cache register.
Subsequent accesses to the LDT can then have their validity checked based on the information held in the cache register.
The LDTR register holds the selector for the current task’s LDT. Therefore, the selector loaded into the LDTR must designate a system-segment descriptor of type LDT located in the GDT—that is, the TI bit in the selector must be 0, and the type field in the descriptor must indicate type LDT.
A null selector may be loaded into the LDTR, indicating that the current task has no LDT. In this case, every selector loaded into a segment register must point to a descriptor in the GDT; that is, all segments involved in the current task are described by descriptors in the GDT.
Loading a selector with TI = 1 into a segment register will cause an exception.
A Detailed Look at the IDT
The IDT is essentially not a segment descriptor table but an interrupt vector table.
The IDTR register points to the IDT.
The IDTR is 48 bits long: the 32-bit base address specifies the base of the IDT, and the 16-bit limit specifies the segment limit of the IDT.
Because the 80386 supports only 256 interrupts/exceptions, the maximum length of the IDT is 2K, with a segment limit of 7FFH in bytes. The IDTR indicates the IDT in the same way that the GDTR indicates the GDT.
Task State Segment (TSS)
Every process (task) in the system has descriptive information about the process that needs to be saved; on x86, this data is saved in a dedicated segment. Each task has such a segment, and these segments’ descriptors are all stored in the GDT with type TSS. The selector of the code-segment descriptor is a register such as CS or ES, and the selector of the data-segment descriptor is the DS register. The selector of the TSS descriptor likewise has a dedicated register called TR. A process can at any time use TR to obtain the information about itself stored in its Task State Segment.
Segment Selectors
On the 8086, CS/DS/ES/SS/GS/FS are 16-bit registers used to hold the upper 16 bits of a segment’s address.
In the 80286, they represent indices into the segment descriptor table. On a memory access, the corresponding descriptor is first looked up in the segment descriptor table; its base and limit are retrieved, and the base is added to IP to obtain the physical memory address. The segment descriptor table itself resides in memory, so every memory access requires fetching a segment descriptor from memory—turning what was a single memory access into two, which is inefficient. Therefore, when actually designing the chip, the registers CS/DS/… are each paired with hidden, inaccessible registers that cache the descriptor corresponding to that segment selector. This way, as long as no segment switch occurs, the descriptor does not need to be re-fetched from memory on every access.

CR0 Mode Switching
Bit 0 of the control register CR0, labeled PE, controls the operation of the segmentation mechanism, and is therefore called the protection enable bit.
PE controls the segmentation mechanism.
PE = 0: the processor runs in real mode;
PE = 1: the processor runs in protected mode.
Switching from real mode to protected mode only requires setting bit 0 of CR0 to 1. The x86 does not support returning from protected mode to real mode.
The code to switch to protected mode is as follows:
; switch to protection mode
switch_proMode:
mov eax, cr0
or eax, 1 ; set CR0's PE bit
mov cr0, eax
Comments