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 map file has three parts, and the order is the point:
| Part | Answers |
|---|---|
| Discarded input sections | What did not make it in, and from which object |
| Memory configuration | What address space is available to place things in |
| Linker script and memory map | What 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.
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.
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.
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.
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:
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.
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.
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.
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.*fill* row. 11 + 4 = 15..text content, against a declared 0x200. The remaining 443 is the file-alignment padding on the section's tail.Two diagnostics that only a map file supports, both of which look identical from the disassembly:
.bss to .data shows as a SizeOfRawData going from 0 to non-zero. From the disassembly you would see only that a memory access changed from mov to lea-with-immediate. The map names the section it moved to and the object it came from, so you know the next step is to find which object defines it.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.
$ 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
*fill* rows should shrink or vanish, and .text should get smaller by exactly the difference. That is the cleanest demonstration of alignment-as-allocation you can produce, and it takes two flag changes.comdat_a.obj. Everything after ?use_a shifts down, the ?use_b row disappears, and the discards list shrinks. Watching a whole placement table reflow is the fastest way to internalise that addresses here are assigned, not discovered..text in the image and locate the 11 bytes at 0x401015. Then objdump -d that range. What the disassembler makes of padding is a good lesson in itself, and the fill pattern tells you which linker wrote it.__section_alignment__, __file_alignment__ and __subsystem__ from the map and find each one at its known offset in the PE optional header. The PE course taught the offsets; the map gives you the values without a hex editor. Doing both is the check that the two documents describe the same link.*fill* bytes per output section and reports the percentage of each section that is not yours. Run it over a real link. It is the highest-leverage twenty lines of tooling you can attach to a build, and it finds alignment problems that no compiler warning will ever mention.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?
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.
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.