Embedded Memory Layout: .text, .data, .bss, Stack and Heap
On a PC you rarely think about where a variable lives. On a microcontroller with 20 KB of SRAM you must. A large local array can overflow the stack and silently corrupt globals; an initialised global costs both flash and RAM; a careless malloc can fragment the heap after a few hours of running. All of these make sense once you know how the compiler and linker divide memory into sections.
The Standard Sections
The compiler puts each piece of your program into a named section. The linker then places each section into flash or RAM according to a linker script. The names below are the ones used by GCC and most other toolchains.
| Section | Contains | Stored in | Lives at run time in |
|---|---|---|---|
| .text | Machine code and, on many targets, the interrupt vector table | Flash | Flash |
| .rodata | const globals, string literals, lookup tables | Flash | Flash |
| .data | Globals and statics with a non-zero initial value | Flash (the initial values) | RAM (copied at startup) |
| .bss | Globals and statics that are zero or have no initialiser | Nothing; only the size is recorded | RAM (zeroed at startup) |
| Heap | Memory returned by malloc | Nothing | RAM, usually above .bss |
| Stack | Local variables, return addresses, saved registers | Nothing | RAM, usually at the top, growing down |
The key insight is that .data costs twice: its initial values sit in flash and the working copy sits in RAM. A .bss variable costs only RAM, and a const table costs only flash on most 32-bit microcontrollers.
Typical single-RAM-bank layout (ARM Cortex-M style)
Flash RAM
+----------------+ low addr +----------------+ low addr
| vector table | | .data |
| .text | | .bss |
| .rodata | | heap (grows up)|
| .data init |-- copied -->| ... |
+----------------+ | stack(grows dn)|
+----------------+ top of RAMWhich Section Does Each Variable Use?
The program below declares one variable of each kind and prints its address. It compiles on any C compiler. On a PC the absolute addresses are meaningless, but variables of the same kind appear close together, and you can confirm the classification with your toolchain's nm or size utilities.
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
const uint16_t sine_table[4] = {0, 23170, 32767, 23170}; /* .rodata */
uint32_t boot_count = 7; /* .data : initial value 7 kept in flash */
uint32_t error_count; /* .bss : zeroed by startup code */
static uint8_t rx_buffer[64]; /* .bss : static, still zero-initialised */
static void show(const char *name, const void *addr)
{
printf("%-14s %p\n", name, addr);
}
int main(void)
{
uint8_t local_buf[32]; /* stack : exists only during main() */
static uint32_t calls = 1; /* .data : static local with initialiser */
uint8_t *dyn = malloc(16); /* heap : pointer on stack, data on heap */
show("sine_table", sine_table);
show("boot_count", &boot_count);
show("calls", &calls);
show("error_count", &error_count);
show("rx_buffer", rx_buffer);
show("heap block", dyn);
show("local_buf", local_buf);
show("main (code)", (const void *)(uintptr_t)main);
free(dyn);
(void)local_buf;
return 0;
}
Converting a function pointer to void * is not strictly portable C, which is why the example goes through uintptr_t; it works on all common compilers. Notice that a static local such as calls is not on the stack. It keeps its value between calls and lives in .data or .bss exactly like a global, only with a narrower name scope.
What Startup Code Does Before main()
After reset, RAM contains random values. Before main runs, a small startup routine must set the stack pointer, copy .data from flash to RAM and fill .bss with zeros. On ARM Cortex-M the hardware loads the initial stack pointer from the first word of the vector table, and the reset handler does the rest. The symbols used below are defined in the linker script; the exact names vary by vendor, and these follow the common STM32 GCC templates.
#include <stdint.h>
extern uint32_t _sidata; /* start of .data initial values in flash */
extern uint32_t _sdata; /* start of .data in RAM */
extern uint32_t _edata; /* end of .data in RAM */
extern uint32_t _sbss; /* start of .bss */
extern uint32_t _ebss; /* end of .bss */
int main(void);
void Reset_Handler(void)
{
uint32_t *src = &_sidata;
uint32_t *dst = &_sdata;
while (dst < &_edata) { /* copy initial values into RAM */
*dst++ = *src++;
}
for (dst = &_sbss; dst < &_ebss; ) {
*dst++ = 0; /* zero the .bss section */
}
main();
for (;;) { } /* main should never return */
}
Vendor startup files usually do this in assembly and also call SystemInit to set up clocks, and __libc_init_array to run C++ constructors, but the logic is the same. If you ever see globals with garbage values, check that the startup code and the linker script agree.
Stack vs Heap on a Microcontroller
| Property | Stack | Heap |
|---|---|---|
| Allocation | Automatic on function entry | Explicit with malloc and free |
| Speed | One instruction to adjust the stack pointer | Allocator search, time varies |
| Lifetime | Until the function returns | Until free is called |
| Failure mode | Silent overflow into other data | malloc returns NULL or memory fragments |
| Size set by | Linker script or RTOS task stack size | Linker script heap size |
Many embedded coding standards, including MISRA C, restrict dynamic allocation after initialisation. The reason is not speed but predictability: a device that runs for months cannot afford an allocation that fails once a week due to fragmentation. Common practice is to allocate everything statically, or allocate once at start-up and never free.
The stack is the bigger day-to-day risk. Deep call chains, recursion, large local arrays and printf with floating-point formatting all consume stack. With an RTOS every task has its own stack, so the problem multiplies.
Measuring Memory Use
- Run
arm-none-eabi-size firmware.elf. It prints text, data and bss. Flash used is text plus data; static RAM used is data plus bss. - Open the map file (add
-Wl,-Map=firmware.mapto the linker flags) to see every symbol, its section, address and size. Sort by size to find what is eating memory. - Compile with
-fstack-usagein GCC to get a .su file listing the stack frame size of each function. - At run time, fill the stack area with a known pattern such as 0xDEADBEEF at start-up, run the product hard, then check how much of the pattern was overwritten. This is called stack painting, and RTOSes such as FreeRTOS offer it through high-water-mark functions.
Common Mistakes
- Declaring a 2 KB buffer as a local variable in a function on a part with a 1 KB stack.
- Forgetting
conston a large table, so it is copied into RAM as.data. - Expecting a global without an initialiser to hold garbage. In C it is guaranteed to be zero, and the startup code makes that true.
- Calling malloc and free repeatedly with different sizes in a long-running loop.
Summary
Code and constants live in flash as .text and .rodata. Initialised globals live in .data, which costs flash for the initial values and RAM for the working copy. Zero-initialised globals live in .bss and cost only RAM. Startup code copies .data and clears .bss before main. The stack holds locals and grows down toward the heap. Check the map file and the size output on every project and you will catch memory problems before they become field failures.