24

The Commodore 64 suffered from a strange bug that caused it to lock up when typing in the full-screen BASIC editor. To trigger the bug, position the cursor on the last line of the screen, type enough text (or even just spaces) to scroll the screen by two lines, and then hit the DEL key while the cursor is on the first column. The following text then gets printed to the display:

                                       L
OAD

?SYNTAX  ERROR
READY.
RUN

READY.

A flashing cursor appears after the last line, but the system is apparently unresponsive to all input, including the usual RUN/STOP or RUN/STOP+RESTORE.

animated screenshot showing the above-noted bug

The problem, and various workarounds, were briefly described on page 12 of the September 1985 issue of COMPUTE!'s Gazette:

The bug does not affect the portable 64 (SX-64), and newer models of the 64 have been revised to eliminate the problem.

The lockup will occur only when the cursor is red, cyan, blue, yellow, light red, dark gray, light blue, or light gray. Safe colors are black, white, purple, green, orange, brown, medium gray, and light green. To avoid the problem altogether, change the cursor color to a safe color before you start programming.

Also, you can defeat the lockup if you own a Datassette. After the computer freezes, simultaneously press the left SHIFT key and 3, or X and 5, or V and 7, and so on (every other key from left to right). The screen will display PRESS PLAY ON TAPE. Press PLAY on the Datassette and then RUN/STOP. Disk drive owners can totally avoid the bug if the first line in the program in memory is OPEN15,8,15: INPUT#15,A$.

The behaviour of the bug, the exact conditions under which it occurs, and the workarounds are all so bizarre that I'd like them all to be explained. In particular:

  1. What is the underlying cause of the bug? I assume that this is a problem in the operating system (KERNAL?) in ROM rather than the hardware itself, so where exactly is the affected code and in what way is it defective?

  2. Why does using certain cursor colours avoid the lockup?

  3. Why does pressing left SHIFT and 3 (or every other corresponding pair of keys on the two rows) and then starting the Datassette break out of the lockup? (And how on earth was anyone able to come up with this solution? Unlike the previous workaround, this isn't something that could have been discovered through trial and error.)

  4. Why does having OPEN15,8,15: INPUT#15,A$ as the first line of the program in memory avoid the lockup?

10
  • 14
    Since this is about the behaviour of a single bug, I assume that the answers will all be interrelated or interdependent. Splitting the question up into several postings is going to result in an awful lot of duplicated material in both the questions and answers, no?
    – Psychonaut
    Jul 19, 2021 at 13:38
  • 3
    The "explanation" for every software bug is "somebody made a mistake, and nobody found it before the code went into production". Without looking at the code (and having never owned a C64 either) I would guess this is some sort of "buffer overrun" problem, in which case inventing "bizzarre work rounds" is probably straightforward once you know what has been overwritten.
    – alephzero
    Jul 19, 2021 at 13:49
  • 7
    @Raffzahn: I don't have enough knowledge of the C64 to write a proper answer, but the problem is most likely that the screen scrolling logic is attempting to write the current foreground color to memory slightly past the end of the color attribute buffer at $D800-$DBFF. The PIA chip which controls the keyboard is mapped at address $DC00, and writing a value where bit 2 is set (as it is in the "dangerous" colors) causes a malfunction, but writing a value where bit 2 is clear doesn't. The indicated code probably causes the system to rewrite the corrupted I/O register with a usable value.
    – supercat
    Jul 19, 2021 at 15:11
  • 3
    @Raffzahn: Yeah, but a complete answer should include information about why the scrolling logic is going beyond the end of the color attribute memory (I find it curious that it goes beyond $DBFF, but the main text-memory logic doesn't overrun $07FF) and what address(es) on the PIA is getting hit. I'd guess the keyboard data-direction register is being clobbered, and that bit 2 is used to read the column with the Run-Stop key, but I don't know that.
    – supercat
    Jul 19, 2021 at 15:39
  • 2
    @Raffzahn: Glad I didn't post an answer, since my speculation turned out to be somewhat wrong. Still, my point with why speculation was that there aren't really four questions here, since they all tie in with a common chain of events.
    – supercat
    Jul 22, 2021 at 15:12

1 Answer 1

21

It’s good ol’ buffer overflow.

I diagnosed this in VICE. It turns out that supercat’s hunch that this is caused by clobbering a register of the CIA chip is mostly correct (they only got the register wrong), though the cause is relatively easily discovered even without that hint.

Since the primary symptom is the keyboard becoming unresponsive, I decided to place a breakpoint in the keyboard-reading routine, SCNKEY, to determine if keypresses are delivered correctly. It took me a couple of tries to discover the best spot to place a breakpoint: eventually I settled on $EAB9, verifying that each key press generated one breakpoint hit, with the character code stored in the accumulator. Then I triggered the bug and got this:

#8 (Stop on  exec eab9)  162/$0a2,  21/$015
.C:eab9  C9 05       CMP #$05       - A:11 X:01 Y:07 SP:e8 ..-..I..  319459539
(C:$eab9) g
#8 (Stop on  exec eab9)  165/$0a5,  51/$033
.C:eab9  C9 05       CMP #$05       - A:01 X:01 Y:0F SP:e8 ..-..I..  319459758
(C:$eab9) g
#8 (Stop on  exec eab9)  168/$0a8,  49/$031
.C:eab9  C9 05       CMP #$05       - A:58 X:01 Y:17 SP:e8 ..-..I..  319459945
(C:$eab9) g
#8 (Stop on  exec eab9)  172/$0ac,  16/$010
.C:eab9  C9 05       CMP #$05       - A:56 X:01 Y:1F SP:e8 ..-..I..  319460164
(C:$eab9) g
#8 (Stop on  exec eab9)  175/$0af,   3/$003
.C:eab9  C9 05       CMP #$05       - A:4E X:01 Y:27 SP:e8 ..-..I..  319460340
(C:$eab9) g
#8 (Stop on  exec eab9)  177/$0b1,  53/$035
.C:eab9  C9 05       CMP #$05       - A:2C X:01 Y:2F SP:e8 ..-..I..  319460516
(C:$eab9) g
#8 (Stop on  exec eab9)  181/$0b5,  20/$014
.C:eab9  C9 05       CMP #$05       - A:2F X:01 Y:37 SP:e8 ..-..I..  319460735
(C:$eab9) g
#8 (Stop on  exec eab9)  184/$0b8,   7/$007
.C:eab9  C9 05       CMP #$05       - A:03 X:01 Y:3F SP:e8 ..-..I..  319460911

The breakpoint was continually being hit, even as I wasn’t pressing any keys, with the accumulator cycling between the values $11, $01, $58, $56, $4E, $2C, $2F and $03. This happens to be the last column of this table:

MODE1
;DEL,3,5,7,9,+,YEN SIGN,1
    .BYT $14,$0D,$1D,$88,$85,$86,$87,$11
;RETURN,W,R,Y,I,P,*,LEFT ARROW
    .BYT $33,$57,$41,$34,$5A,$53,$45,$01
;RT CRSR,A,D,G,J,L,;,CTRL
    .BYT $35,$52,$44,$36,$43,$46,$54,$58
;F4,4,6,8,0,-,HOME,2
    .BYT $37,$59,$47,$38,$42,$48,$55,$56
;F1,Z,C,B,M,.,R.SHIFTT,SPACE
    .BYT $39,$49,$4A,$30,$4D,$4B,$4F,$4E
;F2,S,F,H,K,:,=,COM.KEY
    .BYT $2B,$50,$4C,$2D,$2E,$3A,$40,$2C
;F3,E,T,U,O,@,EXP,Q
    .BYT $5C,$2A,$3B,$13,$01,$3D,$5E,$2F
;CRSR DWN,L.SHIFT,X,V,N,,,/,STOP
    .BYT $31,$5F,$04,$32,$20,$02,$51,$03
    .BYT $FF        ;END OF TABLE NULL

It is used by the keyboard-reading routine to decode hardware signals into key codes. The CIA chip handling the keyboard presents a somewhat unusual interface, more resembling a joystick than a typical keyboard: you set bits in one register to choose the key column, then read another register to see which keys in this column are pressed. This is explained for example in this memory map of the C64:

$DC00, 56320

Port A, keyboard matrix columns and joystick #2. Read bits:

  • Bit #0: 0 = Port 2 joystick up pressed.
  • Bit #1: 0 = Port 2 joystick down pressed.
  • Bit #2: 0 = Port 2 joystick left pressed.
  • Bit #3: 0 = Port 2 joystick right pressed.
  • Bit #4: 0 = Port 2 joystick fire pressed.

Write bits:

  • Bit #x: 0 = Select keyboard matrix column #x.
  • Bits #6-#7: Paddle selection; %01 = Paddle #1; %10 = Paddle #2.

$DC01, 56321

Port B, keyboard matrix rows and joystick #1. Bits:

  • Bit #x: 0 = A key is currently being pressed in keyboard matrix row #x, in the column selected at memory address $DC00.
  • Bit #0: 0 = Port 1 joystick up pressed.
  • Bit #1: 0 = Port 1 joystick down pressed.
  • Bit #2: 0 = Port 1 joystick left pressed.
  • Bit #3: 0 = Port 1 joystick right pressed.
  • Bit #4: 0 = Port 1 joystick fire pressed.

In our case, for some reason, the most-significant bit of the latter register was persistently cleared, causing keys in the last column to be reported as pressed. Since the Commodore 64 doesn’t have automatic key repeat, the effects of each key were triggered once and the computer ignored them from then on. But why was the bit cleared in the first place?

Notice how the bits of port B can simultaneously describe the state of joystick buttons. Apparently the chip is so starved for address space it has to overload register bits with multiple meanings. This led me to the following hypothesis: what if those bits are also repurposed for yet another use, not stated here? Indeed, a little further down, this caught my attention:

$DC0F, 56335

Timer B control register. Bits:

  • Bit #0: 0 = Stop timer; 1 = Start timer.
  • Bit #1: 1 = Indicate timer underflow on port B bit #7.
  • Bit #2: 0 = Upon timer underflow, invert port B bit #7; 1 = upon timer underflow, generate a positive edge on port B bit #7 for 1 system cycle.

It turns out that this is exactly the register that ends up being corrupted. This is the CIA chip register state when the keyboard works normally:

(C:$e5d1) mem $dc00 $dc0f
>C:dc00  7f ff ff 00  08 40 ff ff   .....@..
>C:dc08  00 00 00 01  00 00 01 08   ........

And this is the register state when the lock-up is in effect:

(C:$e5d1) mem $dc00 $dc0f
>C:dc00  7f ff ff 00  3a 3a ff ff   ....::..
>C:dc08  00 00 00 01  00 00 01 0e   ........

I figured I’d set up a tracepoint with tr store $dc0f and see if it hits when the bug is triggered. When I did so, this is what I got after breaking into the monitor:

#9 (Trace store dc0f)  152/$098,  15/$00f
.C:e77a  91 F3       STA ($F3),Y    - A:0E X:18 Y:4F SP:ee ..-.....   11803191

The word at address $F3 at that point is $DBC0. So this stores the accumulator value $0E at $DBC0 + $4F = $DC0F. Looking back at the KERNAL disassembly, $E77A is located in the DEL key handler and is supposed to store the character attribute at the erased character’s position in the colour RAM. The character position is computed with a column number of 79 and a row base address address corresponding to row number ($DBC0$D800) / 40 = 24, causing the address computation to overflow. The value $0E indeed corresponds to the current text colour, light blue; the values of this and all other ‘unsafe’ colours have bit 1 set, which enables the unwanted timer register bit.

Such are the joys of memory-unsafe programming.

The way to escape the lock-up is clear now: we need to reset the timer register to its original value. With the VICE monitor, this is easy:

(C:$e5d1) > $dc0f $08

Contemporaneous users of physical C64s had no access to such luxuries, however. They had to make do with whatever peripheral was available, hoping that the KERNAL code that interacts with it will at some point restore the timer register to a usable value. It just so happened that the Datasette and the Commodore disk drives were such. Both workarounds rely on the fact that the first key in the unfortunate column to be triggered is RUN/STOP.

This key inputs the LOAD command in the editor and triggers its execution. But at screen column 79, this doesn’t work and instead raises a syntax error. Unfazed by this, the KERNAL proceeds to invoke the RUN command afterwards, causing whatever BASIC program happens to be held in memory to be executed. If this BASIC program contains code that could restore the pitiable register to a usable state, it can manage to avoid the subsequent keyboard lock-up. It just so happened that interacting with the disk drive would do the trick. A simpler command, not requiring the disk drive to be present, would be POKE 56335,8 or POKE 56335,PEEK(56335) AND 248.

The Datasette escape method attempts to flip bits in the port B register so that the RUN/STOP keypress can be detected once more, causing the LOAD command to be invoked again. Invoking that command will likewise reprogram the timer on the CIA chip.

4
  • There's quite a lot to unpack here, particularly for someone (like me) with no practical experience in 6502 assembly programming. But I'm able to follow most of this explanation, so thanks! One thing that's still not clear to me is how simultaneously pressing certain pairs of keys on different rows allows the fake RUN/STOP keypress to be detected again. You said that $DC01 is $FF both before and after the lockup. Pressing left SHIFT+3 should change this to $81. So what? How does this affect anything?
    – Psychonaut
    Jul 22, 2021 at 8:27
  • 1
    @Psychonaut Honestly, I don’t entirely understand it either. But first of all, it’s $7E in $DC00, not $81 in $DC01 (it’s bit value 0 that denotes the key is pressed), which I presume the timer is supposed to flip to $FE as per the setting in bit #2 in the Timer B control register, and later back to $7E, allowing the key ‘release’ and subsequent ‘press’ to be detected. Jul 22, 2021 at 10:52
  • But why press two keys simultaneously, I don’t have an idea. Maybe it has to do with how the keyboard handler processes the keypresses, maybe it’s some subtle behaviour of the keyboard circuitry, or maybe it’s just superstition of someone who wrote the advice, but didn’t understand it either. (The fact that the workarounds mentioned the disk drive, but not a POKE suggests the last one is not necessarily implausible.) Jul 22, 2021 at 10:56
  • @Psychonaut I think that will have to be debugged on a real C64, probably with a test program that monitors changes to the Port B register. (In fact, I cannot even reproduce this escape method in VICE, though I am pretty sure is a flaw in the emulation.) Jul 28, 2021 at 19:11

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .