Before you start#
Review ELF and linking, virtual memory, and Wine architecture and prefixes. This chapter reads Wine wine-10.0, commit b073859675060c9211fcbccfd90e4e87520dc2c2, from the official Wine mirror. The supplied PE values are synthetic teaching data, not bytes captured from an executable. No Windows application or Wine guest was run.
Learning goals#
Decode the relationship among file offsets, RVAs and virtual addresses; trace Wine's module loading, relocation and import failure branches; and explain loader-lock and module-lifetime constraints.
Source walkthrough#
- Validate image
Read headers and establish architecture and image layout.
- Map sections
Relate file offsets, image addresses and protection.
- Resolve dependencies
Load modules and bind imported names or ordinals.
- Initialize and unwind
Run applicable initialization; release references on failure.
PE/COFF is the executable and object-file family used by Windows software. A PE image begins with a DOS-compatible header and stub. The DOS header's e_lfanew field identifies the file offset of the PE signature and headers; a parser must bounds-check that offset and all later ranges before trusting them. The PE signature is followed by a COFF file header with a machine type, section count and optional-header size. The optional header is optional in the COFF sense; for a normal executable image it contains important image information, including whether it uses the PE32 or PE32+ form. PE32 and PE32+ differ in field layout and address width; neither should be selected merely from a filename.
An image's section table describes sections such as code and data, with raw file positions and sizes, virtual addresses and characteristics. FileAlignment governs relevant file-layout alignment; SectionAlignment governs image-layout alignment subject to format rules. The raw section bytes can be shorter than the virtual size, with the remaining mapped region represented as zero-filled memory under the image loader's rules. Section characteristics influence permissions, but actual mappings and enforcement depend on the loader and host. An invalid or overlapping header requires careful validation; this lesson does not ask you to execute malformed images.
A relative virtual address (RVA) is an offset relative to an image's mapped base. A virtual address (VA) is a process address after adding a base where that relation applies. A file offset identifies a byte in the on-disk file. These values can be numerically different for the same section. An RVA can also identify zero-filled virtual space that has no corresponding initialized file byte. Translating an RVA to a file offset requires the section's virtual and raw ranges and correct bounds checks, not merely adding the RVA to the start of the file.
Worked header and section values#
Consider this synthetic PE32+ teaching record. The DOS header is at file offset zero, e_lfanew is 0x80, and bytes at file offset 0x80 contain the four-byte PE\\0\\0 signature. Suppose the image's preferred base is 0x140000000, its section alignment is 0x1000 and file alignment is 0x200. One section has virtual address 0x1000, virtual size 0x600, raw file offset 0x400 and raw size 0x400. These values are chosen for arithmetic; they are not claimed as a complete valid executable.
Synthetic teaching data, not a device capture.
Select a field to connect its position, bytes, and meaning.
10 bytes in file order. Offsets start at zero.
Selected: e_magic. DOS header signature "MZ" at file offset 0.
Read the byte table
| Offset | Field | Hex bytes | Interpretation |
|---|---|---|---|
| 0 | e_magic | 4d 5a | DOS header signature "MZ" at file offset 0 |
| 60 | e_lfanew | 80 00 00 00 | File offset of the PE header, stored at offset 0x3C |
| 128 | PE signature | 50 45 00 00 | PE followed by two zero bytes, at file offset 0x80 |
For RVA 0x1120, subtract the section RVA 0x1000 to get section offset 0x120. That offset is within both the stated virtual size and raw size, so the corresponding initialized byte is at file offset 0x400 + 0x120 = 0x520. If the image is mapped at its preferred base, the process VA is 0x140000000 + 0x1120 = 0x140001120. If mapped at another base, use that actual base for the VA. RVA 0x1500 is within the section's stated virtual size but beyond its raw size, so the simplified example does not identify an initialized file byte there. It may correspond to zero-filled image space under the loader's mapping rules; a real image needs full validation and all alignment constraints checked.
Synthetic teaching data, not a device capture.
Read the text version
- RVA 0x1120: Inside the section raw and virtual ranges → File offset 0x520: Raw position: 0x400 plus section offset 0x120 (section offset 0x120)
- RVA 0x1120: Inside the section raw and virtual ranges → VA 0x140001120: Preferred base 0x140000000 plus RVA (preferred base)
- RVA 0x1500: Inside virtual size but beyond raw size → No file byte: Zero-filled under loader rules, if mapped as described (past raw size)
As a counterexample, treating RVA 0x1120 as file offset 0x1120 points to a different position. Treating e_lfanew as an RVA rather than a file offset likewise mislocates the PE headers. A robust parser checks integer overflow, section count, image size, file length, directory bounds and architecture before interpreting data. Wine's loader and mapping code implement version-specific checks; a hand calculation of one section is not proof that an arbitrary input is safe or valid.
Imports, exports and module identity#
An import table names DLLs and functions required by an image. A function can be imported by name or ordinal. An export can be a direct function or data export, or a forwarder that directs resolution to another module and symbol. Delay-load mechanisms can defer some dependency resolution until later, so initial image mapping does not necessarily prove every future function call can be resolved. Search order and configuration decide which candidate module is selected; an application-local file, Wine built-in module or other configured source can have different consequences. Never obtain unknown DLLs from untrusted sites to fix an exercise.
Wine's load_dll searches for a module, checks whether it is already loaded, and follows branches for a PE image or, where available, a Unix shared-object form. It tracks a WINE_MODREF whose loader entry includes a load count and module metadata. Reusing a loaded module can increment its count; a handle or pointer returned by one load does not make the module immortal. The exact rules for pinned modules, dependency references and unload differ by path and must be read in the source.
This exact Wine 10.0 excerpt from load_dll is LGPL-2.1-or-later, copyright Alexandre Julliard, Dmitry Timoshkov and contributors; surrounding logging and string cleanup are omitted:
if (*pwm) /* found already loaded module */
{
if ((*pwm)->ldr.LoadCount != -1) (*pwm)->ldr.LoadCount++;
The first condition identifies a module already found in the loaded-module state. The second increments its load count unless the special -1 value applies. It is a reference/lifetime decision, not a new disk read or proof that a requested function exists. The following source frees the temporary name and returns success for that path. Other error paths still need to close any mapping handle and release temporary name storage.
Relocation, initialization and failure#
An image can be linked for a preferred base that is unavailable at runtime. Base relocations describe locations that need adjustment when the actual mapping base differs, if the image and loader support relocation. ASLR is a placement policy and mitigation, not a promise that every image can be relocated or that its addresses are secret. A missing or invalid relocation directory, incompatible architecture, bad image format or allocation failure can prevent loading. Thread-local storage (TLS) and initialization callbacks add ordering requirements; the existence of a TLS directory alone does not prove callbacks ran successfully.
In the same pinned loader source, build_module validates the NT header, calculates an image mapping size, performs relocations and allocates module metadata before fixing imports. This exact Wine 10.0 LGPL-2.1-or-later excerpt omits the earlier header check and later module setup:
map_size = (nt->OptionalHeader.SizeOfImage + page_size - 1) & ~(page_size - 1);
if ((status = perform_relocations( *module, nt, map_size ))) return status;
The first statement rounds the declared image size up to the relevant page size in this source path. The second calls relocation processing and immediately returns a nonzero status. It does not continue as if the image were ready. The caller must handle the resulting mapping and module state according to its path. This excerpt assumes earlier validation; copying the arithmetic into a standalone parser without checking overflow and input bounds would be unsafe.
After a module record is allocated, fixup_imports or its applicable variant resolves dependencies unless flags and image type select another path. If import resolution fails, the source removes the module from several loader lists and the base-address index, sets the returned module pointer to NULL and returns the error. Crucially, the source contains a FIXME noting that some references, including DLLs loaded before a later failure, can remain and that the module record is not freed in that branch because of possible dangling references. Do not claim that this version performs a perfect complete rollback of all dependencies. A faithful investigation records that limitation and follows the exact caller and process lifetime.
In dlls/ntdll/loader.c, build_module, Wine 10.0, LGPL-2.1-or-later (earlier validation and later module setup omitted):
if (status != STATUS_SUCCESS)
{
/* the module has only be inserted in the load & memory order lists */
RemoveEntryList(&wm->ldr.InLoadOrderLinks);
RemoveEntryList(&wm->ldr.InMemoryOrderLinks);
RemoveEntryList(&wm->ldr.HashLinks);
RtlRbRemoveNode( &base_address_index_tree, &wm->ldr.BaseAddressIndexNode );
/* FIXME: there are several more dangling references
* left. Including dlls loaded by this dll before the
* failed one. Unrolling is rather difficult with the
* current structure and we can leave them lying
* around with no problems, so we don't care.
* As these might reference our wm, we don't free it.
*/
*module = NULL;
return status;
The failed module is removed from several lists and returned as NULL, while the comment states that some dependency references remain and the record is deliberately not freed. The excerpt does not show whether those references are harmless in any particular process.
- Callerbuild_module
Load a new image
- build_moduleperform_relocations
Round map size; apply relocations
- perform_relocationsbuild_module
Return. Nonzero status returns at once
- build_modulefixup_imports
Resolve imports after the module record exists
- fixup_importsbuild_module
Return. Failure: unlink lists, *module = NULL
- build_moduleCaller
Return. Error returned; some dependency references may remain
Time runs downward. Dashed arrows show returns.
Read the text version
- Caller → build_module: Load a new image
- build_module → perform_relocations: Round map size; apply relocations
- perform_relocations → build_module (return): Nonzero status returns at once
- build_module → fixup_imports: Resolve imports after the module record exists
- fixup_imports → build_module (return): Failure: unlink lists, *module = NULL
- build_module → Caller (return): Error returned; some dependency references may remain
Loader lock and DllMain#
The loader serializes important module-list operations with loader_section, a critical section in Wine's PE-facing loader. process_attach walks dependencies and calls module initialization through MODULE_InitDLL for applicable DLLs. DllMain-style process attach or detach runs under loader-related ordering and locking constraints. Initialization code that waits for another thread needing the loader lock, loads modules recursively in an unsafe pattern or assumes arbitrary services are ready can deadlock or fail. The exact Windows compatibility contract and Wine implementation need to be considered together; a short source excerpt does not prove that every Windows loader corner case is implemented identically.
Consider module A importing module B. If B is already loaded and the relevant load count is not pinned, the shown branch increments its count. If B is newly mapped, Wine validates and relocates it, fixes its imports, and performs applicable initialization in the loader's order. If B's required import C cannot be resolved, the error propagates through the relevant path and A must not assume that B initialized successfully. Some references may remain as the cited FIXME describes. A later unload requires correct balancing of references and safe interaction with running threads; an address obtained from a module cannot be used safely after its module is unloaded.
Architecture mismatch is a separate failure from a missing import. The COFF machine field, process mode, installed Wine build and WoW64 support all matter. A 32-bit image is not made valid in an arbitrary 64-bit context simply because the host CPU is 64-bit. Wine 10.0 includes evolving WoW64 support, but the precise build and supported path must be checked; do not assume a later Wine or Proton behavior applies. An invalid image can fail before import resolution, while a valid image can fail when a dependency or symbol is missing.
PE and ELF are different contracts#
ELF commonly uses program headers, dynamic sections, symbol tables and relocations interpreted by the Linux dynamic loader and kernel according to ELF rules. PE uses its own image headers, data directories, imports, exports, relocations and Windows-facing initialization conventions. Both can use shared code and virtual memory, but an ELF shared object is not automatically a Windows DLL, and a PE import table is not an ELF dynamic symbol table. Wine bridges these conventions through its implementation; the Linux kernel does not by itself implement the Windows loader or Win32 APIs.
A PE image can contain both file-oriented and memory-oriented sizes. The loader must map the intended image, apply permitted relocations and set up dependencies before invoking entry points. File mapping errors, host permissions, memory limits or incompatible alignment can fail independently of a Windows-level missing import. Debugging should record the exact Wine build, image architecture, dependency names and first error. A version label or file extension is not enough to identify the format, trustworthiness or source of a library.
Following a dependency through failure#
Suppose a synthetic executable imports function F from library B, and B in turn needs library C. Wine must find the intended B, map it for the supported architecture, apply necessary relocations and resolve the applicable imports before treating its initialization as successful. If C is absent, the error can surface while resolving B even though the original request named only B. The exact error and cleanup depend on which lookup and mapping stages completed. A debugger should preserve the first meaningful status and module name instead of attributing every failure to the main executable.
A loaded module's lifetime also extends beyond a single lookup. Another module may retain a dependency, a thread may currently execute code inside it, and loader bookkeeping can contain list or tree references. A balanced load count is necessary but not a complete proof that unloading is safe. In this v10 source the explicit import-failure FIXME warns that some dependency references may remain, so a simplified diagram that frees everything immediately would teach the wrong behavior. A reliable report needs the exact branch, module state and process lifetime, with any untraced references left unknown.
Practice#
Synthetic PE arithmetic
This short userspace calculation checks the supplied teaching values; it does not create, parse or run a PE executable:
python3 - <<'PY'
image_base, section_rva, raw_offset = 0x140000000, 0x1000, 0x400
rva, raw_size, virtual_size = 0x1120, 0x400, 0x600
delta = rva - section_rva
print('section offset:', hex(delta), 'file offset:', hex(raw_offset + delta), 'VA:', hex(image_base + rva))
print('initialized file byte:', 0 <= delta < raw_size, 'within virtual size:', 0 <= delta < virtual_size)
PY
Expected values are section offset 0x120, file offset 0x520 and VA 0x140001120. Change the RVA on paper to 0x1500: the section offset 0x500 exceeds the raw size even though it is below the stated virtual size. No initialized file byte follows from the supplied raw range. The program does not validate a complete PE header and must not be used as a loader.
Source-only alternative: use those same supplied numbers and trace the two Wine excerpts: already-loaded module increases its non-pinned load count; a relocation error returns before import setup in build_module. Add the documented import-failure FIXME to the cleanup table. Deliver the calculation and table without running a PE image. If Wine or a compiler is unavailable, that is a valid outcome; do not install packages or use an unknown application.
Knowledge check#
- Why are file offset
0x520, RVA0x1120and VA0x140001120different in the example? - What does the already-loaded branch do when the module's load count is not
-1? - What happens when
perform_relocationsreturns an error in the quoted path? - Why is the import-failure cleanup not described as a complete rollback of every dependency?
- Why can a DLL initialization routine deadlock even if the image mapped successfully?
Answers#
- The file offset addresses raw bytes, the RVA is relative to the image base, and the VA includes the actual mapping base. The section table relates the raw and virtual ranges.
- It increments the existing loader entry's load count before returning success from that path, subject to the special pinned value.
build_modulereturns the status immediately rather than proceeding to module allocation and import setup in that branch.- The v10 source explicitly notes possible remaining references and does not free the module record in that branch because those references may still point to it.
- Initialization runs within loader ordering and locking constraints. Waiting for another thread that needs the same loader state or performing unsafe recursive loading can deadlock.
Checkpoint#
Submit the PE address calculation and a loader timeline covering reuse, relocation failure, import failure and initialization. Ready means file offsets are not confused with RVAs, the source's incomplete-unwind FIXME is retained, and no unrun PE is presented as observed behavior. Retry the two pinned branches if a module reference has no owner. This is not certification.