Embedded C
Arrays & Strings
C has no real string type and no bounds-checked array — a string is
just a char array with a convention for where it ends,
and an array is just a contiguous block of memory the compiler will
happily let code read or write past the end of. Both facts are the
direct ancestors of the most common security bug class in C, covered
concretely elsewhere in this notebook.
- What it is
- A fixed-size, contiguous block of same-typed elements, and — for strings — a byte convention (NUL termination) layered on top of a plain char array.
- Mental model
- An array is a row of identical mailboxes with no locks — nothing stops code from reaching into the box next door. A string is that row with a "the message ends here" note in one box, and every string function trusts that note is actually there.
- Key numbers
- A string of N visible characters needs N+1 bytes of storage — the +1 is the NUL terminator, and forgetting to budget for it is a one-byte overflow that's easy to miss.
- Reach for it when
- The maximum size is known ahead of time — the common embedded case — rather than defaulting to a dynamically-sized structure.
Memory layout
An array's elements sit back to back in memory with no gaps —
arr[i] is genuinely just shorthand for
*(arr + i), the exact pointer arithmetic covered in
Pointers.
strlen, strcpy, printf's %s — works by scanning forward until it finds that zero byte. If it's missing, they keep scanning past the buffer into whatever memory comes next.Multi-dimensional arrays
A 2D array (int grid[3][4]) is still one contiguous block
underneath — C lays it out row-major, meaning an
entire row is contiguous before the next row begins. This matters for
more than performance: correctly indexing into a hardware buffer laid
out this way (a framebuffer, a multi-channel sensor's raw reading
array) depends on knowing which dimension is actually contiguous in
memory.
Decay, briefly revisited
An array passed to a function decays to a pointer to its first
element, losing the compile-time size information — covered in full,
with the classic sizeof bug, in
Pointers. Any
function taking an array parameter needs its length passed explicitly
as a separate argument; there's no way to recover it after decay.
Strings are just char arrays
C has no dedicated string type — a string literal like
"HI" is a char array with an automatically
appended NUL byte ('\0', the integer value 0) marking
where it ends. Every standard string function — strlen,
strcpy, strcat, printf's
%s — relies entirely on finding that terminator. Walking
off the end of a buffer that was never actually NUL-terminated is
undefined behavior, and it's the exact root cause behind CWE-787
(out-of-bounds write), already covered as the single most common
memory-safety CWE in Secure
Coding Principles.
Bounded string functions
Bounded variants exist specifically to cap how far these functions
read or write — but they come with their own sharp edge worth knowing
explicitly: strncpy(dst, src, n) does not
guarantee dst ends up NUL-terminated if src
is n bytes or longer. snprintf is generally
the safer, more predictable choice for building a bounded string.
char dst[8];
strncpy(dst, "a string well over 8 bytes long", sizeof(dst));
// dst is now 8 bytes, NONE of which is guaranteed to be '\0' —
// every subsequent string function that touches dst reads past it
/* the safer alternative */
snprintf(dst, sizeof(dst), "%s", "a string well over 8 bytes long");
// dst is guaranteed NUL-terminated, truncated to fit
Real-world applications
- Fixed-size receive buffers — a UART/SPI/I2C driver's receive buffer is almost always a compile-time-sized array, not a dynamically grown one — see UART.
- Calibration/lookup tables — a sensor's calibration coefficients stored as a
constarray land in flash (.rodata), read directly without ever being copied to RAM. - Command parsing — a device accepting text commands over a serial console has to defend every incoming buffer against a missing or attacker-controlled terminator before calling any string function on it.
Gotchas
- Off-by-one indexing — a 10-element array has valid indices 0 through 9;
arr[10]is already out of bounds. - A manually built string missing its NUL terminator looks fine until something calls a string function on it, at which point it reads (or writes) past the buffer with no warning.
- Comparing strings with
==compares the pointers, not the contents — two identical strings at different addresses compare unequal;strcmpis required for content comparison. strncpy's missing-terminator behavior is a genuine footgun, not an obscure edge case — prefersnprintfwhen a guaranteed-terminated bounded copy is the actual goal.