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:
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.#defineSEG_DESCTYPE(x) ((x) <<0x04) // Descriptor type (0 for system, 1 for code/data)#defineSEG_PRES(x) ((x) <<0x07) // Present#defineSEG_SAVL(x) ((x) <<0x0C) // Available for system use#defineSEG_LONG(x) ((x) <<0x0D) // Long mode#defineSEG_SIZE(x) ((x) <<0x0E) // Size (0 for 16-bit, 1 for 32)#defineSEG_GRAN(x) ((x) <<0x0F) // Granularity (0 for 1B - 1MB, 1 for 4KB - 4GB)#defineSEG_PRIV(x) (((x) &0x03) <<0x05) // Set privilege level (0 - 3)#defineSEG_DATA_RD0x00 // Read-Only#defineSEG_DATA_RDA0x01 // Read-Only, accessed#defineSEG_DATA_RDWR0x02 // Read/Write#defineSEG_DATA_RDWRA0x03 // Read/Write, accessed#defineSEG_DATA_RDEXPD0x04 // Read-Only, expand-down#defineSEG_DATA_RDEXPDA0x05 // Read-Only, expand-down, accessed#defineSEG_DATA_RDWREXPD0x06 // Read/Write, expand-down#defineSEG_DATA_RDWREXPDA0x07 // Read/Write, expand-down, accessed#defineSEG_CODE_EX0x08 // Execute-Only#defineSEG_CODE_EXA0x09 // Execute-Only, accessed#defineSEG_CODE_EXRD0x0A // Execute/Read#defineSEG_CODE_EXRDA0x0B // Execute/Read, accessed#defineSEG_CODE_EXC0x0C // Execute-Only, conforming#defineSEG_CODE_EXCA0x0D // Execute-Only, conforming, accessed#defineSEG_CODE_EXRDC0x0E // Execute/Read, conforming#defineSEG_CODE_EXRDCA0x0F // Execute/Read, conforming, accessed#defineGDT_CODE_PL0SEG_DESCTYPE(1) |SEG_PRES(1) |SEG_SAVL(0) | \SEG_LONG(0) |SEG_SIZE(1) |SEG_GRAN(1) | \SEG_PRIV(0) | SEG_CODE_EXRD#defineGDT_DATA_PL0SEG_DESCTYPE(1) |SEG_PRES(1) |SEG_SAVL(0) | \SEG_LONG(0) |SEG_SIZE(1) |SEG_GRAN(1) | \SEG_PRIV(0) | SEG_DATA_RDWR#defineGDT_CODE_PL3SEG_DESCTYPE(1) |SEG_PRES(1) |SEG_SAVL(0) | \SEG_LONG(0) |SEG_SIZE(1) |SEG_GRAN(1) | \SEG_PRIV(3) | SEG_CODE_EXRD#defineGDT_DATA_PL3SEG_DESCTYPE(1) |SEG_PRES(1) |SEG_SAVL(0) | \SEG_LONG(0) |SEG_SIZE(1) |SEG_GRAN(1) | \SEG_PRIV(3) | SEG_DATA_RDWRvoidcreate_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:0printf("0x%.16llX\n", descriptor);}intmain(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));return0;}
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.
The assembly example provided in the used two entries. The GDT is loaded using the lgdt instruction to load the gdt_dsecriptor label. Since you're familiar with assembly, you can see that the gdt_descriptor label is simply calculating the start and end of the gdt with all entries, including the first null entry.