Loading learning content...
After extracting the segment number, looking up the segment table, extracting the offset, and validating it against the limit, we arrive at the culmination of the entire address translation process: physical address formation. This is the moment when abstraction becomes reality—when a logical, programmer-visible address transforms into an actual location in physical memory chips.
The operation itself is elegantly simple: add the segment's base address to the offset. Yet this simplicity belies the profound implications. This single addition is what enables segments to be placed anywhere in physical memory, relocated dynamically, and managed invisibly by the operating system. It's the mathematical heart of the segmentation abstraction.
By the end of this page, you will understand the addition operation at the core of physical address formation, how base registers and segment table entries provide the base address, the timing and parallelism of this operation in hardware, special considerations like wraparound behavior, and how physical address formation completes the translation pipeline.
Physical address formation is fundamentally an addition operation. The base address of the segment is added to the offset to produce the physical memory address.
The Formula:
Physical Address = Base Address + Offset
Where:
Mathematical Properties:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
#include <stdio.h>#include <stdint.h> /* * Physical Address Formation * * The fundamental operation that converts logical addresses * to physical addresses in a segmented memory system. */ // Segment Table Entrytypedef struct { uint32_t base; // Physical starting address uint32_t limit; // Segment size uint8_t present; // Segment in memory?} SegmentEntry; // Simple segment tableSegmentEntry segment_table[] = { { 0x00100000, 8192, 1 }, // Segment 0: Code { 0x00200000, 16384, 1 }, // Segment 1: Data { 0x00300000, 4096, 1 }, // Segment 2: Stack { 0x00400000, 32768, 1 }, // Segment 3: Heap}; /** * Form physical address from segment and offset * This is the heart of segmented address translation */uint32_t form_physical_address(uint16_t segment, uint16_t offset) { if (segment >= sizeof(segment_table)/sizeof(segment_table[0])) { printf("ERROR: Invalid segment number %u\n", segment); return 0xFFFFFFFF; // Invalid marker } SegmentEntry* entry = &segment_table[segment]; if (!entry->present) { printf("ERROR: Segment %u not present in memory\n", segment); return 0xFFFFFFFF; } if (offset >= entry->limit) { printf("ERROR: Offset %u exceeds limit %u for segment %u\n", offset, entry->limit, segment); return 0xFFFFFFFF; } // THE KEY OPERATION: base + offset uint32_t physical_address = entry->base + offset; return physical_address;} void demonstrate_translation(uint16_t segment, uint16_t offset) { printf("Logical: (Segment %u, Offset %u)\n", segment, offset); if (segment < sizeof(segment_table)/sizeof(segment_table[0])) { SegmentEntry* entry = &segment_table[segment]; printf(" Base: 0x%08X\n", entry->base); printf(" Offset: 0x%08X\n", offset); printf(" ------------------------\n"); } uint32_t physical = form_physical_address(segment, offset); if (physical != 0xFFFFFFFF) { printf(" Physical Address: 0x%08X\n", physical); } printf("\n");} int main() { printf("=== Physical Address Formation Demo ===\n\n"); printf("Segment Table:\n"); printf(" Seg 0 (Code): Base=0x00100000, Limit=8192\n"); printf(" Seg 1 (Data): Base=0x00200000, Limit=16384\n"); printf(" Seg 2 (Stack): Base=0x00300000, Limit=4096\n"); printf(" Seg 3 (Heap): Base=0x00400000, Limit=32768\n\n"); printf("=== Translation Examples ===\n\n"); // Valid accesses demonstrate_translation(0, 0); // First byte of code demonstrate_translation(0, 100); // 100 bytes into code demonstrate_translation(1, 1000); // 1000 bytes into data demonstrate_translation(2, 2048); // Middle of stack demonstrate_translation(3, 32767); // Last byte of heap printf("=== Error Cases ===\n\n"); // Invalid accesses demonstrate_translation(0, 10000); // Exceeds limit demonstrate_translation(5, 0); // Invalid segment return 0;} /* * Key Insight: * * The addition base + offset is the entire mechanism for relocation. * * Without it: Programs would need absolute addresses, loading at * a different location would require rewriting all addresses. * * With it: Programs use offsets from 0, the OS can place them * anywhere by setting the base, and no address rewriting is needed. */The base + offset design is a classic example of indirection solving a fundamental problem. By making the physical location undetermined until runtime, and providing it through a modifiable table, we gain the ability to move segments in memory without changing the programs that use them. This is the essence of memory virtualization.
The base address that forms the first operand of the addition comes from different sources depending on the architecture. Understanding these sources is crucial for systems programming.
| Source | Description | Example Architecture |
|---|---|---|
| Base Register | Dedicated CPU register for segment base | Simple/early systems, MIPS |
| Segment Table Entry | Base field in STE, indexed by segment # | Multics, x86 protected mode |
| Descriptor Cache | Cached copy of STE in hidden CPU registers | x86 protected mode (optimization) |
| Segment Register × 16 | Simple shift of segment value | Intel 8086 real mode |
Intel 8086 Real Mode:
The 8086 uses a simple but clever scheme:
Physical Address = (Segment Register × 16) + Offset
The segment register value is shifted left by 4 bits (multiplied by 16) and added to the 16-bit offset. This yields a 20-bit physical address, allowing 1 MB of addressable memory from two 16-bit components.
Intel Protected Mode:
In protected mode, segment registers contain selectors (indices), not base addresses:
1. Selector indexes into GDT or LDT
2. Segment descriptor contains 32-bit base address
3. Descriptor is cached in hidden part of segment register
4. Physical = Cached Base + Offset
The descriptor cache eliminates repeated table lookups for repeated accesses to the same segment.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
#include <stdio.h>#include <stdint.h> /* * Comparing Base Address Computation Methods * * Different architectures obtain the segment base differently. */ // ============================================// Method 1: Intel 8086 Real Mode// Physical = Segment × 16 + Offset// ============================================ uint32_t real_mode_translate(uint16_t segment, uint16_t offset) { // Shift segment left by 4 bits (multiply by 16) uint32_t base = (uint32_t)segment << 4; uint32_t physical = base + offset; // Result is 20 bits (1 MB address space) return physical & 0xFFFFF;} void demo_real_mode() { printf("=== Intel 8086 Real Mode ===\n"); printf("Formula: Physical = Segment × 16 + Offset\n\n"); // Classic example: 0000:0000 = 0x00000 printf("0000:0000 = 0x%05X\n", real_mode_translate(0x0000, 0x0000)); // 1000:0000 = 0x10000 printf("1000:0000 = 0x%05X\n", real_mode_translate(0x1000, 0x0000)); // FFFF:000F = 0xFFFF0 + 0x000F = 0xFFFFF (max address) printf("FFFF:000F = 0x%05X\n", real_mode_translate(0xFFFF, 0x000F)); // Overlapping addresses: different segment:offset, same physical printf("\nOverlap demonstration:\n"); printf("0100:0000 = 0x%05X\n", real_mode_translate(0x0100, 0x0000)); printf("00FF:0010 = 0x%05X\n", real_mode_translate(0x00FF, 0x0010)); printf("00F0:0100 = 0x%05X\n", real_mode_translate(0x00F0, 0x0100)); printf("(All three map to physical 0x01000!)\n\n");} // ============================================// Method 2: Protected Mode with Table Lookup// Physical = Base_from_Descriptor + Offset// ============================================ typedef struct { uint32_t base; uint32_t limit; uint16_t attributes;} DescriptorCache; // Global Descriptor Table (simplified)typedef struct { uint16_t limit_low; uint16_t base_low; uint8_t base_mid; uint8_t access; uint8_t limit_high_flags; uint8_t base_high;} GDTEntry; // Simulated GDTGDTEntry gdt[] = { {0, 0, 0, 0, 0, 0}, // Null descriptor {0xFFFF, 0x0000, 0x10, 0x9A, 0xCF, 0x00}, // Code: base=0x100000 {0xFFFF, 0x0000, 0x20, 0x92, 0xCF, 0x00}, // Data: base=0x200000}; // Extract base address from GDT entryuint32_t get_base_from_descriptor(GDTEntry* entry) { return entry->base_low | ((uint32_t)entry->base_mid << 16) | ((uint32_t)entry->base_high << 24);} uint32_t protected_mode_translate(uint16_t selector, uint32_t offset) { // Extract index (bits 3-15 of selector) uint16_t index = selector >> 3; if (index >= sizeof(gdt)/sizeof(gdt[0])) { printf("Invalid selector\n"); return 0xFFFFFFFF; } GDTEntry* entry = &gdt[index]; uint32_t base = get_base_from_descriptor(entry); return base + offset;} void demo_protected_mode() { printf("=== Intel Protected Mode ===\n"); printf("Formula: Physical = Base_from_GDT[Selector>>3] + Offset\n\n"); // Selector 0x08 = index 1, code segment printf("Selector 0x08, Offset 0x1000:\n"); printf(" Physical = 0x%08X\n\n", protected_mode_translate(0x08, 0x1000)); // Selector 0x10 = index 2, data segment printf("Selector 0x10, Offset 0x2000:\n"); printf(" Physical = 0x%08X\n\n", protected_mode_translate(0x10, 0x2000));} int main() { demo_real_mode(); demo_protected_mode(); printf("=== Key Difference ===\n"); printf("Real Mode: Base is computed from segment value (segment × 16)\n"); printf("Protected Mode: Base is looked up from descriptor table\n"); printf("Protected Mode base can be any 32-bit value, enabling flexible placement.\n"); return 0;}In x86 protected mode, each segment register has a visible part (the selector) and an invisible part (the cached descriptor). When you load a segment register, the CPU fetches the descriptor from the GDT/LDT and caches it. Subsequent accesses use the cached base, avoiding repeated memory lookups. This is why changing a descriptor requires reloading the segment register.
The addition that forms the physical address is implemented as a dedicated hardware adder within the memory management unit. This adder must be fast and operate in parallel with bounds checking.
Adder Design Considerations:
Width: The adder must be as wide as the physical address space. For 32-bit physical addresses, a 32-bit adder is needed even if logical addresses are 16 bits.
Speed: Carry-lookahead adders are typically used to minimize propagation delay. A 32-bit CLA adder completes in O(log n) gate delays.
Parallelism: The adder operates simultaneously with the bounds comparator. Both finish in roughly the same time, and results are gated together.
Integration: In modern CPUs, this adder is part of the Address Generation Unit (AGU), which handles all address calculations.
12345678910111213141516171819202122232425262728293031323334353637383940414243
Physical Address Adder - Hardware View: Inputs: - Base[31:0]: 32-bit base address from segment table - Offset[15:0]: 16-bit offset (zero-extended to 32 bits) Output: - Physical[31:0]: 32-bit physical address Circuit (Carry-Lookahead Adder): Base[31:0] Offset[31:0] (zero-extended) │ │ └───────┬─────────────┘ │ ┌───────▼───────┐ │ 32-bit CLA │ │ Adder │ │ │ │ P = B + O │ └───────┬───────┘ │ Physical[31:0] Timing Analysis: - Input latch: 1 cycle - CLA addition: ~log2(32) = 5 gate delays - Output buffer: 1 cycle - Total: approximately 1-2 clock cycles Parallel with Bounds Check: Time ─────────────────────────────────────▶ Bounds │ Compare │ Result │ Check │ offset │ valid/ │ │ < limit │ fault │ Address│ Add base│ Physical│ Gate │ Output Calc │+ offset │ ready │ check │ to bus Both paths complete in approximately the same time. The gate only passes the physical address if bounds check passed.In well-designed hardware, segment translation adds minimal latency because the addition and comparison happen in parallel during the same cycle that would otherwise be spent on address generation anyway. The abstraction of segmentation can be nearly 'free' when implemented with good hardware design.
Physical address formation involves several special cases and edge conditions that system designers and programmers must understand.
Address Space Wraparound:
What happens if base + offset exceeds the maximum physical address?
Example on a system with 32-bit physical addresses:
Base = 0xFFFFF000
Offset = 0x2000
Sum = 0x100001000 (33 bits!)
Behavior is architecture-dependent:
Most modern systems prevent this through proper segment placement.
| Case | Condition | Typical Behavior | Impact |
|---|---|---|---|
| Zero Base | Base = 0 | Physical = Offset | Logical and physical match |
| Zero Offset | Offset = 0 | Physical = Base | First byte of segment |
| Wraparound | Base + Offset > Max | Truncation or fault | Architecture dependent |
| A20 Gate (8086) | Address bit 20 | Gate can disable bit 20 | Legacy PC compatibility |
| Segment Override | Explicit segment prefix | Different base used | Common in x86 |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
#include <stdio.h>#include <stdint.h> /* * Special Cases in Physical Address Formation */ #define ADDR_BITS 32#define ADDR_MASK ((1ULL << ADDR_BITS) - 1) // Simulate different wraparound behaviorsenum WrapBehavior { WRAP_TRUNCATE, WRAP_FAULT }; uint64_t form_address_with_wrap(uint32_t base, uint32_t offset, enum WrapBehavior behavior, int* wrapped) { uint64_t sum = (uint64_t)base + (uint64_t)offset; *wrapped = (sum > ADDR_MASK); switch (behavior) { case WRAP_TRUNCATE: return sum & ADDR_MASK; case WRAP_FAULT: if (*wrapped) return 0xFFFFFFFFFFFFFFFF; // Invalid marker return sum; } return sum;} void test_special_case(const char* name, uint32_t base, uint32_t offset) { int wrapped; printf("Case: %s\n", name); printf(" Base: 0x%08X, Offset: 0x%08X\n", base, offset); // 64-bit sum to see overflow uint64_t raw_sum = (uint64_t)base + (uint64_t)offset; printf(" Raw sum (64-bit): 0x%016llX\n", (unsigned long long)raw_sum); // Truncating behavior uint64_t truncated = form_address_with_wrap(base, offset, WRAP_TRUNCATE, &wrapped); printf(" Truncate (32-bit): 0x%08X%s\n", (uint32_t)truncated, wrapped ? " [WRAPPED]" : ""); // Fault behavior uint64_t fault_check = form_address_with_wrap(base, offset, WRAP_FAULT, &wrapped); if (fault_check == 0xFFFFFFFFFFFFFFFF) { printf(" Fault check: FAULT (overflow detected)\n"); } else { printf(" Fault check: 0x%08X (valid)\n", (uint32_t)fault_check); } printf("\n");} int main() { printf("=== Special Cases in Physical Address Formation ===\n\n"); // Normal case test_special_case("Normal", 0x00100000, 0x00001000); // Zero base test_special_case("Zero Base", 0x00000000, 0x00001000); // Zero offset test_special_case("Zero Offset", 0x00100000, 0x00000000); // Near end of address space test_special_case("Near Edge", 0xFFFF0000, 0x0000FF00); // Overflow case test_special_case("Overflow", 0xFFFFF000, 0x00002000); printf("=== Intel 8086 A20 Gate Example ===\n\n"); // The A20 gate masked bit 20 of the address for 8086 compatibility uint32_t addr = real_mode_example(0xFFFF, 0x0010); printf("Real mode FFFF:0010 = 0x%05X\n", addr & 0xFFFFF); printf("With A20 gate disabled: 0x%05X (bit 20 masked)\n", addr & 0xEFFFF); return 0;} // Intel 8086 real mode calculationuint32_t real_mode_example(uint16_t seg, uint16_t off) { return ((uint32_t)seg << 4) + off;}Early IBM PCs had a bug-turned-feature called the A20 gate. The 8086's 20-bit addresses wrapped around at 1 MB, and some software depended on this. When the 80286 added more address lines, IBM added the A20 gate to mask bit 20 for compatibility. This gate persists in PCs today, needing to be enabled for protected mode. It's a reminder of how legacy behavior constrains future designs.
With physical address formation understood, we can now see the complete segmented address translation pipeline from logical address to physical memory access.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
#include <stdio.h>#include <stdint.h>#include <stdbool.h> /* * Complete Segmented Address Translation Pipeline * * Implements all steps from logical address to physical address. */ // Access types for permission checkingtypedef enum { ACCESS_READ = 0x01, ACCESS_WRITE = 0x02, ACCESS_EXEC = 0x04} AccessType; // Segment Table Entrytypedef struct { uint32_t base; uint32_t limit; bool present; uint8_t permissions; // Combination of AccessType flags bool accessed; bool dirty;} SegmentTableEntry; // Translation resulttypedef struct { bool success; uint32_t physical_address; char error_message[100];} TranslationResult; // Simulated segment table#define MAX_SEGMENTS 16SegmentTableEntry segment_table[MAX_SEGMENTS];uint32_t STLR = 5; // Segment Table Length Register void init_segment_table() { // Segment 0: Code (Read + Execute) segment_table[0] = (SegmentTableEntry){ .base = 0x00100000, .limit = 8192, .present = true, .permissions = ACCESS_READ | ACCESS_EXEC }; // Segment 1: Data (Read + Write) segment_table[1] = (SegmentTableEntry){ .base = 0x00200000, .limit = 16384, .present = true, .permissions = ACCESS_READ | ACCESS_WRITE }; // Segment 2: Stack (Read + Write) segment_table[2] = (SegmentTableEntry){ .base = 0x00300000, .limit = 4096, .present = true, .permissions = ACCESS_READ | ACCESS_WRITE }; // Segment 3: Heap (Read + Write) segment_table[3] = (SegmentTableEntry){ .base = 0x00400000, .limit = 32768, .present = true, .permissions = ACCESS_READ | ACCESS_WRITE }; // Segment 4: Not present (swapped out) segment_table[4] = (SegmentTableEntry){ .base = 0, .limit = 8192, .present = false, .permissions = ACCESS_READ | ACCESS_WRITE };} /** * Complete translation pipeline */TranslationResult translate_address(uint16_t segment, uint16_t offset, AccessType access_type) { TranslationResult result = { .success = false }; // Step 1: Check segment number against STLR if (segment >= STLR) { snprintf(result.error_message, sizeof(result.error_message), "Invalid segment: %u >= STLR(%u)", segment, STLR); return result; } // Step 2: Look up segment table entry SegmentTableEntry* ste = &segment_table[segment]; // Step 3: Check present bit if (!ste->present) { snprintf(result.error_message, sizeof(result.error_message), "Segment %u not present in memory", segment); return result; } // Step 4: Check permissions if ((ste->permissions & access_type) != access_type) { const char* access_name = access_type == ACCESS_READ ? "read" : access_type == ACCESS_WRITE ? "write" : "execute"; snprintf(result.error_message, sizeof(result.error_message), "Permission denied: cannot %s segment %u", access_name, segment); return result; } // Step 5: Bounds checking if (offset >= ste->limit) { snprintf(result.error_message, sizeof(result.error_message), "Bounds violation: offset %u >= limit %u", offset, ste->limit); return result; } // Step 6: Form physical address result.physical_address = ste->base + offset; result.success = true; // Update accessed/dirty bits ste->accessed = true; if (access_type == ACCESS_WRITE) { ste->dirty = true; } return result;} void test_translation(uint16_t seg, uint16_t off, AccessType access) { const char* access_str = access == ACCESS_READ ? "READ" : access == ACCESS_WRITE ? "WRITE" : "EXEC"; printf("Translate: (Seg %u, Off %u) [%s]\n", seg, off, access_str); TranslationResult result = translate_address(seg, off, access); if (result.success) { printf(" SUCCESS: Physical = 0x%08X\n", result.physical_address); } else { printf(" FAULT: %s\n", result.error_message); } printf("\n");} int main() { printf("=== Complete Translation Pipeline Demo ===\n\n"); init_segment_table(); printf("--- Successful Translations ---\n\n"); test_translation(0, 1000, ACCESS_READ); // Read from code test_translation(0, 1000, ACCESS_EXEC); // Execute from code test_translation(1, 2000, ACCESS_WRITE); // Write to data test_translation(2, 500, ACCESS_READ); // Read from stack printf("--- Fault Conditions ---\n\n"); test_translation(0, 1000, ACCESS_WRITE); // Write to code (no permission) test_translation(1, 20000, ACCESS_READ); // Offset exceeds limit test_translation(4, 0, ACCESS_READ); // Segment not present test_translation(10, 0, ACCESS_READ); // Invalid segment number return 0;}In hardware, many of these steps happen in parallel. The bounds check and base+offset addition occur simultaneously. Permission checking can overlap with offset extraction. This parallelism minimizes the latency impact of segmentation, making the abstraction nearly free in terms of performance.
Physical address formation is the final step that bridges the abstract world of logical addresses with the concrete reality of physical memory. The simple addition of base and offset is the mechanism that enables all the benefits of segmentation.
What's Next:
With all the concepts understood—segment number extraction, offset extraction, bounds checking, and physical address formation—we'll now bring everything together with a comprehensive translation example. This walkthrough will trace a logical address through every step of the translation process, showing concrete values at each stage.
You now understand physical address formation—the final step in segmented address translation. You can compute physical addresses from base and offset, understand the hardware implementation, handle special cases, and see how this step fits into the complete translation pipeline. Next, we'll work through a detailed example.