Back to course

Map Files

18 minintermediateModule 4: The LinkContinues from coff-linking
18 min · Module 4: The Link · Algorithms

Why This Matters

A linker makes hundreds of decisions that nothing in the output file records. Which object contributed which bytes to .text. How much padding was inserted between two functions, and why. Which of two identical COMDAT copies survived. Which input sections were thrown away. Whether the code grew because you added an instruction or because the linker inserted 200 bytes of alignment filler.

None of that is recoverable from the image. The output knows what the code is; it does not know where it came from. So a linker that wants to be debuggable writes a map file: a plain-text transcript of the link, in the order the decisions were made.

This is the highest-value debugging artifact most people never open. Not because it is hard, but because the failure it explains — "this got 400 bytes bigger and I only added one line" — looks exactly like a compiler bug, and no amount of reading the disassembly will tell you that 380 of those bytes are filler inserted to satisfy an alignment rule.

A Simple Model

A map file has three parts, and the order is the point:

PartAnswers
Discarded input sectionsWhat did not make it in, and from which object
Memory configurationWhat address space is available to place things in
Linker script and memory mapWhat was placed where, which input section supplied each run of bytes, and what filler went in between

Discards come first because discarding is what happens first. A section that will not be placed must not consume address space, so the linker has to know the discard set before it can assign addresses. A map file that listed discards last would be describing a process that does not match the real one, and would imply that a discarded section's size affects the layout. It does not.

One honest caveat before the evidence, because it affects how you should read the rest of this concept. The map file shipped with this course was produced by GNU ld in its PE emulation mode, not by Microsoft's link.exe. The three-part structure, the discard list, the placement rows with per-input-section attribution, and the explicit *fill* rows are all general linker behaviour and all observed below. The exact spelling of the linker-defined symbols and the presence of a linker-script preamble are this linker's dialect. Read the concepts; do not expect Microsoft's word-for-word output to match.

The Actual Detail

343 lines for a two-file program. The three parts in order:

$ ld --oformat pei-i386 -m i386pe link_prog.obj link_lib.obj \
      -o link_prog.exe --entry entry -Map link_prog.map

Discarded input sections

 .debug$S       0x00000000       0x5c link_prog.obj
 .llvm_addrsig  0x00000000        0x2 link_prog.obj
 .debug$S       0x00000000       0x5c link_lib.obj
 .llvm_addrsig  0x00000000        0x0 link_lib.obj
 .reloc         0x00000000        0x0 dll stuff

Memory Configuration

Name             Origin             Length             Attributes
*default*        0x00000000         0xffffffff

Each discard row is name, address, size, object. The address is 0x00000000 for all of them, and that is not a formatting quirk — it is the proof that these sections were never placed. A section that was placed has a real address; a section that was discarded never got one. The map is stating the outcome in the same four fields the section table uses, and all of them say "nothing happened to this".

The .reloc row attributed to dll stuff is an artefact of this linker supplying its own synthetic input. It has a zero-length .reloc section, so the row exists and is empty. A map file is a record of what the linker was given, including inputs that contributed nothing.

Part two: the address space

One region, the entire 32-bit space, marked *default*:

Name             Origin             Length             Attributes
*default*        0x00000000         0xffffffff

No attributes means no restriction beyond that one. A link script can carve the space into named regions with permissions — that is what an embedded linker's script does, and it is why a map file from one looks nothing like a map file from a desktop linker. The desktop case has nothing to say about the address space, so it says nothing.

Part three, first: the linker-defined symbols

Before any section, the map lists the symbols the linker itself provides. These are the values that end up in the PE optional header:

                0x00400000          __image_base__ = 0x400000
                0x00000000          __dll__ = 0x0
                0x00400000          ___ImageBase = 0x400000
                0x00001000          __section_alignment__ = 0x1000
                0x00000200          __file_alignment__ = 0x200
                0x00000004          __major_os_version__ = 4
                0x00000003          __subsystem__ = 3
                0x00200000          __size_of_stack_reserve__ = 0x200000
                0x00001000          __size_of_stack_commit__ = 0x1000
                0x00100000          __size_of_heap_reserve__ = 0x100000
                0x00001000          __size_of_heap_commit__ = 0x1000
                0x00000000          __loader_flags__ = 0x0
                0x00000000          __dll_characteristics__ = 0x0

This block is a free gift for a course about the PE header, because every one of those names is a field you decoded in the PE course — and the map tells you the values before you go looking for them in the binary. __section_alignment__ = 0x1000 is the SectionAlignment you read at 0x1000; __file_alignment__ = 0x200 is the FileAlignment; __subsystem__ = 3 is the console subsystem that made file report "console". The names use linker conventions and the fields use PE conventions, which is a small translation step that the map makes unnecessary.

One detail worth catching: the values on the left of the = are addresses and the values on the right are the constants. __image_base__ has address 0x400000 and value 0x400000 — a coincidence, since it sits at the base. __size_of_stack_reserve__ has address 0x200000 and value 0x200000 — also a coincidence, and a misleading one. Read the right-hand column. These are not addresses; they are the header fields, and the left column is where the linker recorded them.

Part three, second: placement, and the filler you did not write

This is where the map earns its keep. The COMDAT example from the next concept, whose map is the most instructive file in this course:

  .text           0x00401000      0x200
   *(SORT_NONE(.init))
   *(.text)
   .text          0x00401000       0x15 comdat_a.obj
                  0x00401000                ?use_a@@YAHXZ
   *fill*         0x00401015        0xb
   .text          0x00401020        0xc comdat_a.obj
                  0x00401020                ?shared@@YAHH@Z
   *fill*         0x0040102c        0x4
   .text          0x00401030       0x15 comdat_b.obj
                  0x00401030                ?use_b@@YAHXZ

Read it as a sentence. Output section .text starts at 0x401000 and is 0x200 bytes. Here is how it is filled. First comdat_a.obj's 0x15 bytes at 0x401000, which contain ?use_a. Then 11 bytes of filler at 0x401015. Then comdat_a.obj's 0xc bytes at 0x401020, which contain ?shared. Then 4 bytes of filler at 0x40102c. Then comdat_b.obj's 0x15 bytes at 0x401030, containing ?use_b.

Now count, and the arithmetic that never appears in the disassembly:

?use_a in comdat_a.obj 0x15 = 21 bytes filler to reach 16-byte alignment 0x0b = 11 bytes ?shared in comdat_a.obj 0x0c = 12 bytes filler to reach 16-byte alignment 0x04 = 4 bytes ?use_b in comdat_b.obj 0x15 = 21 bytes ------ total in the image 69 bytes of which you wrote 54 bytes of which the linker added 15 bytes

Fifteen of sixty-nine bytes — more than a fifth of the code — is padding this linker inserted. The alignment rule is the 0x10 in the characteristics from Module 1: the COMDAT .text carries IMAGE_SCN_ALIGN_16BYTES, so its output address must be a multiple of 16. 0x401015 is not, so the linker skipped forward to 0x401020. The bytes it skipped are not zero — they are whatever fill pattern the linker chose, and the map records them by name rather than by value.

And notice what is missing. comdat_b.obj contributed ?use_b at 0x401030 and nothing else. Its 0xc-byte copy of ?shared is not in the placement list, because it was discarded. The map shows you a section that exists in an input file, is not in the output, and is not filler — and the discard list at the top of the same file names it. The two halves of the map are only readable together.

The other kind of filler

Compare the output section's declared size with the bytes actually attributed to it. .text is declared 0x200 = 512 bytes, but the rows above account for 69. The gap is because .text was rounded up to the 0x200 file alignment as well as the section alignment — the last section in a segment is padded so the next file position is aligned for reading. llvm-readobj reports .text's VirtualSize as 0x61, which is the real content length. The map's 0x200 is the allocated size. Three different numbers for one section, all correct, all answering different questions — which is the single best argument for reading a map file when a size does not match your expectation.

A Real Example

The question a map file is actually for: why is my binary bigger than my code? Here is the method, and it is worth doing by hand once so the reflex is there.

  1. Measure the object. The sum of the SizeOfRawData of every input section that is not in the discard list is the bytes your program actually contains. From the parser shipped with this course: comdat_a.obj contributes 21 + 12 bytes of .text, comdat_b.obj contributes 21 bytes of .text (its 12-byte COMDAT is discarded). Total 54.
  2. Read the placement rows. Add every *fill* row. 11 + 4 = 15.
  3. The difference is alignment. 54 + 15 = 69 bytes of .text content, against a declared 0x200. The remaining 443 is the file-alignment padding on the section's tail.
  4. Now you can answer the question. If the answer is "the code is 54 bytes and the linker made it 512", you know that shrinking your code by 10 bytes will change nothing, and that the only lever is alignment — either a coarser alignment for that COMDAT, or fewer separately-aligned items.

Two diagnostics that only a map file supports, both of which look identical from the disassembly:

And the general reason this artifact exists at all: a link is a many-to-one function. Many objects go in, one image comes out, and the information about which input produced which output is destroyed. Every format that discards information during a transformation and then gets debugged is a format that eventually grows a log. The ar archive's symbol index in Module 3 is the same instinct at a smaller scale — an index that survives the concatenation so the result can still be searched. The map file is the index for a link, and the one artifact you will wish you had when a build produces something you cannot explain.

Try It Yourself

$ ld --oformat pei-i386 -m i386pe comdat_a.obj comdat_b.obj \
      -o comdat_linked.exe --entry use_a -Map comdat_linked.map
$ cat comdat_linked.map

Check Your Understanding

Without looking back: an output section is declared as 0x200 bytes, and its placement rows account for a 21-byte input section, 11 bytes of filler, a 12-byte input section, 4 bytes of filler, and a 21-byte input section. What is the section's real content size, how much of the declared size is padding, and which of those two numbers does a disassembler see?

Apply It

A shared library you maintain is 40 KB. After an unrelated dependency bump, it is 78 KB, and the release notes from the dependency say nothing about code size. You have the old and new objects and can relink. Walk through how you would use the map file to find the 38 KB, naming the specific rows you would look at and what each would tell you. Then say what you would conclude if none of them accounts for it.

The general habit: a file format that throws information away during a transformation is a file format whose output cannot explain itself. The link destroys provenance, so the map is what puts it back. When a build produces an artefact you cannot account for, find the log first and reason second.

Connect

The *fill* rows are the map file's most surprising content, and they exist because of the alignment flags in the section table. Those flags were decoded as data in Module 1; here they are observed being spent. A number in a header that changes the size of your binary is a number worth understanding, and this is where the connection closes.

The linker-defined symbols block connects to the PE course directly. __image_base__, __section_alignment__, __file_alignment__ and __subsystem__ are optional-header fields that the map states as plain named constants. Anyone who has read a PE header by hand and anyone who has read a link map is looking at the same four numbers written in two vocabularies.

And the one row in this concept that is missing from the placement table is the subject of the next concept. comdat_b.obj's 0xc-byte copy of ?shared exists, matches another copy byte for byte, and does not appear in the image. Module 3 taught the mechanism that makes that decision possible. This is what the mechanism does.

Next: COMDAT in the Linker.