Loading content...
Every time a program references a variable, calls a function, or pushes data onto its stack, the processor must translate a logical address specified by the program into a physical address that can be sent to the memory bus. In segmented memory architectures, this translation pivots on a critical data structure: the Segment Table Entry (STE).
The segment table entry is more than just a record—it is the contract between the operating system and the hardware that defines:
Understanding segment table entries at a deep level is essential for anyone seeking to comprehend how operating systems enforce memory protection, enable code sharing, and provide the illusion of a large, contiguous address space to each process.
By the end of this page, you will have a complete understanding of segment table entry structure, including base and limit fields, protection bits, presence indicators, and privilege levels. You'll understand how hardware interprets these fields during address translation and what happens when access violations occur.
Before we dive into the structure of individual entries, we must understand the segment table as a whole. A segment table is a per-process data structure maintained by the operating system and interpreted by the hardware's Memory Management Unit (MMU).
Definition:
A segment table is an array (or more complex data structure) where each entry describes one segment of a process's logical address space. The index into this table is the segment number (or selector), and the value at that index is the segment table entry containing all metadata about that segment.
Key Properties:
Per-Process: Each process typically has its own segment table, ensuring memory isolation between processes.
Hardware-Interpreted: The MMU reads segment table entries directly during address translation, making the format hardware-defined.
OS-Managed: The operating system creates, populates, and maintains these tables as processes are created, modified, and terminated.
Variable Size: Different processes may have different numbers of segments, so segment tables can vary in size.
| Segment Number | Segment Table Entry (STE) | Logical Purpose |
|---|---|---|
| 0 | STE₀: Base=0x10000, Limit=0x2000, R/X | Code Segment |
| 1 | STE₁: Base=0x30000, Limit=0x4000, R/W | Data Segment |
| 2 | STE₂: Base=0x80000, Limit=0x1000, R/W | Stack Segment |
| 3 | STE₃: Base=0x50000, Limit=0x800, R | Read-Only Data |
| 4 | STE₄: (Invalid/Not Present) | Unused |
In this example table, a process has five segment slots. Segments 0-3 are valid and describe code, data, stack, and read-only data regions. Segment 4 is marked invalid—any access to it will trigger a segmentation fault.
The Role of the Segment Number:
When a program generates a logical address, that address typically consists of two parts:
Logical Address = (Segment Number, Offset within Segment)
The segment number serves as an index into the segment table. The MMU retrieves the corresponding STE, extracts the base address and limit, validates the offset, applies protection checks, and finally computes the physical address if all checks pass.
Don't confuse segment tables with page tables. While both are used for address translation, segment tables map variable-sized logical segments to physical memory, whereas page tables map fixed-size pages. Segmentation provides a logical view of memory aligned with program structure (code, data, stack), while paging provides uniform physical memory management. Modern systems often combine both approaches.
A segment table entry is a carefully packed data structure containing all the information the hardware needs to translate addresses and enforce protection for a single segment. While the exact format is architecture-specific, the conceptual components are universal.
Core Fields of an STE:
Visualizing the STE Layout:
Consider a simplified 64-bit segment table entry (actual formats vary by architecture):
┌──────────────────────────────────────────────────────────────────┐
│ Segment Table Entry (64 bits) │
├──────────────────────────────────────────────────────────────────┤
│ Bits 63-32: Base Address (32 bits) │
│ Bits 31-12: Limit (20 bits) │
│ Bits 11-8: Type (4 bits: Code/Data, Conforming, Expand-Down) │
│ Bit 7: Present (P) │
│ Bits 6-5: Privilege Level (DPL, 2 bits) │
│ Bit 4: System/User descriptor │
│ Bit 3: Granularity (G) │
│ Bit 2: Default operation size (D/B) │
│ Bit 1: Long mode (L) - for 64-bit │
│ Bit 0: Accessed (A) │
└──────────────────────────────────────────────────────────────────┘
This layout illustrates how efficiently segment metadata is packed into a single machine word or double-word, allowing the MMU to extract all needed information in a single memory read.
The exact bit layout of segment table entries differs by architecture. Intel x86 protected mode uses 8-byte segment descriptors with base split across non-contiguous fields for backward compatibility. ARM and other RISC architectures traditionally favor paging over segmentation. Understanding the conceptual fields prepares you for any architecture.
The base address is the most fundamental field in a segment table entry. It specifies the starting physical address of the segment in memory. Every memory reference within this segment is computed by adding the offset to this base.
Physical Address = Base Address + Offset
Key Characteristics of the Base Field:
Example: Base Address in Action
Suppose a process's data segment has:
0x004000000x00010000 (64KB)When the program accesses offset 0x1234 within this segment:
Logical Address: Data Segment : 0x1234
Base Address: 0x00400000
Physical Address: 0x00400000 + 0x1234 = 0x00401234
The MMU performs this addition in hardware, transparent to the executing program. From the program's perspective, it simply accesses address 0x1234 in its data segment—the physical location is invisible.
Dynamic Relocation via Base Modification:
If the OS needs to move this segment (perhaps due to memory compaction), it can:
0x00400000 to a new location, say 0x008000000x00800000This is the power of segmented addressing: location independence without recompilation.
In Intel x86 protected mode, the 32-bit base address is split across three non-contiguous locations within the 8-byte segment descriptor for backward compatibility with 16-bit protected mode. This design quirk means parsing x86 segment descriptors requires bit manipulation to reassemble the base. Modern 64-bit mode simplifies this by largely ignoring segmentation for user-mode code.
The limit field defines the size of the segment, establishing the boundary beyond which access is forbidden. This is the mechanism by which segmentation provides memory protection—preventing buffer overflows and out-of-bounds accesses at the hardware level.
Protection Rule:
If (Offset > Limit) then TRAP to Operating System
Every memory access within a segment is checked against the limit. If the offset exceeds the permitted range, the CPU generates a segmentation fault (or general protection fault in x86 terminology), transferring control to the OS exception handler.
Interpretation of the Limit Value:
| Aspect | Description |
|---|---|
| Maximum Valid Offset | The limit typically represents the last valid byte offset. For a 64KB segment, limit = 0xFFFF (65535). |
| Granularity Scaling | With granularity flag=1, the limit is scaled (e.g., ×4096), allowing segments up to 4GB with a 20-bit limit field. |
| Expand-Up vs Expand-Down | Data segments can expand up (standard) or down (for stacks), changing how the limit is interpreted. |
| Zero Limit | A limit of 0 means only offset 0 (one byte) is valid, not that the segment is empty. |
The Granularity Flag:
In architectures like x86, the limit field is only 20 bits, which would restrict segments to 1MB maximum. The granularity (G) bit solves this:
Example with Granularity:
Limit field value: 0xFFFFF (20 bits, all ones = 1,048,575)
With G=0: Maximum offset = 1,048,575 bytes ≈ 1MB
With G=1: Maximum offset = (1,048,575 + 1) × 4096 - 1 = 4,294,967,295 bytes = 4GB
Expand-Down Segments for Stacks:
Stack segments present a unique challenge: they grow downward from high addresses to low addresses. An expand-down segment interprets the limit differently:
For example, with limit = 0x7FFFF in a 32-bit segment:
The beauty of limit checking is that it happens in hardware on every memory access—no software overhead. This makes segmentation a powerful tool for catching buffer overflows and array bounds violations at the point of access, rather than after corruption has occurred.
The type field in a segment table entry classifies the segment and defines behavioral attributes. This field determines what operations are valid on the segment and how the CPU interprets accesses to it.
Primary Classification: Code vs. Data
Segments are fundamentally divided into:
Code Segments — Contain executable instructions. The CPU fetches and executes instructions from these segments.
Data Segments — Contain data (variables, stacks, heaps). The CPU reads and writes data here but cannot execute from them (with proper protection).
Type Field Bit Breakdown (x86 Example):
| Bit | Name | Meaning when 0 | Meaning when 1 |
|---|---|---|---|
| Bit 3 | Descriptor Type | Data Segment | Code Segment |
| Bit 2 | Expand-Down (Data) | Expand-Up | Expand-Down (Stack) |
| Bit 1 | Write Enable (Data) | Read-Only | Read/Write |
| Bit 0 | Accessed | Not Accessed | Has Been Accessed |
| Bit | Name | Meaning when 0 | Meaning when 1 |
|---|---|---|---|
| Bit 3 | Descriptor Type | Data Segment | Code Segment |
| Bit 2 | Conforming | Non-Conforming | Conforming |
| Bit 1 | Readable | Execute-Only | Execute/Read |
| Bit 0 | Accessed | Not Accessed | Has Been Accessed |
Special Type Attributes:
Conforming Code Segments:
A conforming code segment can be called from less privileged code without changing the privilege level. This is used for shared library code that should run at the caller's privilege level rather than elevating privileges.
Non-Conforming Code Segments:
A non-conforming code segment requires exact privilege level matching (or use of call gates) for access. This is the typical behavior for kernel code that should only run at kernel privilege.
Readable Code Segments:
While all code segments are executable, not all are readable. An execute-only segment prevents instructions from reading their own code—useful for protecting proprietary algorithms or preventing certain forms of code analysis.
Writable Data Segments:
Data segments may be read-only or read-write. Constants and string literals would typically reside in read-only data segments, while variables and the stack need read-write access.
The combination of type field and protection bits enables powerful security policies at the hardware level. Code segments that are not readable can't be dumped for reverse engineering. Data segments that are not executable prevent code injection attacks. These protections happen before any OS or application code runs.
The present bit (or valid bit) indicates whether the segment is currently loaded in physical memory. This single bit enables powerful memory management capabilities, including swapping segments to disk and implementing a form of demand segmentation.
Present Bit Semantics:
P=1 (Present): The segment is in physical memory. The base and limit fields are valid, and the MMU can perform address translation.
P=0 (Not Present): The segment is not in physical memory (perhaps swapped to disk). Any access to this segment triggers a segment-not-present fault, transferring control to the OS.
Segment Fault Handling:
When a not-present segment is accessed:
This is analogous to page faults in paging systems, but operates at the segment level.
1234567891011121314151617181920212223242526272829303132333435363738394041
void segment_not_present_handler(interrupt_frame_t* frame) { // Get the segment selector that caused the fault uint16_t selector = frame->error_code & 0xFFF8; // Look up segment in our segment metadata table segment_info_t* info = lookup_segment_info(current_process, selector); if (info == NULL) { // Invalid segment - kill the process terminate_process(current_process, SIGSEGV); return; } // Find the segment on disk if (!info->is_swapped && !info->is_demand_load) { // Segment should exist but doesn't - bug or corruption kernel_panic("Segment metadata inconsistency"); } // Allocate physical memory for the segment void* phys_mem = allocate_physical_pages(info->size); if (phys_mem == NULL) { // Need to swap something else out first swap_out_segment(find_victim_segment()); phys_mem = allocate_physical_pages(info->size); } // Load segment contents from disk if (info->is_swapped) { read_from_swap(info->swap_location, phys_mem, info->size); } else if (info->is_demand_load) { read_from_executable(info->file_offset, phys_mem, info->size); } // Update the segment descriptor in the GDT/LDT segment_descriptor_t* desc = get_descriptor(current_process, selector); desc->base = (uint32_t)phys_mem; desc->limit = info->size - 1; desc->present = 1; // Return - CPU will retry the faulting instruction}While segment swapping is conceptually clean, it has practical drawbacks. Segments vary in size—swapping a 1MB segment is far more expensive than swapping a 4KB page. This is one reason why modern systems favor paging over segmentation for virtual memory. Segments still help with logical organization, but paging handles physical memory management.
The Descriptor Privilege Level (DPL) is a 2-bit field that specifies the minimum privilege required to access the segment. This mechanism is central to protection rings and the separation between kernel mode and user mode.
Protection Rings Hierarchy:
┌───────────────────────┐
│ Ring 0 (DPL=0) │ ← Kernel (highest privilege)
│ Operating System │
└───────────────────────┘
┌───────────────────────┐
│ Ring 1 (DPL=1) │ ← Device Drivers (x86 historical)
│ System Services │
└───────────────────────┘
┌───────────────────────┐
│ Ring 2 (DPL=2) │ ← System Services (rarely used)
│ Privileged Utils │
└───────────────────────┘
┌───────────────────────────┐
│ Ring 3 (DPL=3) │ ← User Applications (lowest privilege)
│ User Applications │
└───────────────────────────┘
Privilege Level Checks:
When code attempts to access a segment, the CPU compares:
Access Rules:
| Segment Type | Access Rule | Effect |
|---|---|---|
| Data Segment | max(CPL, RPL) ≤ DPL | Higher privilege code can access lower privilege data |
| Non-Conforming Code | CPL = DPL and RPL ≤ DPL | Must match exactly for control transfers |
| Conforming Code | CPL ≥ DPL | Lower privilege code can call higher privilege conforming code |
Practical Implications:
Kernel Segments (DPL=0):
User Segments (DPL=3):
Mixed Privilege Scenarios:
When a system call occurs, the kernel often needs to access user buffers. The kernel runs at CPL=0 but may access segments with DPL=3. This is permitted because 0 ≤ 3.
However, the kernel must validate user requests carefully—a user can't trick the kernel by passing a kernel segment selector, because RPL is checked against DPL as well.
Most modern operating systems only use Ring 0 (kernel) and Ring 3 (user). Rings 1 and 2 are rarely used in practice. With virtualization, hypervisors sometimes run in Ring -1 (SMM or VMX root mode), adding another layer below the traditional ring structure.
The accessed bit (A) is automatically set by the CPU when a segment is accessed. This simple bit serves as a building block for sophisticated memory management algorithms.
Accessed Bit Behavior:
Use Cases for the Accessed Bit:
Comparison with Page Table Accessed Bits:
The page table also has accessed and dirty bits, serving similar purposes at the page level. In combined segmentation+paging systems:
The granularity difference matters: segment access tracking is coarse-grained (entire segment) while page access tracking is fine-grained (4KB typically).
Atomic Access Bit Updates:
The CPU must set the accessed bit atomically to prevent race conditions in multi-processor systems. This is done as part of the memory access microcode—if the bit is 0, the CPU performs a locked read-modify-write operation to set it to 1 before the actual memory access completes.
The accessed bit update is a memory write operation to the descriptor table, which can impact performance on frequently-accessed segments in tight loops. Once the bit is set, subsequent accesses don't need to update it. Some systems cache segment descriptors in registers (like segment register caches) to minimize descriptor table accesses.
Let's examine a complete segment table entry in context, seeing how all the fields work together during address translation and protection checking.
Scenario: Accessing a Data Variable
A user-mode program executes: x = array[100];
The compiler generated code that:
0x23 (Data Segment, Ring 3)0x8064 (where array[100] is located)Segment Descriptor at Index 4 (selector 0x23 → index 4):
Base: 0x10000000
Limit: 0x0FFFF (with G=1 → 4GB effective limit)
Type: Data, Expand-Up, Writable
Present: 1
DPL: 3 (User mode)
Granularity: 1 (Page units)
Accessed: 1 (Previously accessed)
Step-by-Step Translation:
Failure Scenarios:
If any check failed, a fault would occur:
| Failure | Fault | Handler Action |
|---|---|---|
| P=0 | Segment Not Present (#NP) | Load segment from disk, retry |
| DPL < max(CPL,RPL) | General Protection (#GP) | Terminate process |
| Offset > Limit | General Protection (#GP) | Terminate process |
| Write to read-only segment | General Protection (#GP) | Terminate process |
| Execute from non-code segment | General Protection (#GP) | Terminate process |
The hardware performs all these checks in a single memory access cycle (assuming descriptor is cached), making segmentation protection essentially free in terms of runtime overhead.
The elegance of segment table entries is that protection is enforced by hardware on every memory access. No software checks are needed—the CPU simply won't allow forbidden operations. This is why buffer overflows in properly segmented systems would be caught immediately, rather than causing silent corruption.
We've thoroughly dissected the segment table entry—the fundamental data structure underpinning segmented memory management. Let's consolidate the key concepts:
What's Next:
With a solid understanding of segment table entry structure, we'll now explore the base address in greater depth—examining how it enables dynamic relocation, memory sharing between processes, and efficient use of physical memory across the system.
The base address may seem like a simple field, but its implications for operating system design are profound.
You now understand the complete anatomy of a segment table entry—from its individual bit fields to its role in the hardware translation and protection process. This foundation prepares you to explore each STE component in depth in the following pages.