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 */
We will have to flush the previous GDT, as the segment registers are still the same in the CPU. We can do this by: