The puzzle
The firmware grew by 11 KiB after a two-line change. Which function is responsible? A crash dump says the program counter was 0x1000_0246; which line of C is that? A colleague insists a library function is not linked in; is it? None of these questions can be answered by reading the source, because the answers depend on what the compiler and linker did with it. The toolchain writes those decisions down in three places you can read: the size report, the map file, and the disassembly. How do you read each?
STEP 1
The size report
arm-none-eabi-size app.elf prints one line:
text data bss dec hex filename
48200 1320 9460 58980 e664 app.elf
In this default (Berkeley) format, text counts code and read-only data, data counts initialised writable data, and bss counts zero-initialised data; dec and hex are simply their sum (binutils manual, size). Turning that into the two numbers that matter:
The data column counts twice because initialised data lives in RAM while the program runs and in flash as the initial values (lesson 4). RAM for the stack and heap comes on top, unless the linker script reserves them as sections that the report includes.
arm-none-eabi-size prints three numbers per image. In its default Berkeley format, text counts code and read-only data, data counts initialised writable data, and bss counts zero-initialised data. Flash holds text and data (the initial values); RAM holds data and bss, plus any stack and heap the linker script reserves.
To find what is big, nm --size-sort --radix=d app.elf lists symbols ordered by size; the last lines are the largest functions and tables.
STEP 2
The map file
Adding -Wl,-Map=app.map to the link command makes the linker write a map file, the complete record of its decisions. The GNU ld manual lists what it contains: where each object file’s sections were placed, how common symbols were allocated, every archive member that was included and the symbol that caused it, and the values assigned to symbols (global symbols and those the linker script defines; static locals are not listed). An abridged, illustrative excerpt:
Archive member included to satisfy reference by file (symbol)
libboard.a(board.o) build/main.o (board_init)
libgpio.a(gpio.o) libboard.a(board.o) (gpio_init)
Memory Configuration
Name Origin Length Attributes
FLASH 0x10000000 0x00200000 xr
RAM 0x20000000 0x00040000 xrw
Linker script and memory map
.text 0x10000100 0xbc48
.text.led_on 0x10000234 0x18 build/led.o
.text.gpio_put 0x10000410 0x2c libgpio.a(gpio.o)
Read it from the top. The first block answers “why is this library code in my image?”: board.o was extracted from libboard.a because main.o referred to board_init, and gpio.o because board.o then referred to gpio_init, the chain lesson 3 described. The memory configuration repeats the script’s MEMORY block. Then, for each output section, every input section with its address, its size in bytes, and the file it came from: led_on is 0x18 = 24 bytes at 0x1000_0234 from led.o. When you use --gc-sections, the map file also lists the discarded input sections, which answers “why is my function missing?” Linker-generated stubs for far calls (lesson 2) appear here too, as small input sections the linker created itself.
STEP 3
Disassembly
arm-none-eabi-objdump -d app.elf disassembles every executable section. Each line gives the address, the encoding, and the instruction:
10000234 <led_on>:
10000234: b510 push {r4, lr}
1000023a: f000 f8e9 bl 10000410 <gpio_put>
1000023e: 4a02 ldr r2, [pc, #8] @ (10000248 <led_on+0x14>)
Thumb encodings are shown as 16-bit halfwords, as numbers; in memory each halfword is stored little-endian, so b510 occupies the bytes 10 b5. objdump names branch targets and literal addresses from the symbol table, which is why an ELF with symbols is far more useful than a raw binary. With -S and a build compiled with -g, it interleaves the source lines each instruction came from; the crash-dump question from the opening is then answered by searching the listing for the address.
↑ This step uses the figure at the top of the page.
Two things in a listing surprise newcomers. Literal pools: constants and addresses that do not fit in an instruction are stored as data words right after the function, and loaded with PC-relative ldr. GCC marks them with mapping symbols, so objdump prints them as .word; from a stripped file or a raw binary, where those markers are gone, it disassembles them as if they were instructions and prints nonsense. And compiler reordering: at -O2 the instructions for one source line may be scattered and interleaved with others, so the listing is the truth and the source is only an approximation of it.
STEP 4
Worked example: decoding two instructions by hand
Take the listing above and trust only the bits.
The BL at 0x1000_023A: f000 f8e9. In the first halfword, bit 10 is the sign S = 0 and the low ten bits imm10 = 0. In the second, J1 (bit 13) = 1, J2 (bit 11) = 1 and imm11 (low eleven bits) = 0xE9. The architecture defines and , and the offset is the concatenation :
which is gpio_put, exactly what objdump printed and what lesson 1 computed from the other direction.
The LDR at 0x1000_023E: 4a02. This is LDR r2, [PC, #imm8×4] with imm8 = 2. The address is computed from the PC value (the instruction’s address + 4), rounded down to a multiple of 4:
At 0x1000_0248 the listing shows the literal word 0x2000_0010: the address of led_count, patched there by the R_ARM_ABS32 relocation of lesson 2.
MYTHS AND FACTS
Common misconceptions
text is the flash size
Flash is text + data; the initial values of .data are stored in flash too.
bss takes space in the binary
It takes RAM, not flash: only its size is recorded, and startup code zeroes it.
The map file is only for linker experts
It is the fastest answer to “why is this in my image?”, “why is this missing?” and “where did this address come from?”.
Disassembly of an optimised build matches the source line by line
Optimisation reorders and merges; the listing is the truth.
objdump output bytes are in memory order
Thumb encodings are printed as halfword numbers; memory holds each halfword little-endian.
A raw .bin is enough to debug
Without the ELF’s symbols and debug information, addresses cannot be turned into names or source lines.
Check yourself
Answer in your head, then open the card.
size reports text 30 112, data 488, bss 6 400. How much flash and RAM does the image use?
Flash = 30 112 + 488 = 30 600 bytes. RAM = 488 + 6 400 = 6 888 bytes, plus the stack and heap if the linker script does not already count them.
A BL at 0x0800_0150 is encoded f7ff ffd6. Where does it branch?
S = 1, imm10 = 0x3FF; J1 = 1, J2 = 1, imm11 = 0x7D6. , . The 25-bit value is all ones except the low bits: offset = (0x7D6 − 0x800) × 2 = −84. Target = 0x0800_0150 + 4 − 84 = 0x0800_0100.
An LDR at 0x0800_0212 is encoded 4b03 (LDR r3, [PC, #12]). Which address does it read?
Align(0x0800_0216, 4) + 3 × 4 = 0x0800_0214 + 12 = 0x0800_0220.
The image grew by 11 KiB after a change. Which two tools would you use to find the cause?
Compare nm --size-sort output (or the map files) before and after: the map file shows which input sections and archive members are new and how big they are, and names the symbol that pulled each archive member in.
Sources (5)
- GNU Binutils manual, “size” — “The Berkeley style output counts read only data in the text column, not in the data column”; dec and hex are the sum of text, data and bss
- GNU ld manual, “Command-line Options”: -M / --print-map and -Map=mapfile — a link map shows where object files are mapped into memory, how common symbols are allocated, “all archive members included in the link, with a mention of the symbol which caused the archive member to be brought in”, and the values assigned to symbols
- GNU Binutils manual, “objdump” (-d, -S, -h, -t) and “nm” (--size-sort) — -d disassembles executable sections; -S intermixes source code when the image has debug information
- Arm, Armv6-M Architecture Reference Manual (DDI0419), §A6.7.13 “BL” and §A6.7.27 “LDR (literal)” — BL encoding T1 (S, imm10, J1, J2, imm11; I1 = NOT(J1 XOR S), I2 = NOT(J2 XOR S)); LDR (literal) address = Align(PC, 4) + imm8 × 4, where PC reads as the instruction address + 4
- Arm, ELF for the Arm Architecture (AAELF32), “Addends and PC-bias compensation” — worked encodings for Thumb BL used to cross-check the decoding