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:
- Symbol table — for each identifier (variable, function, type, label), the compiler stores:
- name
- type code (
int,char,pointer-to-foo,struct bar, etc.) - storage class (auto, static, extern, register)
- declared scope (file, function, block)
-
memory offset (assigned later, in section 2.1.2)
-
Type table — for each type, the compiler records:
- scalar types:
int,long,char,short,unsigned*variants,float,double, and pointer types - 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 toA6(frame pointer) on a 68k system, or the local offset in the function prologueregister→ CPU register, if available; spilled to stack otherwisestatic→.bss(uninitialised) or.data(initialised) sectionextern→ linker-scope,.bssor.dataof 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:
- Pushes arguments onto the stack, right-to-left, in conformance with the C ABI.
- Calls the function via
jsr(jump-to-subroutine on 68k). - 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 brancheswhile→ branch + labelfor→ initial-check loop (orwhileif 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 _namedeclares thatnameis exported by this moduleglobal _name, type_sizedeclares thatnameis 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 _foo → bsr 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
jmpinstead ofjsr + 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:
- Symbol resolution — for every undefined (
global) reference, find the matchingpublicin another module or library - 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 d0for one register) - Examines memory (
x <addr>) - Modifies memory (
a <addr>)
Modern equivalent: SAS/C CodeProbe (built-in, integrated with the IDE), GNU
gdbwithm68k-amiga-elf-gdb, or the Amiga-nativeSashimiandEnforcer.
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
+Iflag (also covered inos-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¶
- Bleek, Jennrich, Schulz, Amiga C for Advanced Programmers, Abacus / Data Becker, ~1991, Chapter 2 (pages 33–88 of the printed book; PDF pages 45–90 of the scanned source).
- Full OCR text:
raw/c-programming/amiga-c-for-advanced-programmers.md. - Cross-references:
architecture/interface-header-files.md,conventions/coding-standards.md,debugging/amiga-debugging-tools.md.
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.