Skip to content
Tech Interview Prep home
Technical interview guide

Embedded C/C++ & Memory Constraints

Why systems programming on bare metal with kilobytes of RAM surfaces correctness issues that rarely bite on a general-purpose OS.

Read
55 min
Practice MCQs
25
Interview QA
25
Edition
v3
Editorial status
Reviewed

Scope: GCC current C/C++ implementation and optimization guidance; CMSIS 6 startup/linker/compiler control; Arm ABI and SEI CERT guidance reviewed 2026-09-04.

Overview

Curated: · Written: · Reviewed:

Embedded C and C++ usually run in a freestanding environment: startup, termination, available library facilities, memory layout, and I/O are supplied by the platform rather than a hosted OS contract. The selected language standard, compiler version and flags, target triple, CPU/FPU options, ABI, C library, linker script, startup objects, and whole-program optimization collectively define the executable. A source file that compiles is not evidence that its behavior, layout, timing, or resource use is portable.

Memory has several budgets: flash load image, executable/read-only data, initialized RAM with a flash load copy, zero-initialized RAM, retained/no-init regions, DMA or tightly coupled memory, stacks, heaps or pools, bootloader slots, metadata, and update scratch. Linker map and symbols are authoritative for static placement; runtime high-water instrumentation and worst-case call/interrupt analysis are needed for stacks and dynamic allocation. Average free memory is not a safety margin.

C and C++ hazards include undefined signed overflow, out-of-bounds and lifetime errors, alignment, strict aliasing, integer promotions, narrowing, shift rules, padding, bit-field layout, initialization order, exceptions/RTTI overhead, recursion, and unbounded library behavior. Fixed-width integers do not by themselves make arithmetic safe. Volatile is for observable access and is not atomicity, mutual exclusion, a general memory barrier, or a DMA cache-coherency mechanism.

Production firmware should make allocation and failure policy explicit, prefer bounded ownership and storage duration appropriate to the phase, ban or isolate dynamic behavior where fragmentation or nondeterminism is unacceptable, and verify release-optimized binaries. The production invariant is binary-budget fidelity: every memory, timing, and language-safety claim is proven against the exact linked image and target ABI with static analysis, warnings, tests, runtime watermarks, fault injection, and enough retained crash evidence to diagnose exhaustion or corruption.

The budgets only exist on the linked image. A Cortex-M4 firmware with -O2 and LTO reported 41,208 bytes of .text in the map; the same sources at -O0 used 79,440 bytes and missed the 64 KiB flash budget by 15 KiB, so the debug build never represented the product. Stack painting on the release image showed the control task at 1,148 bytes of 1,536 allocated — 75% — while the debug build peaked at 892 bytes because inlining and register spills differ. Heap in an ISR is a different failure: malloc from a 10 kHz ADC ISR under FreeRTOS heap_4 takes a critical section; when the heap is fragmented into 40-byte holes a 48-byte request blocks the ISR until a task frees, and the next ADC sample is lost. After 14 minutes the overrun counter hit 1,200 and the filter diverged. Ban allocation after init, or use a lock-free pool sized at compile time.

C undefined behavior is not theoretical on these cores. Signed int16_t accumulation wraps at 32767; with 12-bit ADC samples of ±2048 a burst of 17 samples overflows and the IIR filter sees a negative step of 65,536 counts, which on a 3.3 V full scale is a −10.7 V spike the plant cannot have produced. Packed structs for a 6-byte CAN frame placed a uint32_t id at offset 1; unaligned LDR on Cortex-M0 faults, on M4 it works slowly and splits the access so a concurrent DMA write tears the ID. DMA into cacheable SRAM without clean/invalidate is the cache version of the same bug: a 256-byte SPI RX buffer in write-back cache can leave the CPU reading 192 bytes of old cache lines after the DMA completion interrupt. Put DMA buffers in non-cacheable or MPU Device memory, or clean by address after the transfer, and assert the policy in the linker script rather than in a comment.

C++ features have byte prices on these ABIs. Enabling exceptions on a 64 KiB part pulled 8.4 KiB of personality routines and prevented -fno-rtti from dropping typeinfo; the product had 3.1 KiB of flash slack and failed the next feature. Ban exceptions in firmware, use return codes, and measure .text before and after any language flag. Recursion in a JSON parser with a 512-byte stack and 48-byte frames overflows at depth 11; a 2 KiB document with nested arrays is a crash, not a parse error. Bound depth or use an explicit heap of frames.

Initialization order is another budget. Static C++ constructors run before main and can use the heap before the RTOS exists; a constructor that logs via UART before clocks are up hangs. Keep .init_array empty in firmware or sequence it after SystemInit in startup. BSS that the bootloader skipped because it jumped to Reset_Handler+4 leaves .bss as flash erase 0xFF, so a zero-initialized flag is 0xFFFFFFFF and the first boot looks like "already provisioned." Startup must copy .data from load address to VMA and zero BSS; assert both with known patterns in a power-on test.

Language-safety verification is a release-image ritual that starts at the map file and ends at a watermark. Record SYSCLK, wait states, compiler flags, .map sizes, painted stack high-water marks, ISR GPIO timing, logic-analyzer traces of CS/SCK/SDA, current-shunt waveforms at not less than 100 kHz, reset-cause and fault registers, and the boot slot/security counter after every power-loss injection. A pass is a number that can be recomputed from those artifacts: flash LOAD versus FLASH LENGTH, ISR high-water versus period, Stop current versus the schematic budget, confirm window versus the health checks, and disable-to-deny for debug and keys. If the only evidence is a green LED, a UART log, or a debugger session on an -O0 build, the claim is unpublished. Repeat the same measurements at the temperature and voltage corners the datasheet allows, because flash wait states, Stop leakage, crystal error, and brownout thresholds all move, and a 25 °C passing suite is not a 85 °C passing suite.

No-init RTC backup SRAM of 4 KiB is not a heap: it survives standby only if the backup domain stays powered and the bootloader does not zero it. Treat it as a named section with a magic and CRC, and fail closed when the CRC is wrong rather than trusting a half-written log. Alignment: a uint64_t at offset 4 in a packed wire struct faults on M0; copy through a local aligned object. That copy is part of the ABI, not optional style.

Worked example: -O0 is not the flash budget

Same sources, 64 KiB flash part.

image.text bytesfits 64 KiB?stack peak on control task
-O079,440no (over by 15 KiB)892 B of 1,536
-O2 + LTO41,208yes1,148 B of 1,536 (75%)

The debug binary never represented the product. Paint and measure the signed release image.