Docs › Cross-referencing

Cross-referencing

68000 Assembly Primer

68000 Assembly Primer

This is a one-page reference for reading Motorola 68000 assembly as it appears in the Sonic disassemblies. It covers just enough to follow object routines and understand the code shown in the Mapping Exercises. It is not a complete 68000 architecture guide.

Registers

The 68000 has 16 general-purpose registers:

  • d0-d7 — Data registers. Used for arithmetic, comparisons, and temporary values.
  • a0-a7 — Address registers. Used as pointers to memory locations.

In Sonic object code, there are strong conventions:

RegisterTypical use
a0Pointer to the current object’s data (its “slot” in object RAM)
a1Pointer to another object, or to the player character
d0General scratch register, often holds the routine index
d1Distance results, temporary calculations
d2-d4Temporary values specific to each routine
a7Stack pointer (rarely seen explicitly in object code)

Size Suffixes

Most instructions have a size suffix that controls how many bytes they operate on:

SuffixNameSizeRange (signed)
.bByte1 byte-128 to 127
.wWord2 bytes-32768 to 32767
.lLong4 bytesFull 32-bit range

Example: move.b routine(a0),d0 reads one byte from the object’s routine field.

Common Instructions

Data Movement

InstructionWhat it does
move.b src, dstCopy a byte from src to dst
move.w src, dstCopy a word (2 bytes)
move.l src, dstCopy a long (4 bytes)
moveq #N, dNQuick move: load a small constant (-128 to 127) into a data register
lea addr, aNLoad effective address: put the address itself (not its contents) into an address register
clr.b dstClear (set to zero)

Arithmetic

InstructionWhat it does
add.w src, dstdst = dst + src
sub.w src, dstdst = dst - src
addq.b #N, dstQuick add small constant (1-8)
subq.b #N, dstQuick subtract small constant (1-8)
neg.w dstNegate (two’s complement)
ext.w dNSign-extend byte to word

Bitwise

InstructionWhat it does
and.b src, dstBitwise AND
or.b src, dstBitwise OR
ori.b #N, dstOR immediate value
andi.b #N, dstAND immediate value
btst #N, dstTest bit N (sets zero flag if bit is 0)
bset #N, dstSet bit N to 1
bclr #N, dstClear bit N to 0

Comparison and Branching

The 68000 compares values first, then branches based on the result:

InstructionWhat it does
cmp.w src, dstCompare (dst - src), set condition flags, discard result
cmpi.w #N, dstCompare with immediate value
tst.b dstCompare with zero

Then a conditional branch:

BranchConditionMeaning
beqEqual / zeroResult was zero
bneNot equalResult was not zero
bcc / bhsCarry clear / higher or sameUnsigned >=
bcs / bloCarry set / lowerUnsigned <
bhiHigherUnsigned >
blsLower or sameUnsigned <=
bplPlusResult positive (bit 15/7 clear)
bmiMinusResult negative (bit 15/7 set)
bgeGreater or equalSigned >=
bltLess thanSigned <

bra branches unconditionally (like a goto). bsr branches to a subroutine (like a function call), and rts returns from it.

Jumps and Subroutines

InstructionWhat it does
jmp addrJump to address (unconditional, absolute)
jsr addrJump to subroutine (pushes return address onto stack)
rtsReturn from subroutine
bra labelBranch always (relative, short jump)
bsr labelBranch to subroutine (relative)

Loops

InstructionWhat it does
dbf dN, labelDecrement dN; if dN != -1, branch to label (loop counter)

Object Field Access

Object data in the disassembly is accessed as offsets from the a0 register. Named constants make these readable:

move.b  routine(a0),d0       ; Read this object's current routine number
move.w  x_pos(a0),d0         ; Read this object's X position
addq.b  #2,routine(a0)       ; Advance to the next routine (next state)
move.b  #$9B,collision_flags(a0) ; Set collision type to "hurts player"

The field names (routine, x_pos, collision_flags, etc.) are defined in the disassembly’s constants file and resolve to byte offsets. See the Per-Game Notes for the field name differences between S1 and S2.

The Routine Dispatch Pattern

Almost every object starts with the same four-instruction pattern:

Obj22:                                    ; Object entry point
    moveq   #0,d0                         ; Clear d0
    move.b  routine(a0),d0                ; Load routine number (0, 2, 4, 6...)
    move.w  Obj22_Index(pc,d0.w),d1       ; Look up offset in jump table
    jmp     Obj22_Index(pc,d1.w)          ; Jump to the routine

Obj22_Index:
    dc.w Obj22_Init - Obj22_Index         ; routine 0: initialization
    dc.w Obj22_Main - Obj22_Index         ; routine 2: main behavior
    dc.w Obj22_ShootArrow - Obj22_Index   ; routine 4: shoot arrow

This is a state machine. The routine field determines which behavior runs. Routine values increment by 2 (not 1) because the jump table contains 16-bit word offsets, and each word is 2 bytes. So the states are numbered 0, 2, 4, 6, etc.

When an object advances to the next state, it does:

addq.b  #2,routine(a0)   ; Move from routine 0 to routine 2

Animation Script Format

Animation scripts are inline data tables with a specific command byte convention:

Ani_obj22:
    dc.w byte_idle - Ani_obj22       ; Animation 0 offset
    dc.w byte_detect - Ani_obj22     ; Animation 1 offset
    dc.w byte_fire - Ani_obj22       ; Animation 2 offset

byte_idle:   dc.b $1F, 1, $FF             ; delay=$1F, frame 1, then loop
byte_detect: dc.b $03, 1, 2, $FF          ; delay=$03, frames 1,2, then loop
byte_fire:   dc.b $07, 3, 4, $FC, 4, 3, 1, $FD, 0  ; complex sequence

The first byte is the frame delay (how many game frames each animation frame is shown). Then a sequence of mapping frame indices, terminated by a command byte:

ByteCommandMeaning
$FFLoopRestart the animation from the beginning
$FEResetChange to a different animation (next byte is the animation ID)
$FDRoutine incrementAdvance the object’s routine by 2 (change state), next byte is the animation to switch to
$FCChange animationSwitch to a different animation (next byte is the animation ID), without resetting routine

In the firing animation above: frames 3, 4 are shown, then $FC loops back to animation index in the next byte, then frames 4, 3, 1 are shown, then $FD increments the routine (triggering the arrow spawn state) and sets animation to 0.

Fixed-Point Numbers

Velocities and sub-pixel positions use 8.8 fixed-point format: the high byte is the integer part (pixels) and the low byte is the fractional part (sub-pixels).

Hex valueDecimalMeaning
$040010244.0 pixels per frame
$01803841.5 pixels per frame
$0040640.25 pixels per frame
$FC00-1024-4.0 pixels per frame (leftward/upward)

When you see move.w #$400,x_vel(a0), that sets the horizontal velocity to 4 pixels per frame.

dc Directives

These are data definitions, not instructions. They place raw values into the ROM:

DirectiveSizeExample
dc.bBytedc.b $1F, 1, $FF — three bytes
dc.wWorddc.w $0400 — one 16-bit value
dc.lLongdc.l Obj22_MapUnc_25804 — a 32-bit address

Putting It Together

Here is a complete annotated example — the ArrowShooter’s player detection routine:

Obj22_DetectPlayer:
    move.w  x_pos(a0),d0          ; d0 = shooter's X position
    sub.w   x_pos(a1),d0          ; d0 = shooter X - player X
    bcc.s   +                     ; if result >= 0 (player is to the left), skip
    neg.w   d0                    ; make d0 positive (absolute distance)
+
    cmpi.w  #$40,d0               ; is distance < 64 pixels?
    bhs.s   +                     ; if distance >= 64, skip (not detected)
    moveq   #1,d2                 ; player detected: set d2 = 1
+
    rts                           ; return

This reads as: “Calculate the horizontal distance between the shooter and the player. If the absolute distance is less than 64 pixels, set the detection flag.”

Next Steps