Loading content...
Computer programs reference memory through addresses. An instruction like MOV R1, [0x1000] loads data from memory address 0x1000. But what if the program is loaded at physical address 0x50000 instead of 0x00000? The instruction would read from the wrong location—possibly crashing or producing incorrect results.
In the early days of computing, programs were absolute—compiled for a specific memory location and unable to run elsewhere. This was inflexible and made multiprogramming difficult. The solution: relocation—the ability to run programs at any memory location without modification.
The relocation register is the hardware mechanism that makes dynamic relocation possible. It provides a base address that is automatically added to every memory reference, transparently translating the program's logical addresses to actual physical addresses. This page explores relocation in comprehensive detail—from the abstract concept to hardware implementation to practical applications.
By the end of this page, you will understand the address binding problem, the difference between static and dynamic relocation, how the relocation register works at the hardware level, the relationship between relocation and protection, performance implications, and how modern systems have evolved these concepts into MMU-based virtual memory.
To understand relocation, we must first understand address binding—the process of associating symbolic program addresses with actual memory locations.
From Source Code to Execution
Consider a simple C program:
int global_counter = 0; // Where in memory is this?
void increment() { // Where is this function's code?
global_counter++; // How do we find global_counter?
}
int main() {
increment(); // How do we jump to increment()?
return global_counter;
}
At compile time, the compiler doesn't know where in memory global_counter or increment() will reside. It uses symbolic references. At some point, these must be bound to actual addresses.
| Binding Time | Who Binds | Flexibility | Example |
|---|---|---|---|
| Compile Time | Compiler | None | MS-DOS .COM files |
| Link Time | Linker | Within file | Static executables |
| Load Time | Loader | Any location | Traditional relocation |
| Execution Time | Hardware (MMU) | Full flexibility | Virtual memory systems |
Compile-Time Binding
The simplest approach: the compiler generates code for a fixed address. If the program must start at address 0x10000, all addresses in the compiled code reflect that:
; Compiled for load address 0x10000
main: ; At address 0x10100
CALL 0x10200 ; Jump to increment() at 0x10200
MOV R1, [0x10300] ; Load global_counter from 0x10300
RET
increment: ; At address 0x10200
INC [0x10300] ; Increment global_counter at 0x10300
RET
global_counter: ; At address 0x10300
.word 0
This code cannot run if loaded at address 0x50000. The CALL 0x10200 would jump to whatever happens to be at 0x10200, not to increment().
The Need for Relocation
In a multiprogrammed system, the OS cannot guarantee where each program will be loaded. Programs must be able to run at any address. This requires either:
An alternative to relocation is Position-Independent Code, where all addresses are relative rather than absolute. CALL +0x0100 instead of CALL 0x10200 works regardless of load address. PIC is common in shared libraries but historically was more complex to generate and slightly slower to execute.
Static relocation modifies the program's addresses when it is loaded into memory. The loader adds the actual load address to every relocatable address in the program.
How Static Relocation Works
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051
// Original program compiled for address 0// Relocation table included in executableEXECUTABLE FILE: Header: entry_point: 0x0100 // Relative to start text_size: 0x1000 // Code section size data_size: 0x0500 // Data section size relocation_entries: 4 // Number of addresses to fix Relocation Table: // Offsets needing adjustment [0x0102, 0x0108, 0x0202, 0x0208] Code Section (at relative 0x0000): 0x0100: CALL 0x0200 // Call increment() - NEEDS RELOCATION 0x0106: MOV R1, [0x0300] // Load global_counter - NEEDS RELOCATION 0x010C: RET 0x0200: INC [0x0300] // Write global_counter - NEEDS RELOCATION 0x0206: RET Data Section (at relative 0x0300): 0x0300: .word 0 // global_counter // Loader relocates to address 0x50000PROCEDURE LoadWithRelocation(executable: File, load_address: ADDRESS): // Load code and data into memory at load_address CopyToMemory(executable.code, load_address) CopyToMemory(executable.data, load_address + executable.text_size) // Apply relocation to each entry FOR EACH offset IN executable.relocation_table: physical_location := load_address + offset original_value := ReadMemory(physical_location) adjusted_value := original_value + load_address WriteMemory(physical_location, adjusted_value) END FOR // Calculate actual entry point entry := load_address + executable.entry_point RETURN entry // After relocation at 0x50000:MEMORY AT 0x50000: 0x50100: CALL 0x50200 // Fixed: 0x0200 + 0x50000 0x50106: MOV R1, [0x50300] // Fixed: 0x0300 + 0x50000 0x5010C: RET 0x50200: INC [0x50300] // Fixed: 0x0300 + 0x50000 0x50206: RET 0x50300: .word 0 // global_counter at 0x50300Advantages of Static Relocation
Disadvantages of Static Relocation
A program with many global variables and function calls can have thousands of relocatable addresses. The relocation table can become significant—sometimes 10-20% of program size. This was a real concern in memory-constrained early systems.
Dynamic relocation translates addresses at runtime using specialized hardware. Instead of modifying the program at load time, the CPU automatically adds a base address to every memory reference. This is where the relocation register comes in.
The Relocation Register
The relocation register (also called the base register in this context) holds the program's load address. Every memory access issued by the program is intercepted by hardware, which adds the relocation register's value before the access reaches memory.
Physical Address = Logical Address + Relocation Register
The program generates addresses starting at 0. The hardware transparently translates these to physical addresses.
Dynamic Relocation in Action
Consider the same program loaded at 0x50000:
| Program Instruction | Logical Address Generated | Relocation Reg | Physical Address Accessed |
|---|---|---|---|
CALL 0x0200 | 0x0200 | 0x50000 | 0x50200 |
MOV R1, [0x0300] | 0x0300 | 0x50000 | 0x50300 |
INC [0x0300] | 0x0300 | 0x50000 | 0x50300 |
The program's code is identical regardless of where it's loaded. Only the relocation register value changes!
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
// Hardware unit for dynamic address relocationHARDWARE_UNIT RelocationUnit: // The relocation register (privileged - only OS can modify) RelocationRegister: REGISTER[32 bits] // This function executes on EVERY memory access // It happens in parallel with address generation (zero additional cycles) FUNCTION TranslateAddress(logical_address: ADDRESS) -> ADDRESS: // Simply add the relocation register value physical_address := logical_address + RelocationRegister RETURN physical_address END FUNCTION // Context switch updates the relocation registerKERNEL_PROCEDURE SwitchProcess(old_proc: PCB, new_proc: PCB): // Save old relocation value IF old_proc != NULL THEN old_proc.saved_relocation_reg := CPU.RelocationRegister END IF // Load new relocation value CPU.RelocationRegister := new_proc.saved_relocation_reg // All subsequent memory accesses from new_proc will // be translated using the new relocation value END PROCEDURE // Loading a process with dynamic relocationKERNEL_PROCEDURE LoadProcess(program: Executable, base_address: ADDRESS): // Copy program code and data to memory (no address modification needed!) CopyToMemory(program.code, base_address) CopyToMemory(program.data, base_address + program.code_size) // Create process control block pcb := AllocatePCB() pcb.saved_relocation_reg := base_address // Key: store base address pcb.program_counter := program.entry_point // Logical entry point // When this process runs, hardware will translate addresses AddToReadyQueue(pcb) END PROCEDUREWith dynamic relocation, the program doesn't need a relocation table. The executable can be simpler and smaller. Loading is faster because there's no patching. The program can even be moved during execution simply by changing the relocation register (after copying the memory contents).
Both static and dynamic relocation solve the position-independence problem, but with different tradeoffs.
Comparison Matrix
| Aspect | Static Relocation | Dynamic Relocation |
|---|---|---|
| When binding occurs | Load time | Execution time (every access) |
| Who performs binding | Software (loader) | Hardware (CPU) |
| Hardware support needed | None | Relocation/base register + adder |
| Load time overhead | Moderate (patch all addresses) | Minimal (just set register) |
| Runtime overhead | None (addresses fixed) | None (parallel hardware) |
| Program movability | Cannot move after load | Can move by changing register |
| Executable format | Needs relocation table | Simpler format possible |
| Memory compaction | Requires re-relocation | Simple: copy + update register |
| Sharing (libraries) | Each copy separately relocated | Can share with different bases |
Historical Evolution
Early systems (1950s-1960s) used static relocation because hardware was expensive and simple. As transistors became cheaper, dynamic relocation hardware became standard. The CDC 6600 (1964) had base/limit registers. IBM System/360 (1964) supported both static and dynamic relocation. By the 1970s, dynamic relocation via base registers was universal, and paging was emerging.
Today's systems use a hybrid. Executables contain relocation information for the linker/loader (adjusting addresses for ASLR—Address Space Layout Randomization). But at runtime, the MMU provides dynamic address translation via page tables, essentially a sophisticated form of dynamic relocation at page granularity.
The relocation register and limit register work together to provide both position-independence AND memory protection. This combination is fundamental to safe multiprogramming.
The Combined Mechanism
The CPU contains both:
On each memory access:
logical < Limit?physical = Base + logical123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
/* * Combined relocation and protection hardware simulation * This represents what happens in hardware on every memory access */ #include <stdint.h>#include <stdbool.h> typedef struct { uint32_t base; /* Relocation register (physical base) */ uint32_t limit; /* Limit register (region size) */ bool user_mode; /* Current execution mode */} cpu_protection_state; typedef enum { ACCESS_OK, ACCESS_VIOLATION, ACCESS_PRIVILEGE_ERROR} access_result; /* This function represents hardware logic - executes every memory access */access_result translate_and_check( cpu_protection_state *cpu, uint32_t logical_address, uint32_t *physical_address_out, int access_type /* READ, WRITE, EXECUTE */) { /* * Supervisor mode: no translation or checking * The kernel accesses physical memory directly */ if (!cpu->user_mode) { *physical_address_out = logical_address; return ACCESS_OK; } /* * User mode: enforce protection * Step 1: Check bounds BEFORE translation */ if (logical_address >= cpu->limit) { /* * PROTECTION VIOLATION * The hardware will: * 1. Cancel the memory access (it never reaches the bus) * 2. Save the current PC and state * 3. Switch to supervisor mode * 4. Jump to the protection fault handler */ return ACCESS_VIOLATION; } /* * Step 2: Address translation (relocation) * Add base to get physical address */ *physical_address_out = cpu->base + logical_address; /* * The translated address is now used for the actual memory access * The process NEVER sees the physical address */ return ACCESS_OK;} /* * Example: Process A loaded at 0x50000 with 64KB region * Process B loaded at 0x60000 with 128KB region */void demonstrate_isolation(void) { cpu_protection_state proc_a = { .base = 0x50000, .limit = 0x10000, /* 64KB */ .user_mode = true }; cpu_protection_state proc_b = { .base = 0x60000, .limit = 0x20000, /* 128KB */ .user_mode = true }; uint32_t physical; /* Process A: legitimate access to its own memory */ /* Logical 0x1000 -> Physical 0x51000 (in A's region) */ translate_and_check(&proc_a, 0x1000, &physical, ACCESS_READ); /* Result: ACCESS_OK, physical = 0x51000 */ /* Process A: attempt to access beyond its limit */ /* Logical 0x15000 (exceeds 64KB limit) */ translate_and_check(&proc_a, 0x15000, &physical, ACCESS_READ); /* Result: ACCESS_VIOLATION (trapped to OS) */ /* Process A: cannot access B's memory */ /* Even if A knew B's physical address (0x60000), it can't issue it */ /* A's logical address 0x10000 exceeds its limit of 0x10000 */ /* Any address >= 0x10000 violates A's protection */ /* Process B: separate address space */ /* Logical 0x1000 -> Physical 0x61000 (in B's region) */ translate_and_check(&proc_b, 0x1000, &physical, ACCESS_READ); /* Result: ACCESS_OK, physical = 0x61000 */ /* Note: same logical address, different physical address! */}Key Insight: Logical Address Isolation
The combination of relocation and protection creates complete isolation:
This is the foundation of process isolation that persists in modern systems.
This model gives each process its own virtual address space starting at 0. The process thinks it has memory from 0 to (limit-1). The fact that this maps to different physical memory is invisible to the process. This abstraction is the conceptual ancestor of full virtual memory.
Variable partitioning leads to external fragmentation—free memory scattered in small holes. Compaction consolidates free memory by moving processes. Dynamic relocation makes this possible.
The Compaction Process
Without dynamic relocation, compaction requires:
With dynamic relocation, compaction becomes:
Step 3 is trivial—just update a single register value per process!
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364
// Memory compaction with dynamic relocationPROCEDURE CompactMemory(): // Suspend all processes (no scheduling during compaction) PauseScheduler() // Disable interrupts (critical section) DisableInterrupts() // Calculate new positions for all processes new_base := OS_BOUNDARY // Start just after OS FOR EACH process IN processes_ordered_by_current_base: process.new_base := new_base new_base := new_base + process.size END FOR // Move processes that need moving // IMPORTANT: Move in order to avoid overwriting uncopied data FOR EACH process IN processes_ordered_by_current_base: IF process.new_base != process.base THEN // Copy entire process image to new location CopyMemory( source: process.base, destination: process.new_base, size: process.size ) // Update relocation register in process's PCB process.saved_relocation_reg := process.new_base // If this process is current, update hardware register too IF process == current_process THEN CPU.RelocationRegister := process.new_base END IF // Old location is now free (part of consolidated hole) process.base := process.new_base END IF END FOR // Update free list: now one large hole at top free_list := CreateHole(new_base, MEMORY_TOP - new_base) // Resume normal operation EnableInterrupts() ResumeScheduler() // All processes continue transparently // They don't know they were moved! END PROCEDURE // The key insight: no process code is modified during compaction// Only the relocation register values change// Processes are completely unaware of the move EXAMPLE: Before: Process P2 at Base=0x30000, accessing logical 0x1000 -> Physical 0x31000 After compaction: P2 moved to Base=0x20000 P2 still accesses logical 0x1000 -> Physical 0x21000 (different location, same content) P2's code is identical. Only the PCB's relocation register changed.While dynamic relocation makes compaction possible, it's still expensive. Copying megabytes of memory takes significant time. All processes are frozen during compaction. For systems requiring high availability, compaction is problematic. This motivated the development of paging, which avoids external fragmentation entirely.
Let's examine how real systems implemented relocation, from historical machines to modern concepts.
CDC 6600 (1964)
The CDC 6600 supercomputer used an 18-bit relocation register per job. Memory addresses were 18 bits, allowing up to 256K words (each word was 60 bits). The relocation register provided both base address and protection. The 10 peripheral processors (PPs) had their own separate memories without relocation.
IBM System/360 (1964)
System/360 offered more sophisticated options:
The /360 could operate in three modes:
| System | Year | Relocation Type | Register Size | Protection |
|---|---|---|---|---|
| CDC 6600 | 1964 | Dynamic | 18 bits | Base/Bound |
| IBM S/360 MFT | 1966 | Static | N/A (patched) | Memory Keys |
| IBM S/360 MVT | 1967 | Dynamic | 24 bits | Memory Keys |
| PDP-11 | 1970 | Dynamic | 16 bits (segmented) | Per-segment |
| Intel 8086 | 1978 | Dynamic | 20 bits (segments) | None (real mode) |
| Motorola 68000 | 1979 | Static/Dynamic | 24 bits | Supervisor bit only |
Modern Echoes: ASLR and Position-Independent Executables
Modern systems still use relocation concepts:
Address Space Layout Randomization (ASLR):
Position-Independent Executables (PIE):
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162
/* * Modern relocation concepts: GOT and PLT * This is how shared libraries achieve position independence */ /* * Traditional code (non-PIC) - problematic for shared libraries: * * CALL 0x12345678 ; Absolute address of printf * MOV EAX, [0x87654321] ; Absolute address of global_var * * These addresses would need to change based on where the * library is loaded - requiring separate copies of the code! */ /* * Position-Independent Code (PIC) - modern approach: * * Code references (function calls) use PLT (Procedure Linkage Table): * * CALL printf@PLT ; Call through PLT entry * ; PLT entry contains: * ; JMP *[GOT entry for printf] * * Data references use GOT (Global Offset Table): * * MOV EAX, global_var@GOT ; Get GOT offset (known at compile time) * ADD EAX, [GOT_BASE] ; Add GOT base (found at runtime via PC) * MOV EAX, [EAX] ; Load actual address from GOT * MOV EBX, [EAX] ; Finally access the variable */ /* Example of how GOT works conceptually */struct global_offset_table { void *printf_addr; /* Filled by dynamic linker */ void *malloc_addr; /* Filled by dynamic linker */ int *global_var_addr; /* Filled by dynamic linker/loader */ /* ... more entries */}; /* The GOT itself is at a fixed offset from the code (known at link time) *//* The GOT entries are filled at load time with actual addresses */ /* * Runtime flow: * 1. Loader loads shared library at random address (ASLR) * 2. Loader/dynamic linker fills in GOT entries with actual addresses * 3. Code uses PC-relative access to find GOT * 4. Code uses GOT entries to find actual function/data addresses * * Result: Same code works at any load address! * This is essentially dynamic relocation, but more sophisticated */ /* * Compiling for position independence: * * $ gcc -fPIC -shared -o libexample.so example.c * * -fPIC: Generate position-independent code * -shared: Create shared library (not executable) */While the simple relocation register has been superseded by MMUs and page tables, the concept of address translation at runtime remains central to computing. Every modern system translates virtual addresses to physical addresses—it's just done with more sophisticated mechanisms (page tables, TLBs) rather than a single register.
We have explored the relocation register mechanism in comprehensive detail. Let's consolidate the essential concepts:
physical = base + logical.What's Next:
With both protection and relocation mechanisms understood, we're ready to examine the advantages and limitations of contiguous allocation as a whole. This comprehensive analysis will reveal why paging and segmentation were developed.
You now understand relocation registers in complete detail: the address binding problem they solve, static vs dynamic relocation tradeoffs, hardware implementation, integration with protection, support for compaction, and evolution into modern systems. This foundation is essential for understanding virtual memory and modern MMU operation.