Back to course

Linker Directives in a Section

18 minintermediateModule 5: Other Targets and Other SectionsContinues from coff-weak-externals
18 min · Module 5: Other Targets and Other Sections · Algorithms

Why This Matters

Every section in Modules 1 to 4 held data for the program. .text holds instructions, .data holds initialised values, .rdata holds things that should not be written, and the COMDAT sections hold code that one object is allowed not to contribute. In every case the contents are the program's contents.

.drectve is a section whose contents are not the program's contents at all. They are text addressed to the linker: options, exactly the kind you would type on a command line, produced by a #pragma in a source file and carried in the object so that a build does not have to know about them. The compiler cannot apply them, because it does not know the link context; only the linker can. So it writes them down and gets out of the way.

It is a small idea and it is the clearest example in the course of a section as a channel rather than a container. And it has a property that makes it worth the last concept of the module: the section does not survive the link. The linker reads it, obeys it, and discards it, and the map file records the discarding. So it is a section whose entire purpose is to not be in the output — which is a good final note for a module about the parts of a format that exist outside the program.

A Simple Model

Produce one. A single pragma, on a line that looks like a comment:

Three lines: a pragma, and two one-line functions where bar calls foo.

$ cat directive_used.c
#pragma comment(linker, "/alternatename:foo=bar")

$ clang --target=i686-pc-windows-msvc -c directive_used.c -o directive_used.obj
$ llvm-readobj --sections directive_used.obj | grep drectve
  Name: .drectve (2E 64 72 65 63 74 76 65)

A section named .drectve appears, and nothing else about the object changes. The section's entire contents:

size = 23 bytes

20 2f 61 6c 74 65 72 6e 61 74 65 6e 61 6d 65 3a 66 6f 6f 3d 62 61 72
 |  |  |                                          |                       |
 |  |  |                                          |                       +-- "bar"
 |  |  |                                          +-------------------------- "foo"
 |  |  +---------------------------------------------------------- "alternatename:"
 |  +------------------------------------------------------------- "/"
 +-------------------------------------------------------------------- " " (space)

So the encoding is trivially simple, and the leading byte is the only subtlety: a space, not a NUL and not a length. The section's SizeOfRawData is the length of the whole blob, and the payload is space-separated linker options. Every option begins with a /, which is the MSVC convention, and a comma between options is also permitted.

Read the option itself: /alternatename:foo=bar. It means "wherever the symbol foo is referenced and undefined, call it bar instead". That is a real, widely used directive — it is how a program overrides a library function, and how kernel32 redirects to api-ms-win-* sets work. So the section contains an instruction that rewrites the program's symbol resolution, and the program has no bytes for it at all.

The Actual Detail

The characteristics say the same thing twice

Read the section header with the parser shipped with this course:

section .drectve  rawsize=23  char=0x00100a00
  -> ['IMAGE_SCN_LNK_INFO', 'IMAGE_SCN_LNK_REMOVE']

Two flags, and they are the whole story of this section's life:

FlagBitMeans
IMAGE_SCN_LNK_INFO0x00000200This section is for the linker. It is not program data and not to be placed
IMAGE_SCN_LNK_REMOVE0x00000800Remove it. Do not put it in the output at all

And the alignment bits, 0x00100000, say 16-byte alignment for a section that will never occupy an address. Which is the neat observation: the format sets alignment requirements on a section whose entire purpose is to not exist in the output. The alignment is a property of the input record, not of anything placed, and it is a leftover of the general rule that every section carries alignment rather than only the ones that need it.

Note also what is not here, by comparison with the other sections in this object. No MEM_READ, no MEM_EXECUTE, no CNT_CODE, no CNT_INITIALIZED_DATA. The characteristics do not describe the data at all, because the data is not data. They describe the section's relationship to the linker, and the only content flags present are the two that express that relationship.

Observed being discarded

Now the part that closes the loop. Link the object and look at the map file, the same technique the map files concept used to watch .debug$S and .llvm_addrsig disappear:

$ ld --oformat pei-i386 -m i386pe directive_used.obj -o out.exe --entry bar -Map out.map
$ grep -A6 'Discarded input sections' out.map
 .drectve       0x00000000      0x17 directive_used.obj
 .debug$S       0x00000000      0x5c directive_used.obj
 .llvm_addrsig  0x00000000       0x1 directive_used.obj

.drectve is in the discard list, at 0x17 = 23 bytes, which is exactly the section's size. And the control is worth running, because it is the cleanest possible demonstration that the discard is caused by the pragma and not by something incidental:

$ cat directive.c            # the same source WITHOUT the pragma
# (the two one-line functions, no pragma)

$ clang --target=i686-pc-windows-msvc -c directive.c -o directive.obj
$ grep -A6 'Discarded input sections' plain.map
 .debug$S       0x00000000      0x5c directive.obj
 .llvm_addrsig  0x00000000       0x1 directive.obj

No .drectve row. Same compiler, same flags, same day, same everything — the only difference is the #pragma, and the section appears and is discarded. Cause established by controlled comparison rather than by inference from a flag.

Why the linker obeys a string

Worth being explicit about, because it is unusual. The linker parses the contents: it reads /alternatename, splits on the colon, reads the two symbol names, and installs a rule in its resolution table. That is a text protocol, not a fixed-layout record. The format specifies:

So the format has delegated the vocabulary to the tool. A new linker option needs no format change, needs no new field, and is simply understood by a newer linker — which is how a closed binary format ends up with an open extension point. It is the same instinct as the ar archive's symbol index in Module 3: a small, general container plus a tool-defined payload.

The portability price, stated honestly. Because the payload is free text interpreted by one specific linker, a .drectve section is not portable between linkers. Hand the object to a linker that does not implement /alternatename and the section is not rejected — the LNK_REMOVE flag still removes it — it is silently ignored, and the redirection you asked for does not happen. The program links, runs, and calls the original symbol. There is no error, because from the linker's point of view it did exactly what it was told with an option it did not recognise.

This is the general hazard of text protocols inside binary formats, and it is worth naming as a design lesson rather than only as a gotcha: an open extension point trades away error detection. A fixed-layout record with a type code can be rejected when the code is unknown. Free text cannot, because there is nothing to reject — it is just words.

A Real Example

What a tool has to know about .drectve, and the trap that catches anyone who does not. The section is invisible in the output, so every question about it has to be answered before the link.

WHO NEEDS TO KNOW .drectve EXISTS, AND WHY the linker must read it, or the directives are silently lost -> this is why LNK_INFO is set at all an object-file dumper must not present 23 bytes of linker text as program data -> check IMAGE_SCN_LNK_INFO before printing contents a security scanner must not skip it -> /alternatename can redirect a call to another symbol, so this section changes which function runs a binary differ sees no difference at all -> two objects differing only in .drectve produce programs that differ only in behaviour, and the difference is invisible in the linked image a linker that does not implement the option removes the section anyway -> no diagnostic; the redirection silently does not happen

The two rows in the middle are the ones that matter in practice, and they pull in opposite directions. A dumper wants to hide the section, because showing 23 bytes of /alternatename:foo=bar as though it were data is misleading. A security scanner wants to surface it, because /alternatename is a mechanism for changing which function a call reaches — which is exactly the shape of a thing worth reviewing, and exactly the sort of thing that would be missed by a tool that treats every section as either code or data.

And the fourth row is the most unsettling, so it is worth sitting with. Two objects that differ only in their .drectve sections link to byte-identical images if the directives resolve to the same thing, and to behaviourally different programs if they do not. The section is consumed and the evidence is destroyed. That is a real property of the design rather than a defect — the information was only ever meant to be read once, by the one process that could act on it — but it means a build's reproducibility cannot be checked by comparing binaries. /alternatename and a handful of other options can change what a binary does without changing a byte of it.

The honest summary of the whole module is in that sentence. TLS, weak externals and directives all store something outside the program: a per-thread base, a permitted-absence decision, and a text instruction to the linker. None of the three is in the output, all three change what the output does, and a format that has to carry them has to make room for sections that are not program data. The flags in the section header are how it says so — and, as this concept shows, the only reliable way to identify such a section is that the flags say so, because the name is the one thing a tool is tempted to rely on instead.

Try It Yourself

$ clang --target=i686-pc-windows-msvc -c directive_used.c -o directive_used.obj
$ llvm-readobj --sections directive_used.obj
$ ld --oformat pei-i386 -m i386pe directive_used.obj -o out.exe --entry bar -Map out.map

Check Your Understanding

Without looking back: an object has a section .drectve, 23 bytes of contents beginning with a space, and characteristics 0x00100a00. Its SizeOfRawData is 23. The linked image has no section of that name, and the map file lists .drectve 0x00000000 0x17 directive_used.obj among the discarded input sections. What did the linker do with those 23 bytes, and what would have happened if the characteristics had not included IMAGE_SCN_LNK_REMOVE?

Apply It

Your team ships a shared library built from vendored third-party sources. Two builds of the same commit, on two machines, produce binaries of identical size whose every section matches byte for byte — and the two libraries behave differently: one of them calls your logging function, the other calls the third party's. You have both objects and both link scripts. Given this concept, what is the most likely explanation, and what is the one check that settles it?

The general habit: when two binaries from one source are byte-identical but behave differently, the difference was consumed by the build. Go compare the inputs, and expect to find it in something the output cannot contain.

Connect

This closes the COFF course's format coverage, and the three sections in this module form a set worth stating as a whole. TLS carries what the loader needs, per thread. Weak externals carry a link-time decision in a symbol's storage class. And .drectve carries what the linker needs, as text. Three different mechanisms, one shared property: the information is not in the program. Two of the three are consumed and forgotten, and the third survives only as a runtime behaviour.

That is the natural companion to the section characteristics, which taught that the flags are instructions rather than descriptions. This module found the three cases where the instruction is not "put this at this address" but something else entirely, and the flag that carries it in each case: MEM_DISCARDABLE and LNK_REMOVE for compiler and linker scratch, LNK_COMDAT for the deduplication contract, and LNK_INFO for this one. All four are one-byte fields, and all four change what the linker does rather than what the program contains.

The map files concept is what made this module's claims checkable. Every discard observed here — .debug$S, .llvm_addrsig, and now .drectve — is a row in a map file, and every one of them was confirmed by a controlled comparison against an object built without the cause. That is the general pattern this course has tried to teach from the beginning: a flag in a header is a claim about what a tool will do, and the only way to know whether the claim is true is to run the tool and look.

What remains in the COFF format is enumerated with reasons in courses/coff/research.md: import libraries and the short-import format, which need a dlltool this machine does not have; MSVC's incremental linking and LTCG, which need an MSVC toolchain; archives as link inputs, because GNU ld's PE mode will not read ar; and reading a bigobj, because no implementation exists anywhere on this machine. None of those is unwritten for want of effort, and each is blocked on something specific and nameable.