GDT Tutorial

From OSDev Wiki
Jump to: navigation, search
Difficulty level
Difficulty 1.png
Beginner

On the IA-32 and x86-64 architectures, and more precisely in Protected Mode or Long Mode, Interrupt Service Routines and a good deal of memory management are controlled through tables of descriptors. Each descriptor stores information about a single object (e.g. a service routine, a task, a chunk of code or data) the CPU might need at some time. If you try, for instance, to load a new value into a Segment Register, the CPU needs to perform safety and access control checks to see whether you're actually entitled to access that specific memory area. Once the checks are performed, useful values (such as the lowest and highest addresses) are cached in invisible CPU registers.

On these architectures, there are three of this type of table: The Global Descriptor Table, the Local Descriptor Table and the Interrupt Descriptor Table (which supplants the Interrupt Vector Table). Each table is defined using their size and linear address to the CPU through the LGDT, LLDT, and LIDT instructions respectively. In almost all use cases, these tables are only placed into memory once, at boot time, and then edited later when needed.

Contents

Survival Glossary

Segment
A logically contiguous chunk of memory with consistent properties (from the CPU's perspective).
Segment Register
A register of your CPU that refers to a segment for a particular purpose (CS, DS, SS, ES) or for general use (FS, GS)
Segment Selector
A reference to a descriptor, which you can load into a segment register; the selector is an offset into a descriptor table pointing to one of its entries. These entries are typically 8 bytes long, therefore bits 3 and up only declare the descriptor table entry offset, while bit 2 specifies if this selector is a GDT or LDT selector (LDT - bit set, GDT - bit cleared), and bits 0 - 1 declare the ring level that needs to correspond to the descriptor table entry's DPL field. If it doesn't, a General Protection Fault occurs; if it does correspond then the CPL level of the selector used is changed accordingly.
Segment Descriptor
An entry in a descriptor table. These are a binary data structure that tells the CPU the attributes of a given segment.

What to Put In a GDT

Basics

For the sake of sanity, you should always have these items stored in your GDT:

  • Entry 0 in a descriptor table, or the Null Descriptor, is never referenced by the processor, and should always contain no data. Certain emulators, like Bochs, will complain about limit exceptions if you do not have one present. Some use this descriptor to store a pointer to the GDT itself (to use with the LGDT instruction). The null descriptor is 8 bytes wide and the pointer is 6 bytes wide so it might just be the perfect place for this.
  • A DPL 0 Code Segment descriptor (for your kernel)
  • A Data Segment descriptor (writing to code segments is not allowed)
  • A Task State Segment segment descriptor (its very useful to have at least one)
  • Room for more segments if you need them (e.g. user-level, LDTs, more TSS, whatever)

Flat / Long Mode Setup

If you do not desire to use Segmentation to separate memory into protected areas, you can get away with using only a few segment descriptors. One reason may be that you desire to only use paging to protect memory. As well, this model is strictly enforced in Long Mode, as the base and limit values are ignored.

In this scenario, the only Segment Descriptors necessary are the Null Descriptor, and one descriptor for each combination of privilege level, segment type, and execution mode desired, as well as system descriptors. Usually this will consist of the one code and one data segment for kernel and user mode, and a Task State Segment.

32-bit
Offset Use Content
0x0000 Null Descriptor Base = 0
Limit = 0x00000000
Access Byte = 0x00
Flags = 0x0
0x0008 Kernel Mode Code Segment Base = 0
Limit = 0xFFFFF
Access Byte = 0x9A
Flags = 0xC
0x0010 Kernel Mode Data Segment Base = 0
Limit = 0xFFFFF
Access Byte = 0x92
Flags = 0xC
0x0018 User Mode Code Segment Base = 0
Limit = 0xFFFFF
Access Byte = 0xFA
Flags = 0xC
0x0020 User Mode Data Segment Base = 0
Limit = 0xFFFFF
Access Byte = 0xF2
Flags = 0xC
0x0028 Task State Segment Base = &TSS
Limit = sizeof(TSS)-1
Access Byte = 0x89
Flags = 0x0
64-bit
Offset Use Content
0x0000 Null Descriptor Base = 0
Limit = 0x00000000
Access Byte = 0x00
Flags = 0x0
0x0008 Kernel Mode Code Segment Base = 0
Limit = 0xFFFFF
Access Byte = 0x9A
Flags = 0xA
0x0010 Kernel Mode Data Segment Base = 0
Limit = 0xFFFFF
Access Byte = 0x92
Flags = 0xC
0x0018 User Mode Code Segment Base = 0
Limit = 0xFFFFF
Access Byte = 0xFA
Flags = 0xA
0x0020 User Mode Data Segment Base = 0
Limit = 0xFFFFF
Access Byte = 0xF2
Flags = 0xC
0x0028 Task State Segment
(64-bit System Segment)
Base = &TSS
Limit = sizeof(TSS)-1
Access Byte = 0x89
Flags = 0x0

Small Kernel Setup

If you desire to separate memory into protected areas of code and data, you will have to set the Base and Limit value of each entry in the table to the desired format.

For example, you may want to have two segments, a 4MiB Code Segment starting at 4MiB, and a 4MiB Data Segment starting at 8MiB, both accessible only to Ring 0. In that case, your GDT may look like this:

Small Kernel
Offset Use Content
0x0000 Null Descriptor Base = 0
Limit = 0x00000000
Access Byte = 0x00
Flags = 0x0
0x0008 Kernel Mode Code Segment Base = 0x00400000
Limit = 0x003FFFFF
Access Byte = 0x9A
Flags = 0xC
0x0010 Kernel Mode Data Segment Base = 0x00800000
Limit = 0x003FFFFF
Access Byte = 0x92
Flags = 0xC
0x0018 Task State Segment Base = &TSS
Limit = sizeof(TSS)-1
Access Byte = 0x89
Flags = 0x0

That means whatever you load at physical address 4 MiB will appear as code at CS:0 and what you load at physical address 8 MiB will appear as data at DS:0.

This specifically is not a recommended design, but shows how to think about using the GDT to define separated segments.

SYSENTER / SYSEXIT

If you are using the Intel SYSENTER / SYSEXIT routines, the GDT must contain four special entries, the first one pointed to by the value in the IA32_SYSENTER_CS Model Specific Register (MSR 0x0174).

For more information, see the sections on SYSENTER and SYSEXIT in Chapter 4.3: Instructions (M-U) of the Intel Software Developer Manual, Volume 2-B.

GDT
Offset Use
Preceding Entries Null Descriptor
Kernel Segments
etc.
IA32_SYSENTER_CS + 0x0000 DPL 0 Code Segment
SYSENTER Code
IA32_SYSENTER_CS + 0x0008 DPL 0 Data Segment
SYSENTER Stack
IA32_SYSENTER_CS + 0x0010 DPL 3 Code Segment
SYSEXIT Code
IA32_SYSENTER_CS + 0x0018 DPL 3 Data Segment
SYSEXIT Stack
Successive Entries Any Other Descriptors

The actual values stored in these segments will depend on your system design.

How to Set Up The GDT

Disable Interrupts

If they're enabled, be absolutely sure to turn them off or you could run into undesired behavior and exceptions. This can be achieved through the CLI assembly instruction.

Filling the Table

The above structure of the GDT doesn't show you how to write entries in the correct format. The actual structure of descriptors is a little messy for backwards compatibility with the 286's GDT. Base address are split across three different fields and you cannot encode any limit you want.

void encodeGdtEntry(uint8_t *target, struct GDT source)
{
    // Check the limit to make sure that it can be encoded
    if (source.limit > 0xFFFFF) {kerror("GDT cannot encode limits larger than 0xFFFFF");}
 
    // Encode the limit
    target[0] = source.limit & 0xFF;
    target[1] = (source.limit >> 8) & 0xFF;
    target[6] = (source.limit >> 16) & 0x0F;
 
    // Encode the base
    target[2] = source.base & 0xFF;
    target[3] = (source.base >> 8) & 0xFF;
    target[4] = (source.base >> 16) & 0xFF;
    target[7] = (source.base >> 24) & 0xFF;
 
    // Encode the access byte
    target[5] = source.access_byte;
 
    // Encode the flags
    target[6] |= (source.flags << 4);
}

In order to fill your table, you will want to use this function once for each entry, with *target pointing to the logical address of the Segment Descriptor and source being a struct of your design containing the necessary information.

You can hard-code values in the GDT rather than converting them at runtime, of course.

Telling the CPU Where the Table Is

Some assembly is required here. While you could use inline assembly, the memory packing expected by the LGDT and LIDT instructions makes it much easier to write a small assembly routine instead. As said above, you'll use LGDT instruction to load the base address and the limit of the GDT. Since the base address should be a linear address, you'll need a bit of tweaking depending of your current MMU setup.

Real Mode

The linear address should here be computed as segment * 16 + offset. GDT and GDT_end are assumed to be symbols in the current data segment.

gdtr DW 0 ; For limit storage
     DD 0 ; For base storage
 
setGdt:
   XOR   EAX, EAX
   MOV   AX, DS
   SHL   EAX, 4
   ADD   EAX, ''GDT''
   MOV   [gdtr + 2], eax
   MOV   EAX, ''GDT_end''
   SUB   EAX, ''GDT''
   MOV   [gdtr], AX
   LGDT  [gdtr]
   RET

Protected Mode, Flat Model

"Flat" meaning the base of your Data Segment is 0 (regardless of whether Paging is enabled). This is the case if your code has just been booted by GRUB, for instance. In the System V ABI, arguments are passed on reverse order in the stack, so a function that can be called as setGdt(limit, base) might look like the following example code.

gdtr DW 0 ; For limit storage
     DD 0 ; For base storage
 
setGdt:
   MOV   AX, [esp + 4]
   MOV   [gdtr], AX
   MOV   EAX, [ESP + 8]
   MOV   [gdtr + 2], EAX
   LGDT  [gdtr]
   RET

Protected Mode, Non-Flat Model

If your data segment has a non-zero base, you'll have to adjust the instructions of the sequence above to include the ability to add the base offset of your data segment, which should be a known value to you. You can pass it in as an argument and call this function as setGdt(limit, base, offset).

gdtr DW 0 ; For limit storage
     DD 0 ; For base storage
 
setGdt:
   MOV   AX, [esp + 4]
   MOV   [gdtr], AX
   MOV   EAX, [ESP + 8]
   ADD   EAX, [ESP + 12]
   MOV   [gdtr + 2], EAX
   LGDT  [gdtr]
   RET

Long Mode

In Long Mode, the length of the Base field is 8 bytes, rather than 4. As well, the System V ABI passes the first two arguments via the RDI and RSI registers. Thus, this example code can be called as setGdt(limit, base). As well, only a flat model is possible in long mode, so no considerations have to be made otherwise.

gdtr DW 0 ; For limit storage
     DQ 0 ; For base storage
 
setGdt:
   MOV   [gdtr], DI
   MOV   [gdtr+2], RSI
   LGDT  [gdtr]
   RET

Reload Segment Registers

Whatever you do with the GDT has no effect on the CPU until you load new Segment Selectors into Segment Registers. For most of these registers, the process is as simple as using MOV instructions, but changing the CS register requires code resembling a jump or call to elsewhere, as this is the only way its value is meant to be changed.

Protected Mode

In this case, reloading CS is as simple as performing a far jump to the required segment, directly after the jump instruction:

reloadSegments:
   ; Reload CS register containing code selector:
   JMP   0x08:.reload_CS ; 0x08 is a stand-in for your code segment
.reload_CS:
   ; Reload data segment registers:
   MOV   AX, 0x10 ; 0x10 is a stand-in for your data segment
   MOV   DS, AX
   MOV   ES, AX
   MOV   FS, AX
   MOV   GS, AX
   MOV   SS, AX
   RET

An explanation of the above code can be found here.

Long Mode

In Long Mode the process of changing CS is not simple as far jumps cannot be used. Using a far return is recommended instead:

reloadSegments:
   ; Reload CS register:
   PUSH 0x08                 ; Push code segment to stack, 0x08 is a stand-in for your code segment
   LEA RAX, [rel .reload_CS] ; Load address of .reload_CS into RAX
   PUSH RAX                  ; Push this value to the stack
   RETFQ                     ; Perform a far return, RETFQ or LRETQ depending on syntax
.reload_CS:
   ; Reload data segment registers
   MOV   AX, 0x10 ; 0x10 is a stand-in for your data segment
   MOV   DS, AX
   MOV   ES, AX
   MOV   FS, AX
   MOV   GS, AX
   MOV   SS, AX
   RET

The LDT

Much like the GDT (global descriptor table), the LDT (local descriptor table) contains descriptors for memory segments description, call gates, etc. The good thing with the LDT is that each task can have its own LDT and that the processor will automatically switch to the right LDT when you use hardware task switching.

Since its content may be different in each task, the LDT is not a suitable place to put system stuff such as TSS or other LDT descriptors: Those are the sole property of the GDT. Since it is meant to change often, the command used for loading an LDT is a bit different from the GDT and IDT loading. Rather than giving directly the LDT's base address and size, those parameters are stored in a descriptor of the GDT (with proper "LDT" type) and the selector of that entry is given.

               GDTR (base + limit)
              +-- GDT ------------+
              |                   |
SELECTOR ---> [LDT descriptor     ]----> LDTR (base + limit)
              |                   |     +-- LDT ------------+
              |                   |     |                   |
             ...                 ...   ...                 ...
              +-------------------+     +-------------------+

Note that with 386+ processors, the paging has made LDT almost obsolete, and there's no longer need for multiple LDT descriptors, so you can almost safely ignore the LDT for OS developing, unless you have by design many different segments to store.

The IDT and why it's needed

As said above, the IDT (Interrupt Descriptor Table) loads much the same way as the GDT and its structure is roughly the same except that it only contains gates and not segments. Each gate gives a full reference to a piece of code (code segment, privilege level and offset to the code in that segment) that is now bound to a number between 0 and 255 (the slot in the IDT).

The IDT will be one of the first things to be enabled in your kernel sequence, so that you can catch hardware exceptions, listen to external events, etc. See Interrupts for more information about the interrupts of X86 family.

Some stuff to make your life easy

Tool for easily creating GDT entries.

// 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;
}

See Also

Articles

Threads

External Links

Personal tools
Namespaces
Variants
Actions
Navigation
About
Toolbox