Appearance
Last review: Sept 15,2025
As described in the previous sections, the expected address where the image should be loaded is hard-coded in a field named ImageBase. In a perfect world, @BASE would be equal to ImageBase. This allows some pointers to be hard-coded:

However, it is not always possible to load the image at a given address, at least for security reasons. One consequence of this is that it breaks some pointers:

To solve this issue, a loader needs to compute the difference between the expected address and the actual one. Then, the pointers are updated by applying this difference. This process is called relocation.
The PE format has a special section called .reloc for this purpose. This section can be found through the IMAGE_DIRECTORY_ENTRY_BASERELOC directory in the NT header. It is a table of blocks, where each block begins with the structure IMAGE_BASE_RELOCATION:
C++
typedef struct _IMAGE_BASE_RELOCATION {
DWORD VirtualAddress;
DWORD SizeOfBlock;
// WORD TypeOffset[1];
} IMAGE_BASE_RELOCATION;
typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;This structure is followed by a series of entries that are simple 16-bit numbers:
The 12 most significant bits define a base relocation offset that is used in combination with the VirtualAddress field to find the location of the pointer to be updated. More formally, it can be found by computing @BASE + VirtualAddress + offset in the entry.
The 4 least significant bits define the type of relocation:
| type | description |
|---|---|
| IMAGE_REL_BASED_DIR64 | The base relocation applies the difference to the 64-bit field at offset. |
| IMAGE_REL_BASED_HIGHLOW | The base relocation applies all 32 bits of the difference to the 32-bit field at offset |
| IMAGE_REL_BASED_HIGH | The base relocation adds the high 16 bits of the difference to the 16-bit field at offset. The 16-bit field represents the high value of a 32-bit word |
| IMAGE_REL_BASED_LOW | The base relocation adds the low 16 bits of the difference to the 16-bit field at offset. The 16-bit field represents the low half of a 32-bit word |
More information can be found in the Microsoft documentation: https://learn.microsoft.com/en-us/windows/win32/debug/pe-format

The following C code provides a well-known example of how a PE loader can perform relocation:
CPP
// Get difference
unsigned long diff = (unsigned long)base_mem - (unsigned long)nt_header->OptionalHeader.ImageBase;
IMAGE_DATA_DIRECTORY base_relocation_directory = nt_header->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];
DWORD size = base_relocation_directory.Size;
DWORD read_size = 0;
while (read_size != size) {
PIMAGE_BASE_RELOCATION base_relocation = (PIMAGE_BASE_RELOCATION)(base_mem + base_relocation_directory.VirtualAddress + read_size);
DWORD virtAddr = base_relocation->VirtualAddress;
DWORD nb_entries = (base_relocation->SizeOfBlock - 2 * sizeof(DWORD)) / sizeof(WORD);
WORD* entries = (WORD *)((unsigned char*)base_relocation + 2 * sizeof(DWORD));//skip IMAGE_BASEçRELOCATION HEADER
for (int entry_idx = 0; entry_idx < nb_entries; entry_idx++) {
WORD offset = entries[entry_idx] & 0x0fff;
WORD type = (entries[entry_idx] & 0xf000)>>12;
unsigned long* ptr = (unsigned long *)(base_mem + virtAddr + offset);
switch (type) {
case IMAGE_REL_BASED_DIR64:
*((ULONG_PTR*)ptr) += (ULONG_PTR)offset;
break;
case IMAGE_REL_BASED_HIGHLOW:
*((DWORD*)ptr) += (DWORD)offset;
break;
case IMAGE_REL_BASED_HIGH:
*((WORD*)ptr) += HIWORD(offset);
break;
case IMAGE_REL_BASED_LOW:
*((WORD*)ptr) += LOWORD(offset);
break;
case IMAGE_REL_BASED_ABSOLUTE:
break;
default:
printf("FAIL\n");
exit(0);
}
}
read_size += base_relocation->SizeOfBlock;
}