Every executable and every DLL on Windows shares one on-disk container: the Portable Executable (PE) format. When you double-click a program or a process loads a library, the Windows loader reads that PE file, maps it into memory, wires up the functions it depends on, and finally jumps to its entry point. Understanding the PE layout and what the loader does with it is foundational for reverse engineering, for debugging mysterious load failures, and for reasoning about a large family of injection and hijacking techniques. This article walks the PE structure field by field and then follows the loader as it turns a file on disk into running code.
PE File Structure
A PE file begins, for historical reasons, with a DOS header. Its first two bytes are the ASCII characters MZ (the initials of Mark Zbikowski), and it is followed by a small DOS stub — the little program that prints “This program cannot be run in DOS mode.” The only field in the DOS header that still matters is e_lfanew, a file offset pointing at the real header.
At that offset sits the PE signature (PE\0\0), immediately followed by the COFF File Header (machine type, number of sections, timestamp, characteristics) and the Optional Header — which, despite the name, is mandatory for images. The Optional Header carries the fields the loader cares about most:
ImageBase— the preferred virtual address at which the image wants to be loaded.AddressOfEntryPoint— the RVA of the first instruction to run (an EXE’s start thunk, or a DLL’sDllMain).SectionAlignmentandFileAlignment— how sections are aligned in memory versus on disk.- The Data Directory array — a table of (RVA, size) pairs locating the import table, export table, base relocation table, resources, TLS directory, and more.
After the headers come the section headers and then the sections themselves: .text (executable code), .data (writable initialized data), .rdata (read-only data, including the import and export tables), .rsrc (resources), and .reloc (base relocations). Almost every address inside a PE is expressed as an RVA (Relative Virtual Address) — an offset from the image’s base once loaded — rather than an absolute address, precisely so the image can be placed anywhere in memory.
Imports and the IAT
Almost no program is self-contained; it calls functions in other DLLs. The Import Directory lists every DLL the image depends on and, for each, the functions it needs. For every imported function there are two parallel arrays: the Import Name Table (the “hint/name” entries, describing what to import) and the Import Address Table (IAT). At load time the loader resolves each import to the function’s real address and writes that address into the IAT. From then on, a call like CreateFileW is really an indirect call through the IAT slot the loader filled in.
Functions can be imported by name (the usual case) or by ordinal (by numeric index into the target DLL’s export table, saving the name lookup). Because the IAT is just a table of function pointers that gets patched at runtime, it is a natural target for both instrumentation and abuse, as we will see.
Exports
The other side of the contract is the Export Directory, present in DLLs that expose functions. It contains three related arrays: the Export Address Table (EAT), holding the RVAs of the exported functions; the name pointer table, holding the exported names in sorted order; and the ordinal table, mapping each name to an index into the EAT. When code calls GetProcAddress(hModule, "SomeFunc"), the loader binary-searches the name table, uses the matching ordinal to index the EAT, adds the module base to the RVA, and returns the function pointer.
An export can also be a forwarded export: instead of an RVA into the DLL’s own code, the EAT entry is a string like NTDLL.RtlAllocateHeap, telling the loader to resolve the call in another DLL entirely. This is how, for example, many kernel32 functions transparently forward into ntdll or the API-set schema DLLs.
The Loader Process
Turning a PE file into running code is the job of the loader — the Ldr* routines inside ntdll.dll. The sequence, roughly, is:
- Map the image. The file is mapped into the address space as an image section, with each PE section placed at its RVA and given the appropriate page protection (
.textexecute-read,.dataread-write, and so on). - Register the module. The loader records the new module in the three linked lists hanging off
PEB_LDR_DATA—InLoadOrderModuleList,InMemoryOrderModuleList, andInInitializationOrderModuleList— the same lists we walked in the earlier article on the process and the PEB. - Resolve imports. For each imported DLL, the loader loads it (recursively resolving its imports), then fills the IAT with resolved addresses.
- Apply relocations if the image did not land at its preferred
ImageBase(see below). - Run initializers. Any TLS callbacks run first, then the entry point is called — for a DLL,
DllMainwithDLL_PROCESS_ATTACH.
The detail that TLS callbacks execute before the entry point is important for analysts: malware often hides its first actions in a TLS callback precisely because a debugger set to break on the entry point will already have run it.
Base Relocations and ASLR
Because RVAs are relative, most of a PE is position-independent — but not all of it. Some instructions and data embed absolute addresses that assume the image sits at ImageBase. When ASLR loads the image somewhere else (which is the common case today), those absolute values are wrong. The .reloc section, the base relocation table, is a list of every location that needs fixing. The loader computes the delta between the actual load address and ImageBase and adds it to each listed location. A DLL with its relocations stripped can only load at its preferred base, and will fail if that base is already occupied — one reason relocations still matter in an ASLR world.
Inspecting PE Files and Loaded Modules
All of this is directly observable, both statically on the file and dynamically in a debugger:
# Static analysis of a PE on disk (Visual Studio tools):
dumpbin /headers target.dll # DOS/PE/Optional headers, sections
dumpbin /imports target.dll # imported DLLs and the IAT layout
dumpbin /exports target.dll # the export table (EAT / names / ordinals)
# Dynamic view in WinDbg:
lm # list loaded modules (from the PEB Ldr lists)
!dh target # dump the PE headers of a loaded module
!dh -f target # include the section and data-directory detail
x target!* # enumerate a module's symbols/exports
dumpbin shows you exactly what the loader will read; !dh shows the same structures after the image is mapped. Crucially, lm and Process Explorer build their module lists by walking the PEB_LDR_DATA lists described above — which means anything that never registered with the loader will not appear there. That gap is the whole point of the injection techniques below.
Security Relevance
The PE format and the loader sit underneath a remarkable amount of offensive and defensive tradecraft:
- DLL search-order hijacking and phantom DLLs. When an application imports a DLL by name without a full path, the loader searches a defined order of directories. If an attacker can drop a malicious DLL earlier in that search order — or supply a DLL the application tries to load but that does not exist (“phantom” DLL) — their code is loaded into the target process legitimately.
- IAT and EAT hooking. Because the IAT is just a table of pointers, overwriting a slot redirects every call through it. Both EDR products (for visibility) and malware (for control) hook the IAT or EAT to intercept API calls in user mode.
- Manual mapping / reflective loading. An attacker can replicate the loader’s steps — map sections, resolve imports, apply relocations, call the entry point — without calling the real loader. The payload DLL is therefore never registered in the PEB module lists, so tools that enumerate modules the normal way do not see it. This is exactly why memory forensics cross-references the VAD tree (ground truth of what is mapped) against the loader’s advertised module list, as covered earlier in the series.
- TLS-callback anti-analysis. Running code in a TLS callback before the entry point is a classic trick to execute before a debugger’s entry-point breakpoint fires.
For defenders, the takeaways are concrete: prefer fully-qualified DLL paths and safe search modes to blunt hijacking, watch for modules present in memory but absent from the loader lists, and treat unexpected IAT/EAT modifications in sensitive processes as suspicious. (All of this is described for defensive understanding and authorized testing only.)
Conclusion
The PE format is the blueprint and the loader is the builder: headers describe where everything goes, the import and export tables define the contract between modules, relocations reconcile that blueprint with ASLR, and the loader stitches it all together and hands control to the entry point. Because so much of this happens by patching tables at runtime — the IAT, the module lists, the relocations — it is also where a great deal of injection and evasion lives. Having followed code from disk into memory, the next article turns to how the processor interrupts that code: interrupts, IRQLs, DPCs, and APCs.
You Might Also Like
This article is part of the Windows Internals series. These related deep-dives cover adjacent parts of the system:



Comments