Strings: Bytes with a Zero
The null terminator and every trap it sets
- › Explain that a C string is a byte array ending in a '\0', with no length field
- › Trace how strlen, strcpy and friends walk memory until the terminator
- › Predict the off-by-one and overflow bugs that the null-terminator convention causes
- › See why unbounded string functions are the classic source of buffer overflows
A string is not a type — it's a convention
C has no string type. A "string" is just a char array (day 15/17) that, by *convention*, ends with
a zero byte — the null terminator, written '\0' (value 0). There's no stored length. The end of
the string is wherever the first 0 byte appears. That single convention makes strings simple to
represent and a minefield to handle — and understanding it is understanding a huge class of real
bugs.
char s[] = "Hi"; // occupies 3 bytes, not 2
index: 0 1 2
┌────┬────┬────┐
│'H' │'i' │'\0'│ 'H'=0x48 'i'=0x69 '\0'=0x00
└────┴────┴────┘
The 0x00 is the string. Remove it and s is no longer a valid string —
functions would keep reading past index 2 into whatever follows.Every string function walks to the zero
Because there's no length field, the standard functions scan for the terminator. strlen is
literally "count bytes until you hit 0":
1size_t my_strlen(const char *s) {2 size_t n = 0;3 while (s[n] != '\0') { // walk until the null terminator4 n++;5 }6 return n; // number of bytes BEFORE the '\0'7}strcpy(dst, src) copies bytes from src into dst including the terminating 0, and stops
when it copies that 0. Note what it does *not* do: it never checks how big dst is. It trusts that
src has a terminator and that dst is large enough. Both assumptions are where things go wrong.
'\0'). Allocate exactly N and
the terminator overwrites the byte after your buffer. (2) The overflow: strcpy and gets keep
writing until *the source's* terminator — with no regard for the destination's size — so a source
longer than the destination writes right past the end. Recall day 14: past the end of a stack buffer
lies the saved rbp and the return address.The canonical vulnerability
Here is, essentially, the most famous bug in computing:
1void greet(const char *name) {2 char buf[16];3 strcpy(buf, name); // if name is longer than 15 chars + '\0', this4 // writes past buf — into saved rbp and the return5 // address (day 14). Classic stack buffer overflow.6 printf("Hello, %s\n", buf);7}buf holds 16 bytes; strcpy will copy however long name is. Feed a 100-byte name and 84 bytes
spill past buf, over the saved registers and the return address the function will ret to. You
learned the mechanism on day 12/14; this is the C code that opens the door. The fix is to use
bounded functions that take the destination size — strncpy, snprintf, or better,
length-aware handling — and to always reserve room for the terminator.
char buf[8]; strcpy(buf, "abcdefgh"); — the literal "abcdefgh" is 8 visible characters. How many
bytes does strcpy write, and what goes wrong with an 8-byte buffer?strncpy is safer but has its own trap: if the source is as long as or longer than the size, it does
not write a terminator, leaving dst un-terminated — so the *next* strlen/printf runs off the
end. snprintf(dst, size, "%s", src) always terminates and is usually the better choice. The lesson:
bounded functions help, but you must still understand the terminator, because the bugs hide in its
edges.Feel the terminator, then break a buffer safely
In strings.c: (1) implement my_strlen and my_strcpy from scratch (walking to '\0'), and test them against the real ones. (2) Show the off-by-one: allocate exactly strlen(src) bytes, copy into it, and watch valgrind/ASan report the terminator write past the end; then fix it with +1. (3) Write the greet()-style vulnerable function with a small stack buffer and, in YOUR OWN program only, pass it an over-long string and run under `-fsanitize=address` to see the overflow detected. Uses only this lesson plus days 14/17.
$ gcc -O0 -g -Wall -fsanitize=address strings.c -o strings && ./strings- › my_strlen and my_strcpy match the standard versions on several inputs
- › You reproduced the +1 off-by-one and fixed it by reserving room for '\0'
- › AddressSanitizer reported your deliberate overflow
- › You can explain why strcpy is unbounded and what to use instead