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:
- text is your code plus read-only data, such as constants, string literals, and lookup tables. It lives in flash.
- data is initialized variables that have a non-zero starting value. It is counted twice: once in flash, where the initial values are stored, and once in RAM, where the variables live at runtime.
- bss is zero-initialized variables. It occupies RAM only and costs nothing in flash.
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:
- Which commit introduced the growth, and how much did each one add?
- What changed between this build and the last release?
- Is any section trending steadily toward its limit?
- Can a pull request be blocked automatically when it pushes the image past a defined budget?
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.