Skip to content

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

  1. Build anything with link /MAP or find a .map in a Windows SDK sample, and grep -n 'Publics by Value' file.map. Then count the lines until the next blank one.
  2. Pick a symbol line and compute section start + offset + preferred load address from the tables above it. Compare with column three.
  3. Insert a blank line in the middle of the Publics section and count what the program's parser keeps.
  4. Prepend 0x to one address and parse the line in Python and in jshell with Long.parseLong(s, 16).
  5. Rename the file .txt. The loader is gone from Ghidra's list; the content did not change.

See also