UNIT 04 · LESSON 5 OF 6

Reading a Map File and Disassembly

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? How do you read each?

INTERACTIVEReading a disassembly listing
An objdump listing with one line decoded10000234 <led_on>:10000234:  b510        push {r4, lr}10000236:  2101        movs r1, #110000238:  2019        movs r0, #251000023a:  f000 f8e9   bl   10000410 <gpio_put>1000023e:  4a02        ldr  r2, [pc, #8]  @ (10000248)10000240:  6813        ldr  r3, [r2, #0]10000242:  1c5b        adds r3, r3, #110000244:  6013        str  r3, [r2, #0]10000246:  bd10        pop  {r4, pc}10000248:  20000010    .word 0x20000010A 32-bit BL: two halfwords. Decoding the offset field gives 466 bytes; target =0x1000023A + 4 + 466 = 0x10000410, which the symbol table names gpio_put.
An objdump listing with one line decoded10000234 <led_on>:0234: b510      push {r4, lr}0236: 2101      movs r1, #10238: 2019      movs r0, #25023a: f000 f8e9 bl   10000410023e: 4a02      ldr  r2, [pc, #8]  0240: 6813      ldr  r3, [r2, #0]0242: 1c5b      adds r3, r3, #10244: 6013      str  r3, [r2, #0]0246: bd10      pop  {r4, pc}0248: 20000010  .word 0x20000010A 32-bit BL: two halfwords. Decoding the offsetfield gives 466 bytes; target = 0x1000023A + 4 + 466= 0x10000410, which the symbol table names gpio_put.

Try this

1000023a: bl 10000410 . A 32-bit BL: two halfwords. Decoding the offset field gives 466 bytes; target = 0x1000023A + 4 + 466 = 0x10000410, which the symbol table names gpio_put.

objdump -d prints each instruction’s address, its encoding (Thumb halfwords, as numbers) and its mnemonic, and names branch targets from the symbol table. This listing is the led_on function from lesson 1 after linking; pick a line to decode it. Targets are computed from the encoded bits, not copied from the listing.

What you will be able to do
  • Convert a size report (text, data, bss) into flash and RAM usage and explain why data counts twice.
  • Use a linker map file to find where a function or variable was placed, which object or library member it came from, and why a library member was pulled in.
  • Read an objdump disassembly listing: addresses, encodings, mnemonics, literal pools and symbol names.
  • Decode a Thumb BL and a PC-relative LDR from their encodings to find the branch target and the literal’s address.
  • Choose the right tool (size, nm --size-sort, the map file, objdump -d/-S) for a given question about an image.
Before you start
  • Linker scripts, VMA and LMA (lesson 4).
  • Thumb instructions and the BL offset (lessons 1 and 2).
Steps in this lesson
  1. The size report
  2. The map file
  3. Disassembly
  4. Worked example: decoding two instructions by hand
  5. Common misconceptions

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:

flash=text+data,\text{flash} = \text{text} + \text{data}, RAM=data+bss\text{RAM} = \text{data} + \text{bss}

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.

INTERACTIVEReading the size report
The text, data and bss columns of a size report turned into flash and RAM usagetext48200data1320bss9460dec58980hexe664filenameapp.elfflash = text + data49 520 / 262 144 B = 18.9 %RAM = data + bss10 780 / 65 536 B = 16.4 %Fits. RAM left for stack and heap: 54 756 B.
The text, data and bss columns of a size report turned into flash and RAM usagetext48200data1320bss9460dec58980flash = text + data49 520 / 262 144 B = 18.9 %RAM = data + bss10 780 / 65 536 B = 16.4 %Fits. RAM left for stack and heap: 54 756 B.
Device
flash = 48 200 + 1 320 = 49 520 B (18.9 %); RAM = 1 320 + 9 460 = 10 780 B (16.4 %). Fits. RAM left for stack and heap: 54 756 B.

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 I1=¬(J1⊕S)=0I_1 = \lnot(J_1 \oplus S) = 0 and I2=¬(J2⊕S)=0I_2 = \lnot(J_2 \oplus S) = 0, and the offset is the concatenation S:I1:I2:imm10:imm11:0S{:}I_1{:}I_2{:}\text{imm10}{:}\text{imm11}{:}0:

offset=0xE9×2=466,\text{offset} = \text{0xE9} \times 2 = 466, target=P+4+offset=0x1000_023A+4+466=0x1000_0410\text{target} = P + 4 + \text{offset} = \text{0x1000\_023A} + 4 + 466 = \text{0x1000\_0410}

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:

address=Align(0x1000_0242,4)+2×4=0x1000_0240+8=0x1000_0248\text{address} = \text{Align}(\text{0x1000\_0242}, 4) + 2 \times 4 = \text{0x1000\_0240} + 8 = \text{0x1000\_0248}

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. I1=¬(1⊕1)=1I_1 = \lnot(1 \oplus 1) = 1, I2=1I_2 = 1. 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)
  1. 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
  2. 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
  3. 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
  4. 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
  5. Arm, ELF for the Arm Architecture (AAELF32), “Addends and PC-bias compensation” — worked encodings for Thumb BL used to cross-check the decoding