amiga-bootcamp/05_reversing/static/compilers/lattice_c.md

6.2 KiB
Raw Permalink Blame History

← Home · Reverse Engineering · Static Analysis · Compilers

Lattice C 3.x/4.x — Reverse Engineering Field Manual

Overview

Lattice C (versions 3.x4.x, 19851989) is the direct predecessor to SAS/C. When SAS Institute acquired the Lattice C product line in 1988, they rebranded version 5.0 as "SAS/C". Lattice C 3.x and 4.x binaries represent the first generation of commercial C compilers for AmigaOS. Their code generation is recognizably similar to SAS/C but with less aggressive optimization and some distinct early patterns.

Key constraints:

  • The transition point: Lattice C 3.x → 4.x → SAS/C 5.x form a continuous evolution. Code from 3.x looks noticeably "early" — simpler register allocation, less peephole optimization, longer function prologues.
  • LINK A5 + D2-D5/A2-A3 save — Lattice C 3.x typically saves fewer registers than SAS/C (D2-D5 + A2-A3, 6 registers total) but more than Aztec C (5 regs, data only).
  • Startup code evolution — Lattice C 3.x's lc.o startup is simpler than SAS/C's c.o — may not handle Workbench launches correctly, may not support argc/argv parsing.
  • Hunk names: CODE, DATA, BSS (same as SAS/C — established this convention)
; Lattice C 3.x function prologue (less aggressive than SAS/C):
_func:
    LINK    A5, #-$14
    MOVEM.L D2-D5/A2-A3, -(SP)    ; 4 data + 2 address = 6 registers
    ; Compare: SAS/C saves D2-D7/A2-A4 (9 registers)
    ; Compare: Aztec C saves D3-D7 only (5 registers, data only)

Binary Identification — Lattice C vs SAS/C

Criterion Lattice C 3.x Lattice C 4.x SAS/C 5.x/6.x
Register save D2-D5, A2-A3 (6 regs) D2-D6, A2-A3 (7 regs) D2-D7, A2-A4 (9 regs)
D6/D7 usage Rarely used Sometimes used Frequently used
Peephole optimization Minimal Moderate Aggressive
MOVEQ for small values Inconsistent Common Always
Stack frame LINK A5 always LINK A5 always LINK A5 always
Library calls JSR -$XXX(A6) JSR -$XXX(A6) JSR -$XXX(A6)
Startup lc.o (simpler) lc.o (improved) c.o (full-featured)
Era 19851987 19871989 19881996

Evolutionary Markers

The Lattice→SAS/C evolution is visible in the binary:

  1. Register save set grows — 6→7→9 registers as the optimizer learned to use more registers effectively
  2. MOVEQ adoption — Lattice 3.x uses MOVE.L #0, D0; Lattice 4.x uses MOVEQ #0, D0; SAS/C always uses MOVEQ
  3. Library call density — Lattice 3.x loads A6 before every single library call; SAS/C may reuse A6 across calls
  4. Stack frame size — Lattice 3.x often allocates oversized frames (locals * sizeof(LONG) rounded up to nice boundary)

Historical Context

Lattice, Inc. was an early cross-platform compiler vendor. Their C compiler for the Amiga was the first commercially viable option, shipping in 1985. Commodore itself used Lattice C for some system development before adopting SAS/C.

Key timeline:

  • 1985: Lattice C 3.0 — first commercial Amiga C compiler
  • 1986: Lattice C 3.1 — improved optimizer, bug fixes
  • 1987: Lattice C 4.0 — major update, AmigaOS 1.2 support
  • 1988: SAS Institute acquires Lattice C product line
  • 1989: Rebranded as SAS/C 5.0

Any binary from 19851989 is likely Lattice C. After 1989, the brand transitioned to SAS/C, though Lattice C was still sold through existing channels for a time.

Software likely compiled with Lattice C:

  • Commodore's early Amiga utilities (19851986)
  • Early third-party tools like DiskMon, CLImate, Memacs
  • Amiga 1000 launch-era software
  • Early versions of ARP (AmigaDOS Replacement Project) components
  • Early WShell / ZShell versions

Same C Function — Lattice C Output

; CountWords() — Lattice C 4.x:
; (Notably simpler than SAS/C — less aggressive optimizer)

_CountWords:
    LINK    A5, #-$08
    MOVEM.L D2-D4/A2, -(SP)       ; 4 regs saved (D2-D4 + A2)
    ; Note: A2 saved even though it's not used — Lattice C saves a fixed set
    
    MOVEQ   #0, D2                 ; D2 = count
    MOVEQ   #0, D3                 ; D3 = in_word
    
    MOVEA.L $08(A5), A0            ; A0 = str
    
    BRA     .loop_test              ; Lattice C uses BRA (long), not BRA.S

.loop_body:
    MOVE.L  #' ', D0               ; MOVE.L for char constant (should be MOVEQ!)
    CMP.B   (A0), D0
    BEQ     .not_word               ; BEQ (long), not BEQ.S
    
    MOVE.L  #'\t', D0
    CMP.B   (A0), D0
    BEQ     .not_word
    
    MOVE.L  #'\n', D0
    CMP.B   (A0), D0
    BEQ     .not_word
    
    TST.B   D3
    BNE     .next_char
    
    ADDQ.L  #1, D2
    MOVEQ   #1, D3
    BRA     .next_char

.not_word:
    MOVEQ   #0, D3

.next_char:
    ADDQ.L  #1, A0

.loop_test:
    TST.B   (A0)
    BNE     .loop_body

    MOVE.L  D2, D0
    MOVEM.L (SP)+, D2-D4/A2
    UNLK    A5
    RTS

Lattice C observations:

  1. MOVE.L #' ', D0 instead of MOVEQ #' ', D0 — Lattice C doesn't always use MOVEQ for constants that fit in 8 bits. This wastes 2 bytes and 4 cycles per constant load.
  2. BRA/BEQ/BNE (long, 4-byte) instead of BRA.S/BEQ.S/BNE.S (short, 2-byte) — Lattice C's branch target distance calculation is conservative.
  3. A2 saved but unused — Lattice C saves a fixed register set rather than analyzing which registers are actually needed.

Differences from SAS/C — Summary

Lattice C 3.x/4.x → SAS/C 5.x/6.x improvements visible in disassembly:
  ✓ MOVEQ substituted for MOVE.L #small_const
  ✓ BRA.S/BEQ.S/BNE.S used where target is within 8-bit range
  ✓ Dead register saves eliminated (per-function save analysis)
  ✓ Common subexpression elimination (CSE) more aggressive
  ✓ Loop induction variables kept in registers, not on stack
  ✓ Struct copy inlined as MOVE.L (A0)+, (A1)+ for small structs
  ✓ Tail-call optimization in some cases (rare but present in SAS/C 6.x)

References