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
#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
#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
#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
#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
#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
#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
#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
$03. This happens to be the last column of this table:
.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:
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.
- Bit #x: 0 = Select keyboard matrix column #x.
- Bits #6-#7: Paddle selection; %01 = Paddle #1; %10 = Paddle #2.
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:
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
$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 (
$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.