C Compiler Operations — How a C Compiler Works

This article is a structured synthesis of Chapter 2 ("C Compiler Operations") of Amiga C for Advanced Programmers by Bleek, Jennrich, Schulz (Abacus / Data Becker, ~1991). The chapter is a deep dive into how the Aztec C compiler (Manx) actually works — compiler options, internal symbol-table organization, software organisation of variables, the assembler, the linker, the in-CLI debugger (ap), and a handful of "tricks and tips" (function tables).

This page covers the same ground as the chapter at a higher level. For the verbatim text of every page, see the raw OCR source (pages 45–90 of the printed book; PDF pages 45–90 of the scanned source).

Editorial note: page numbers in this article refer to the PDF page index in the raw OCR source, not the book-printed page numbers.


2.1 How a C compiler works (pages 45–72)

The chapter opens with a 1-pass / 1.5-pass overview of C compilation. The Aztec C compiler used at the time was a single-pass compiler with backpatching — it reads the source top-to-bottom and emits assembly language as it parses, deferring decisions about forward references (e.g. goto past an undeclared label) until enough context is available.

The compiler pipeline in the book (adapted to NDK 3.x terminology):

.c source  →  cc(1) → assembly (.asm)
              ↓
            as(1) → object (.o)
              ↓
              ln → executable

Where the modern equivalent today is one of:

GCC:    cc -S foo.c → as -o foo.o foo.s → ld -o foo foo.o
VBCC:   vc +aos68k -o foo.s foo.c → as → ld
SAS/C:  SC foo.c → OLW obj/ → blink foo.o lib:sc.lib lib:amiga.lib

2.1.1 Internal organization of variables and structures (page 50)

The compiler's first job is to scan the source and build two internal tables:

  1. Symbol table — for each identifier (variable, function, type, label), the compiler stores:
  2. name
  3. type code (int, char, pointer-to-foo, struct bar, etc.)
  4. storage class (auto, static, extern, register)
  5. declared scope (file, function, block)
  6. memory offset (assigned later, in section 2.1.2)

  7. Type table — for each type, the compiler records:

  8. scalar types: int, long, char, short, unsigned* variants, float, double, and pointer types
  9. aggregate types: array, struct, enum, bit-field, union

The exact details of the C-types used in AmigaOS code are documented in architecture/interface-header-files.md — the modern NDK provides clib/, proto/, pragma/, pragma/* and inline/ headers that wrap the same register-based OS calls.

Editorial note [claim — unverified]: the specific Aztec C internals description above is reconstructed from OCR. Modern compilers (GCC, VBCC, SAS/C) use similar symbol-table structures but with different details. Verify against an Aztec C manual if quoting.

2.1.2 Software organisation of variables and structures (page 59)

Once the compiler has built the symbol table, it assigns each variable a storage location based on its scope:

  • auto → stack frame, relative to A6 (frame pointer) on a 68k system, or the local offset in the function prologue
  • register → CPU register, if available; spilled to stack otherwise
  • static.bss (uninitialised) or .data (initialised) section
  • extern → linker-scope, .bss or .data of another module

The book also covers string storage (in the .data segment as initialized byte arrays) and bit-fields (packed into int storage with the layout specified by the compiler). The 1991 bit-field layout rules differ from C99 — the book calls out layout assumptions explicitly.

2.1.3 Function calls (page ~70)

The compiler emits code that:

  1. Pushes arguments onto the stack, right-to-left, in conformance with the C ABI.
  2. Calls the function via jsr (jump-to-subroutine on 68k).
  3. Adjusts the stack pointer on return.

The chapter explicitly notes that the Aztec compiler does not check that the correct number of arguments were passed — a feature (or limitation, depending on viewpoint) inherited from K&R C. Modern Amiga C compilers add prototype checking through <proto/...> headers.

2.1.4 Control statements (in 2.1)

The control-statement translation section shows how:

  • if/else → conditional branches
  • while → branch + label
  • for → initial-check loop (or while if converted)
  • switch/case → jump table generated by compiler when the cases are dense enough; otherwise a chain of compares

The compiler emits a case table in the .data segment when -Ynumber is passed (this option enables case tables).


Aztec C compiler options (pages 46–48)

The author catalogued all the Aztec C options in use at the time:

Flag Effect
-2 Don't run the assembler automatically after compilation
-Dsymbol[=value] #define from the command line
-A Don't invoke the assembler (similar to -2)
-O Enable basic optimization
-n Add debugging info for ap (the Aztec debugger)
-B Create "large code" model (see 2.3.1)
-D Create "large data" model (see 2.3.1)
-c Compile only (no link)
-E Pre-process only, send to stdout
-r Allow A4 as a register variable (requires large code/data model + cl.lib or cl32.lib)
+Hfilename Override the default header path
+Ifilename Use a precompiled-include file (see also the build pipeline section in Chapter 4's article)
+L Save the symbol table for a separate linking step
+P Equivalent to +C +D +L, plus D2/D3 saved before each function call (requires cl32.lib)
+Q Allocate a case table (used for switch statements)
+Znumber Allocate a string table of number bytes
+B Suppress public .begin in the assembly output (required when combining multiple modules)

The abortive support for K&R-style function-pointer declarations and the A4 register as a register variable (with the appropriate linker library) are direct ancestors of the modern AmigaOS conventions in conventions/coding-standards.md.

Editorial note [claim — unverified]: the option names and meanings are reconstructed from OCR. Quoting Aztec C flag spellings in modern articles requires double-checking against an Aztec C 6.x manual if precision matters.


2.2 The assembler (page 73)

After compilation, the Aztec assembler (as) translates the assembly source (.asm) into an object file (.o):

as [>output_file] [options] program.asm

The object file:

  • Contains executable machine code for instructions and data
  • Has unresolved external references (e.g. _exit, _Open) that the linker must resolve
  • Has a symbol table of public (public) and global (global) labels

2.2.1 Public and global directives (page 74)

  • public _name declares that name is exported by this module
  • global _name, type_size declares that name is referenced by this module and expects the linker to find it (or report an undefined reference)

The first pass of the assembler collects these into the symbol table. The second pass converts absolute references into PC-relative instructions where possible (jsr _foobsr label(pc) if _foo is in the same module and the displacement fits in 16 bits).

2.2.2 Assembler optimisations (page 76)

The Aztec assembler applies several local optimisations:

  • Dead stores like movem.l d0,-(sp) / movem.l (sp)+,d0 (no register variables in use) are removed
  • Branch-to-next sequences (bra .L / .L:) are removed
  • Tail calls become jmp instead of jsr + ret

These are what's referred to today as peephole optimisations. Modern compilers (GCC -O2/-O3, VBCC -O3, SAS/C -O) implement similar local passes plus cross-function inlining.


2.3 The linker (page 77)

The linker (ln for Aztec, blink for SAS/C, ld for GCC) does two things:

  1. Symbol resolution — for every undefined (global) reference, find the matching public in another module or library
  2. Relocation — apply fixups so that absolute references work after the modules are placed at their final addresses

The standard Amiga link sequence uses linker libraries:

c.lib          small-data, small-code defaults
cl.lib         large-data, large-code (with `-r`)
c32.lib        C-only compiler stubs, but 32-bit-int aware
amiga.lib      Amiga system stubs (open() = _open via jumps)

2.3.1 Large data, small data, large code, small code (page 80)

This is an Amiga-specific layout decision that has bitten every C programmer at least once:

Model Code pointers Data pointers Notes
Small code, small data (default) 16-bit relative 16-bit absolute (A4-relative) All code + data in 32 KB near segment
Large code, small data 32-bit absolute 16-bit absolute Code can be anywhere; data near
Small code, large data 16-bit relative 32-bit absolute (A4-relative) Data near; code elsewhere
Large code, large data 32-bit absolute 32-bit absolute Nothing fits in 16-bit; full model

Why this matters: in the small-data model, global/static data is addressed as A4 + 16-bit signed offset. The compiler emits lea (offset,A4),An to load an address. If the program's data segment grows past 32 KB, you must recompile with -r (register variables + cl32.lib) and all code that uses globals must use the large-data addressing.

Historical context [claim — unverified]: 32 KB was the original MC68000 limit on the addressing mode d16(An). With MC68020+, full 32-bit absolute addressing is available without that constraint, but the Amiga C ABI keeps the small-data model for backward compatibility.


2.4 The monitor / debugger (page 84)

The book introduces the Aztec debugger ap, a CLI monitor that:

  • Attaches to a running task by task number
  • Sets breakpoints at symbol addresses (bp _main)
  • Single-steps (s)
  • Inspects registers (r, r d0 for one register)
  • Examines memory (x <addr>)
  • Modifies memory (a <addr>)

Modern equivalent: SAS/C CodeProbe (built-in, integrated with the IDE), GNU gdb with m68k-amiga-elf-gdb, or the Amiga-native Sashimi and Enforcer.

The exact CLI syntax for ap is reproduced in section 2.4 of the book. See also debugging/alerts-and-gurus.md for modern debugging strategies.


2.5 Tricks and tips (page 87)

A small collection of useful patterns:

  • Function tables (page 89): arrays of function pointers for state machines and dispatch tables
  • Memory pools (referenced in this section): thinking about memory as a constrained resource
  • Pre-compiled headers: the +I flag (also covered in os-programming-overview.md)

2.5.1 Function table example

typedef int (*state_fn)(void);

state_fn state_table[] = {
    state_idle,
    state_active,
    state_error,
    state_done
};

void dispatch(int state) {
    state_table[state]();
}

This is a jump-table idiom that the C compiler may emit for switch statements under the right optimisation.


What Chapter 2 does not cover

  • Modern C standards (C99 / C11 / C23). The book targets K&R-era C; modern code should use the conventions in conventions/coding-standards.md.
  • Cross-compilation setup (host → Amiga). This is covered implicitly by examples/getting-started-amiga-c.md, which uses VBCC + WinUAE.
  • AmigaOS 4 / PowerPC considerations. None of the patterns in Chapter 2 survive unchanged on MorphOS or AmigaOS 4 PPC; see conventions/powerpc-futureos-compatibility.md.
  • Modern compiler flags (-O3, -funroll-loops, profile-guided optimisation). The book predates these by 25+ years.

Sources

Editorial markers [claim — unverified] indicate content reconstructed from OCR that should be cross-checked against the printed book or an Aztec C manual before quoting verbatim.