Home / Alt manpages / x86_64-w64-mingw32-ld(1)

  • x86_64-w64-mingw32-ld(1)
  • User command
  • linux

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.

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.

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.

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 --trace to print input files, or --trace-symbol=NAME to 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 --entry names a defined symbol. A numeric entry value is easy to mistype.
  • Repeat builds differ: check the PE timestamp first. Use --no-insert-timestamp only 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 --version reports the expected toolchain.
  • The input object is pe-x86-64, and the output is pei-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.