How to Configure GDT and IDT Using C Structs

In x86 systems programming, the Global Descriptor Table (GDT) and Interrupt Descriptor Table (IDT) are essential data structures used by the CPU to manage memory segmentation and hardware or software interrupts. This article details how to define, configure, and initialize both GDT and IDT entries directly in C using byte-aligned structures, demonstrating the precise field layouts required by the processor and the inline assembly needed to load them into CPU registers.

Packing Structures in C

By default, C compilers align structure members to boundaries matching their data types for performance reasons. Because the x86 CPU expects exact, unpadded binary layouts for GDT and IDT entries, you must use compiler-specific attributes—such as GCC's __attribute__((packed))—to eliminate padding bytes.


Configuring the Global Descriptor Table (GDT)

The GDT defines the characteristics of the various memory areas (segments) used during program execution. Each 8-byte entry contains base address pieces, limit pieces, and access flags scattered across specific bit positions.

1. Defining the GDT Structures

#include <stdint.h>

struct gdt_entry {
    uint16_t limit_low;       // Lower 16 bits of segment limit
    uint16_t base_low;        // Lower 16 bits of segment base
    uint8_t  base_middle;     // Middle 8 bits of segment base
    uint8_t  access;          // Access flags (present, privilege, type)
    uint8_t  granularity;     // Granularity and upper 4 bits of limit
    uint8_t  base_high;       // Upper 8 bits of segment base
} __attribute__((packed));

struct gdt_ptr {
    uint16_t limit;           // Table size minus 1
    uint32_t base;            // Address of the first gdt_entry
} __attribute__((packed));

2. Setting GDT Entries

A helper function shifts and masks the values into their correct positions:

#define GDT_ENTRIES 3

struct gdt_entry gdt[GDT_ENTRIES];
struct gdt_ptr gp;

void gdt_set_gate(int num, uint32_t base, uint32_t limit, uint8_t access, uint8_t gran) {
    gdt[num].base_low    = (base & 0xFFFF);
    gdt[num].base_middle = (base >> 16) & 0xFF;
    gdt[num].base_high   = (base >> 24) & 0xFF;

    gdt[num].limit_low   = (limit & 0xFFFF);
    gdt[num].granularity = (limit >> 16) & 0x0F;

    gdt[num].granularity |= gran & 0xF0;
    gdt[num].access      = access;
}

void gdt_init(void) {
    gp.limit = (sizeof(struct gdt_entry) * GDT_ENTRIES) - 1;
    gp.base  = (uint32_t)&gdt;

    // Entry 0: Mandatory Null Descriptor
    gdt_set_gate(0, 0, 0, 0, 0);

    // Entry 1: Kernel Code Segment (Base=0, Limit=4GB, 4KB granularity, 32-bit protected)
    gdt_set_gate(1, 0, 0xFFFFFFFF, 0x9A, 0xCF);

    // Entry 2: Kernel Data Segment (Base=0, Limit=4GB, 4KB granularity, 32-bit protected)
    gdt_set_gate(2, 0, 0xFFFFFFFF, 0x92, 0xCF);

    // Load into CPU GDTR register
    __asm__ volatile("lgdt (%0)" : : "r" (&gp));
}

Configuring the Interrupt Descriptor Table (IDT)

The IDT tells the processor which Interrupt Service Routine (ISR) to execute when an exception, software interrupt, or hardware interrupt occurs. In 32-bit protected mode, each IDT gate is 8 bytes long.

1. Defining the IDT Structures

struct idt_entry {
    uint16_t offset_low;      // Lower 16 bits of the ISR address
    uint16_t selector;        // Kernel segment selector (e.g., Code Segment 0x08)
    uint8_t  zero;            // Always set to 0
    uint8_t  type_attr;       // Type and attributes (P, DPL, Gate Type)
    uint16_t offset_high;     // Upper 16 bits of the ISR address
} __attribute__((packed));

struct idt_ptr {
    uint16_t limit;           // Table size minus 1
    uint32_t base;            // Address of the first idt_entry
} __attribute__((packed));

2. Setting IDT Entries

To configure an interrupt gate, mask the function pointer representing your handler into the upper and lower halves of the descriptor:

#define IDT_ENTRIES 256

struct idt_entry idt[IDT_ENTRIES];
struct idt_ptr ip;

void idt_set_gate(uint8_t num, uint32_t base, uint16_t sel, uint8_t flags) {
    idt[num].offset_low  = base & 0xFFFF;
    idt[num].offset_high = (base >> 16) & 0xFFFF;
    idt[num].selector    = sel;
    idt[num].zero        = 0;
    idt[num].type_attr   = flags;
}

// Example ISR signature
void default_exception_handler(void);

void idt_init(void) {
    ip.limit = (sizeof(struct idt_entry) * IDT_ENTRIES) - 1;
    ip.base  = (uint32_t)&idt;

    // Clear all entries
    for (int i = 0; i < IDT_ENTRIES; i++) {
        idt_set_gate(i, 0, 0, 0);
    }

    // Set a gate (e.g., Interrupt 0 for Divide Error)
    // 0x08 is the Kernel Code Segment selector, 0x8E is a 32-bit Interrupt Gate (Present, Ring 0)
    idt_set_gate(0, (uint32_t)default_exception_handler, 0x08, 0x8E);

    // Load into CPU IDTR register
    __asm__ volatile("lidt (%0)" : : "r" (&ip));
}

Using packed structures alongside bitwise operators guarantees that memory matches the x86 specification. Once the pointer structures are filled, executing lgdt and lidt commits the changes to the processor, establishing basic memory mapping and interrupt delivery.