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.

SectionContainsStored inLives at run time in
.textMachine code and, on many targets, the interrupt vector tableFlashFlash
.rodataconst globals, string literals, lookup tablesFlashFlash
.dataGlobals and statics with a non-zero initial valueFlash (the initial values)RAM (copied at startup)
.bssGlobals and statics that are zero or have no initialiserNothing; only the size is recordedRAM (zeroed at startup)
HeapMemory returned by mallocNothingRAM, usually above .bss
StackLocal variables, return addresses, saved registersNothingRAM, 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 RAM

Which 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.

One variable per memory section, with comments showing where each one is placed
#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.

MCU-specific: a minimal Cortex-M reset handler written in C (symbol names come from the linker script)
#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

PropertyStackHeap
AllocationAutomatic on function entryExplicit with malloc and free
SpeedOne instruction to adjust the stack pointerAllocator search, time varies
LifetimeUntil the function returnsUntil free is called
Failure modeSilent overflow into other datamalloc returns NULL or memory fragments
Size set byLinker script or RTOS task stack sizeLinker 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.map to the linker flags) to see every symbol, its section, address and size. Sort by size to find what is eating memory.
  • Compile with -fstack-usage in 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 const on 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.