Module 6: Relocation Processing
Patching live code: turning placeholder offsets into valid addresses with ADDR64, REL32, and ADDR32NB fixups.
Why This Module?
After sections are loaded and symbols are resolved, the BOF's machine code still contains placeholder values. A CALL instruction might reference offset 0x00000000 where the real target is at 0x00007FFA1A2B3C4D. Relocation entries tell COFFLoader exactly which bytes to patch and how to compute the correct value. This is the final step before the code becomes executable.
Lab Files: step06-relocations
The loader becomes functional. Adds the full relocation loop: ADDR64, REL32, REL32_1-5, ADDR32NB fixups. After this step, the loader can parse, load, link, and execute BOFs.
The Relocation Processing Loop
COFFLoader iterates over every section, and for each section, processes its relocation entries. Each relocation identifies a location in the section that needs patching, the target symbol, and the type of fixup to apply:[1]
Relocation Patching Flow
offset, symIdx, type
internal → section base + value
__imp_ → functionMapping slot
external → GetProcAddress
ADDR64: write 8-byte abs addr
REL32: write 4-byte RIP offset
ADDR32NB: write 4-byte RVA
C// Relocation processing: for each section, apply all relocations
for (int secIdx = 0; secIdx < coff_header->NumberOfSections; secIdx++) {
// Get the relocation table for this section
coff_reloc_t* relocs = (coff_reloc_t*)(
coff_data + sections[secIdx].PointerToRelocations
);
for (int relIdx = 0; relIdx < sections[secIdx].NumberOfRelocations; relIdx++) {
// 1. Where to patch: base of loaded section + VirtualAddress
char* fixupAddress = sectionMapping[secIdx] + relocs[relIdx].VirtualAddress;
// 2. What symbol is referenced
int symIdx = relocs[relIdx].SymbolTableIndex;
// 3. Resolve the symbol to an address (internal, Beacon, or DLL)
void* symbolAddress = resolve_symbol(symIdx, ...);
// 4. Apply the fixup based on relocation Type
apply_relocation(relocs[relIdx].Type, fixupAddress, symbolAddress);
}
}
Resolving the Target Address
The target address depends on the symbol category. COFFLoader determines this during the relocation loop:
C// Determine the target address for a relocation
void* symbolAddress;
int symIdx = relocs[relIdx].SymbolTableIndex;
if (coff_symbol_is_defined(&symbols[symIdx])) {
// Internal symbol: address is section base + symbol value
int targetSection = symbols[symIdx].SectionNumber - 1;
symbolAddress = sectionMapping[targetSection] + symbols[symIdx].Value;
}
else if (/* symbol starts with __imp_ */) {
// External symbol with __imp_ prefix:
// The address points to the functionMapping SLOT (indirect reference)
// functionMapping is indexed by the symbol table index (symIdx)
// The resolved address was already stored during Phase 4:
// functionMapping[symIdx] = process_symbol(symbolName);
// We give the relocation the address OF that slot, not the value in it.
symbolAddress = (char*)&functionMapping[symIdx];
}
else {
// External symbol WITHOUT __imp_ prefix (direct reference):
// Use the resolved address directly from process_symbol()
symbolAddress = process_symbol(symbolName);
}
Internal vs. External: Where the Address Points
For internal symbols, the address points directly into loaded section memory (e.g., a string in .rdata or a helper function in .text). For external __imp_ symbols, the address points to a slot in functionMapping that contains the real address. The BOF code dereferences this slot at runtime via an indirect CALL. This distinction is critical: patching an indirect call target with a direct address (or vice versa) will crash.
AMD64 Relocation Types
COFFLoader handles the following AMD64 relocation types. These are the most common types found in x64 BOFs:
IMAGE_REL_AMD64_ADDR64 (Type 0x0001)[2]
A 64-bit absolute address. The fixup location receives the full 8-byte address of the target symbol. This is used for data pointers, function pointer tables, and any reference that needs a full virtual address.
Ccase IMAGE_REL_AMD64_ADDR64:
// Write the full 64-bit address of the symbol at the fixup location
*(uint64_t*)fixupAddress = (uint64_t)symbolAddress;
break;
TEXTExample: ADDR64 relocation
Before: fixupAddress contains 0x0000000000000000 (placeholder)
Symbol resolves to: 0x00007FFA1A2B3C4D
After: fixupAddress contains 0x00007FFA1A2B3C4D (absolute 64-bit address)
Use case: Global function pointer variable
void (*fnPtr)(void) = SomeFunction;
// The compiler emits an ADDR64 relocation for the initializer
IMAGE_REL_AMD64_ADDR32NB (Type 0x0003)[10]
A 32-bit address without an image base (RVA). In a standard PE file this would be the symbol address minus the image base. COFFLoader has no single image base, so it computes the offset from the fixup location to the target symbol. This relocation is commonly seen in exception handling data (.pdata/.xdata).
Ccase IMAGE_REL_AMD64_ADDR32NB:
// Write a 32-bit offset from fixup location to symbol
// (approximates an RVA for in-memory loaded sections)
*(int32_t*)fixupAddress = (int32_t)(
(uint64_t)symbolAddress - (uint64_t)fixupAddress
);
break;
IMAGE_REL_AMD64_REL32 (Type 0x0004)
The most common relocation type in x64 code. A 32-bit RIP-relative offset. The x64 instruction set uses RIP-relative addressing extensively.[3] The fixup computes the signed 32-bit distance from the end of the instruction to the target:
Ccase IMAGE_REL_AMD64_REL32:
// 32-bit relative offset: target - (fixup_location + 4)
// The +4 accounts for the 4-byte fixup field itself
*(int32_t*)fixupAddress = (int32_t)(
(uint64_t)symbolAddress - ((uint64_t)fixupAddress + 4)
);
break;
TEXTExample: REL32 relocation for a CALL instruction
Instruction: E8 00 00 00 00 (CALL with placeholder offset)
fixupAddress points to the 00 00 00 00 bytes (offset field of CALL)
symbolAddress = 0x00007FF8A1230100 (target function)
fixupAddress = 0x00007FF8A1230050 (location of the offset bytes)
Calculation:
offset = symbolAddress - (fixupAddress + 4)
= 0x00007FF8A1230100 - (0x00007FF8A1230050 + 4)
= 0x00007FF8A1230100 - 0x00007FF8A1230054
= 0xAC
After patching: E8 AC 00 00 00 (CALL +0xAC)
When executed at fixupAddress-1, RIP after fetching = fixupAddress+4,
so target = RIP + 0xAC = fixupAddress + 4 + 0xAC = symbolAddress. Correct!
IMAGE_REL_AMD64_REL32_1 through REL32_5 (Types 0x0005-0x0009)[4]
Variants of REL32 with additional displacement. These handle instructions where the 32-bit relocation field is not at the end of the instruction. The _N suffix means there are N more bytes of instruction after the relocation field:
Ccase IMAGE_REL_AMD64_REL32_1:
// REL32 + 1: the instruction has 1 extra byte after the relocation field
*(int32_t*)fixupAddress = (int32_t)(
(uint64_t)symbolAddress - ((uint64_t)fixupAddress + 4 + 1)
);
break;
case IMAGE_REL_AMD64_REL32_2:
*(int32_t*)fixupAddress = (int32_t)(
(uint64_t)symbolAddress - ((uint64_t)fixupAddress + 4 + 2)
);
break;
case IMAGE_REL_AMD64_REL32_3:
*(int32_t*)fixupAddress = (int32_t)(
(uint64_t)symbolAddress - ((uint64_t)fixupAddress + 4 + 3)
);
break;
case IMAGE_REL_AMD64_REL32_4:
*(int32_t*)fixupAddress = (int32_t)(
(uint64_t)symbolAddress - ((uint64_t)fixupAddress + 4 + 4)
);
break;
case IMAGE_REL_AMD64_REL32_5:
*(int32_t*)fixupAddress = (int32_t)(
(uint64_t)symbolAddress - ((uint64_t)fixupAddress + 4 + 5)
);
break;
When Do REL32_N Variants Appear?
The REL32_1 variant commonly appears with MOV instructions that have a ModR/M byte after the 32-bit displacement, or with LEA instructions that use RIP-relative addressing with an additional immediate byte. For example, mov [rip+disp32], imm8 would use REL32_1 because the 1-byte immediate follows the relocation field. REL32_2 through REL32_5 are progressively rarer but handle instructions with larger trailing data.
i386 Relocation Types
COFFLoader also supports 32-bit x86 relocations for x86 BOFs:[5]
Ccase IMAGE_REL_I386_DIR32: // 0x0006
// 32-bit absolute address (direct)
*(uint32_t*)fixupAddress += (uint32_t)(uintptr_t)symbolAddress;
break;
case IMAGE_REL_I386_REL32: // 0x0014
// 32-bit relative offset (existing value is an addend)
*(uint32_t*)fixupAddress += (uint32_t)(
(uintptr_t)symbolAddress -
((uintptr_t)fixupAddress + 4)
);
break;
Complete Relocation Example
Let us trace through a complete relocation for a BOF that calls KERNEL32$GetCurrentProcessId:
TEXTStep-by-step: Resolving a CALL to KERNEL32$GetCurrentProcessId
1. Section .text is loaded at sectionMapping[0] = 0x1A0000
2. Symbol table entry #7: "__imp_KERNEL32$GetCurrentProcessId"
- SectionNumber = 0 (undefined/external)
- StorageClass = 2 (EXTERNAL)
3. Relocation entry in .text:
- VirtualAddress = 0x1C (offset within .text)
- SymbolTableIndex = 7
- Type = IMAGE_REL_AMD64_REL32 (0x0004)
4. Symbol resolution:
- process_symbol("__imp_KERNEL32$GetCurrentProcessId")
- Strip __imp_ -> "KERNEL32$GetCurrentProcessId"
- Not in InternalFunctions table
- Split on $ -> library="KERNEL32", function="GetCurrentProcessId"
- LoadLibraryA("KERNEL32") -> hKernel32
- GetProcAddress(hKernel32, "GetCurrentProcessId") -> 0x7FFA1A2B0000
5. Store in functionMapping (indexed by symbol table index):
- Symbol table index is 7 (from step 2)
- functionMapping[7] = 0x7FFA1A2B0000 (stored during Phase 4)
- The symbolAddress for relocation = &functionMapping[7] = 0x2A0038
6. Apply REL32 fixup:
- fixupAddress = sectionMapping[0] + 0x1C = 0x1A001C
- offset = 0x2A0038 - (0x1A001C + 4) = 0x100018
- *(int32_t*)0x1A001C = 0x00100018
7. Machine code at 0x1A001A:
Before: FF 15 00 00 00 00 CALL [rip + 0x0]
After: FF 15 18 00 10 00 CALL [rip + 0x100018]
When executed: RIP = 0x1A0020, target = 0x1A0020 + 0x100018 = 0x2A0038
At 0x2A0038: the 8-byte value 0x7FFA1A2B0000 (the real function address)
CPU reads the pointer -> calls GetCurrentProcessId at 0x7FFA1A2B0000
The Two-Level Indirection
For __imp_ symbols, there are two levels of indirection. The REL32 relocation patches the code to point at a functionMapping slot. That slot contains the actual function address. The CPU's CALL [rip+offset] instruction dereferences the pointer automatically. This is identical to how the PE loader handles DLL imports via the Import Address Table (IAT) -- the functionMapping buffer is COFFLoader's equivalent of the IAT.[6]
Error Handling in Relocations
COFFLoader prints a debug message for unrecognized relocation types but does not abort. It also handles the case where process_symbol() returns NULL (unresolvable symbol) by logging the error.[7] In practice, an unresolved symbol usually means the BOF references a DLL function from a library that is not present on the system.
C// COFFLoader handles unknown relocation types with a debug message
default:
printf("ERROR: Unhandled relocation type: 0x%x\n", relocs[relIdx].Type);
break;
Edge Cases and Pitfalls
Relocation processing can fail silently in ways that produce crashes or corrupted behavior rather than clear error messages. Understanding these failure modes is essential for debugging a COFFLoader implementation.
Unknown Relocation Types
The PE/COFF specification defines AMD64 relocation types that COFFLoader does not handle, such as IMAGE_REL_AMD64_SREL32 (0x000E) and IMAGE_REL_AMD64_PAIR (0x000F).[2] When the loader encounters an unrecognized type, the current implementation logs a debug message and continues execution. The fixup location retains its original placeholder value -- typically zero -- and any instruction referencing that location will dereference an invalid address at runtime. A production-quality loader should treat an unknown relocation type as a fatal error, refusing to execute the affected BOF rather than allowing silent corruption.
REL32 Overflow: The 2GB Barrier
REL32 relocations encode a signed 32-bit displacement, limiting the maximum distance between the fixup location and the target symbol to approximately plus or minus 2GB.[3] If COFFLoader allocates section memory at an address far from the resolved DLL function addresses, the computed displacement overflows the 32-bit field. The cast to (int32_t) silently truncates the upper bits, producing an incorrect offset that points to an unrelated memory location. This is a practical concern on 64-bit systems when using VirtualAlloc without specifying a preferred base address near loaded system DLLs. Some loader implementations mitigate this by requesting memory within 2GB of kernel32.dll via a carefully chosen lpAddress hint, or by falling back to a trampoline mechanism that uses an intermediate jump for out-of-range targets.
ADDR32NB in Large Address Spaces
ADDR32NB writes a 32-bit unsigned offset representing the distance from a base address (typically the first loaded section). If sections are allocated in separate VirtualAlloc calls whose results span more than 4GB of virtual address space, this 32-bit offset overflows and wraps around. While this rarely affects typical BOFs with simple section layouts, it can occur if the loader allocates each section independently rather than carving them from a single contiguous memory block. Keeping all section allocations contiguous eliminates this class of failure.
x86 vs. x64 Relocation Differences
The i386 and AMD64 relocation type sets occupy entirely separate namespaces with different type constants and different semantics. The same numeric type value means different things on each architecture. A loader must determine the target architecture from the COFF header's Machine field (0x14C for i386, 0x8664 for AMD64)[8] before interpreting any relocation entry. Applying AMD64 relocation logic to an i386 object file, or vice versa, produces silent data corruption rather than a clean error.
Key architectural differences that affect relocation processing:
- Address width: i386 uses 32-bit absolute addresses (IMAGE_REL_I386_DIR32), while AMD64 requires 64-bit absolute addresses (IMAGE_REL_AMD64_ADDR64) for full pointer values. An i386 BOF cannot reference virtual addresses above the 4GB boundary.
- RIP-relative addressing: AMD64 relies heavily on RIP-relative addressing for both code and data references, making REL32 the dominant relocation type.[9] The i386 architecture has no equivalent of RIP-relative addressing; instead, it uses absolute addresses or constructs position-independent references through the Global Offset Table (GOT) pattern.
- Indirect call mechanics: On x64, an indirect call through
functionMappinguses the instructionCALL [rip+disp32], which requires a REL32 relocation pointing to the slot. On x86, the same pattern usesCALL [disp32]with an IMAGE_REL_I386_DIR32 relocation, since any 32-bit absolute address can reach any location in the 4GB address space. - Pointer size in functionMapping: Each
functionMappingslot is 8 bytes on x64 and 4 bytes on x86. Using the wrong slot size causes all subsequent indirect call targets to be misaligned, producing cascading failures across every resolved external function.
Pop Quiz: Relocation Processing
Q1: An IMAGE_REL_AMD64_REL32 relocation has fixupAddress=0x5000, symbolAddress=0x6100. What 32-bit value is written?
Q2: What is the difference between IMAGE_REL_AMD64_ADDR64 and IMAGE_REL_AMD64_REL32?
Q3: IMAGE_REL_AMD64_REL32_2 differs from REL32 how?
References
- Microsoft, "PE Format: COFF Relocations (Object Only)," Microsoft Learn, 2024. Defines the relocation entry structure fields: VirtualAddress, SymbolTableIndex, and Type.
- Microsoft, "PE Format: Type Indicators, AMD64 Processors," Microsoft Learn, 2024. Documents IMAGE_REL_AMD64_ADDR64 (0x0001), ADDR32NB (0x0003), REL32 (0x0004), SREL32 (0x000E), PAIR (0x000F), and related constants.
- Intel Corporation, Intel 64 and IA-32 Architectures Software Developer's Manual, Volume 2A, Section 2.2.1.6, "RIP-Relative Addressing," 2024. Describes the signed 32-bit displacement encoding and its plus-or-minus 2GB range.
- Microsoft, "PE Format: Type Indicators, AMD64 Processors," Microsoft Learn, 2024. REL32_1 through REL32_5 (0x0005-0x0009) displacement adjustment variants for instructions with trailing bytes after the relocation field.
- Microsoft, "PE Format: Type Indicators, i386 Processors," Microsoft Learn, 2024. IMAGE_REL_I386_DIR32 (0x0006) for absolute 32-bit addresses and IMAGE_REL_I386_REL32 (0x0014) for relative offsets.
- Microsoft, "PE Format: The .idata Section," Microsoft Learn, 2024. Import Address Table (IAT) structure and runtime function pointer resolution via indirect calls.
- TrustedSec, "COFFLoader," GitHub, 2021. Reference implementation of a Beacon Object File loader including process_symbol resolution and relocation error handling.
- Microsoft, "PE Format: COFF File Header," Microsoft Learn, 2024. Machine field values: 0x14C (IMAGE_FILE_MACHINE_I386), 0x8664 (IMAGE_FILE_MACHINE_AMD64).
- Intel Corporation, Intel 64 and IA-32 Architectures Software Developer's Manual, Volume 1, Section 3.3.7, "Addressing in 64-Bit Mode," 2024. Differences between 32-bit and 64-bit addressing modes including the availability of RIP-relative addressing.
- Microsoft, "PE Format: Type Indicators, AMD64 Processors," Microsoft Learn, 2024. ADDR32NB (Type 0x0003) usage for relative virtual addresses in exception handling tables (.pdata/.xdata).
Further Reading
- Microsoft PE Format: COFF Relocations — Official documentation for all COFF relocation types, including AMD64, i386, ARM, and ARM64 type indicators.
- TrustedSec COFFLoader — The reference open-source BOF loader implementation. Study the relocation switch statement in COFFLoader.c for a complete working example.
- Writing a COFF Loader in C — Step-by-step walkthrough of building a COFF loader from scratch, with detailed relocation processing coverage.
- Building Your Own BOF Loader — TrustedSec blog post explaining the design decisions behind COFFLoader, including relocation handling strategies.
- OSDev Wiki: PE Format — Community-maintained reference for the PE/COFF format with practical implementation notes on relocations and section loading.
- x64 Software Conventions — Microsoft documentation on x64 calling conventions, register usage, and stack frame layout that influence relocation patterns.
- Load-time Relocation of Shared Libraries — Eli Bendersky's explanation of how relocations work in ELF loaders, providing useful cross-reference context for understanding PE/COFF relocations.