Loading learning content...
Software-only protection is fundamentally unreliable. A malicious program can modify any software protection mechanism, and even a buggy program can accidentally corrupt protection data structures. For memory protection to be trustworthy, it must be enforced by hardware.
The base and limit register mechanism provides exactly this guarantee. Every memory access issued by the CPU is checked by hardware logic before it reaches the memory bus. If the access violates the process's allocated region, the hardware traps to the operating system before any damage occurs. The process never even sees the protected memory.
This page examines the base/limit protection mechanism in comprehensive detail—its hardware implementation, the address translation it provides, its role in context switching, and its inherent limitations. Understanding this mechanism is essential because its principles underlie all modern memory protection, including paging-based systems.
By the end of this page, you will understand how base/limit registers work at the hardware level, how they provide both protection and relocation, the timing of protection checks in the memory access pathway, how the OS manages these registers during context switches, and why this mechanism, despite its elegance, was insufficient for modern operating systems.
Before examining the base/limit mechanism specifically, let's establish what memory protection must achieve in a multiprogrammed system.
Core Protection Goals
Memory protection must guarantee:
Isolation: Each process operates as if it owns the entire machine. It cannot observe or modify other processes' memory.
OS Integrity: User processes cannot access operating system code or data. The kernel's data structures remain inviolate.
Containment: Faulty or malicious code in one process cannot damage other processes or the system.
Recoverable Failures: When a process violates protection, the system can terminate that process and continue operating.
Why Software Protection Fails
Software protection mechanisms are inherently vulnerable:
| Aspect | Software Protection | Hardware Protection |
|---|---|---|
| Bypassability | Can be disabled or modified | Cannot be bypassed by software |
| Performance | Significant overhead per access | Zero overhead (parallel check) |
| Reliability | Depends on software correctness | Guaranteed by physical logic |
| Granularity | Flexible (any size) | Fixed by hardware design |
| Flexibility | Easy to modify | Fixed until hardware redesign |
| Trusted Base | Entire OS must be correct | Only hardware + minimal kernel |
Hardware provides the enforcement mechanism that cannot be subverted by user code. Software (the operating system) manages the mechanism—setting register values, handling violations, and making allocation decisions. Neither can provide adequate protection alone; they work together to create a secure system.
The base/limit mechanism uses two special CPU registers:
Base Register: Contains the starting physical address of the process's memory region.
Limit Register: Contains the size of the process's memory region (in bytes or memory units).
Together, these define a contiguous region of physical memory: [Base, Base + Limit).
Register Characteristics
| Property | Description |
|---|---|
| Register Size | Same width as physical address bus (e.g., 32 bits for 4GB addressable) |
| Accessibility | Privileged—only modifiable in supervisor/kernel mode |
| Context | Per-process—saved and restored during context switch |
| Check Timing | Every memory reference checked before bus access |
| Granularity | Typically byte-level (some systems used larger units) |
Alternative Formulation: Base and Bound
Some architectures use a base and bound scheme instead of base and limit:
The check becomes: Base ≤ Physical_Address ≤ Bound
This is mathematically equivalent but changes the comparison logic:
| Scheme | Registers | Valid Address Range | Check Logic |
|---|---|---|---|
| Base/Limit | Base=0x40000, Limit=0x10000 | 0x40000 to 0x4FFFF | logical < Limit |
| Base/Bound | Base=0x40000, Bound=0x4FFFF | 0x40000 to 0x4FFFF | physical ≤ Bound |
The choice affects hardware implementation details but not the fundamental protection model.
Base/Limit allows checking BEFORE address translation (against the logical address), catching violations slightly earlier. Base/Bound requires translation first, then checking. Modern paging systems generally check during translation, combining the operations for efficiency.
The base/limit mechanism provides not just protection but also address translation. This is a crucial feature that enables position-independent program execution.
The Translation Process
User programs are compiled assuming they start at address 0. The CPU addresses they generate are logical addresses (or virtual addresses). The base register converts these to physical addresses:
Physical Address = Base Register + Logical Address
Example:
A process is loaded at physical address 0x40000 (Base = 0x40000):
| Logical Address | Base Register | Physical Address |
|---|---|---|
| 0x0000 (start) | 0x40000 | 0x40000 |
| 0x1000 (code) | 0x40000 | 0x41000 |
| 0x5000 (data) | 0x40000 | 0x45000 |
| 0xFFFF (stack) | 0x40000 | 0x4FFFF |
The program "sees" memory starting at 0, but every access is transparently redirected to its actual physical location.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960
// This logic is implemented IN HARDWARE for performance// It executes on EVERY memory access - loads, stores, instruction fetches HARDWARE_UNIT MemoryManagementUnit: // Protection registers (privileged access only) BaseRegister: REGISTER[32 bits] // Physical base address LimitRegister: REGISTER[32 bits] // Region size // Translate and validate a memory access FUNCTION TranslateAddress(logical_address: ADDRESS[32 bits], access_type: [READ | WRITE | EXECUTE], mode: [USER | SUPERVISOR]) -> ADDRESS[32 bits]: // STEP 0: Supervisor Mode Bypass // OS kernel has unrestricted access (no translation, no check) IF mode = SUPERVISOR THEN RETURN logical_address // Direct physical access END IF // STEP 1: Bounds Check (BEFORE translation) // Ensure logical address is within allocated region IF logical_address >= LimitRegister THEN RAISE PROTECTION_FAULT( type: "SEGMENTATION_FAULT", faulting_address: logical_address, access_type: access_type ) // Execution halts here - control transfers to OS END IF // STEP 2: Address Translation // Add base to get physical address physical_address := BaseRegister + logical_address // STEP 3: Return Physical Address // Memory controller receives this address RETURN physical_address END FUNCTION // Protection fault handling (transfers control to OS) PROCEDURE RAISE PROTECTION_FAULT(type, faulting_address, access_type): // Save current processor state SaveProcessorState() // Switch to supervisor mode CurrentMode := SUPERVISOR // Jump to kernel fault handler (fixed address) ProgramCounter := FAULT_HANDLER_VECTOR[PROTECTION_FAULT] // Kernel will examine fault, typically terminate offending process END PROCEDURE END HARDWARE_UNIT // Note: The entire translation takes 0 additional clock cycles// because the comparison and addition happen IN PARALLEL with// the memory access pipeline - this is pure hardware logicRelocation Benefits
This translation mechanism provides several crucial benefits:
For user-mode processes, address translation is mandatory. There is no way for a user process to issue a physical address directly. This ensures that protection cannot be bypassed by clever programming—the hardware intercepts every memory access before it reaches the memory bus.
To fully understand base/limit protection, we must trace the complete pathway of a memory access from instruction to physical memory.
The Access Pipeline
When the CPU executes a memory-referencing instruction (load, store, or instruction fetch):
logical_address ≥ LimitRegister: TRAP (protection violation)logical_address < LimitRegister: Continuephysical_address = BaseRegister + logical_address
This addition uses a dedicated hardware adder, also parallel with the checkZero-Overhead Protection
Note that the protection check and address translation add zero additional cycles to memory access. The comparison (addr < Limit) and addition (Base + addr) happen using dedicated hardware that operates in parallel with other pipeline stages.
This is critical for system performance. If protection added overhead on every memory access (and a modern program performs billions of accesses per second), the system would slow to a crawl.
Parallel Execution Timing
Cycle 1: | Address Gen | Compare | Add |
|-------------|---------|-----|
| 0x00001234 | < Limit | + Base |
Cycle 2: | Bus Request with Physical Address |
The comparison and addition complete within the address generation cycle.
In modern pipelined CPUs, address translation is typically part of the Address Generation Unit (AGU) or is immediately after it. The translation logic is optimized to complete within a single cycle. More complex schemes (like multi-level page tables) require caching (TLB) to achieve similar performance.
With multiple processes sharing the CPU, the protection registers must be updated every time the CPU switches from one process to another. This is a critical part of the context switch.
Context Switch Requirements
When switching from Process A to Process B:
From that moment, all memory accesses use Process B's base and limit, automatically restricting it to its allocated memory region.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
/* * Context switch code (simplified from x86-style kernel) * This runs in the kernel with interrupts disabled */ struct process_control_block { uint32_t pid; uint32_t state; /* General purpose registers */ uint32_t eax, ebx, ecx, edx; uint32_t esi, edi, ebp, esp; uint32_t eip; /* Instruction pointer */ uint32_t eflags; /* CPU flags */ /* Segment/Protection registers (for base/limit systems) */ uint32_t base_register; /* Memory base address */ uint32_t limit_register; /* Memory region size */ /* Modern systems would have CR3 (page table base) instead */}; typedef struct process_control_block pcb_t; /* Currently running process */pcb_t *current_process; /* Switch from current to next process */void context_switch(pcb_t *next) { pcb_t *prev = current_process; /* * PHASE 1: Save current process state * In real hardware, some of this is done by the trap/interrupt mechanism */ if (prev != NULL) { /* Save general registers (via inline assembly in reality) */ asm volatile ( "movl %%eax, %0 " "movl %%ebx, %1 " "movl %%ecx, %2 " "movl %%edx, %3 " : "=m"(prev->eax), "=m"(prev->ebx), "=m"(prev->ecx), "=m"(prev->edx) ); /* Save protection registers */ /* (On real systems, these are special register reads) */ prev->base_register = read_base_register(); prev->limit_register = read_limit_register(); /* Save stack and instruction pointers */ prev->esp = read_esp(); prev->eip = read_eip(); prev->eflags = read_eflags(); prev->state = PROCESS_READY; } /* * PHASE 2: Load new process state */ current_process = next; next->state = PROCESS_RUNNING; /* CRITICAL: Load protection registers BEFORE restoring user context */ /* This prevents any possibility of accessing wrong memory */ write_base_register(next->base_register); write_limit_register(next->limit_register); /* Restore general registers */ asm volatile ( "movl %0, %%eax " "movl %1, %%ebx " "movl %2, %%ecx " "movl %3, %%edx " : : "m"(next->eax), "m"(next->ebx), "m"(next->ecx), "m"(next->edx) ); /* Restore stack pointer */ write_esp(next->esp); /* Restore flags and jump to user code */ /* This typically involves IRET or similar instruction */ restore_and_jump(next->eflags, next->eip); /* Never reaches here - we're now running as 'next' process */} /* * Protection register access (privileged operations) * These would be implemented as special CPU instructions */static inline uint32_t read_base_register(void) { uint32_t base; asm volatile ("mov %%db0, %0" : "=r"(base)); /* Hypothetical */ return base;} static inline void write_base_register(uint32_t base) { asm volatile ("mov %0, %%db0" : : "r"(base)); /* Hypothetical */} static inline uint32_t read_limit_register(void) { uint32_t limit; asm volatile ("mov %%db1, %0" : "=r"(limit)); /* Hypothetical */ return limit;} static inline void write_limit_register(uint32_t limit) { asm volatile ("mov %0, %%db1" : : "r"(limit)); /* Hypothetical */}Critical Ordering Requirement
Notice the comment: Load protection registers BEFORE restoring user context.
This ordering is essential for security:
The window between register loads is typically just a few instructions, but even a few-cycle window is a security vulnerability in a malicious context.
Modern systems often provide atomic mechanisms to update multiple protection registers. For example, x86-64 switches the entire page table (via CR3 register) in a single instruction. This eliminates any window where protection is partially updated.
When a process attempts to access memory outside its allocated region, the hardware generates a protection fault (also called a memory access violation or segmentation fault). Understanding the fault handling mechanism is essential for system design.
The Fault Sequence
When the limit check fails:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
/* * Protection fault handler (kernel code) * Called by hardware when a process violates memory bounds */ #include <kernel.h>#include <process.h>#include <signal.h>#include <console.h> struct fault_info { uint32_t faulting_address; /* Address that caused the fault */ uint32_t instruction_pointer; /* EIP at time of fault */ uint32_t error_code; /* Hardware error code */ uint32_t access_type; /* READ, WRITE, or EXECUTE */}; /* * Main protection fault handler * This is registered at FAULT_VECTOR[0x0D] (example number) */void protection_fault_handler(struct fault_info *info) { pcb_t *faulting_process = current_process; printk("PROTECTION FAULT"); printk(" Process: %d (%s)", faulting_process->pid, faulting_process->name); printk(" Faulting Address: 0x%08x", info->faulting_address); printk(" Instruction at: 0x%08x", info->instruction_pointer); printk(" Access Type: %s", info->access_type == ACCESS_READ ? "READ" : info->access_type == ACCESS_WRITE ? "WRITE" : "EXECUTE"); printk(" Process Base: 0x%08x", faulting_process->base_register); printk(" Process Limit: 0x%08x", faulting_process->limit_register); /* * DECISION POINT: What to do with the process? * * Option 1: Terminate immediately (unsafe process) * Option 2: Send signal to process (SIGSEGV) - allows handler * Option 3: Core dump for debugging * Option 4: Log and continue (some embedded systems) */ /* Check if process has a signal handler for SIGSEGV */ if (has_signal_handler(faulting_process, SIGSEGV)) { /* * Process registered a handler - deliver signal * This allows the process to attempt recovery or clean exit */ printk(" Delivering SIGSEGV to process handler"); deliver_signal(faulting_process, SIGSEGV, info); /* * Return from handler will eventually re-execute faulting * instruction unless the handler modifies the context */ return_to_user(); } else { /* * No handler - terminate process with core dump */ printk(" No handler registered - terminating process"); /* Generate core dump if configured */ if (faulting_process->dump_on_fault) { generate_core_dump(faulting_process, info); } /* Release process resources */ release_process_resources(faulting_process); /* Remove from ready queue */ dequeue_process(faulting_process); /* Free memory partition */ free_partition(faulting_process->base_register, faulting_process->limit_register); /* Mark process as terminated */ faulting_process->state = PROCESS_TERMINATED; faulting_process->exit_code = -SIGSEGV; /* Notify parent process (if waiting) */ wakeup_parent(faulting_process); /* Schedule another process to run */ schedule(); /* Never returns - now running different process */ }} /* * Analyze the fault for debugging purposes */void analyze_fault(struct fault_info *info, pcb_t *proc) { uint32_t addr = info->faulting_address; uint32_t base = proc->base_register; uint32_t limit = proc->limit_register; if (addr < base) { /* Access below allocated region */ printk(" Analysis: Access BELOW base (0x%x < 0x%x)", addr, base); printk(" Possible cause: NULL pointer dereference"); } else if (addr >= base + limit) { /* Access above allocated region */ printk(" Analysis: Access ABOVE limit"); printk(" Logical address 0x%x >= limit 0x%x", addr - base, limit); printk(" Possible cause: Buffer overflow, stack overflow"); } /* Check if near the stack boundary */ if (addr >= base + limit - 4096 && addr < base + limit) { printk(" This may be a stack overflow!"); }}Protection faults are the operating system's first line of defense against buggy or malicious programs. By catching violations immediately, the system prevents any actual memory corruption. The faulting instruction never completes—the illegal write never happens—so other processes and the kernel remain untouched.
Despite its elegance, the base/limit mechanism has significant limitations that eventually led to its replacement by paging-based systems.
Inherent Limitations
| Capability | Base/Limit | Paging |
|---|---|---|
| Non-contiguous allocation | ❌ No | ✅ Yes |
| Memory sharing between processes | ❌ No | ✅ Yes |
| Dynamic growth (heap, stack) | ❌ No (requires relocation) | ✅ Yes (add pages) |
| Mixed access permissions | ❌ No | ✅ Yes (per-page) |
| Virtual memory (exceed RAM) | ❌ No | ✅ Yes |
| Protection granularity | Entire partition | Individual pages (4KB) |
| External fragmentation | ❌ Severe | ✅ None |
| Implementation complexity | ✅ Simple | Moderate |
| Hardware requirements | ✅ Minimal | MMU + TLB |
Evolution to Paging
These limitations drove the development of paging, which:
The page table replaces the base/limit registers, providing far richer capabilities at the cost of additional hardware (MMU, TLB) and software complexity.
Modern x86 processors still have base/limit registers as part of their segmentation hardware (GDT/LDT entries contain base and limit fields). However, modern operating systems configure "flat" segments that span the entire address space (base=0, limit=max), effectively disabling segmentation in favor of pure paging. The segment hardware exists for backward compatibility but is not used for protection in 64-bit mode.
We have examined the base/limit register protection mechanism in detail. Let's consolidate the essential concepts:
logical_address < Limit, then translated: physical = Base + logical.What's Next:
With base/limit protection understood, we're ready to explore the closely related relocation register concept, which focuses specifically on the address translation aspect and how it enables position-independent program execution.
You now understand base/limit register protection in comprehensive detail: its hardware implementation, address translation mechanics, context switch handling, fault processing, and inherent limitations. This foundation is essential for understanding both historical systems and the design decisions behind modern memory protection.