Author: Doug, 10+ year veteran of embedded development.

Public number: [IOT town], focusing on: C/C++, Linux operating system, application design, Internet of Things, SCM and embedded development and other fields. Public account reply [books], get Linux, embedded field classic books.

Transfer: welcome to reprint the article, reprint need to indicate the source.

  • From 16 bits to 32 bits
    • 16 bit mode for the 8086
    • 32-bit mode of the 80386
  • Went from solid mode to protected mode
    • How do I enter protected mode
    • GDT global descriptor table
    • GDTR Global descriptor list register
  • The principle of finding segment descriptors

In the last seven articles, we’ve been studying the underlying fundamentals of the original 8086 processor, focusing on the way memory is addressed.

That is, how the CPU addresses memory by segment address.

I don’t know if you found a problem:

All programs can read and modify data at any location in memory, even if the location does not belong to the application.

This is very dangerous, think of all those malicious black hat hackers who, if they want to do something bad, can say at will!

The processor is helpless in the face of such unsafe behavior.

So, starting with the 80286, Intel added a mechanism called protected mode.

PS: Accordingly, the processor execution mode in the previous 8086 was called “real mode.”

Although the 80286 did not take off, it provided the foundation for the later 80386 processors that made the 386 hugely successful.

In this article, we will start with the 80386 processor and talk about it

Who does protected mode protect?

What is the underlying mechanism to implement the protection pattern?

Our learning goal is to understand the following graph:

From 16 bits to 32 bits

16 bit mode for the 8086

In an 8086 processor, all registers are 16 bits.

Because of this, in order to get a 20-bit physical address, the processor needs to move the contents of the segment register four bits to the left, and then add the contents of the offset register to get a 20-bit physical address, and finally access the maximum memory space of 1MB.

For example, when accessing a code segment, move the CS register 4 bits to the left and add the IP register to get a 20-bit physical address.

For a 20-bit address, the maximum addressable range is 2 ^ 20 = 1 MB;

Remember from our first article Linux From Scratch 01: How does a CPU execute an instruction? Register diagram in?

These registers are all 16 bits, and in this mode, access to memory can only be segmented.

The offset address of each segment can only be up to 64 KB (2 ^ 16).

When accessing code snippets, use the CS: IP register;

When accessing the data segment, use the DS register;

When accessing the stack, use the SS: SP register;

32-bit mode of the 80386

After entering the 32-bit processor, these registers are extended to 32 bits:

From the register name, the letter E is added at the top to indicate Extend.

These 32-bit registers, the lower 16 bits, remain compatible with 16-bit processors, meaning that 16-bit registers (e.g. AX) can be used as well as 8-bit registers (e.g. AH, AL).

Note: The high 16 bits cannot be used independently.

Here are four more general purpose registers for a 32-bit processor (note that they cannot be used as 8-bit registers) :

In 32-bit mode, the address line in the processor reaches 32 bits, and the maximum memory space addressable capacity reaches 4 GB(2 ^ 32).

In 32-bit processors, 16-bit processing mode is still compatible, and 16-bit registers are still used.

If not compatible, will lose a lot of market share;

Do you feel something missing from the register diagram above?

Yes, segment registers (CS, DS, SS, etc.) are not shown in the figure.

This is because in 32-bit mode, the segment register is still 16 bits long, but the interpretation of its contents has changed very, very dramatically.

Instead of representing the base address of the segment, they represent an index value and other information.

Use this index value (or index number) to find the actual base address of the segment in a table (somewhat similar to the interrupt direction table lookup) :

Some books call descriptors segment selectors;

In some books, the value in the segment register is called the index value, and the offset of the segment descriptor in the GDT is called the selector.

Don’t bother with the title, just understand the reason;

Because the processor has 32 address lines, the addressable range is already very large (4 GB), so in theory it does not need to address as in the 8086 (segment address shifted 4 bits to the left + offset address).

But because of the x86 processor’s DNA, memory is still accessed in segments in 32-bit mode.

The segment register is only used to describe how to find the base address of a segment.

  1. For 8086, the contents of the segment register are shifted four bits to the left, which is the base address of the segment.

  2. For 80386, the contents in the segment register are the index number of a table. Through this index number, the contents in the corresponding position in the table are searched. This content has the base address of the segment (how to find, described below).

Once the base address of the segment is found, access to memory is still based on the segment mechanism + offset.

Since the registers used to store offset addresses on 32-bit processors are 32-bit and the offset address is up to 4 GB, we can set the base address of the segment to 0x0000_0000:

Such segmentation, called a “flat model,” can also be understood as no segmentation.

In a previous article, we drew a segmented model of the Linux operating system:

Now is it possible to understand why the base addresses and lengths of the four segments are the same?

Went from solid mode to protected mode

How do I enter protected mode

How does the CPU determine whether it is executing real mode? Protected mode?

Inside the processor, there is a register CR0. The value of bit0 in this register determines the current operating mode:

Bit0 = 0: real mode; Bit1 = 1: protected mode;

After the processor is powered on, it works in real mode by default.

When the operating system is ready to go into protected mode, bit0 in the CR0 register is set to 1, and the CPU starts working in protected mode.

That is, until bit0 is set to 1, the CPU addresses in real mode (segment address moved 4 bits to the left + offset address).

When bit0 is set to 1, the CPU addresses in protected mode (looking up the base address of the segment in a table by an index number in the segment register, and then adding the offset address).

GDT global descriptor table

Description (GDT) is a description of the basic information of a segment, including base address, segment length limit, security level, etc.

It is called global because each application can also put segment descriptor information in its own private Local descripper Table (LDT), which will be introduced in future articles.

The handler specifies that the first descriptor must be null, mainly to avoid some programming error.

As can be seen from the figure above, each entry in the GDT is 8 bytes long, which describes the specific information of a segment, as shown below:

Yellow: indicates the base address of this segment in memory.

Green: indicates what the maximum length of this segment is.

When you first saw this picture, did you have two questions in mind?

  1. Why is the base address of the segment not represented by consecutive 32 bits?

  2. How come the segment boundary is 20 bits? 20 bits is a 1 MB range.

The answer to the first question is: historical reasons (compatibility).

The answer to the second question is: the flag bit G in each descriptor provides a further granular description of segment boundaries:

  1. If G = 0: indicates that segment boundaries are expressed in bytes, then the maximum range of segment boundaries is 1 MB.

  2. If G = 1: indicates that the segment limit is expressed in units of 4KB, then the maximum range of segment limit is 4 GB(1 MB by 4KB).

For completeness, I have summarized the meanings of all flag bits as follows for easy reference:

D/B (bit22) : Used to determine whether the offset register used for the data segment or stack segment is 16 or 32 bits.

L (bit21) : used only on 64-bit systems, so ignore it for now.

AVL (bit20) : The processor does not use this bit of content, and the operating system can use this bit to do something.

P (bit15) : indicates whether the contents of this segment already reside in physical memory.

On Linux, each application has 4 GB of virtual memory (32-bit processor), and multiple applications can exist on a system at the same time.

The application’s code segments, data segments, and so on in virtual memory are eventually mapped to physical memory.

However, the space of physical memory is limited after all, when the physical memory is limited, the operating system will temporarily save the contents of the segment that is not currently executed on the hard disk (at this time, the P bit of the segment descriptor is set to 0), which is called swap out.

When the swapped segment needs to be executed, the processor finds that the P bit is 0, and knows that the contents of the segment are not in physical memory, so it finds a free space in the physical memory, and copies the contents of the hard disk to the physical memory, and sets the P bit to 1, which is called swap in.

DPL (bit14-13) : specifies the segment privilege level. The processor supports four privilege levels: 0,1,2,3 (the lowest privilege level).

For example, if the privilege level of an operating system code segment is 0, and the operating system assigns a privilege level of 3 to an application when it is first started, the application cannot be directly transferred to the operating system code segment for execution.

In Linux, only the 0 and 3 privilege levels are used.

S (bit12) : Determines the type of this segment.

TYPE (bit11 ~ 8) : describes some attributes of the segment, such as readability, writability, extension direction, and execution characteristics of the code segment.

The compliance property here is a little trickier to understand, and is primarily used to determine whether code at one lower privilege level can be entered at another higher privilege level.

If so, whether the request level RPL of the current task has changed (this will be discussed later).

In addition, the operating system can incorporate the A flag into the swap out/swap in calculation of physical memory.

In this way, you can avoid swapping out recently accessed physical memory and achieve better system performance.

GDTR Global descriptor list register

There is one more problem to deal with: the GDT table itself is data and also needs to be stored in memory.

So: where is it stored in memory? How does the CPU know where to start?

Inside the processor, there is a single Register, GDTR (GDT Register), which stores two pieces of information:

We can learn from the last article Linux 07: [Interruption] So important, what is the nature of it? Interrupt the analogy during the installation of the scale:

  1. The program code puts the address of each interrupt handler in the corresponding position in the interrupt direction table.

  2. The start address of the interrupt vector table is placed at address 0 of memory.

In other words, the processor looks for the interrupt vector table at a fixed address, 0, which is a fixed address.

In the case of GDT tables, the starting address is not fixed, but can be placed anywhere in memory.

By storing this location in register GDTR, the processor can use GDTR to locate the starting address of the GDT when needed.

In fact, the GDT cannot be placed anywhere in memory at the beginning of power-on.

Since the processor is still working in real mode and can only address 1 MB of memory space before entering protected mode, the GDT can only be placed in 1 MB of address space.

After entering protected mode and addressing a larger address space, you can replace the GDT in a larger address space and store the new start address in the GDTR register.

As you can see from the contents of the GDTR register, it not only stores the starting address of the GDT, but also limits the length of the GDT.

The total length is 16 bits, the maximum is 64 KB(2 ^ 16), and the segment descriptor information is 8 B, so 64 KB of space can hold a maximum of 8192 descriptors.

This is more than enough for an operating system or a general application.

The principle of finding segment descriptors

In the segment register diagram above, we only show that the segment register is still 16 bits.

In protected mode, the interpretation of the content is quite different from that in real mode.

Let’s take the code segment register CS as an example:

RPL: represents the request privilege level of the currently executing code segment;

TI: Indicates which table to look for the description of this segment: global descriptor table (GDT) or local descriptor table (LDT)?

When TI = 0, find the segment descriptor in GDT; When TI = 1, find the segment descriptor in LDT;

Assuming the value of the current code segment register CS is 0x0008, the processor interprets its contents according to the mechanism of protection mode:

  1. TI = 0, which means to find the segment descriptor in GDT;

  2. RPL = 0, indicating that the request privilege level is 0.

  3. The descriptor index is 1, indicating that the segment descriptor is in the first entry in the GDT. Since each descriptor takes up 8 bytes, the descriptor starts at an offset of 8 in the GDT (1 * 8 = 8);

Once you have found the segment descriptor entry, you can get details about the code segment from it:

  1. Where in memory is the base address of the code segment;

  2. What is the maximum length of the code segment (if the offset address exceeds this length, an exception is thrown when fetching instructions);

  3. What is the privileged level of the code segment, whether it currently resides in physical memory, and so on;

In addition, as you can see from the GDTR storage described above, it limits the GDT to a maximum of 8192 descriptors.

From the 13 bits occupied by the descriptor index field in the code segment register, a maximum of 8192 segment descriptors can be found.

2 to the 13th is 8192.

At this point, the processor has found all the information for a segment in protected mode.

The next step is to go to the segment and execute the code or read and write the data.

Next article we continue…





This article describes the 80386 processor in the protected mode, the use of the segment register, and through the segment descriptor to find the specific information of the segment.

From the content of the description, we are getting closer and closer to our ultimate goal: execution in Linux!

Because these basic knowledge, is the Linux operating system on the basis of operation.

Once you understand the basics, you’ll be able to look back at the underlying support at the processor level when you learn about Linux’s specific modules.

Finally, if this article is of any help to you, please forward it to technical partners around you, which is also the biggest encouragement and motivation for me to continue to output this article!

Let us set out together, toward the goal to continue to stride forward!

Recommended reading

[1] C language pointer – from the underlying principle to the tricks, with graphics and code to help you explain thoroughly [2] step by step analysis – how to use C to achieve object-oriented programming [3] The original GDB underlying debugging principle is so simple [4] inline assembly is terrible? Finish this article and end it!

Other series albums: selected articles, C language, Linux operating system, application design, Internet of Things