Link a Minimal Windows PE Program with x86_64-w64-mingw32-ld
You will assemble one tiny x86-64 object, link it into a Windows PE executable, and inspect the result. The workflow keeps the entry point, subsystem, relocation table, timestamp and link map explicit enough to debug. It uses x86_64-w64-mingw32-ld from binutils-mingw-w64-x86-64 version 2.41.90.20240122-1ubuntu1+11.4, whose linker reports GNU Binutils 2.41.90.20240122.
The route
Jump straight to the step you need, or tick off Done means at the end.
- 1. Check the installed linker
- 2. Make a harmless object file
- 3. Link a PE executable with an explicit entry point
- 4. Inspect the header before sharing the file
- 5. Make a reproducible link when you need one
- 6. Capture a link map when a link is confusing
- 7. Keep DLL-specific changes deliberate
- Common failure modes
Allow about fifteen minutes. You need the cross assembler, linker and object inspection tools from the MinGW-w64 toolchain. The commands below only create files under /tmp; they do not install anything, change a service or require elevated privileges.
1. Check the installed linker
First confirm that the command resolves to the cross toolchain you intend to use:
$ command -v x86_64-w64-mingw32-ld
$ x86_64-w64-mingw32-ld --version
x86_64-w64-mingw32-ld (GNU Binutils) 2.41.90.20240122
The aliases x86_64-w64-mingw32-ld.bfd and x86_64-w64-mingw32ucrt-ld are packaged alongside the same linker documentation here. Use one command name consistently in scripts so a toolchain change is easy to spot.
Checkpoint: run x86_64-w64-mingw32-ld --verbose if you need to see the supported emulations and the internal default linker script. This installed linker reports i386pep and i386pe; the first is the 64-bit PE emulation used by this target.
2. Make a harmless object file
Create a source file and object in a disposable directory. The symbol _start gives the linker an entry point for this deliberately minimal example:
$ mkdir -p /tmp/ld-guide-test
$ printf '.text\n.globl _start\n_start:\n nop\n ret\n' > /tmp/ld-guide-test/start.s
$ x86_64-w64-mingw32-as -o /tmp/ld-guide-test/start.o /tmp/ld-guide-test/start.s
$ x86_64-w64-mingw32-objdump -f /tmp/ld-guide-test/start.o
/tmp/ld-guide-test/start.o: file format pe-x86-64
This is not a complete Windows application. It has no C runtime, imports or user interface. It is useful because every input and linker decision is small enough to inspect. A real C or C++ program is normally linked through gcc or g++, which add runtime objects and libraries for you.
Checkpoint: if the assembler is missing, stop here and install or enable the matching toolchain through your normal package-management process. Do not substitute a host assembler and assume that its object format is interchangeable.
3. Link a PE executable with an explicit entry point
Link the object and choose the console subsystem. --entry selects the symbol where execution starts. --subsystem console records a Windows CUI subsystem in the PE header:
$ x86_64-w64-mingw32-ld \
--entry _start \
--subsystem console \
-o /tmp/ld-guide-test/sample.exe \
/tmp/ld-guide-test/start.o
$ x86_64-w64-mingw32-objdump -f /tmp/ld-guide-test/sample.exe
/tmp/ld-guide-test/sample.exe: file format pei-x86-64
architecture: i386:x86-64
With no explicit entry symbol, ld uses its normal entry-point rules. An unresolved name supplied to --entry may also be interpreted as a numeric address, so a typo can become a surprising start address. Prefer a symbol and inspect the result.
The --subsystem value accepts names such as console and windows, with an optional version. A console subsystem does not add a console API or a C runtime; it only sets the PE header value.
4. Inspect the header before sharing the file
Use the target-aware object dumper to verify the fields that matter:
$ x86_64-w64-mingw32-objdump -p /tmp/ld-guide-test/sample.exe \
| grep -E 'Subsystem|DLL characteristics|Entry|Base Relocation'
Subsystem 00000003 (Windows CUI)
Entry 5 0000000000000000 00000000 Base Relocation Directory [.reloc]
Exact spacing varies between binutils builds. The useful checks are that the file format is PE x86-64, the subsystem is Windows CUI, and a relocation directory is present. The installed manpage says the relocation section is enabled by default. Do not disable it casually: a Windows loader may need it when the image is not loaded at its preferred base.
On a 64-bit PE image, high-entropy virtual-address support is enabled by default in this linker documentation. Dynamic base and data-execution-prevention compatibility are also enabled by default for the PE target. Treat those as properties to verify, not as a reason to pass a long list of redundant switches.
5. Make a reproducible link when you need one
PE images normally receive a real link timestamp, so two links from identical inputs can differ. Pass --no-insert-timestamp when byte-for-byte repeatability matters, and keep the subsystem and entry point explicit:
$ x86_64-w64-mingw32-ld \
--no-insert-timestamp \
--entry _start \
--subsystem console \
-o /tmp/ld-guide-test/repro.exe \
/tmp/ld-guide-test/start.o
$ cmp -s /tmp/ld-guide-test/repro.exe /tmp/ld-guide-test/repro.exe
$ echo $?
0
The cmp line only compares the file with itself, so it proves that the command completed and the path is readable, not that two builds match. To test reproducibility, link a second output in a clean build and compare the two outputs. If you prefer a meaningful timestamp, leave the option out and set SOURCE_DATE_EPOCH to a deliberately chosen Unix time; the manpage documents that variable for the inserted timestamp.
Warning: changing timestamps or image-layout options changes the binary. Do not use a reproducibility switch as a general security switch, and do not replace a signed release artefact without checking the signing and release process.
6. Capture a link map when a link is confusing
A link map shows where input files and sections were placed and which symbols were included. Send it to a separate file with --print-map:
$ x86_64-w64-mingw32-ld \
--entry _start \
--subsystem console \
--print-map \
-o /tmp/ld-guide-test/mapped.exe \
/tmp/ld-guide-test/start.o \
> /tmp/ld-guide-test/mapped.map
$ grep -E 'LOAD|\.text|_start' /tmp/ld-guide-test/mapped.map
LOAD /tmp/ld-guide-test/start.o
.text 0x0000000140001000 0x2
0x0000000140001000 _start
Addresses and section details depend on the linker script and inputs. The map is often the fastest way to find an unexpected object, a misplaced section or a missing symbol. Keep it with a build log when reporting a linker failure.
7. Keep DLL-specific changes deliberate
For a DLL, --dll changes the output kind. --out-implib FILE writes an import library for clients, and --output-def FILE writes a module-definition file. --export-all-symbols exports all eligible global symbols, while --exclude-symbols and --exclude-all-symbols narrow automatic export.
These switches change the public interface of a binary. Prefer an explicit DEF file or source-level export declarations for a maintained library. Before using them, confirm which symbols should be callable by other programs and which are implementation details. A careless automatic export can expose internal functions or data and make later API changes harder.
This guide does not build a DLL because a useful DLL example needs a compatible entry routine, exported symbol and client-side verification. Use the installed --help output and the linker map as the next checkpoint rather than copying a guessed export list.
Common failure modes
- "cannot find" or an undefined symbol: check the input path, object format and spelling of the entry symbol. Use
--traceto print input files, or--trace-symbol=NAMEto find which linked file mentions a troublesome symbol. - The output is not a Windows executable: invoke the target-prefixed linker and inspect its format with
objdump -f. Do not mix a host linker with MinGW objects. - The program starts in the wrong place: inspect the entry address and confirm that
--entrynames a defined symbol. A numeric entry value is easy to mistype. - Repeat builds differ: check the PE timestamp first. Use
--no-insert-timestamponly when your release process permits it, and also control other inputs such as absolute paths and generated metadata. - A DLL client cannot link: check that an import library was generated and that the intended symbol was exported. The linker cannot repair a missing or accidentally renamed public interface.
Done means
x86_64-w64-mingw32-ld --versionreports the expected toolchain.- The input object is
pe-x86-64, and the output ispei-x86-64. - The entry symbol, Windows CUI subsystem and relocation directory are visible in inspection output.
- A map file is available when section placement or symbol resolution needs evidence.
- Any DLL exports, import libraries, timestamps or image-layout changes are recorded as intentional build decisions.