memprobe is in free beta. Sign up now during beta to get 40% off Pro for life. See pricing
All posts

Finding What Uses Your Firmware's Flash and RAM

Most embedded engineers have seen this message at the worst possible time:

region `FLASH' overflowed by 4096 bytes

It usually shows up late in a project, when the schedule is tight and the feature that pushed you over the limit is one you cannot drop. The linker tells you that you are 4 KB over, but not why, and not which part of the build is responsible. The code compiled cleanly last week, and nothing in the error points to a cause.

The information you need is already sitting in your ELF file. The standard toolchain can show you exactly where your flash and RAM went, as long as you know which tool answers which question. This post works through those tools in the order I reach for them, from the broad totals down to individual symbols, and then looks at where they stop being enough.

Start with the totals, but know their limits

Before optimizing anything, it helps to be precise about what you are measuring. The first tool is size:

arm-none-eabi-size firmware.elf
   text    data     bss     dec     hex filename
 312040    2204   42118  356362   570ca firmware.elf

The column names are misleading, so they are worth pinning down:

So your flash usage is roughly text + data, and your RAM usage is roughly data + bss. That single distinction clears up a lot of confusion, including why one large global array can hurt both budgets at once. What it will not do is tell you where any of it came from. Knowing you have used 312 KB of text does not help you find the few kilobytes you need to recover.

Narrow the problem to a section

The next step is to break the totals down by section with the -A flag:

arm-none-eabi-size -A firmware.elf
section              size        addr
.text              298604   134221824
.rodata             13436   134520428
.data                2204   536870912
.bss                42118   536873116
.isr_vector           388   134217728

This tells you whether the growth is in code or in data. A large .rodata section usually points to constant tables, fonts, or assets compiled into the image. A large .text section points to code, and in practice the usual cause is a library that was pulled in for a single function and linked in its entirety.

The section view narrows the search, but a section is still a bucket holding thousands of symbols. To find the specific items responsible, you have to go one level deeper.

Get down to the individual symbols

The nm tool lists every symbol in the binary. Sorted by size, it gives you a ranked list of your largest functions and variables:

arm-none-eabi-nm --print-size --size-sort --radix=d firmware.elf
00018922 00008192 T crypto_aes_expand_key
00021016 00007680 R wifi_country_table
00028704 00003072 T json_parse_recursive

The second column is the size in bytes. From the largest entries here, you can see that an AES key-expansion routine, a WiFi country table, and a recursive JSON parser are your three biggest contributors. This is the level of detail that actually directs an optimization effort: you now know whether to swap a library, move a table into a different region, or reconsider an algorithm.

For a complementary view, readelf -S firmware.elf prints the full section header table, and objdump -d disassembles the code when you need to understand a specific function rather than just measure it.

Where the standard tools stop

Used together, size, nm, and readelf can locate almost any single-build size problem. What they were never designed to do is answer the questions that come up once you are shipping firmware on a team and maintaining it over time:

Comparing builds is outside what these tools do, so teams end up maintaining shell scripts that diff nm output.

That is the part I wanted to fix, so I built memprobe. It reads the same ELF metadata and gives you a visual breakdown, a diff between any two builds, and history over time. The binary never leaves your machine. A GitHub Action runs it in CI and can fail a pull request that goes over a flash or RAM budget you set.

Summary

When the linker reports an overflow, the fastest path to a cause is to work from the general to the specific. Use size for the totals, size -A to identify the section, and nm to find the individual symbols. For most one-off problems, that sequence is enough. Once you are tracking size continuously, or enforcing a budget across a team, it is worth moving the analysis out of the terminal and into something that records it for you, so the next overflow is one you saw coming rather than one that stopped your build.

Stop reading nm output by hand
Drop in an ELF and see exactly where your flash and RAM go, free during the beta.
Open the analyzer