Before understanding the GDT, let's first dive into history of memory addressing.
Earlier system architecture used to have the flat memory model, where memory is treated as a single contiguous address space, which makes it difficult to protect regions and support multi-tasking environments.
With the introduction of 8086
processor, which is 16-bit
processor however able to address 1 MB
of address space with the use of segmentation. In Segmented memory model, memory is divided into different segments, with the use of segmented memory model 8086 able to address 1 MB
of memory with 16 Bit registers. The concept of segmentation is a memory management technique, however this technique lacks the features of memory protection, access-level privileges. Any process can access any memory location.
Then comes the Intel 80286
processor which further expanded the concept of segmentation by introducing protected mode, which enabled hardware-level support for memory protection, privilege levels, and more sophisticated multitasking. The GDT
is a core component in this mode, enabling these features by defining the characteristics of different memory segments.
In protected mode:
- We can address 4GB of memory.
- Memory addresses are specified using a flat model, without segmentation.
- We can specify which memory areas a process can write to, and detect when some process writes to non-allowed areas. This is the main reason why this mode is called protected mode and it is achieved with the use of GDT.
- Kernel code run with a higher privilege level than other processes, thus doing things which a normal are not allowed to do.
In real mode memory is shared between all processes running and there was no protection. One can easily access the area of memory allocated to other process and could overwrite it causing the system to crash. With protected mode if a process tries to write to memory which is not assigned to it, the processor will detect this and notify the kernel. The kernel can then terminate the culprit process. It is all achieved at hardware level with the use of GDT.
Memory Addressing in Protected Mode
Similar to real mode, protected mode uses the segments, but in a different way. Instead of accessing a segment:offset pair directly, like we normally do in real mode, in protected mode the processor is loaded with a table of descriptors (GDT
) pointed by the special register. These descriptors contains the information about the particular memory region or area, its characteristics like base address, range, privileges.
When the processor accesses memory, it uses the segment selector to find the corresponding segment descriptor in the GDT. The segment descriptor provides the base address, which is added to the offset specified in the instruction to obtain the linear address.
Example:
- Logical Address:
0x08:0x0040
(where0x08
is the segment selector and0x0040
is the offset) - Segment Descriptor: Located in the GDT, defines the segment with a base address (e.g.,
0x00010000
) and a limit. - Linear Address:
0x00010040
(base address + offset)
Thus if a process tries to write to a memory address not included in the selector's memory range. Since the processor know which memory belongs to the selector and which memory doesn’t, it intervenes and lets the kernel know that there was a memory access violation, using a mechanism known as an interrupt. The kernel takes action against the culprit process.
What is GDT?
It is a data structure or simply an array, specific to x86
which holds the segment descriptors that describe the different memory areas or regions and their characteristics. These memory areas are called “segments
”. These segments include code segments, data segments, stack segments, and more. Characteristics of memory segments are like; the base address, the size and access privileges like execute and write.
The GDT is a table of 8-byte entries. Each entry is individual descriptor.
Structure of the GDT
The GDT consists of multiple entries, each 8 bytes (64 bits) in size. Each entry, or descriptor, describes a single segment. Here's the structure of a GDT entry:
GDT Entry Layout
Bits | Field |
---|---|
63 - 56 | Base Address (31:24) |
55 - 52 | Granularity |
51 - 48 | Segment Limit (19:16) |
47 - 40 | Access Byte |
39 - 32 | Base Address (23:16) |
31 - 16 | Base Address (15:0) |
15 - 0 | Segment Limit (15:0) |
Breakdown of Access and Granularity Bytes:
Byte | Bits | Description |
---|---|---|
Access | 7 | Present bit (1 = segment is present) |
6 - 5 | Descriptor Privilege Level (DPL), defines privilege level (0-3) | |
4 | Descriptor type (0 = system, 1 = code/data) | |
3 | Executable bit (1 = code segment, 0 = data segment) | |
2 | Direction/Conforming bit (data segment: direction; code segment: conforming) | |
1 | Readable/Writable bit (code segment: readable; data segment: writable) | |
0 | Accessed bit (set by the CPU when the segment is accessed) | |
Granularity | 7 | Granularity bit (0 = limit in bytes, 1 = limit in 4 KB blocks) |
6 | Size bit (0 = 16-bit protected mode, 1 = 32-bit protected mode) | |
5 | Long mode bit (for 64-bit segments) | |
4 | Available for system software (always 0 for standard GDT entries) | |
3 - 0 | Upper 4 bits of segment limit |
Example of GDT Entries:
Entry Number | Base Address | Limit | Access Byte | Granularity Byte | Description |
---|---|---|---|---|---|
0 | 0x00000000 | 0x0000 | 0x00 | 0x00 | Null segment |
1 | 0x00000000 | 0xFFFFF | 0x9A | 0xCF | Code segment |
2 | 0x00000000 | 0xFFFFF | 0x92 | 0xCF | Data segment |
GDT Structure in C:
struct gdt_entry {
uint16_t limit_low; // Segment Limit (15:0)
uint16_t base_low; // Base Address (15:0)
uint8_t base_middle; // Base Address (23:16)
uint8_t access; // Access flags
uint8_t granularity; // Granularity flags and Segment Limit (19:16)
uint8_t base_high; // Base Address (31:24)
} __attribute__((packed));
Fields of a GDT Entry:
- Limit (20 bits): Specifies the size of the segment. The limit is divided into two parts:
limit_low
(16 bits): The lower part of the segment limit.- The higher 4 bits are stored in the
granularity
byte.
- Base (32 bits): Specifies the starting address of the segment. The base is divided into three parts:
base_low
(16 bits): The lower part of the base address.base_middle
(8 bits): The middle part of the base address.base_high
(8 bits): The higher part of the base address.
- Access Byte: Defines the segment type and access privileges, including whether the segment is present in memory, its privilege level, and whether it is a code or data segment.
- Granularity Byte: Contains the granularity flag and the upper part of the segment limit. The granularity flag determines whether the segment limit is in 1-byte units or 4KB units.
GDT Entries:
The gdt entries are defined using the array of structures which are of type descriptor i.e gdt_entry
in above section.
// GDT entries array.
struct gdt_entry gdt[3];
gdt
is an array here of typegdt_entry
which can accommodate3
entries.
This structure is stored somewhere in the memory and it contains the descriptors. So we need a way to tell to the processor about these entries. Because processor will read these entries while accessing the memory and do protect the memory according to the descriptors. In order to tell to processor, the processor has a special register for this purpose which stores the address of this gdt table and its size. That register name is GDTR
(Global Descriptor Table Register).
However, we need a structure to store the address of this gdt table and its size. Then we would store that structure into the GDTR
. The structure we would need be needing is GDT Pointer
.
GDT Pointer
The GDT pointer is a special structure that contains the base address and the limit (size) of the Global Descriptor Table (GDT). It is of 48
bits in size. This structure is used by the lgdt
(Load Global Descriptor Table) instruction to load the GDT into the CPU.
Structure of the GDT Pointer
The GDT pointer structure typically looks like this:
struct gdt_ptr {
uint16_t limit; // Size of the GDT - 1
uint32_t base; // Base address of the GDT
} __attribute__((packed));
Field | Size (bits) | Description |
---|---|---|
Limit | 16 | Size of the GDT in bytes - 1 |
Base | 32 | 32-bit address of the GDT |
- Limit: The size of the GDT in bytes minus one. This value is used because the
lgdt
instruction expects the size of the table in this format. - Base: The 32-bit linear base address of the GDT in memory.