Loading learning content...
If the segment number answers which logical unit we're accessing, the offset answers where within that unit. The offset is the second half of every segmented address—a displacement that pinpoints the exact byte, word, or data structure within a segment that the program wants to read or write.
The offset transforms segment-level abstraction into byte-level precision. Without it, we'd know we want something from the code segment, but not which instruction. We'd know we're accessing the stack, but not which local variable. The offset provides the granularity needed for actual computation, making it equally as important as the segment number in the address translation process.
By the end of this page, you will understand the precise definition and properties of the segment offset, how it's extracted from logical addresses, the relationship between offset width and maximum segment size, how offsets relate to pointers within compiled programs, and the critical role offsets play in the bounds-checking phase of address translation.
The offset (also called displacement or effective address within segment) is the portion of a logical address that specifies the byte position within the selected segment.
More formally:
The offset is an unsigned integer representing the distance, in bytes, from the beginning of a segment to the target memory location. It is added to the segment's base address during translation to produce the physical address.
This definition highlights several key properties that distinguish offsets from other address components.
physical = base + offset. No multiplication or other transformation occurs.The Mathematician's View:
Consider a segment S with:
For an offset d to be valid:
0 ≤ d < L
The resulting physical address P is:
P = B + d
This means the physical addresses covered by segment S range from B to B + L - 1. Every valid offset maps to exactly one physical address within this range.
1234567891011121314151617181920212223242526
Segment Offset Properties Illustrated: Segment: Code (Segment #0) Physical Base: 0x00100000 Limit: 8192 bytes (8 KB) Valid Offsets: 0 to 8191 ┌────────────────────────────────────────────────┐ │ Offset 0 → Physical 0x00100000 (first byte) │ │ Offset 1 → Physical 0x00100001 │ │ Offset 100 → Physical 0x00100064 │ │ Offset 4096 → Physical 0x00101000 (midpoint) │ │ Offset 8191 → Physical 0x00101FFF (last byte) │ ├────────────────────────────────────────────────┤ │ Offset 8192 → INVALID (exceeds limit) │ │ Offset 10000 → INVALID (exceeds limit) │ └────────────────────────────────────────────────┘ Key Insight: The offset is always relative to the segment's beginning,never to physical address 0. This enables segments to be relocatedin physical memory without changing the logical addresses used by programs. Programmer's Perspective: - Sees addresses 0 to 8191 within the code segment - Never knows or cares about physical address 0x00100000 - Every pointer, instruction address, jump target uses offsetsThe beauty of segment offsets is relocation independence. If the operating system moves a segment from physical address 0x00100000 to 0x00500000, it only updates the base in the segment table. All the offsets used by the program—every pointer, every jump target, every data reference—remain valid without modification. This is the power of indirection through segmentation.
Extracting the offset from a logical address is the complement of segment number extraction. While the segment number comes from the high-order bits, the offset is extracted from the low-order bits through masking.
The Extraction Formula:
For a logical address where:
The offset is extracted using a bit mask:
offset = logical_address & ((1 << m) - 1)
The mask (1 << m) - 1 produces m consecutive 1-bits, preserving only the low-order m bits of the address while zeroing the segment number.
Example with m = 12:
mask = (1 << 12) - 1
= 4096 - 1
= 4095
= 0x0FFF
= 0000111111111111 (binary)
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
#include <stdio.h>#include <stdint.h> /* * Offset Extraction from Segmented Logical Addresses * * Demonstrates the bit masking operation used by hardware * to extract the offset (displacement) component. */ // Architecture configuration#define LOGICAL_ADDR_BITS 16#define SEGMENT_BITS 4#define OFFSET_BITS 12 // LOGICAL_ADDR_BITS - SEGMENT_BITS // Offset mask: m low-order bits set to 1// For 12 bits: 0x0FFF = 4095 = 111111111111 (binary)#define OFFSET_MASK ((1U << OFFSET_BITS) - 1) /** * Extract offset from logical address using bit masking */uint16_t extract_offset(uint16_t logical_address) { return logical_address & OFFSET_MASK;} /** * Visualize the extraction process */void demonstrate_offset_extraction(uint16_t logical_address) { uint16_t offset = extract_offset(logical_address); printf("Logical Address: 0x%04X\n", logical_address); printf(" Binary: "); for (int i = 15; i >= 0; i--) { printf("%d", (logical_address >> i) & 1); if (i == OFFSET_BITS) printf(" "); // Visual separator } printf("\n"); printf("Offset Mask: "); for (int i = 15; i >= 0; i--) { printf("%d", (OFFSET_MASK >> i) & 1); if (i == OFFSET_BITS) printf(" "); } printf(" (0x%04X)\n", OFFSET_MASK); printf(" AND Result: "); for (int i = 15; i >= 0; i--) { printf("%d", (offset >> i) & 1); if (i == OFFSET_BITS) printf(" "); } printf("\n"); printf("Extracted Offset: %u (0x%03X)\n\n", offset, offset);} /** * Demonstrate offset range for given segment width */void show_offset_range() { uint16_t max_offset = OFFSET_MASK; printf("=== Offset Range Analysis ===\n"); printf("Offset bits: %d\n", OFFSET_BITS); printf("Minimum offset: 0\n"); printf("Maximum offset: %u (0x%03X)\n", max_offset, max_offset); printf("Total addressable bytes per segment: %u\n\n", max_offset + 1);} int main() { printf("=== Offset Extraction Demo ===\n\n"); show_offset_range(); printf("=== Extraction Examples ===\n\n"); // Test various addresses demonstrate_offset_extraction(0x0000); // Offset = 0 demonstrate_offset_extraction(0x0001); // Offset = 1 demonstrate_offset_extraction(0x0FFF); // Offset = 4095 (max) demonstrate_offset_extraction(0x1000); // Seg 1, Offset = 0 demonstrate_offset_extraction(0x1234); // Seg 1, Offset = 564 demonstrate_offset_extraction(0x3A5F); // Seg 3, Offset = 2655 demonstrate_offset_extraction(0xFFFF); // Seg 15, Offset = 4095 return 0;} /* * Sample Output: * * === Offset Extraction Demo === * * === Offset Range Analysis === * Offset bits: 12 * Minimum offset: 0 * Maximum offset: 4095 (0xFFF) * Total addressable bytes per segment: 4096 * * === Extraction Examples === * * Logical Address: 0x3A5F * Binary: 0011 101001011111 * Offset Mask: 0000 111111111111 (0x0FFF) * AND Result: 0000 101001011111 * Extracted Offset: 2655 (0xA5F) */| Logical Address | Segment # | Offset (Decimal) | Offset (Hex) |
|---|---|---|---|
| 0x0000 | 0 | 0 | 0x000 |
| 0x0001 | 0 | 1 | 0x001 |
| 0x0100 | 0 | 256 | 0x100 |
| 0x0FFF | 0 | 4095 | 0xFFF |
| 0x1000 | 1 | 0 | 0x000 |
| 0x2800 | 2 | 2048 | 0x800 |
| 0x3A5F | 3 | 2655 | 0xA5F |
| 0xFFFF | 15 | 4095 | 0xFFF |
In hardware, offset extraction doesn't require an actual AND operation. The address bus is simply wired so that the low-order m bits are routed directly to the adder that computes the physical address. The masking is implicit in the circuit design—instantaneous and zero-cost.
The number of bits allocated to the offset directly determines the maximum size any segment can have. This is one of the most important constraints in segmented architecture design.
The Fundamental Relationship:
Maximum Segment Size = 2^m bytes
Where m is the number of offset bits.
This is an absolute architectural limit—no segment can exceed this size, regardless of how much physical memory is available. If the hardware provides 12 offset bits, segments are capped at 4,096 bytes (4 KB), period.
Why This Matters:
The maximum segment size has profound implications:
Code Size Limits: The code segment can't exceed the maximum. Very large programs might need multiple code segments.
Array Limits: A single array can't span more than the maximum segment size (unless it crosses segment boundaries, which is complex).
Stack Depth: The stack segment has a maximum size, limiting recursion depth and local variable space.
Object Size: In object-oriented systems where objects are segments, this caps object size.
| Offset Bits | Maximum Segment Size | Typical Use Case |
|---|---|---|
| 8 | 256 bytes | Embedded microcontrollers |
| 10 | 1 KB | Minimal systems |
| 12 | 4 KB | Small-scale segmentation |
| 14 | 16 KB | Traditional segmented systems |
| 16 | 64 KB | Intel 8086 real mode |
| 20 | 1 MB | Intel 80286 protected mode |
| 24 | 16 MB | Extended systems |
| 32 | 4 GB | Intel 80386+ protected mode |
Variable Segment Sizes:
It's crucial to understand that the offset width determines the maximum possible segment size, not the actual size of any particular segment. Individual segments can be smaller:
This allows efficient memory usage—segments only consume the physical memory they actually need.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980
#include <stdio.h>#include <stdint.h> /* * Analyzing Segment Size Constraints * * Demonstrates the relationship between offset width * and maximum segment size, plus actual vs maximum usage. */ typedef struct { uint32_t base; // Physical base address uint32_t limit; // Actual segment size (may be < maximum) const char* name; // Segment name for display} Segment; void analyze_segment_sizes(int offset_bits) { uint64_t max_size = 1ULL << offset_bits; printf("=== Offset Width: %d bits ===\n", offset_bits); printf("Maximum possible segment size: "); if (max_size >= (1ULL << 30)) { printf("%.2f GB\n", max_size / (double)(1ULL << 30)); } else if (max_size >= (1ULL << 20)) { printf("%.2f MB\n", max_size / (double)(1ULL << 20)); } else if (max_size >= (1ULL << 10)) { printf("%.2f KB\n", max_size / (double)(1ULL << 10)); } else { printf("%llu bytes\n", max_size); } printf("Maximum addressable offset: %llu (0x%llX)\n\n", max_size - 1, max_size - 1);} void show_typical_segment_layout() { printf("=== Typical Process Segment Layout (12-bit offset = 4KB max) ===\n\n"); const int MAX_SEGMENT_SIZE = 4096; // 12-bit offset Segment segments[] = { {0x00100000, 2048, "Code"}, // Uses 50% of maximum {0x00101000, 1024, "Data"}, // Uses 25% of maximum {0x00102000, 512, "BSS"}, // Uses 12.5% of maximum {0x00103000, 4096, "Heap"}, // Uses 100% of maximum {0x00104000, 3072, "Stack"}, // Uses 75% of maximum }; int num_segments = sizeof(segments) / sizeof(segments[0]); printf("%-10s %-12s %-10s %-10s %-15s\n", "Segment", "Base", "Limit", "Max Size", "Utilization"); printf("%-10s %-12s %-10s %-10s %-15s\n", "-------", "----", "-----", "--------", "-----------"); for (int i = 0; i < num_segments; i++) { Segment* s = &segments[i]; double utilization = (s->limit * 100.0) / MAX_SEGMENT_SIZE; printf("%-10s 0x%08X %-10u %-10u %.1f%%\n", s->name, s->base, s->limit, MAX_SEGMENT_SIZE, utilization); } printf("\nKey Insight: Segments use only the memory they need.\n"); printf("The limit field allows sizes smaller than the architectural maximum.\n");} int main() { // Analyze different offset widths analyze_segment_sizes(8); analyze_segment_sizes(12); analyze_segment_sizes(16); analyze_segment_sizes(20); analyze_segment_sizes(32); show_typical_segment_layout(); return 0;}The Intel 8086's 16-bit offset limited segments to 64 KB. This constraint haunted PC programming for decades—the 'far pointer' vs 'near pointer' distinction, segment wrap-around bugs, and the need for memory models (tiny, small, medium, large, huge) all stemmed from this architectural decision. It's a cautionary tale about how offset width choices have long-lasting consequences.
From a programmer's perspective, offsets manifest as the addresses they work with daily—variable addresses, function pointers, array indices. Understanding how compiler-generated code produces offsets is crucial for systems programming.
How Offsets Are Generated:
Global Variables: Assigned fixed offsets within the data segment at link time. The linker determines each variable's position.
Local Variables: Assigned offsets relative to the stack frame (base) pointer. Known at compile time, actual stack segment offset computed at runtime.
Function Addresses: Assigned offsets within the code segment at link time. Jump and call instructions use these offsets.
Array Elements: Computed as base_offset + (index × element_size). The computation happens at runtime.
Struct Fields: Each field has a fixed offset from the struct's base. Accessing struct.field adds this offset.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
#include <stdio.h>#include <stdint.h>#include <stddef.h> // for offsetof() /* * How Compilers Generate Offsets * * This demonstrates the various ways offsets appear * in compiled code for a segmented architecture. */ // Global variables - fixed offsets within data segmentint global_a = 100; // Data segment offset determined at link timeint global_b = 200; // Assigned next available offset // Struct with field offsetsstruct Example { int field1; // Offset 0 char field2; // Offset 4 // 3 bytes padding for alignment double field3; // Offset 8 char field4[10]; // Offset 16}; // Total size: 26 bytes (with padding: likely 32) void demonstrate_offsets() { // Local variables - offsets relative to stack frame int local_x = 50; // Offset from BP, e.g., [BP-4] int local_y = 60; // Offset from BP, e.g., [BP-8] // Array - base offset + index computation int array[10]; // array[5] computes: base_offset + (5 * sizeof(int)) printf("=== Offset Analysis ===\n\n"); // Global variable offsets (from data segment base) printf("Global Variables (Data Segment Offsets):\n"); printf(" global_a at address: %p\n", (void*)&global_a); printf(" global_b at address: %p\n", (void*)&global_b); printf(" Relative offset: %td bytes\n\n", (char*)&global_b - (char*)&global_a); // Local variable offsets (from stack frame) printf("Local Variables (Stack Frame Offsets):\n"); printf(" local_x at address: %p\n", (void*)&local_x); printf(" local_y at address: %p\n", (void*)&local_y); printf(" Relative offset: %td bytes\n", (char*)&local_y - (char*)&local_x); printf(" (Negative = stack grows downward)\n\n"); // Struct field offsets printf("Struct Field Offsets:\n"); printf(" struct Example size: %zu bytes\n", sizeof(struct Example)); printf(" field1 offset: %zu\n", offsetof(struct Example, field1)); printf(" field2 offset: %zu\n", offsetof(struct Example, field2)); printf(" field3 offset: %zu\n", offsetof(struct Example, field3)); printf(" field4 offset: %zu\n\n", offsetof(struct Example, field4)); // Array element offsets printf("Array Element Offsets:\n"); printf(" array[0] at: %p\n", (void*)&array[0]); printf(" array[5] at: %p\n", (void*)&array[5]); printf(" Offset computation: 0 + (5 × %zu) = %zu bytes\n", sizeof(int), 5 * sizeof(int)); // Function address (code segment offset) printf("\nFunction Address (Code Segment Offset):\n"); printf(" demonstrate_offsets at: %p\n", (void*)demonstrate_offsets); printf(" main at: %p\n", (void*)main);} /* * Assembly perspective (x86 example): * * ; Accessing struct.field3 (offset 8) * mov eax, [ebx+8] ; EBX contains struct base, 8 is field offset * * ; Accessing array[5] (offset = 5 * 4 = 20) * mov eax, [esi+20] ; ESI contains array base, 20 is computed offset * * ; Accessing local variable (offset from BP) * mov eax, [ebp-4] ; Local at offset -4 from BP in stack segment * * These offsets are all within the current segment. * The segment base is added by hardware during translation. */ int main() { demonstrate_offsets(); return 0;}MOV EAX, [1000] accesses offset 1000)MOV EAX, [EBX] uses EBX as offset)MOV EAX, [EBX+100] computes EBX+100)MOV EAX, [EBX+ECX*4+100] for array access)MOV EAX, [RIP+0x1000])Programmers typically work with 'addresses' that are actually offsets within segments. When you take the address of a variable (&var), you get an offset within some segment. The compiler and linker ensure these offsets are consistent and valid. The physical address is never visible to user-space code—only offsets within the logical segment view.
The offset's most critical property, from a protection standpoint, is that it must be validated against the segment's limit. This bounds checking is the heart of segmentation's memory protection capability.
The Bounds Check:
Before translation can complete, the hardware verifies:
offset < limit
Or equivalently (for segments that include limit byte):
offset ≤ limit
If this check fails, the hardware generates a segment violation fault (similar to a modern segmentation fault), and the offending instruction is aborted.
Why This Matters:
Buffer Overflow Prevention: Writing beyond array bounds into adjacent memory is caught if it exceeds the segment limit
Stack Protection: Stack overflow (using more stack than allocated) triggers a fault rather than corrupting other segments
Code Integrity: Attempting to execute data (wrong segment) or modify code (read-only segment) is prevented
Process Isolation: A process cannot access memory outside its segments, protecting other processes and the OS
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
#include <stdio.h>#include <stdint.h>#include <stdbool.h> /* * Segment Offset Bounds Checking * * Demonstrates how hardware validates offsets * against segment limits before allowing access. */ typedef struct { uint32_t base; uint32_t limit; const char* name;} Segment; // Check if offset is valid for segmentbool is_offset_valid(Segment* seg, uint32_t offset, uint32_t access_size) { // Check if entire access is within bounds // (offset + access_size - 1) must be < limit // Using careful arithmetic to avoid overflow if (offset >= seg->limit) { return false; // Starting offset beyond limit } if (access_size > (seg->limit - offset)) { return false; // Access would extend beyond limit } return true;} void test_access(Segment* seg, uint32_t offset, uint32_t size) { printf("Access: Segment '%s', Offset %u, Size %u bytes\n", seg->name, offset, size); printf(" Segment limit: %u bytes\n", seg->limit); if (is_offset_valid(seg, offset, size)) { printf(" Result: VALID - access permitted\n"); printf(" Physical range: 0x%08X to 0x%08X\n", seg->base + offset, seg->base + offset + size - 1); } else { printf(" Result: INVALID - SEGMENT FAULT!\n"); printf(" Reason: Offset %u + size %u exceeds limit %u\n", offset, size, seg->limit); } printf("\n");} int main() { printf("=== Bounds Checking Demo ===\n\n"); Segment data_seg = { .base = 0x00200000, .limit = 4096, .name = "Data" }; // Valid accesses printf("--- Valid Accesses ---\n"); test_access(&data_seg, 0, 4); // First 4 bytes test_access(&data_seg, 100, 100); // Middle range test_access(&data_seg, 4092, 4); // Last 4 bytes (4092 + 4 = 4096) // Invalid accesses printf("--- Invalid Accesses (Would Cause Faults) ---\n"); test_access(&data_seg, 4096, 1); // Offset exactly at limit test_access(&data_seg, 4000, 100); // Access extends past limit test_access(&data_seg, 5000, 4); // Offset way beyond limit // Edge case: zero-size access printf("--- Edge Case ---\n"); test_access(&data_seg, 4095, 1); // Last valid byte test_access(&data_seg, 4095, 2); // Would extend 1 byte past return 0;} /* * Hardware Implementation Note: * * The bounds check is a simple comparison operation: * if (offset >= limit) generate_fault(); * * For multi-byte accesses, hardware may check: * if (offset + size > limit) generate_fault(); * * This comparison occurs in parallel with other translation * steps, adding minimal latency. The result gates whether * the physical address is actually used. */Different architectures interpret the limit field differently. Some treat it as the last valid offset (limit is inclusive), others as the size (limit is exclusive). Intel x86 in protected mode uses limit as the last valid offset, so a segment of size N has limit = N - 1. Always check the architecture specification!
The offset is the second essential component of every segmented logical address, providing byte-level precision within the segment identified by the segment number.
What's Next:
With both the segment number and offset extracted, the next step is bounds checking—the critical security mechanism that ensures the offset falls within the segment's allocated size. This check is what makes segmentation a protection mechanism, not just an addressing scheme. We'll examine exactly how this validation works and what happens when it fails.
You now understand segment offsets—the byte-level precision that makes segmented addressing practical. You can extract offsets from logical addresses, understand their architectural limits, recognize them in compiled code, and appreciate their role in bounds checking. Next, we'll dive deep into the bounds checking mechanism itself.