Global Descriptor Table

The Global Descriptor table is used to describe the memory segments to the CPU; which, of course, is necessary to switch from 16-bit Real Mode to 32-bit protected mode.

The Global Descriptor Table has the following struture

Each entry is 8 bytes (64 bits) and contains information that describes the segment.

In the example MBR presented in the MBR post, there were two segments defined:

```asm-intel-x86-generic
CODE_SEG equ gdt_code - gdt_start   ;; Code segment (.text)
DATA_SEG equ gdt_data - gdt_start   ;; Data Segment (.data)
```

Typically, additional segments for kernel code and data segments will accompany these.

Proper segmentation is layout is paramount to guarantee memory safety for an OS running in protected mode. As described in the Hardware section of the Memory Management article, the hardware must provide facilities to control memory access because the operating system does not intervene with CPU instructions that access memory.

The image above shows the format of a descriptor table entry. The base and segment limit fields are ignored in 64-bit protected mode. Those values are used to describe the beginning and endpoint of the segment; on 64-bit OSs running in 64-bit mode, the full linear address space of the segment is used, thus allowing those values to be ignored.

OSDev Wiki Example

OSDev Wiki provides a more elegant solution that uses a minimal amount of assembly to construct and populate the GDT. It is advisable that you read the tutorial and the GDT page on OSDev wiki.

// Used for creating GDT segment descriptors in 64-bit integer form.
 
#include <stdio.h>
#include <stdint.h>
 
// Each define here is for a specific flag in the descriptor.
// Refer to the intel documentation for a description of what each one does.
#define SEG_DESCTYPE(x)  ((x) << 0x04) // Descriptor type (0 for system, 1 for code/data)
#define SEG_PRES(x)      ((x) << 0x07) // Present
#define SEG_SAVL(x)      ((x) << 0x0C) // Available for system use
#define SEG_LONG(x)      ((x) << 0x0D) // Long mode
#define SEG_SIZE(x)      ((x) << 0x0E) // Size (0 for 16-bit, 1 for 32)
#define SEG_GRAN(x)      ((x) << 0x0F) // Granularity (0 for 1B - 1MB, 1 for 4KB - 4GB)
#define SEG_PRIV(x)     (((x) &  0x03) << 0x05)   // Set privilege level (0 - 3)
 
#define SEG_DATA_RD        0x00 // Read-Only
#define SEG_DATA_RDA       0x01 // Read-Only, accessed
#define SEG_DATA_RDWR      0x02 // Read/Write
#define SEG_DATA_RDWRA     0x03 // Read/Write, accessed
#define SEG_DATA_RDEXPD    0x04 // Read-Only, expand-down
#define SEG_DATA_RDEXPDA   0x05 // Read-Only, expand-down, accessed
#define SEG_DATA_RDWREXPD  0x06 // Read/Write, expand-down
#define SEG_DATA_RDWREXPDA 0x07 // Read/Write, expand-down, accessed
#define SEG_CODE_EX        0x08 // Execute-Only
#define SEG_CODE_EXA       0x09 // Execute-Only, accessed
#define SEG_CODE_EXRD      0x0A // Execute/Read
#define SEG_CODE_EXRDA     0x0B // Execute/Read, accessed
#define SEG_CODE_EXC       0x0C // Execute-Only, conforming
#define SEG_CODE_EXCA      0x0D // Execute-Only, conforming, accessed
#define SEG_CODE_EXRDC     0x0E // Execute/Read, conforming
#define SEG_CODE_EXRDCA    0x0F // Execute/Read, conforming, accessed
 
#define GDT_CODE_PL0 SEG_DESCTYPE(1) | SEG_PRES(1) | SEG_SAVL(0) | \
                     SEG_LONG(0)     | SEG_SIZE(1) | SEG_GRAN(1) | \
                     SEG_PRIV(0)     | SEG_CODE_EXRD
 
#define GDT_DATA_PL0 SEG_DESCTYPE(1) | SEG_PRES(1) | SEG_SAVL(0) | \
                     SEG_LONG(0)     | SEG_SIZE(1) | SEG_GRAN(1) | \
                     SEG_PRIV(0)     | SEG_DATA_RDWR
 
#define GDT_CODE_PL3 SEG_DESCTYPE(1) | SEG_PRES(1) | SEG_SAVL(0) | \
                     SEG_LONG(0)     | SEG_SIZE(1) | SEG_GRAN(1) | \
                     SEG_PRIV(3)     | SEG_CODE_EXRD
 
#define GDT_DATA_PL3 SEG_DESCTYPE(1) | SEG_PRES(1) | SEG_SAVL(0) | \
                     SEG_LONG(0)     | SEG_SIZE(1) | SEG_GRAN(1) | \
                     SEG_PRIV(3)     | SEG_DATA_RDWR
 
void
create_descriptor(uint32_t base, uint32_t limit, uint16_t flag)
{
    uint64_t descriptor;
 
    // Create the high 32 bit segment
    descriptor  =  limit       & 0x000F0000;         // set limit bits 19:16
    descriptor |= (flag <<  8) & 0x00F0FF00;         // set type, p, dpl, s, g, d/b, l and avl fields
    descriptor |= (base >> 16) & 0x000000FF;         // set base bits 23:16
    descriptor |=  base        & 0xFF000000;         // set base bits 31:24
 
    // Shift by 32 to allow for low part of segment
    descriptor <<= 32;
 
    // Create the low 32 bit segment
    descriptor |= base  << 16;                       // set base bits 15:0
    descriptor |= limit  & 0x0000FFFF;               // set limit bits 15:0
 
    printf("0x%.16llX\n", descriptor);
}
 
int
main(void)
{
    create_descriptor(0, 0, 0);
    create_descriptor(0, 0x000FFFFF, (GDT_CODE_PL0));
    create_descriptor(0, 0x000FFFFF, (GDT_DATA_PL0));
    create_descriptor(0, 0x000FFFFF, (GDT_CODE_PL3));
    create_descriptor(0, 0x000FFFFF, (GDT_DATA_PL3));
 
    return 0;
}

This example elegantly uses C macros that are expanded and preprocessed to dynamically set access permissions.

A few things to note. First, notice that the code segment entries for user (PL0) and kernel (PL3) allow execute and read permission; they do not support write permission. A writable code segment is a major security vulnerability.

Next, you'll notice that kernel code and data segments are given a privilege level of three, while the user equivalents have zero.

The accessed bit is set for each of these descriptor entries. If the accessed bit is not set, the CPU will set it when the segment is accessed, unless it's already set to 1 (here, it is not). If the GDT descriptor is loaded read-only pages, a page fault will be triggered. The recommendation is to set 1. How does this program guard against this condition?

  • The use of macros provides facilities for consistent entry creation; none of the entries in this example are read-only, thus the threat is negligible.

Last updated