Storage Class Examples: nm, size, const Tables, static Buffers and the Start-up Model-Free Embedded C Training in Hyderabad,

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 · EXAMPLES

Storage Class Examples: nm, size, const Tables, static Buffers and the Start-up Model

Nine worked examples with real tool output: classify variables with nm, measure flash and SRAM with size, compare const tables, big local versus static arrays, register, and the reset copy/clear model. Study this one of the storage class examples carefully.

10
Worked Examples
Intermediate
Level
Real output
Compiled and run

Storage Class Examples: What This Page Covers

These storage class examples use the real tools, nm, size, GCC diagnostics and running programs, to show where each variable lives and how long it lasts. Section letters and sizes come from a PC build; the classification rules are identical on the STM32F407. This of the storage class examples is the one interviewers love.

#ExampleSkill
1Classify every variable with nmSection placement
2How const changes the sectionFlash vs SRAM
3Cost of a big table: const or not (size)Memory budgeting
4A big local array versus a static oneStack vs .bss
5Stack versus static addressesWhere objects sit relative to each other
6The register keyword and its one hard ruleregister
7Zero cost of .bss: size of a 4000-byte arraydata vs bss
8Start-up model: what happens to .data and .bssReset sequence
9extern + static: a small module with hidden stateEncapsulation
10Mini exercisesSelf-test

Example 1: Classify Every Variable With nm

Problem: For each of the variables A to I predict the section, then check the real symbol table. The storage class examples on this page use real tool output.

classify.c
int counter; /* A */ int limit = 100; /* B */ static int mode; /* C */ static int retries = 3; /* D */ const int MAX_USERS = 8; /* E */ const char banner[] = “STM32F4”; /* F */ void task(void) { static int calls; /* G */ static int last = –1; /* H */ int tmp = counter; /* I */ calls++; last = tmp; mode += retries + limit + MAX_USERS + banner[0] + calls + last; }
VarYour predictionReason
A counter.bssglobal, no initializer
B limit.dataglobal, non-zero initializer
C mode.bssfile static, zero
D retries.datafile static, non-zero
E MAX_USERS.rodataconst global
F banner.rodataconst array
G calls.bssstatic local, zero
H last.datastatic local, -1 is non-zero
I tmpstack/registerauto: no symbol
Real nm output (gcc -c -O0). Lower case = local to the file
R MAX_USERS -> .rodata R banner -> .rodata b calls.1 -> .bss B counter -> .bss d last.0 -> .data D limit -> .data b mode -> .bss d retries -> .data T task -> .text

Every prediction matches. Notice that tmp has no symbol: automatic variables never appear in the object file’s symbol table. An initializer of -1 forces .data even though the variable is static. Keep these storage class examples in mind when reading a map file.

Example 2: How const Changes the Section

Problem: The same four numbers are stored three ways. Which version uses SRAM?

const_tables.c
int table_ram[4] = { 10, 20, 30, 40 }; const int table_flash[4] = { 10, 20, 30, 40 }; int lookup(int i) { return table_ram[i] + table_flash[i]; }
Real nm output
T lookup -> .text R table_flash -> .rodata D table_ram -> .data

table_ram is in .data: 16 bytes in flash for the initial image and 16 bytes of SRAM. table_flash is in .rodata: 16 bytes of flash and no SRAM. Now the same table as a const local:

const_local.c
int lookup(int i) { const int local_tab[4] = { 10, 20, 30, 40 }; /* const LOCAL */ return local_tab[i]; }
Real nm output
T lookup -> .text (no data symbol at all)
Meaning: A const local array gets no symbol of its own: the compiler builds it on the stack at run time (or keeps the constants in code). At higher optimization levels it may place an anonymous read-only copy in .rodata, but you cannot rely on that. To guarantee flash, write static const at file scope or inside the function.

Example 3: The Cost of a Big Table With and Without const

Problem: A 256-entry table of 16-bit values is added to the firmware. How much flash and SRAM does it use in each form?

DeclarationSectionFlashSRAM
unsigned short t[256] = {...};.data512 B512 B
const unsigned short t[256] = {...};.rodata512 B0 B
Real size output
without const: text data bss dec hex filename 32 512 0 544 220 t.o with const: text data bss dec hex filename 544 0 0 544 220 t.o

Without const the 512 bytes appear in the data column (text = 32 is just the small function that uses them); with const they move into text (544 = 32 + 512) and data drops to 0. Only the second form leaves SRAM free. On the STM32F407 the arithmetic is the same: 512 B of the 128 KB is 0.4 %, but a few dozen such tables (waveforms, CRC tables, font bitmaps, calibration curves) quickly add up.

Example 4: A Big Local Array Versus a Static One

Problem: Two functions compute the same sum. What differs in memory?

stack_vs_bss.c
#include <stdio.h> #include <string.h> static unsigned sum_stack(void) { unsigned char buf[4096]; /* 4 KB on the STACK of every call */ memset(buf, 1, sizeof buf); unsigned s = 0; for (unsigned i = 0; i < sizeof buf; i++) s += buf[i]; return s; } static unsigned sum_static(void) { static unsigned char buf[4096]; /* 4 KB in .bss, once */ memset(buf, 1, sizeof buf); unsigned s = 0; for (unsigned i = 0; i < sizeof buf; i++) s += buf[i]; return s; } int main(void) { printf(“stack version : %u\n”, sum_stack()); printf(“static version: %u\n”, sum_static()); return 0; }
Output (verified by compiling and running)
stack version : 4096 static version: 4096
size of a file with a 4096-byte static buffer: the bytes show up in bss
text data bss dec hex filename 50 0 4096 4146 1032 t.o
VersionWhere the 4096 bytes areWhen they are reservedRisk on 128 KB SRAM
sum_stackStackOn every callStack overflow if the stack is small; invisible to the linker
sum_static.bssAlways (fixed at link time)Linker reports if RAM overflows; not re-entrant

The output is identical; the difference is who notices a problem and when. A static buffer fails at link time with a clear message. A stack buffer fails at run time, possibly in the field, with a hard fault or silent corruption.

Example 5: Static Objects Versus the Stack, by Address

Problem: Where do globals, statics, constants and the heap sit relative to a stack variable? Absolute addresses vary by system, so the program prints only the relationships.

addresses.c
#include <stdio.h> #include <stdint.h> int g_zero; /* .bss */ int g_data = 5; /* .data */ const int g_const = 7; /* .rodata */ static void show(const char *name, const void *a, const void *ref) { printf(“%-8s is %s the stack variable\n”, name, (uintptr_t)a < (uintptr_t)ref ? “below” : “above”); } int main(void) { int local = 0; /* stack */ static int s_local; /* .bss */ int *heap = (int *)__builtin_malloc(sizeof(int)); /* heap */ show(“g_zero”, &g_zero, &local); show(“g_data”, &g_data, &local); show(“g_const”, &g_const, &local); show(“s_local”, &s_local, &local); show(“heap”, heap, &local); printf(“g_data and g_zero are %s 1 MB of each other\n”, ((uintptr_t)&g_data > (uintptr_t)&g_zero ? (uintptr_t)&g_data – (uintptr_t)&g_zero : (uintptr_t)&g_zero – (uintptr_t)&g_data) < 1048576u ? “within” : “more than”); __builtin_free(heap); return 0; }
Output (verified by compiling and running)
g_zero is below the stack variable g_data is below the stack variable g_const is below the stack variable s_local is below the stack variable heap is below the stack variable g_data and g_zero are within 1 MB of each other

On a PC with address-space randomisation the exact numbers change on every run, but static objects typically sit far below the stack. On an STM32F407 the layout is stricter: .data and .bss start at 0x20000000, the heap follows them, and the stack starts at the top of SRAM (0x20020000) and grows toward the heap.

STM32F407 SRAM: who is where
.data0x20000000
.bss…
heap ↓…
free…
stack ↑0x20020000

Example 6: The register Keyword and Its One Hard Rule

Problem: Is register ever useful, and what does the compiler enforce?

reg2.c
#include <stdio.h> static int sum(const int *a, int n) { register int total = 0; /* allowed: no address taken */ for (register int i = 0; i < n; i++) total += a[i]; return total; } int main(void) { int v[4] = { 1, 2, 3, 4 }; printf(“%d\n”, sum(v, 4)); return 0; }
Output (verified by compiling and running)
10

Taking the address is an error

reg3.c
int main(void) { register int x = 1; int *p = &x; return *p; }
Real GCC output
reg3.c: In function ‘main’: reg3.c:4:5: error: address of register variable ‘x’ requested 4 | int *p = &x; | ^~~

The program works with or without the keyword, because the compiler places total and i in registers anyway. The only observable effect of register is the error above. Remove it from your own code and let the optimizer decide.

Example 7: The Zero Cost of .bss in Flash

Problem: Compare two 4000-byte arrays: one uninitialized, one filled with a non-zero pattern. What do they cost?

bss_vs_data.c
unsigned char zeros[4000]; /* .bss */ unsigned char ones[4000] = { [0] = 1 }; /* .data: image must be stored */
Real size output
only zeros[]: text data bss dec hex filename 32 0 4000 4032 fc0 t.o only ones[] (first byte 1): text data bss dec hex filename 32 4000 0 4032 fc0 t.o
ArraySectionFlash usedSRAM used
zeros.bss04000
ones.data4000 (the image)4000
A hidden trap: Writing = {1} for a single non-zero element turns the whole 4000-byte array into .data, because the other bytes must be zero in the flash image too (unless the toolchain compresses it). Initialize such buffers at run time instead.

Example 8: Start-up Model, Watching .data and .bss Get Their Values

Problem: Show the three states of two initialized and two uninitialized variables around reset: garbage, after the .data copy, and after the .bss clear.

reset_model.c
#include <stdio.h> #include <stdint.h> #include <string.h> /* Layout of a tiny “SRAM”: two initialized variables followed by two zero ones */ typedef struct { uint32_t baud; uint32_t timeout; uint32_t rx_count; uint32_t err_count; } Uart; static const Uart flash_data = { 115200, 50, 0, 0 }; /* what the flash image holds */ int main(void) { Uart ram; memset(&ram, 0xEE, sizeof ram); /* power-up garbage */ printf(“after power-up : baud=%08X timeout=%08X rx=%08X err=%08X\n”, (unsigned)ram.baud, (unsigned)ram.timeout, (unsigned)ram.rx_count, (unsigned)ram.err_count); ram.baud = flash_data.baud; /* the copy loop for .data */ ram.timeout = flash_data.timeout; printf(“after .data : baud=%08X timeout=%08X rx=%08X err=%08X\n”, (unsigned)ram.baud, (unsigned)ram.timeout, (unsigned)ram.rx_count, (unsigned)ram.err_count); ram.rx_count = 0; ram.err_count = 0; /* the clear loop for .bss */ printf(“after .bss : baud=%u timeout=%u rx=%u err=%u\n”, (unsigned)ram.baud, (unsigned)ram.timeout, (unsigned)ram.rx_count, (unsigned)ram.err_count); return 0; }
Output (verified by compiling and running)
after power-up : baud=EEEEEEEE timeout=EEEEEEEE rx=EEEEEEEE err=EEEEEEEE after .data : baud=0001C200 timeout=00000032 rx=EEEEEEEE err=EEEEEEEE after .bss : baud=115200 timeout=50 rx=0 err=0
Power-up
garbage
➜
Copy .data
Flash → SRAM
➜
Zero .bss
fill 0
➜
main()
values valid

Hexadecimal EEEEEEEE stands for what a real SRAM contains after power-up: unpredictable. If your own start-up file skipped the first loop, baud would stay garbage and the UART would never run at 115200.

Example 9: A Small Module Combining All the Storage Classes

Problem: Build a moving-average sensor module whose state is hidden, whose limit is read-only and shared, and whose helper variables use the least possible scope.

sensor.h
#ifndef SENSOR_H #define SENSOR_H #include <stdint.h> extern const uint16_t g_sensor_limit; /* read-only, shared */ void sensor_sample(uint16_t raw); uint16_t sensor_average(void); #endif
sensor.c
#include “sensor.h” const uint16_t g_sensor_limit = 4095; /* .rodata */ static uint16_t s_window[4]; /* .bss, private */ static uint8_t s_index; /* .bss, private */ void sensor_sample(uint16_t raw) { s_window[s_index] = raw > g_sensor_limit ? g_sensor_limit : raw; s_index = (uint8_t)((s_index + 1) % 4); } uint16_t sensor_average(void) { uint32_t sum = 0; for (int i = 0; i < 4; i++) sum += s_window[i]; return (uint16_t)(sum / 4); }
main.c
#include <stdio.h> #include “sensor.h” int main(void) { uint16_t raw[6] = { 100, 200, 300, 400, 9000, 500 }; for (int i = 0; i < 6; i++) sensor_sample(raw[i]); printf(“limit = %u\n”, g_sensor_limit); printf(“average = %u\n”, sensor_average()); return 0; }
Output (verified by compiling and running)
limit = 4095 average = 1323
ItemStorage classSectionVisibility
g_sensor_limitexternal, const.rodata (flash)all files via extern
s_window[4]static (internal).bss (SRAM)sensor.c only
s_indexstatic (internal).bss (SRAM)sensor.c only
sum, iautoregister/stackinside sensor_average()

Only two small functions are exported. The 9000 reading was clamped to 4095 before it entered the window, so the average of 300, 400, 4095 and 500 is 1323 (integer division). The same file compiles unchanged for the STM32F407.

Example 10: Mini Exercises

Exercise 1: In which section is static int n = 0; at file scope?
.bss. An explicit zero initializer is still zero, so no flash image is needed.
Exercise 2: How many bytes of SRAM does const uint8_t tab[100] = {...}; (file scope) use?
0. It is in .rodata (flash).
Exercise 3: uint32_t buf[256]; inside a function, called from a task with a 512-byte stack. What happens?
The 1024-byte array overflows the task stack; fix with static or a larger stack.
Exercise 4: Flash used and static SRAM used for text=2000, data=300, bss=700?
Flash = 2000 + 300 = 2300 bytes; static SRAM = 300 + 700 = 1000 bytes.
Exercise 5: Is extern int x; in .data or .bss?
Neither: it allocates nothing. The definition elsewhere decides.
Exercise 6: Why is register int x; &x an error?
A register has no memory address.
Exercise 7: Which section holds the string literal in printf("hi")?
.rodata (flash).

Key Takeaways from the Storage Class Examples

  • These storage class examples show that nm classifies variables exactly as the language rules predict.
  • Non-zero initializer → .data (flash + SRAM); zero or none → .bss (SRAM only); const global → .rodata (flash only).
  • const on a local does not move it to flash; use static const.
  • A big stack array fails at run time; a static one is counted by the linker.
  • register changes nothing except forbidding &.
  • Start-up code turns the flash image into valid .data and clears .bss before main().
  • Hide module state with static and export only functions and read-only constants.

Ready for the quiz?

Next: match variables to sections, MCQs, code challenges and interview questions on storage classes.

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 *