Theory-Storage Classes in Embedded C: auto, register, static, extern and Where They Live-Free Embedded C Tutorial Online

Need Help or More Information?

Questions about this free C programming course, embedded C training or career guidance? Get in touch.

MODULE 4 · LESSON 4.3 · THEORY

Storage Classes in Embedded C: auto, register, static, extern and Where They Live

How the four storage class specifiers decide lifetime, linkage and memory placement, which sections (.text, .rodata, .data, .bss, stack) hold each variable on the STM32F407, and how the start-up code prepares them. Remember this whenever you study storage classes in embedded C.

50 min
Read Time
Beginner → Intermediate
Level
M4 · L3
Module / Lesson

What You Will Learn

✓ The four storage class specifiers: auto, register, static and extern
✓ How each class decides lifetime, linkage and initial value
✓ Where every kind of variable lands: .text, .rodata, .data, .bss, stack
✓ How the STM32F407 start-up code prepares .data and .bss
✓ Reading real nm and size output to see the placement
✓ Why const data costs flash instead of SRAM
✓ Practical SRAM budgeting for firmware
✓ Common mistakes and best practices

Key Terms

Storage classautoregisterstaticextern.text.rodata.data.bssStackHeapFlashSRAMLoad addressLinker scriptStart-up code

1. What Is a Storage Class?

Every variable in C has a type (what the bits mean) and a storage class (where and for how long the bits are kept). Lesson 4.2 showed that scope and lifetime are separate ideas; the storage classes in embedded C are the keywords that choose between them and, on a microcontroller, choose which physical memory the variable occupies. Every one of the storage classes in embedded C comes down to this.

C has four storage class specifiers that you will meet in everyday code. Each one answers three questions: how long does the object live, which other files can see it, and what does it contain before you assign anything? This is why storage classes in embedded C matter on a small SRAM.

SpecifierTypical placementLifetimeLinkageInitial value if not given
autoStack or CPU registerBlock (one call)NoneIndeterminate (garbage)
registerCPU register (hint)Block (one call)NoneIndeterminate (garbage)
static.data / .bss in SRAMWhole programInternal at file scope, none in a blockZero
externWherever it is definedWhole programExternalZero (at the definition)
One keyword, two decisions: how long, and where
auto
created on entry
destroyed on exit
stack
register
like auto, but
asks for a CPU register
no & allowed
static
created before main()
never destroyed
.data / .bss
extern
refers to an object
defined elsewhere
no storage here
Only one per declaration: A declaration can have at most one of these specifiers (the language also has typedef and _Thread_local, which follow the same rule). You cannot write static extern int x;.

2. auto: The Default for Locals

Inside a function every variable is auto unless you say otherwise. The keyword is almost never written, because writing int x; already means auto int x;. An auto variable is created when execution enters its block and disappears when the block ends. The compiler normally keeps it in a CPU register; if it needs more variables than there are registers, or you take its address, it spills the variable to the stack in SRAM.

auto_default.c
void f(void) { int a = 1; /* same as: auto int a = 1; */ auto int b = 2; /* legal, never written in practice */ int c; /* indeterminate: not zeroed */ }

Because the stack is reused by every call, an uninitialized auto variable contains whatever an earlier function left behind. On a Cortex-M4 the stack lives at the top of SRAM and grows downward, and it is shared by main(), every function it calls and every interrupt handler. A large automatic array is therefore a classic cause of stack overflow.

Do not put big buffers on the stack: A function with uint8_t buf[2048]; consumes 2 KB of stack on every call and again on every nested call. In an RTOS task with a 1 KB stack this overwrites the next task’s memory without any error message.

3. register: A Hint That Modern Compilers Ignore

register asks the compiler to keep a variable in a CPU register for speed. Two things matter in practice. First, it comes with a rule: you cannot take the address of a register variable, because registers have no addresses. Second, modern compilers allocate registers far better than a programmer can, so with optimization on, register changes nothing except that rule.

reg.c
#include <stdio.h> int main(void) { register int counter = 0; int *p = &counter; /* taking the address of a register variable */ *p = 5; printf(“%d\n”, counter); return 0; }
Real GCC output when you take the address of a register variable
reg.c: In function ‘main’: reg.c:6:5: error: address of register variable ‘counter’ requested 6 | int *p = &counter; /* taking the address of a register variable */ | ^~~

On the STM32F407 the Cortex-M4 core has thirteen general purpose registers (R0–R12), so only the hottest few variables of a function ever fit. The compiler decides which. The only remaining legitimate use is in rare hand-tuned inner loops on very old compilers; otherwise leave it out. In C++17 the keyword was removed altogether.

Interview answer in one line: register is a hint, the compiler may ignore it, and it forbids & on the variable.

4. static: Program Lifetime in Two Flavours

Lesson 4.2 covered static in detail. The memory view adds one more fact: a static variable never lives on the stack. The linker gives it a fixed address in SRAM at build time. That has three consequences that matter in firmware:

  • Its address is the same on every run and can be read from the .map file.
  • It is never freed, so it permanently consumes SRAM whether or not its function ever runs.
  • It starts at zero or at its initializer without any code in your function: the start-up code did the work before main().
Where writtenLifetimeVisibilityExample
Inside a functionWhole programThat function onlystatic uint8_t last;
File scope (variable)Whole programThat file only (internal linkage)static uint32_t s_ticks;
File scope (function)n/a (code)That file onlystatic void helper(void);

5. extern: A Name Without Storage

extern does not allocate memory. It tells the compiler that an object with this name and type exists in another translation unit and the linker will connect them. Every global you share between files follows the pattern from Lesson 4.1: one definition in a .c file, an extern declaration in a header.

led.h
#ifndef LED_H #define LED_H #include <stdint.h> extern uint8_t g_led_mask; /* declaration: no storage here */ void led_toggle(uint8_t bit); #endif
led.c
#include “led.h” uint8_t g_led_mask = 0x00; /* definition: 1 byte of .bss */ void led_toggle(uint8_t bit) { g_led_mask ^= (uint8_t)(1u << bit); }
main.c
#include <stdio.h> #include “led.h” int main(void) { led_toggle(0); led_toggle(3); printf(“mask = 0x%02X\n”, g_led_mask); return 0; }
Output (verified by compiling and running)
mask = 0x09

A function declared without a body behaves the same way: void led_toggle(uint8_t bit); is implicitly extern, which is why headers full of prototypes work.

6. The Memory Sections of a Firmware Image

When the linker builds your program it sorts everything into sections. A storage class decides which section a variable goes into, so the section names are essential vocabulary for the storage classes in embedded C:

SectionContainsLives inInitialised by
.textMachine code of functionsFlashProgramming the chip
.rodataconst data, string literals, lookup tablesFlashProgramming the chip
.dataInitialized globals and staticsSRAM (initial values stored in Flash)Start-up code copies Flash → SRAM
.bssUninitialized (zero) globals and staticsSRAMStart-up code fills with zero
stackauto variables, return addresses, saved registersSRAM (top)Nothing: garbage
heapmalloc memorySRAM (between .bss and stack)Nothing
STM32F407 memory used by a typical program
Flash (1 MB) from 0x08000000
vector table (0x08000000)
.text — your code
.rodata — const tables, strings
load image of .data (initial values)
SRAM (128 KB) from 0x20000000
.data — copied here at reset
.bss — cleared at reset
heap grows up ↓
(free space)
stack grows down ↑ (top 0x20020000)
The stack top: Reset loads the initial stack pointer from the first word of the vector table. On the STM32F407 start-up files it points just above SRAM, at 0x20020000, so the first push lands at 0x2001FFFC.

7. Proof: Where Each Variable Really Goes

Instead of trusting a table, ask the toolchain. The program below is compiled to an object file and inspected with nm, which lists every symbol with a letter for its section. (Upper case = visible to the linker, lower case = local to the file.)

placement.c
int g_init = 5; /* initialized global */ int g_zero; /* uninitialized global */ const int g_const = 7; /* const global */ const char g_msg[] = “hello”; /* const array */ static int s_file = 3; /* file-scope static, init */ static int s_blank; /* file-scope static, blank */ int work(int v) { static int s_calls; /* static local, blank */ static int s_total = 9; /* static local, init */ int a = v; /* auto: no symbol at all */ s_calls++; s_total += a; return s_total + g_init + g_zero + g_const + s_file + s_blank + g_msg[0]; }
Real nm output, compiled with gcc -c -O0 (section letters: R/r = .rodata, D/d = .data, B/b = .bss, T = .text)
R g_const -> .rodata D g_init -> .data R g_msg -> .rodata B g_zero -> .bss b s_blank -> .bss b s_calls.1 -> .bss d s_file -> .data d s_total.0 -> .data T work -> .text
  • g_init and s_total have non-zero initializers, so they are in .data.
  • g_zero, s_blank and s_calls are zero, so they are in .bss: no initial value needs to be stored in flash.
  • g_const and g_msg are const, so they are read-only data.
  • a, the automatic variable, does not appear at all: it is a register or a stack slot decided at run time.
  • Note the name s_total.N style suffix on static locals: the compiler invents a unique file-level name because the real name is only meaningful inside the function.

The section letters above come from a PC build. The classification is a rule of the C language, so an ARM build with arm-none-eabi-gcc sorts these same variables into the same sections, although the addresses differ. One ARM detail: with the default options, GCC for ARM may also place small const data in .rodata with extra names such as .rodata.str1.1 for string literals.

8. How Much Memory Does a Program Use? The size Command

Embedded builds end with a size report. The numbers map directly to the sections above and they answer the question “will this fit?”:

Real size output for placement.c (PC object file, in bytes)
text data bss dec hex filename 148 12 12 172 ac t.o
ColumnMeaningWhich memory is used
textCode plus read-only data (.text + .rodata)Flash
dataInitialized variablesBoth: flash (initial image) and SRAM (working copy)
bssZero-initialized variablesSRAM only

Rule of thumb for an STM32F407 build: Flash used = text + data and static SRAM used = data + bss. The heap and the stack come on top, so you still need free SRAM for them. A linker error such as “region ‘RAM’ overflowed by N bytes” means data + bss (+ the heap and stack you reserved) exceeded the 128 KB.

9. What the Start-up Code Does Before main()

Power-up leaves SRAM full of random bits. Before your main() can rely on the guarantees of the language, the reset handler must make two memory regions match the C rules. On the STM32F407 it runs this sequence on every reset:

1. Load SP
from vector table
➜
2. Copy .data
Flash → SRAM
➜
3. Zero .bss
fill with 0
➜
4. SystemInit()
clocks
➜
5. main()
your code

The start-up file generated for the STM32F407 uses symbols created by the linker script to find the regions (typically _sidata for the load address in flash, _sdata/_edata for .data in SRAM and _sbss/_ebss for .bss). A host-side model of the two loops makes the idea visible:

startup_model.c
#include <stdio.h> #include <stdint.h> #include <string.h> /* A host-side model of what Reset_Handler does for a Cortex-M. */ static uint32_t flash_image[4] = { 5, 7, 9, 11 }; /* load image of .data in “flash” */ static uint32_t sram[8]; /* [0..3] = .data [4..7] = .bss */ int main(void) { memset(sram, 0xA5, sizeof sram); /* SRAM holds random garbage at power-up */ printf(“power-up : %08X %08X | %08X %08X\n”, (unsigned)sram[0], (unsigned)sram[1], (unsigned)sram[4], (unsigned)sram[5]); memcpy(&sram[0], flash_image, sizeof flash_image);/* step 1: copy .data from flash */ printf(“copy data: %08X %08X | %08X %08X\n”, (unsigned)sram[0], (unsigned)sram[1], (unsigned)sram[4], (unsigned)sram[5]); memset(&sram[4], 0, 4 * sizeof sram[0]); /* step 2: clear .bss */ printf(“clear bss: %08X %08X | %08X %08X\n”, (unsigned)sram[0], (unsigned)sram[1], (unsigned)sram[4], (unsigned)sram[5]); printf(“main() starts: .data holds %u %u %u %u, .bss all zero\n”, (unsigned)sram[0], (unsigned)sram[1], (unsigned)sram[2], (unsigned)sram[3]); return 0; }
Output (verified by compiling and running)
power-up : A5A5A5A5 A5A5A5A5 | A5A5A5A5 A5A5A5A5 copy data: 00000005 00000007 | A5A5A5A5 A5A5A5A5 clear bss: 00000005 00000007 | 00000000 00000000 main() starts: .data holds 5 7 9 11, .bss all zero
Why .data costs flash AND SRAM, but .bss costs only SRAM
int g = 5; (.data)
Flash stores the number 5 (4 bytes)
SRAM reserves 4 bytes for the working copy
Total: 4 B flash + 4 B SRAM
int buf[1000]; (.bss)
Flash stores nothing: zero needs no image
SRAM reserves 4000 bytes, cleared at reset
Total: 0 B flash + 4000 B SRAM
A custom linker script or bootloader changes the rules: If the start-up code is replaced and the copy or clear loop is missing, initialized globals contain garbage and uninitialized ones are not zero. Always check that the start-up file matches the linker script.

10. Storage Classes in Embedded C: Embedded Significance

On a PC, memory is plentiful and the operating system hides placement. On an STM32F407 you have 1 MB of flash and 128 KB of main SRAM (plus 64 KB of core-coupled memory), so the storage class of each variable is a design decision.

GoalWhat to doEffect on memory
Keep a lookup table out of SRAMstatic const uint16_t sine[256] = {...};Lives in flash (.rodata), 0 bytes of SRAM
Keep a file-private counterstatic uint32_t s_ticks;4 bytes of .bss, hidden name
Share a flag with an ISRextern volatile uint8_t g_flag; in a header1 byte of .bss, visible everywhere
Avoid stack overflow from a large bufferMake it static in the function or a file-private globalMoves from stack to .bss, fixed size known at link time
Save flash from zero-filled arraysLeave them uninitialized (.bss) instead of = {0} where the compiler would store explicit zerosUsually the same, but check with size
Know the worst-case RAMRead the linker map and size outputdata + bss + heap + stack must fit

The const trap

const on a global usually means flash, but const on a local only prevents assignment: the object is still on the stack. To keep a table in flash, make it a file-scope or static const object. Lesson 13 returns to this.

11. Common Mistakes With Storage Classes

Mistake 1 — assuming locals are zero: auto variables are indeterminate. Only static-duration objects are zero-initialized.
Mistake 2 — taking the address of a register variable: It is a hard compile error, as the GCC output above shows.
Mistake 3 — a huge array in a function body: It consumes stack on every call. Make it static or move it to file scope.
Mistake 4 — a lookup table without const: uint16_t sine[256] = {...}; is copied into SRAM at start-up, using 512 bytes of RAM and 512 of flash. Add const.
Mistake 5 — defining a variable in a header: Every includer gets its own definition: multiple-definition errors or separate copies.
Mistake 6 — relying on .bss zeroing after a custom start-up change: Test it: a global that starts non-zero after reset proves the clear loop is missing.
Mistake 7 — forgetting the heap and stack in the RAM budget: size reports only data + bss. A program at 120 KB of 128 KB of static data has almost no stack left.

12. Best Practices for Storage Classes in Embedded C

  • Use the storage class that gives the smallest lifetime and visibility that works: locals first, file static next, true globals last.
  • Never write auto; do not write register.
  • Mark constant tables const at file scope so they stay in flash.
  • Keep big buffers out of the stack: static or file-scope, with a clear owner.
  • Declare shared variables extern in one header and define them in one source file that includes that header.
  • After every build, read the size report and watch data + bss against the 128 KB limit.
  • Open the linker map file at least once per project to see the real addresses.
  • Initialize automatic variables where you declare them.

Key Takeaways — Lesson 4.3

  • The four storage classes in embedded C are auto, register, static and extern.
  • auto locals live on the stack (or in registers) and are not initialized.
  • register is only a hint and forbids the address-of operator.
  • static objects live at a fixed address in .data or .bss for the whole program.
  • extern creates no storage; it refers to a definition elsewhere.
  • .data uses flash and SRAM; .bss uses SRAM only; const globals use flash only.
  • The start-up code copies .data and clears .bss before main().
  • Tools: nm shows the section of each symbol and size totals them.

Frequently Asked Questions

What are the storage classes in C?
The storage class specifiers are auto, register, static, extern (and typedef, _Thread_local grammatically). They control the lifetime, linkage and placement of a variable.
Where is a static variable stored on an STM32?
In SRAM: in .data if it has a non-zero initializer and in .bss otherwise. It has a fixed address chosen by the linker and is never on the stack.
What is the difference between .data and .bss?
.data holds variables with non-zero initial values; their initial image is stored in flash and copied to SRAM at start-up. .bss holds zero-initialized variables; no image is stored and the start-up code just fills the region with zeros.
Does the register keyword make code faster?
Almost never with a modern optimizing compiler. It is a hint that may be ignored, and it forbids taking the variable’s address.
Is a const global stored in flash or RAM?
On the STM32F407 a file-scope const object normally goes to .rodata in flash and uses no SRAM. A const local is just a normal stack variable that you promised not to modify.
How can I see how much SRAM my program uses?
Run arm-none-eabi-size firmware.elf: static SRAM = data + bss. Add the stack and heap sizes from the linker script to get the total, and check the .map file for the exact addresses.

Ready to practise?

Next: nm and size on real programs, static versus automatic addresses, const in flash and a start-up copy demo.

Need Help or More Information?

Questions about this free C programming course, embedded C training or career guidance? Get in touch.

2 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *