gcc runs four stages, each of which can be stopped early:
preprocessorcpp: .c→.i, stop with gcc -E
compilercc1: .i→.s assembly, stop with gcc -S
assembleras: .s→.o relocatable object file, stop with gcc -c
linkerld: .o files → executable, named with -o (default a.out)
Compilation Unit
A compilation unit is a single .c file after preprocessing, i.e. together with everything it #includes. Each unit is compiled independently; only the linker sees all of them.
Example:gcc -Wall -o program program.c print_name.c compiles two units and links them. Headers are never listed, because they are already pasted into the units.
1.2. Types and Operators
1.2.1. Types and Sizes
x86-64 Data Sizes
Type
Bytes
char
1
short
2
int, float
4
long, double, any T *, size_t
8
stdint.h gives exact widths: int8_t to int64_t, uint8_t to uint64_t. Whether plain char is signed is implementation-defined (signed on x86 gcc, unsigned on ARM).
1.2.2. Operators and Precedence
Precedence Pitfalls
From high to low: unary (~ ! * & ++ --, casts) >* / %>+ -><< >>> relational >== !=>&>^>|>&&>||>?:> assignment.
Hence bitwise operators bind weaker than comparisons, and shifts bind weaker than arithmetic.
Example:x & 7 == 7 parses as x & (7 == 7)=x & 1. The HS22 expression P & ((1<<X)-1) >> Y parses as P & (((1<<X)-1) >> Y): the mask is shifted, not the result. *p++ parses as *(p++): it reads *p, then advances p.
Short-Circuit and Expressions
a && b evaluates b only if a is nonzero; a || b only if a is zero. Both return 0 or 1. Assignment is an expression whose value is the assigned value. A cast keeps the bits (for same-width integers) and changes only the interpretation.
Example:if (p != NULL && *p > 0) never dereferences NULL. while ((c = getchar()) != EOF) assigns and tests in one expression. (unsigned) -1 == UINT_MAX.
1.3. Arrays and Strings (CS:APP 3.8)
1.3.1. Arrays
Array Layout
T A[N] occupies N⋅sizeof(T) contiguous bytes, with &A[i]=A+i⋅sizeof(T). A multidimensional T A[R][C] is stored row-major:
&A[i][j]=A+(iC+j)⋅sizeof(T)
Partial initializers zero-fill the rest. There is no bounds checking: A[N] is UB, not an error.
Example:int a[3] = {1}; gives {1, 0, 0}. For best locality when summing a[X][Y][Z] accessed as a[k][i][j], the loop over the last index j must be innermost: k outer, i middle, j inner.
1.3.2. Strings
C String
A string is a char array terminated by '\0'. strlen(s) counts the characters before the terminator; sizeof gives the size of the object: the declared array size, or 8 for a pointer.
Literal Storage
char s[] = "abc"; copies the literal into a writable array (on the stack for a local). char *s = "abc"; makes s point to the literal in read-only .rodata; writing through s is UB and segfaults on Linux.
Example (HS22 Q9): Given char s1[100] = "Simonetta Sommaruga"; char s2[100] = "Guy Parmelin"; char *s3 = "Ignazio Cassis";
strcpy(d, s) copies until '\0' with no bound, so it overflows d if s is too long. strncpy(d, s, n) writes exactly n bytes: it pads with '\0' if strlen(s)<n, and leaves dunterminated if strlen(s)≥n.
Example:strncpy(name, "Rosalinda", 4) writes Rosa with no terminator. A later printf("%s", name) reads past the buffer.
1.4. Preprocessor
Macro
#define NAME(args) body performs token substitution before compilation, with no knowledge of types or precedence.
Example: With #define SQ(x) x*x, the call SQ(a+1) becomes a+1*a+1. The fix is #define SQ(x) ((x)*(x)). Even then, SQ(i++) increments i twice.
Include and Header Guards
#include "f.h" pastes the file’s text, searching the including file’s directory first and then the system paths. #include <f.h> searches only the system paths. A header guard makes repeated inclusion in one unit harmless:
A declaration states that a name exists and gives its type. A definition additionally provides the storage or the function body. A name may be declared many times but defined once per program.
extern int x; declares x, which is defined in some unit.
static at file scope gives internal linkage: the name is invisible to other units.
Headers contain only the public interface: declarations, typedefs, structs and macros.
1.5.2. Symbol Resolution
Strong and Weak Symbols
Functions and initialized globals are strong; uninitialized globals are weak (only under -fcommon). The linker resolves duplicate names as follows:
Multiple strong symbols with the same name are a link error.
One strong symbol and any number of weak ones: the strong one wins.
Only weak symbols: an arbitrary one is chosen.
Under -fno-common (the default since GCC 10), uninitialized globals are ordinary definitions in .bss, so duplicates are errors.
Example (HS23 Q3):
main.c defines int a = 1; static int b = 2; int c = 3;, and main declares a local int c = 4;.
init.c declares extern int a, c;, defines int b;, and sets a = 5; b = 6; c = 7;.
The two bs never clash, because main.c’s b is static. init writes to the global a and c and to its own b. printf in main sees the global a, the static b and the local c, so the output is a=5, b=2, c=4.