1.1. Toolchain and Compilation Units (CS:APP 1.2)

Compilation Pipeline

gcc runs four stages, each of which can be stopped early:

  • preprocessor cpp: .c .i, stop with gcc -E
  • compiler cc1: .i .s assembly, stop with gcc -S
  • assembler as: .s .o relocatable object file, stop with gcc -c
  • linker ld: .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

TypeBytes
char1
short2
int, float4
long, double, any T *, size_t8

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 contiguous bytes, with . A multidimensional T A[R][C] is stored row-major:

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";

  • strlen(s1) , sizeof(s1) , sizeof(s3) , strlen(s3) .
  • s1[strlen(s1)] , the terminator.
  • s3 += 4 prints zio Cassis.
  • *(s1 + 4) = '\0' prints Simo.
  • *(s2 + 6) = 0 gives strlen(s2) .
  • *(s3 + 13) = 0 writes into .rodata and segfaults.

strcpy and strncpy

strcpy(d, s) copies until '\0' with no bound, so it overflows d if s is too long. strncpy(d, s, n) writes exactly bytes: it pads with '\0' if , and leaves d unterminated if .

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:

#ifndef PRINT_NAME_H
#define PRINT_NAME_H
void print_name(void);
#endif

1.5. Modularity and Linkage (CS:APP 7.5–7.6)

1.5.1. Declarations vs Definitions

Declaration and Definition

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:

  1. Multiple strong symbols with the same name are a link error.
  2. One strong symbol and any number of weak ones: the strong one wins.
  3. 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.