Skip to content

x86_64 GDT

The Global Descriptor Table is a data structure in the x86_64 and x86 architectures. It is used for defining memory segments and their attributes. The Interrupt Descriptor Table (IDT) is a very similar structure and implementations of both tables can be very similar.

The GDT should be among, if not the first thing ever initialized. Also note that any protected/long mode bootloader initializes a minimal working GDT for you, since the GDT is required to go from 16 bit to protected or long mode.

Each entry is either 8 or 16 bytes long, also holding a segment descriptor that defines the properties of the segment.

The GDT in protected mode

In protected mode, the GDT defines the following:

  • Code segments
  • Data segments
  • System segments, such as the Task State Segment (TSS). Used for multitasking (in a now obsolete way) and defining specific stacks for the kernel and interrupt routines.

The GDT in long mode

This is the GDT implementation this page will be covering.

In long mode, segmentation is disabled and paging reigns as king. All segment bases and limits are zero. The only thing that you actually need to do in long mode in the GDT is define the segment permissions and initialize the TSS.

Creating the structure

First of all, we need a structure. It will be our GDT. Here's a table to illustrate the entries we will need:

Segment Mode Access Granularity Description
Null 32/64-bit 0x00 0x00 Required null descriptor
Kernel Code 32-bit 0x9A 0xCF Ring 0 code segment
Kernel Data 32-bit 0x92 0xCF Ring 0 data segment
User Code 32-bit 0xFA 0xCF Ring 3 code segment
User Data 32-bit 0xF2 0xCF Ring 3 data segment
Kernel Code 64-bit 0x9A 0x20 Ring 0 code segment (base/limit ignored)
Kernel Data 64-bit 0x92 0x00 Ring 0 data segment (base/limit ignored)
User Code 64-bit 0xFA 0x20 Ring 3 code segment
User Data 64-bit 0xF2 0x00 Ring 3 data segment
TSS 32/64-bit 0x89 0x00 Task State Segment

Now, this is how to implement the actual structures. Be sure to pack the structures, as the compiler could break the structure completely without it.

/* The register */
typedef struct
{
  uint16_t limit; /* This will be the size of our GDT */
  uint64_t base; /* This will be the address */
} __attribute__ ((packed)) gdt_register;

/* An entry in the GDT */
/* limit_low, base_low, base_mid and base_high don't actually matter in long mode -
 * they should be zero. What actually matters is granularity and access.
 */
typedef struct
{
  uint16_t limit_low;
  uint16_t base_low;
  uint8_t base_mid;
  uint8_t access;
  uint8_t granularity;
  uint8_t base_high;
} __attribute__ ((packed)) gdt_entry;

/* We will have to create another structure with this, since the gdt_register             
 * structure is packed (and for a good reason!).
 */
gdt_register gdt_reg;
/* 7 should be enough for every entry and TSS */
gdt_entry gdt[7];

Filling the entries

Now, we will have to fill the entries with the data we want.

/* This should be called like this:
*  gdt_fill_entry (1, 0x9A, 0x20, 0, 0);
*/
void gdt_fill_entry (int num, uint8_t access, uint8_t granularity, uint32_t base, uint32_t limit)
{
  gdt[num].limit_low = limit & 0xFFFF;
  gdt[num].base_low = base & 0xFFFF;
  gdt[num].base_mid = (base >> 16) & 0xFF;
  gdt[num].access = access;
  gdt[num].granularity = ((limit >> 16) & 0x0F) | (granularity & 0xF0);
  gdt[num].base_high = (base >> 24) & 0xFF;
}

void fill_gdt_with_entries ()
{ 
  gdt_fill_entry (0, 0, 0, 0, 0);       /* Null descriptor */
  gdt_fill_entry (1, 0x9A, 0x20, 0, 0); /* Kernel code - 0x9A = Code segment, present, ring 0 */
  gdt_fill_entry (2, 0x92, 0x00, 0, 0); /* Kernel data - 0x92 = Data segment, present, ring 0 */
  gdt_fill_entry (5, 0xFA, 0x20, 0, 0); /* Userspace code - 0xFA = Code segment, present, ring 3 */
  gdt_fill_entry (6, 0xF2, 0x00, 0, 0); /* Userspace data - 0xF2 = Data segment, present, ring 3 */
}

Loading the GDT

You can load the GDT using the lgdt instruction. For example, to load a GDT you can:

/* Fill every entry... */

/* Now, we need to actually load the GDT. */
  gdt_reg.limit = sizeof (gdt) - 1; /* Size of the GDT struct */
  gdt_reg.base = (uint64_t)&gdt; /* The GDT struct address */
  asm volatile ("lgdt %0" ::"m"(gdt_reg)); /* Load the GDT */
So, we have loaded the GDT. Now what?

We will have to flush the previous GDT, as the segment registers are still the same in the CPU. We can do this by:

  asm volatile ("mov $0x10, %%ax\n" /* Reload segment registers */
                "mov %%ax, %%ds\n"
                "mov %%ax, %%es\n"
                "mov %%ax, %%fs\n"
                "mov %%ax, %%gs\n"
                "mov %%ax, %%ss\n"
                "pushq $0x08\n"
                "lea 1f(%%rip), %%rax\n"
                "pushq %%rax\n"
                "lretq\n" /* Far return to reload CS */
                "1:\n"
                :
                :
                : "rax");