Series directory

  • introductory
  • The preparatory work
  • BIOS starts in real mode
  • GDT and Protected Mode
  • Exploration of Virtual Memory
  • Load and enter the kernel
  • Display and print
  • The global descriptor table GDT
  • Interrupt handling
  • Virtual memory improvement
  • Implement heap and malloc
  • The first kernel thread
  • Multithreading switch
  • The lock is synchronized with multithreading
  • Implementation of a process
  • Enter user mode
  • A simple file system
  • Load the executable program
  • Implementation of system calls
  • The keyboard driver
  • To run a shell

Extend and overload the GDT

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

GDT has been preliminarily defined and loaded once in the Loader phase, where we only defined the code and data segments of the kernel, because up until now, and for a long time to come, we have been in the kernel space, Run at CPU privilege level 0. However, as an OS, it is ultimately necessary to run and manage user programs, so GDT also needs to add user-mode code and data sections.

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

Create the GDT

Knowledge of GDT, and of segment, is a nasty legacy of the x86 architecture. But Intel, for historical compatibility, has always had to keep this baggage. We also do not have to spend too much thought and brain in this above, as long as in accordance with the document specification, should fill in, should write write, gently take over can be. It’s not a core part of our project.

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

For documentation on GDT, you can refer to it here.

First we need to define the data structure of 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 a 64 bit structure like this:

Base is the segment’s memory address, and limit is the segment’s length. It can be in either 1 or 4KB.

The rest are the flag bits shown in Figure 2. No more writing here, but proofread carefully against the document.

Then we define the GDT table:

static gdt_entry_t gdt_entries[7];

We have seven entries assigned here:

  • The 0th term is reserved;
  • The first is thekernelcode segment;
  • The second is thekerneldata segment;
  • The third is video segment, which is not necessary and can be ignored.
  • The fourth is theusercode segment;
  • The fifth isuserdata segment;
  • The sixth istss;

From the fourth, all user mode needs to be used. The sixth TSS does not need to be explored at present, and we will come back to this part when we enter user mode later.

Then we define a function to set 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 each of the entries in the GDT table to:

  // 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 the differences between kernel and user, there are mainly two points:

  • Privl in the Access Byte: There are two bits, 00 for kernel and 11 for user. This is DBL (Descriptor Privilege Level), which represents the minimum CPU Privilege Level required to access this segment.

  • Limit: Because the user space is limited to under 3GB, itsLimit0xBFFFF, pay attention toFlagsGr (Granularity)The bits are 1, soLimitThe unit is4KB, can be calculated(0xBFFFF + 1) * 4KB = 3GB;

With these two limitations, a CPU in user mode cannot access more than 3GB of kernel space. This is where the segment mechanism comes into play.

Reload the GDT

The new GDT is ready, and the next step is to reload it, with the code in 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 as follows in the C source file:

extern void load_gdt(gdt_ptr_t*);

Parameter to GDT pointer:

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

Loading the GDT table with the instruction LGDT, and then giving each data segment register, pointing to the kernel data segment with an offset of 0x10, because it is the second item in the GDT table.

A FAR JMP instruction, JMP 0x08:. FLUSH, flushing the CS register to point to the kernel code segment. Note that 0x08 is because the kernel code segment is the first item in the GDT table.

OK, the new GDT is now loaded.