7

Series catalog

Extend and reload GDT

In this article, we will redefine and expand the global descriptor table GDT in the kernel and load it again. The content of this article will also be relatively simple, and it is more about consulting and familiarizing with x86 related manual documents.

We have preliminarily defined and loaded GDT once in the loader stage, where we only defined the code and data segments of the kernel, because so far, and for a long period of time later, we have always been in the kernel space. Privilege level 0 runs. But as an OS, it is ultimately necessary to run and manage user programs, so the user mode code and data sections need to be added to the GDT.

In addition, we also hope to rearrange the previous GDT, after all, it is rather chaotic under assembly, and many data structures are not clear to manage.

Create GDT

GDT and segment is a historical legacy of the x86 architecture, which is very annoying. But for historical compatibility, Intel has to always retain these historical baggages. We don't have to spend too much thought and brains on this, just follow the document specification, fill in all that should be filled in, write all that should be written, and just take it lightly. It is not a core part of our project.

By convention, the code link is given first. The main source file is src/mem/gdt.c .

For GDT documentation, you can refer to here .

First, we need to define the data structure of the GDT entry:

struct gdt_entry {
  uint16 limit_low;
  uint16 base_low;
  uint8  base_middle;
  uint8  access;
  uint8  attributes;
  uint8  base_high;
} __attribute__((packed));
typedef struct gdt_entry gdt_entry_t;

It corresponds to such a 64 bit structure:

Among them, base refers to the memory base address of the segment, and limit is the length, which can have two units of 1 or 4KB.

The rest are some of the flag bits shown in Figure 2, so I don't need to spend much time here, and I still need to proofread the document carefully.

Then we define the GDT table:

static gdt_entry_t gdt_entries[7];

We have allocated 7 entries here:

  • Item 0 is reserved;
  • The first one is kernel of code segment ;
  • The second one is kernel of data segment ;
  • The third is the video segment, this is not necessary and can be ignored;
  • The fourth is user of code segment ;
  • The fifth is user of data segment ;
  • The sixth is tss ;

From the fourth onwards, all user mode needs to be used. The sixth one, tss present, we will come back and take a closer look at this part when we enter the user mode later.

Then we define the function to set the GDT entry:

static void gdt_set_gate(
    int32 num, uint32 base, uint32 limit, uint8 access, uint8 flags) {
  gdt_entries[num].limit_low = (limit & 0xFFFF);
  gdt_entries[num].base_low = (base & 0xFFFF);
  gdt_entries[num].base_middle = (base >> 16) & 0xFF;
  gdt_entries[num].access = access;
  gdt_entries[num].attributes = (limit >> 16) & 0x0F;
  gdt_entries[num].attributes |= ((flags << 4) & 0xF0);
  gdt_entries[num].base_high = (base >> 24) & 0xFF;
}

Just look at the picture above.

Set these entries in the GDT table:

  // kernel code
  gdt_set_gate(1, 0, 0xFFFFF, DESC_P | DESC_DPL_0 | DESC_S_CODE | DESC_TYPE_CODE, FLAG_G_4K | FLAG_D_32);
  // kernel data
  gdt_set_gate(2, 0, 0xFFFFF, DESC_P | DESC_DPL_0 | DESC_S_DATA | DESC_TYPE_DATA, FLAG_G_4K | FLAG_D_32);
  // video: only 8 pages
  gdt_set_gate(3, 0, 7, DESC_P | DESC_DPL_0 | DESC_S_DATA | DESC_TYPE_DATA, FLAG_G_4K | FLAG_D_32);

  // user code
  gdt_set_gate(4, 0, 0xBFFFF, DESC_P | DESC_DPL_3 | DESC_S_CODE | DESC_TYPE_CODE, FLAG_G_4K | FLAG_D_32);
  // user data
  gdt_set_gate(5, 0, 0xBFFFF, DESC_P | DESC_DPL_3 | DESC_S_DATA | DESC_TYPE_DATA, FLAG_G_4K | FLAG_D_32);

Comparing kernel and user , there are two main points:

  • Access Byte in Privl : There are two bits in total. For kernel , it is 00 , and for user it is 11 , which means DPL (Descriptor Privilege Level) , which represents the minimum CPU privilege level required to access this segment.

  • Limit : Because the user space 3GB in the following limits, so it Limit is 0xBFFFF , note Flags the Gr (Granularity) bit is 1, Limit units are 4KB , can be calculated (0xBFFFF + 1) * 4KB = 3GB ;

With these two limitations, when the CPU is in the user mode, it cannot access the kernel space above 3GB, so the segment mechanism is brought into play.

Reload GDT

The new GDT is ready, the next step is to reload it, the code is src/mem/gdt_load.S .

load_gdt:
  mov eax, [esp + 4]
  lgdt [eax]

  mov ax, 0x10
  mov ds, ax
  mov es, ax
  mov fs, ax
  mov ss, ax
  
  mov ax, 0x18
  mov gs, ax

  jmp 0x08:.flush
.flush:
   ret

Where load_gdt is declared in the C source file as follows:

extern void load_gdt(gdt_ptr_t*);

The parameter is the GDT pointer:

struct gdt_ptr {
  uint16 limit;
  uint32 base;
} __attribute__((packed));
typedef struct gdt_ptr gdt_ptr_t;

Load the GDT table with the instruction lgdt , and then give each data segment register, pointing to the kernel data segment, the offset is 0x10 , because it is the second entry in the GDT table.

Then use a far jmp instruction jmp 0x08:.flush to refresh the cs register to point to segment kernel code Note 0x08 because kernel code segment is the first entry in the GDT.

OK, now the new GDT is loaded.


navi
612 声望191 粉丝

naive