Makefile basics for a small C project

The Makefile for a one-binary C project, explained piece by piece. CC = gcc CFLAGS = -std=gnu23 -Og -g3 -Wall -Werror -Wextra -Wwrite-strings OUT = out BIN = $(OUT)/tinybox all: $(BIN) # Any out/<name> from <name>.c, e.g. `make out/scratch` $(OUT)/%: %.c Makefile | $(OUT) $(CC) $(CFLAGS) -o $@ $< $(OUT): mkdir -p $@ tinybox: $(BIN) scratch: $(OUT)/scratch run: $(BIN) ./$(BIN) clean: rm -rf $(OUT) .PHONY: all run clean tinybox scratch Rules target: prerequisites recipe make rebuilds target if it doesn’t exist or any prerequisite is newer (by file modification time). That’s the whole algorithm. The recipe lines must start with a tab. Spaces give *** missing separator. Stop. A rule with no recipe (tinybox: $(BIN)) just says “to make this, make that”. Variables CC, CFLAGS are conventional names (make’s built-in rules use them too). Use with $(NAME). Override from the command line: make CFLAGS=-O2. ...

October 3, 2026 · 3 min · Kshitij Patil

Reading GCC error messages

Every GCC diagnostic has the same shape: scratch.c:3:13: error: initialization discards 'const' qualifier from pointer target type [-Werror=discarded-qualifiers] 3 | char *s = "scratching surface"; | ^~~~~~~~~~~~~~~~~~~~ cc1: all warnings being treated as errors Line 1: file:line:col: severity: message [flag] scratch.c:3:13: where. Columns start at 1. Most terminals/editors make this clickable. severity: error stops the build; warning doesn’t; note: adds context to the diagnostic above it. [-Werror=discarded-qualifiers]: the flag that controls this check. -Werror= means it’s really the warning -Wdiscarded-qualifiers, promoted to an error by -Werror. Search man gcc for /Wdiscarded-qualifiers. -Wno-<name> disables it, which is rarely the right fix. The message has a fixed vocabulary The first words say where in the code the problem is: ...

October 3, 2026 · 2 min · Kshitij Patil

Reading C declarations: const, pointers, and argv

Read from the name outward, right to left char * const argv[] argv[] → argv is an array * const → of const pointers char → to char Rules of thumb: [] and () next to the name bind first (“array of”, “function returning”). Then read leftward: each * is “pointer to”. A const applies to whatever is immediately to its left; if nothing is to its left, it applies to the thing on its right. Declaration Can change the chars? Can repoint? char *p yes yes const char *p (same as char const *p) no yes char *const p yes no const char *const p no no The duplicate-const trap static const char const *v[]; // looks like "const pointers to const chars" Both consts are left of the *, so both apply to char: you’ve made the chars const twice and the pointers not at all. GCC says: ...

October 3, 2026 · 2 min · Kshitij Patil

C string literals are read-only, but not const

A string literal like "/bin/sh" is stored in .rodata, which is mapped read-only (see where things live in a C program). But its C type is plain char[N], not const char[N] (C++ fixed this, C kept it for historical reasons). So the compiler happily lets you do this: char *p = "/bin/sh"; // compiles without a warning by default p[0] = 'X'; // undefined behavior; in practice SIGSEGV $ ./ub Segmentation fault (exit status 139 = 128 + SIGSEGV) -Wwrite-strings: make the compiler tell you -Wwrite-strings gives literals the type const char[N], so pointing a plain char * at one becomes a warning (an error with -Werror): ...

October 3, 2026 · 2 min · Kshitij Patil

Where things live in a C program: .text, .rodata, .data, .bss, stack

A compiled C program is an ELF file split into sections. The loader maps them into memory with different permissions, and those permissions are what the CPU enforces at runtime. Section Holds Permissions Example .text machine code read + execute main .rodata read-only data read (no write) string literals, static const tables .data initialized globals / statics read + write static char buf[] = "hi"; .bss zero-initialized globals / statics read + write static int counter; stack locals, return addresses read + write char buf[8]; inside a function .data and .rodata are both in the binary; “in the binary” doesn’t mean read-only. The section decides that. .bss takes no space in the file (type NOBITS): the loader just hands out zeroed memory. Writing to .rodata (e.g. through a pointer to a string literal) → the page isn’t writable → SIGSEGV. See it yourself Sections and their flags (A = alloc, W = write, X = exec): ...