Chapter 6: GDT

Thanks to GRUB, your kernel is no longer in real-mode, but already in protected mode, this mode allows us to use all the possibilities of the microprocessor such as virtual memory management, paging and safe multi-tasking.

What is the GDT?

The GDT (“Global Descriptor Table”) is a data structure used to define the different memory areas: the base address, the size and access privileges like execute and write. These memory areas are called “segments”.

We are going to use the GDT to define different memory segments:

  • “code”: kernel code, used to stored the executable binary code
  • “data”: kernel data
  • “stack”: kernel stack, used to stored the call stack during kernel execution
  • “ucode”: user code, used to stored the executable binary code for user program
  • “udata”: user program data
  • “ustack”: user stack, used to stored the call stack during execution in userland

How to load our GDT?

GRUB initializes a GDT but this GDT is does not correspond to our kernel.
The GDT is loaded using the LGDT assembly instruction. It expects the location of a GDT description structure:

GDTR

And the C structure:

  1. struct gdtr {
  2. u16 limite;
  3. u32 base;
  4. } __attribute__ ((packed));

Caution: the directive __attribute__ ((packed)) signal to gcc that the structure should use as little memory as possible. Without this directive, gcc include some bytes to optimize the memory alignment and the access during execution.

Now we need to define our GDT table and then load it using LGDT. The GDT table can be stored wherever we want in memory, its address should just be signaled to the process using the GDTR registry.

The GDT table is composed of segments with the following structure:

GDTR

And the C structure:

  1. struct gdtdesc {
  2. u16 lim0_15;
  3. u16 base0_15;
  4. u8 base16_23;
  5. u8 acces;
  6. u8 lim16_19:4;
  7. u8 other:4;
  8. u8 base24_31;
  9. } __attribute__ ((packed));

How to define our GDT table?

We need now to define our GDT in memory and finally load it using the GDTR registry.

We are going to store our GDT at the address:

  1. #define GDTBASE 0x00000800

The function init_gdt_desc in x86.cc initialize a gdt segment descriptor.

  1. void init_gdt_desc(u32 base, u32 limite, u8 acces, u8 other, struct gdtdesc *desc)
  2. {
  3. desc->lim0_15 = (limite & 0xffff);
  4. desc->base0_15 = (base & 0xffff);
  5. desc->base16_23 = (base & 0xff0000) >> 16;
  6. desc->acces = acces;
  7. desc->lim16_19 = (limite & 0xf0000) >> 16;
  8. desc->other = (other & 0xf);
  9. desc->base24_31 = (base & 0xff000000) >> 24;
  10. return;
  11. }

And the function init_gdt initialize the GDT, some parts of the below function will be explained later and are used for multitasking.

  1. void init_gdt(void)
  2. {
  3. default_tss.debug_flag = 0x00;
  4. default_tss.io_map = 0x00;
  5. default_tss.esp0 = 0x1FFF0;
  6. default_tss.ss0 = 0x18;
  7. /* initialize gdt segments */
  8. init_gdt_desc(0x0, 0x0, 0x0, 0x0, &kgdt[0]);
  9. init_gdt_desc(0x0, 0xFFFFF, 0x9B, 0x0D, &kgdt[1]); /* code */
  10. init_gdt_desc(0x0, 0xFFFFF, 0x93, 0x0D, &kgdt[2]); /* data */
  11. init_gdt_desc(0x0, 0x0, 0x97, 0x0D, &kgdt[3]); /* stack */
  12. init_gdt_desc(0x0, 0xFFFFF, 0xFF, 0x0D, &kgdt[4]); /* ucode */
  13. init_gdt_desc(0x0, 0xFFFFF, 0xF3, 0x0D, &kgdt[5]); /* udata */
  14. init_gdt_desc(0x0, 0x0, 0xF7, 0x0D, &kgdt[6]); /* ustack */
  15. init_gdt_desc((u32) & default_tss, 0x67, 0xE9, 0x00, &kgdt[7]); /* descripteur de tss */
  16. /* initialize the gdtr structure */
  17. kgdtr.limite = GDTSIZE * 8;
  18. kgdtr.base = GDTBASE;
  19. /* copy the gdtr to its memory area */
  20. memcpy((char *) kgdtr.base, (char *) kgdt, kgdtr.limite);
  21. /* load the gdtr registry */
  22. asm("lgdtl (kgdtr)");
  23. /* initiliaz the segments */
  24. asm(" movw $0x10, %ax \n \
  25. movw %ax, %ds \n \
  26. movw %ax, %es \n \
  27. movw %ax, %fs \n \
  28. movw %ax, %gs \n \
  29. ljmp $0x08, $next \n \
  30. next: \n");
  31. }