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.

Quick reference
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.

An int array's five elements sit at consecutive addresses four bytes apart; a string is a char array whose final byte is a zero-value NUL terminator marking where it ends int arr[5] — five 4-byte elements, contiguous arr[0] arr[1] arr[2] arr[3] arr[4] 0x1000 0x1004 0x1008 0x100C 0x1010 char greeting[] = "HI" — 3 bytes: two characters, then the terminator 'H' 'I' '\0' NUL terminator — this byte is 0x00
Every string function — 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

Gotchas

Field notes