MAP¶
Level: 201 · for anyone with a linker map open
One line: An MSVC map file is text in columns, and the loader keeps two of them — the name, and the Rva+Base column parsed as hexadecimal with no prefix — ignores the segment:offset column entirely, needs the .map extension, and stops at the first blank line after it has found a symbol.
What the loader reads¶
A linker map is the text report link /MAP writes beside an executable: the preferred load address, a section table, and then the symbols, one per line under the heading Address Publics by Value Rva+Base Lib:Object, and again under Static symbols. Ghidra reads it to put names back on a stripped PE. The sample the source itself documents:
Address Publics by Value Rva+Base Lib:Object
0000:00000000 ___safe_se_handler_table 0000000000000000 <absolute>
0001:00000040 foo 0000000140001040 f foo.obj
Four columns of text, of which the loader reads two.
In Python¶
Verified output of program_mapfile_py.py — regenerated by tools/run_examples.py, never hand-typed.
1. THE LOADER KEEPS THE SECOND AND THIRD COLUMNS
------------------------------------------------------------------------
__ImageBase 0x0000000140000000
main 0x0000000140001040
café_string 0x00000001400010c0
?greeting@@3PEBDEB 0x0000000140002010
helper 0x0000000140001200
Each symbol line is split on whitespace into at most four parts:
parts[0] is the segment:offset, ignored; parts[1] the name;
parts[2] the Rva+Base column, parsed as hexadecimal; parts[3] the
rest, ignored. A line with fewer than three parts is skipped, and
the section ends at the first blank line after a symbol was added.
2. THE ADDRESS COLUMN IS HEX WITH NO PREFIX
------------------------------------------------------------------------
'0000000140001040'
int(col, 16) 0x140001040 what the loader does: Long.parseLong(s, 16)
int(col) 140,001,040 what a reader who did not know the base would get
int('140001040', 16) 0x140001040
int('0x140001040', 16) 0x140001040 Python accepts a 0x prefix in base 16; Java's parseLong does not
int('140001040h', 16) ValueError -- and NumberFormatException in Java: the line is logged and skipped
Sixteen digits and no 0x: the base is in the linker's documentation
and nowhere in the file. And the one prefix a reader might add is
a parser differential between the two languages on this page.
3. THE COLUMN THE LOADER IGNORES SAYS THE SAME THING
------------------------------------------------------------------------
preferred load address 0x140000000 sections {1: ('.text', 0), 2: ('.rdata', 0)}
0001:00000040 main computed 0x140001040 column 0x140001040 agree
0001:000000c0 café_string computed 0x1400010c0 column 0x1400010c0 agree
0002:00000010 ?greeting@@3PEBDEB computed 0x140002010 column 0x140002010 agree
0001:00000200 helper computed 0x140001200 column 0x140001200 agree
segment:offset plus the section's start plus the load address is
the Rva+Base column, so the column is redundant and the loader
reads the one that needs no arithmetic. It does mean that a map
file with the wrong preferred load address is wrong in the column
Ghidra trusts and right in the one it ignores.
4. WHERE THE SECTION ENDS
------------------------------------------------------------------------
as written 5 symbols
blank line after main 4 symbols -- the loop stops at the first blank line once it has added one
'Static symbols' opens a second section that is parsed the same
way, so symbols after it are found; a stray blank line inside
the first section loses everything after it, silently.
Two columns of four¶
Section 1 is the whole parser: trim the line, split on runs of whitespace into at most four parts, take part 1 as the name and part 2 as a hexadecimal number, and ignore parts 0 and 3. The segment:offset column is never read, and section 3 shows why it need not be — section start plus offset plus the preferred load address is the Rva+Base column, so the two say the same thing and the loader takes the one that needs no arithmetic. The cost is in the last line of that section: a map whose preferred load address is wrong, because the image was rebased after the map was written, is wrong in the column Ghidra trusts and right in the one it ignores.
Hex with no prefix, in two languages¶
Section 2 is the encodings lesson. 0000000140001040 is sixteen hex digits with nothing to say so, and Long.parseLong(s, 16) is what makes it an address rather than the decimal 140,001,040 — Which base did you mean? is the page about fields like this. The finding beside it is a parser differential: Python's int(s, 16) accepts a 0x prefix and Java's parseLong throws on it, so a map file with 0x in that column parses in one language and logs an error per line in the other, and the Python programmer who tested the format never sees the difference. Two readers, one byte string is the general form.
Where the section ends¶
Section 4: the parser reads lines until the first blank line after it has added at least one symbol, then goes back to scanning for the next heading. A blank line that a tool inserted mid-section loses every symbol after it, silently; Static symbols opens a second section that is parsed the same way. The name test comes before any of this — the file must end in .map, or the parser never runs.
What Ghidra checks¶
MapLoader ↗ accepts a file whose lower-cased name ends in .map and whose parse finds at least one symbol; like the DEF loader it is a wrapper that adds labels to an open program, and like it, it opens the file through an InputStreamReader with no charset, so the JVM's default — UTF-8 since Java 18 — decodes it. Its name is Program Mapfile (MAP). GNU ld's map format, which looks nothing like this, is not what it parses.
If you are coming from Python or ABAP¶
Python. re.split(r'\s+', line.strip(), maxsplit=3) is the line, and int(parts[2], 16) the address — with the caveat above that Python is more lenient than Java about what it will read as hex. re.finditer with a pattern per line shape, as section 3 does, is the honest way to read the section table; the loader does not read it at all.
ABAP. (Not machine-checked — CI cannot run ABAP.) A columnar text report parsed by splitting on spaces is what SPLIT ... AT space INTO TABLE does, and it has the same hazard: a name with a space in it becomes two columns. Reading a hex column is a conversion the language does not do by assignment — '0000000140001040' into an i is a syntax error, not a number — so the digit string goes through an xstring first, which makes the base explicit, which is the point.
Try it¶
- Build anything with
link /MAPor find a.mapin a Windows SDK sample, andgrep -n 'Publics by Value' file.map. Then count the lines until the next blank one. - Pick a symbol line and compute
section start + offset + preferred load addressfrom the tables above it. Compare with column three. - Insert a blank line in the middle of the Publics section and count what the program's parser keeps.
- Prepend
0xto one address and parse the line in Python and injshellwithLong.parseLong(s, 16). - Rename the file
.txt. The loader is gone from Ghidra's list; the content did not change.
See also¶
- DEF — the other text file the loader family reads, by keyword rather than by column
- PE — what
Rva+Baseis an address into - Which base did you mean? — a column of digits with its base in a document
- Two readers, one byte string —
int(s, 16)againstparseLong(s, 16) cutand fixed columns — reading columnar text without a parser- /MAP (Generate Mapfile) ↗ — the linker option that writes the format