Loading content...
While the Global Descriptor Table provides segments available to all tasks in the system, the x86 architecture also supports Local Descriptor Tables (LDTs)—per-process descriptor tables that can define segments unique to individual tasks. This mechanism was part of Intel's original vision for segmented memory management, where each process could have its own memory segments with independent addresses, limits, and protections.
The LDT represents an elegant solution to a fundamental operating system challenge: how to give each process its own view of memory while maintaining protection between processes. Though rarely used in modern systems (paging has effectively replaced this role), understanding the LDT provides crucial insights into x86 architecture history and remains relevant for legacy code compatibility.
By the end of this page, you will understand the purpose and design of Local Descriptor Tables, the relationship between the LDT and GDT, how the LDTR register and LDT selectors work, the process of creating and loading LDTs, historical use cases that motivated the LDT's design, and why modern operating systems rarely use LDTs.
The Local Descriptor Table was designed to provide per-process isolation through segmentation. Intel's original vision for protected mode imagined a world where:
The Two-Table Architecture:
The x86 protected mode architecture provides two descriptor tables:
Global Descriptor Table (GDT): Contains segments accessible by all processes—typically kernel segments, TSS, and shared system segments.
Local Descriptor Table (LDT): Contains segments specific to a single process—user code, data, and any process-specific memory regions.
This separation creates a natural boundary: system-wide resources in the GDT, process-private resources in the LDT.
Conceptual Benefits of the LDT Model:
Isolation by Design: Each process literally cannot see other processes' segments—they don't exist in the process's LDT.
Simplified Relocation: With per-process segment bases, the same logical address in different processes can map to different physical locations.
Fine-Grained Protection: Different segments can have different permissions (read-only code, read-write data, etc.).
Clear Separation: System resources (GDT) are clearly distinguished from process resources (LDT).
The TI Bit Revealed:
Remember the segment selector's Table Indicator (TI) bit? This is where it comes into play:
This single bit determines which descriptor table the processor consults for a given selector.
LDT selectors have bit 2 set to 1. For example, selector 0x0F (binary: 0000 0000 0000 1111) has TI=1 and indexes entry 1 of the LDT with RPL=3. Compare to selector 0x0B (binary: 0000 0000 0000 1011) which also indexes entry 1 but from the GDT. This bit pattern is crucial for understanding which table a selector references.
Here's a crucial architectural point: the LDT itself is defined by a descriptor in the GDT. The LDT doesn't float in memory independently—it must be described by an LDT system segment descriptor (type 0x2) in the GDT.
This creates a hierarchy:
12345678910111213141516171819202122232425
LDT Descriptor in GDT (8 bytes for 32-bit mode):┌───────────────────────────────────────────────────────────────┐│ Byte 7 │ Byte 6 │ Byte 5 │ Byte 4 ││ Base[31:24] │ G 0 0 AVL │ Access │ Base[23:16] ││ │ Limit[19:16] │ Byte │ │├──────────────┴───────────────┴──────────────┴─────────────────┤│ Bytes 3-2 │ Bytes 1-0 ││ Base Address [15:0] │ Segment Limit [15:0] │└───────────────────────────────────────────────────────────────┘ Access Byte for LDT Descriptor:┌────────┬───────┬───────┬────────────────────────────────────┐│ Bit 7 │ 6-5 │ Bit 4 │ Bits 3-0 ││ P │ DPL │ S │ Type ││ 1 │ 00 │ 0 │ 0010 (0x2 = LDT) │└────────┴───────┴───────┴────────────────────────────────────┘ Result: Access byte = 0b10000010 = 0x82 Example LDT Descriptor: Base = 0x00200000 (LDT at 2MB mark) Limit = 0x0007 (8 bytes × 1 = 8 bytes, so 1 entry) Or with page granularity: Limit = 0x0001, G=1 (8 bytes × 4096 = 8KB = 1024 entries)The LDTR Register:
The Local Descriptor Table Register (LDTR) is a 16-bit register that holds a selector pointing to the LDT descriptor in the GDT. When the processor needs to resolve a selector with TI=1, it:
LLDT and SLDT Instructions:
Resolving an LDT selector requires more work than a GDT selector: the processor must first look up the LDT descriptor in the GDT, then look up the segment descriptor in the LDT. This double indirection adds latency. However, the processor caches descriptor information in hidden parts of segment registers, so this overhead only occurs on segment register loads, not on every memory access.
Creating an LDT for a process involves several steps:
Allocate Memory for the LDT: Determine how many entries the process needs and allocate contiguous memory.
Populate LDT Entries: Create segment descriptors for the process's code, data, stack, and other segments.
Create LDT Descriptor in GDT: Add an LDT system segment descriptor to the GDT pointing to the LDT.
Load LDTR: Use the LLDT instruction to load the selector for the LDT descriptor.
Example Implementation:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
/* LDT Management for Per-Process Segments */ #include <stdint.h>#include <string.h> #define LDT_ENTRIES 8 /* Number of segments per process */#define MAX_PROCESSES 64 /* Maximum concurrent processes */ /* Segment descriptor structure (same as GDT entries) */struct segment_descriptor { uint16_t limit_low; uint16_t base_low; uint8_t base_middle; uint8_t access; uint8_t granularity; uint8_t base_high;} __attribute__((packed)); /* Per-process LDT */struct process_ldt { struct segment_descriptor entries[LDT_ENTRIES];} __attribute__((packed)); /* All process LDTs */static struct process_ldt process_ldts[MAX_PROCESSES]; /* GDT slot indices for LDT descriptors (one per process) */#define GDT_LDT_BASE_INDEX 10 /* LDTs start at GDT index 10 */ /* External GDT (defined elsewhere) */extern struct segment_descriptor gdt[]; /* Set a segment descriptor */void set_segment_descriptor(struct segment_descriptor *desc, uint32_t base, uint32_t limit, uint8_t access, uint8_t flags) { desc->limit_low = limit & 0xFFFF; desc->base_low = base & 0xFFFF; desc->base_middle = (base >> 16) & 0xFF; desc->access = access; desc->granularity = ((limit >> 16) & 0x0F) | (flags & 0xF0); desc->base_high = (base >> 24) & 0xFF;} /* Initialize LDT for a new process */void ldt_init_process(int pid, uint32_t code_base, uint32_t code_limit, uint32_t data_base, uint32_t data_limit, uint32_t stack_base, uint32_t stack_limit) { struct process_ldt *ldt = &process_ldts[pid]; uint32_t ldt_base = (uint32_t)ldt; uint16_t ldt_limit = sizeof(struct process_ldt) - 1; /* Clear the LDT */ memset(ldt, 0, sizeof(struct process_ldt)); /* Entry 0: Null descriptor (required) */ set_segment_descriptor(&ldt->entries[0], 0, 0, 0, 0); /* Entry 1: Process Code Segment (selector 0x0F = index 1, TI=1, RPL=3) */ set_segment_descriptor(&ldt->entries[1], code_base, code_limit, 0xFA, /* P=1, DPL=3, S=1, E=1, RW=1 */ 0xCF); /* G=1, D=1 */ /* Entry 2: Process Data Segment (selector 0x17 = index 2, TI=1, RPL=3) */ set_segment_descriptor(&ldt->entries[2], data_base, data_limit, 0xF2, /* P=1, DPL=3, S=1, E=0, RW=1 */ 0xCF); /* G=1, D=1 */ /* Entry 3: Process Stack Segment (selector 0x1F = index 3, TI=1, RPL=3) */ set_segment_descriptor(&ldt->entries[3], stack_base, stack_limit, 0xF2, /* P=1, DPL=3, S=1, E=0, RW=1 */ 0xCF); /* G=1, D=1 */ /* Create LDT descriptor in GDT */ int gdt_index = GDT_LDT_BASE_INDEX + pid; set_segment_descriptor(&gdt[gdt_index], ldt_base, ldt_limit, 0x82, /* P=1, DPL=0, S=0, Type=LDT */ 0x00); /* G=0, no flags needed */} /* Switch to a process's LDT (called during context switch) */void ldt_switch(int pid) { /* Calculate selector for this process's LDT descriptor in GDT */ uint16_t ldt_selector = ((GDT_LDT_BASE_INDEX + pid) << 3) | 0x00; /* Bits 15-3: index, Bit 2: TI=0 (GDT), Bits 1-0: RPL=0 */ __asm__ volatile("lldt %0" : : "r"(ldt_selector));} /* Clear a process's LDT (process termination) */void ldt_destroy_process(int pid) { /* Clear the GDT LDT descriptor */ int gdt_index = GDT_LDT_BASE_INDEX + pid; set_segment_descriptor(&gdt[gdt_index], 0, 0, 0, 0); /* Clear the LDT memory */ memset(&process_ldts[pid], 0, sizeof(struct process_ldt));}Context Switch Considerations:
When switching between processes that use LDTs:
Performance Impact:
Loading a new LDT (LLDT instruction) is relatively expensive because:
The LDT was extensively used in operating systems of the late 1980s and early 1990s, before paging became the dominant memory management mechanism. Understanding these historical use cases illuminates why the LDT exists.
OS/2: The LDT Showcase
IBM's OS/2 operating system was perhaps the most significant user of LDTs. OS/2 used segmentation extensively for memory management:
Windows 3.x (Enhanced Mode)
Microsoft's Windows 3.0 and 3.1 in 386 Enhanced Mode used LDTs:
| Operating System | LDT Usage | Memory Model |
|---|---|---|
| OS/2 1.x-2.x | Extensive per-process LDTs | Full segmentation model |
| Windows 3.x (Enhanced) | Per-application LDTs | Segmented with paging |
| Windows 95/98 | Limited LDT for Win16 apps | Flat model + legacy support |
| Windows NT 3.x | Minimal LDT usage | Flat model primary |
| Linux (early) | Per-process LDTs available | Flat model primary |
| Xenix/SCO Unix | LDTs for process isolation | Mixed segmentation/paging |
The Wine Project and LDTs:
The Wine compatibility layer, which runs Windows applications on Linux/Unix, makes interesting use of LDTs. When running 16-bit Windows (Win16) applications, Wine must provide segment selectors that the application expects. Linux provides the modify_ldt() system call specifically for this purpose, allowing Wine to create and manage LDT entries for Win16 compatibility.
Embedded Systems and Specialized Uses:
Some embedded and real-time operating systems used LDTs to create strictly isolated memory domains:
The DOS Protected Mode Interface (DPMI) specification standardized how DOS extenders and protected mode DOS programs could allocate LDT entries. Programs could request new LDT selectors from the DPMI host, enabling protected mode DOS applications (like games and professional software) to use more than 640KB of memory while maintaining protection.
Despite the elegant architecture of per-process LDTs, modern operating systems almost universally use a flat memory model with paging for memory management. The LDT has fallen into disuse for several compelling reasons:
The Flat Model Triumph:
The flat memory model—where all segments have base=0 and limit=4GB (or the maximum address space in 64-bit mode)—combined with paging provides all the protection benefits of segmentation with none of the complexity:
Process Isolation: Each process has its own page tables; it cannot see (or even address) another process's pages.
Fine-Grained Protection: Individual 4KB pages can be marked read-only, no-execute, supervisor-only, etc.
Simple Programming Model: Applications use linear pointers without segment management.
Demand Paging: Pages can be loaded on demand, not entire segments.
Copy-on-Write: Shared pages can be efficiently duplicated when written.
Modern LDT Usage (Rare but Existent):
LDTs still appear in a few contexts:
The modify_ldt() system call in Linux has been associated with security vulnerabilities. Allowing user-space programs to create LDT entries can be exploited by attackers if descriptor validation is imperfect. Some security-focused kernels (like grsecurity) restrict or disable modify_ldt() for this reason. The general trend is toward reducing LDT exposure, not increasing it.
With the transition to 64-bit long mode, the LDT's role diminished even further. While LDTs remain supported for compatibility, they are even less relevant in the 64-bit world.
64-bit LDT Changes:
LDT Descriptor is 16 bytes: Like the TSS, LDT descriptors in long mode are 16 bytes to accommodate 64-bit base addresses. This means an LDT descriptor occupies two GDT slots.
Segmentation Mostly Disabled: In 64-bit mode, segment bases for CS, DS, ES, SS are fixed at 0 (except FS/GS for TLS).
Limited Utility: With a flat 47-bit (or larger) address space and paging, there's almost no reason to use LDT segments.
Compatibility Mode Support: LDTs remain relevant for running 32-bit applications in compatibility mode, where they might expect segment-based behavior.
1234567891011121314151617181920
64-bit LDT Descriptor (16 bytes, occupies 2 GDT slots): Bytes 0-7 (Standard 8-byte format):┌───────────────────────────────────────────────────────────────┐│ Byte 7 │ Byte 6 │ Byte 5 │ Byte 4 ││ Base[31:24] │ G 0 0 AVL │ P DPL 0 0010 │ Base[23:16] ││ │ Limit[19:16] │ (LDT type) │ │├──────────────┴───────────────┴──────────────┴─────────────────┤│ Bytes 3-2 │ Bytes 1-0 ││ Base Address [15:0] │ Segment Limit [15:0] │└───────────────────────────────────────────────────────────────┘ Bytes 8-15 (64-bit extension):┌───────────────────────────────────────────────────────────────┐│ Bytes 12-15 (Reserved, must be 0) │├───────────────────────────────────────────────────────────────┤│ Bytes 8-11: Base Address [63:32] │└───────────────────────────────────────────────────────────────┘ This allows the LDT to be located anywhere in the 64-bit address space.Modern Operating System Approaches:
Linux (x86-64):
modify_ldt() for compatibilityWindows (x86-64):
FreeBSD, NetBSD, OpenBSD:
When running 32-bit applications in compatibility mode on a 64-bit OS, the application runs in a 32-bit code segment (L=0, D=1). If that application expects to use LDT selectors (common in very old software), the OS must provide LDT support. This is one of the few remaining cases where LDTs matter in modern 64-bit systems.
While the LDT was designed for process isolation, modern systems use various other mechanisms to achieve similar goals. Understanding where LDT fits in this landscape provides perspective on its current role.
| Mechanism | Isolation Level | Granularity | Overhead | Modern Relevance |
|---|---|---|---|---|
| LDT Segments | Per-process segments | Variable (segment size) | Moderate (LLDT cost) | Low (legacy only) |
| Paging (Page Tables) | Per-process address space | 4KB/2MB/1GB pages | Low (CR3 write) | High (primary mechanism) |
| Intel MPK (Memory Protection Keys) | Within-process domains | 4KB pages | Very low (register write) | Medium (increasing) |
| Hardware Virtualization (VT-x) | VM isolation | Complete guest spaces | Higher (VMENTER/VMEXIT) | High (cloud, containers) |
| ARM Memory Domains | Region-based | Variable regions | Low | High (ARM systems) |
| SGX Enclaves | Encrypted memory regions | Enclave pages | Higher (encryption) | Medium (specialized) |
Why Paging Won:
Paging emerged as the dominant isolation mechanism because:
Universal Hardware Support: All modern architectures implement paging; segmentation is x86-specific.
Uniform Granularity: Fixed-size pages simplify memory allocation algorithms.
Better for Virtual Memory: Demand paging and page replacement algorithms work naturally with fixed-size units.
Simpler Software Model: Applications don't need to reason about segments or manage selectors.
Hardware Optimization: Paging is heavily optimized with TLBs, huge pages, and hierarchical structures.
Niche Uses Where Segments Might Excel:
There are theoretical scenarios where segment-based protection could be valuable:
Bounds Checking: Segment limits provide automatic bounds checking. Some research explores re-enabling this for security.
Object-Based Protection: Each object could be a segment, with access rights per segment. This was Intel's original vision.
Capability Systems: Segment selectors resemble capabilities—unforgeable tokens granting access rights.
However, these niches haven't overcome paging's dominance and breadth of support.
We've explored the Local Descriptor Table—the per-process segment repository that was central to Intel's original protected mode vision but has largely been superseded by paging.
What's Next:
Having covered both the Global Descriptor Table and Local Descriptor Table, we'll turn to Segment Selectors—examining in depth how these 16-bit values encode table selection, index, and privilege information to form the bridge between software and the descriptor table architecture.
You now understand the Local Descriptor Table's design, purpose, and historical significance. While LDTs are rarely used in modern practice, this knowledge is essential for understanding legacy code, compatibility layers like Wine, and the complete picture of x86 memory management architecture.