Skip to content

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:

typedescription
IMAGE_REL_BASED_DIR64The base relocation applies the difference to the 64-bit field at offset.
IMAGE_REL_BASED_HIGHLOWThe base relocation applies all 32 bits of the difference to the 32-bit field at offset
IMAGE_REL_BASED_HIGHThe 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_LOWThe 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;
	}