Keyboard Input Routines
and Character Coded Output
on the 68000
(Debug Session -- Init Error)
After I had finished the code for keyboard input and character code output on the 6809, while I was preparing the code for this chapter, I became aware of a bug I introduced in the code for binary output on the 68000.
Became aware of?
The code went BOOM!
Well, it threw some sort of memory addressing fault or something.
It took me a few tens of minutes of stepping, setting breakpoints, continuing
on to the breakpoints, checking registers, and so forth to find what I thought
was the problem, but I could not convince myself it was the only problem.
Something about the stacks did not seem right, and I was too tired to focus
when I could work on it, and I kept getting lost in code and thinking that, in
fixing one problem I must be causing another.
I'm getting old. Hard
to program meaningfully at night.
Anyway, the upshot of that is all
that I went back and wrote
a chapter attempting to talk about a few debugging techniques
using artificial bugs -- Artificial? I could call them puzzles or riddles, I guess.
And I wrote
another chapter detailing how to set up mechanisms to prove that the stack
is balanced, correct, and not out of bounds at certain points in a
program. (And that chapter sent me down a
detour discussion of stack frames, which I probably should have left for another day.)
And I
dug back to where the bug was introduced in
the binary output for the 68000 chapter
and left notes to reduce reader surprise, and now it's time to recap the experience.
You don't really notice these kinds of bugs unless you execute a piece of code several times. So it wouldn't show up in the binary output chapter where I introduced the bug, because we only used those functions once.
Buuuuuuuuuuut -- now I'm trying to reproduce the bug and it won't.
Well, I remember vaguely fixing one problem and leaving it fixed and fixing another problem and not leaving it fixed and ... yeah, I think I remember something. Maybe I didn't leave the real buggy version source code behind.
So while I'm scratching my head and nosing around, I do a listing of my working directory from my bash shell in Ubuntu:
$ ls -lart
合計 96
-rw-rw-r-- 1 nova nova 25101 10月 14 16:36 inkey_68K.lst
-rw-rw-r-- 1 nova nova 1692 10月 14 16:36 inkey_68K.PRG
-rw-rw-r-- 1 nova nova 7250 10月 14 16:50 rt_rig03_68K_ww.s
-rw-rw-r-- 1 nova nova 7250 10月 14 16:50 rt_rig03_68K.s
drwxrwxr-x 3 nova nova 4096 10月 24 14:05 ..
-rw-rw-r-- 1 nova nova 1629 10月 24 14:07 inkey_68K_ww.s
-rw-rw-r-- 1 nova nova 1629 10月 24 14:07 inkey_68K.s
-rw-rw-r-- 1 nova nova 25101 10月 25 00:07 inkey_68K.list
-rw-rw-r-- 1 nova nova 1692 10月 25 00:07 INKEY_68K.PRG
drwxrwxr-x 2 nova nova 4096 10月 25 11:15 .
Well, I'm going to have a hard time telling which of inkey_68K.PRG and INKEY_68K.PRG I'm running when I try to run that from the hatari EMUCON shell. They look the same to EmuTOS, and the shell will probably just grab the first one it sees.
So (in the bash shell where I can do this) I rename inkey_68K.PRG to BINKY_68K.PRG, and I'm thinking about this and I pull inkey_68K.lst into a text editor -- Note the difference between .lst and .list and the difference in the dates! -- and, lo and behold the one I just renamed is left over from my fixing things without tracks in the middle of the night. That's why it wouldn't bomb.
And, now that I can, I run INKEY_68K.PRG, and it does bomb.
Hatari's configuration dialog, in the Hatari screen subdialog (not Atari screen), has a screenshot option. Way cool. Let's see if I can convince Blogger to let me upload a pic today. Foul language moment. Google seems to think it needs cookies to upload. I even tried giving it cookies, and it wouldn't take them. How did I get around this last time? Ah. Had to break Chrome out of the mothballs then. And it's giving me error messages and won't let me quite because I didn't enable cookies that Firefox enabled. The save message says it's saved, so kill Firefox and edit in Chrome. Yuck. Guess it's time to do it again.
Got it uploaded and now I'm back in Firefox. Yay.
Not an address violation, an illegal instruction. PC is someplace it shouldn't be and the CPU is trying to execute stuff that isn't code, which is often the result of mucking with the return stack in a single interleaved stack runtime, which this is not, so probably one stack overflowed into data or onto the other. Looking at A6 and A7, though, neither is where it should be -- $0003e4c and $00007e5e. A5, our DP substitute, doesn't look right, either. All three should be up in the $14000 area, like A3 and A4 are.
Time to step through. Nice that I managed to fail to overwrite both the listing and the object code when I compiled these late last night. It gives me something to work with. Man I'm getting both old and lucky
Restart Hatari, Ctrl-Z to break out to the EMUCON shell, cd where I'm working, Alt-Break to break into the debugger, set the breakpoint pc=TEXT, (c)ontinue, run inkey_68K.PRG (since the saved one is now BINKY_68K.PRG), and when it breaks back to the debugger, (s)tep. Step, step, step ...
----------------------------------------------------------------------
You have entered debug mode. Type c to continue emulation, h for help.
CPU=$e1d7e2, VBL=1331, FrameCycles=128, HBL=0, LineCycles=128, DSP=N/A
00e1d7e2 46c0 move.w d0,sr
> b pc=TEXT
CPU condition breakpoint 1 with 1 condition(s) added:
pc = TEXT
> c
Returning to emulation...
1. CPU breakpoint condition(s) matched 1 times.
pc = TEXT
Reading symbols from program '/home/nova/usr/share/hatari/C:/primer/char_io/stepinch/INKEY_68K.PRG' symbol table...
TOS executable, DRI / GST symbol table, reloc=0, program flags: PRIVATE (0x0)
Program section sizes:
text: 0x350, data: 0x0, bss: 0x0, symtab: 0x32c
Trying to load DRI symbol table at offset 0x36c...
Offsetting BSS/DATA symbols from TEXT section.
Skipping duplicate address & symbol name checks when autoload is enabled.
Loaded 56 symbols (41 TEXT) from '/home/nova/usr/share/hatari/C:/primer/char_io/stepinch/INKEY_68K.PRG'.
CPU=$13d10, VBL=1911, FrameCycles=34524, HBL=33, LineCycles=996, DSP=N/A
00013d10 6000 02a0 bra.w #$02a0 == $00013fb2 (T)
> s
CPU=$13fb2, VBL=1911, FrameCycles=34536, HBL=33, LineCycles=1008, DSP=N/A
00013fb2 6100 ff34 bsr.w #$ff34 == $00013ee8
> s
CPU=$13ee8, VBL=1911, FrameCycles=34556, HBL=34, LineCycles=12, DSP=N/A
00013ee8 205f movea.l (a7)+ [00013fb6],a0
>
CPU=$13eea, VBL=1911, FrameCycles=34568, HBL=34, LineCycles=24, DSP=N/A
00013eea 47fa fe24 lea.l (pc,$fe24) == $00013d10,a3
>
CPU=$13eee, VBL=1911, FrameCycles=34576, HBL=34, LineCycles=32, DSP=N/A
00013eee 48eb f000 0008 movem.l a4-a7,(a3,$0008) == $00013d18
>
CPU=$13ef4, VBL=1911, FrameCycles=34620, HBL=34, LineCycles=76, DSP=N/A
00013ef4 3a4b movea.w a3,a5
>
CPU=$13ef6, VBL=1911, FrameCycles=34624, HBL=34, LineCycles=80, DSP=N/A
00013ef6 4fed 0148 lea.l (a5,$0148) == $00003e58,a7
>
CPU=$13efa, VBL=1911, FrameCycles=34632, HBL=34, LineCycles=88, DSP=N/A
00013efa 4ded 01d0 lea.l (a5,$01d0) == $00003ee0,a6
>
What's this? A7 being set to $00003e58? and A6 to $00003ee0?
Ah.
movea.w a3,a5
There it is. One of the bugs, anyway. That should not be a 16-bit MOVE, move address or otherwise.
Here's the stack initialization code, with the culprit line:
SSTKLIM DS.L 16 ; 16 levels of call, max
* ; 68000 is pre-dec (decrement-before-store) push
SSTKBAS DS.L 2 ; a little bumper space
PSTKLIM DS.L 32 ; roughly 16 levels of call at two parameters per call
PSTKBAS DS.L 2 ; bumper space -- parameter stack is pre-dec
******************************
* Maintain local static area:
INITRT MOVE.L (A7)+,A0 ; get the return address
LEA LOCBAS(PC),A3 ; temporary local base (pseudo-DP)
MOVEM.L A4-A7,A4SAVE-LOCBAS(A3) ; Store away what the BIOS gives us.
MOVE A3,A5 ; set up our local base (pseudo-DP)
LEA SSTKBAS-LOCBAS(A5),A7 ; set up our return stack
LEA PSTKBAS-LOCBAS(A5),A6 ; set up our parameter stack
JMP (A0) ; return via A0
Look at the line with the comment "set up our local base (pseudo-DP)". Default option is to assume 16-bit when nothing is specified, thus "move.w".
This is the one that I couldn't figure out, and had me worried a week and a half ago, and sent me off on that detour. Let's see whether fixing just this one only will result in running code with the other bug unfixed.
And ... (drumroll)
Nope, it didn't. Illegal instruction again. This time, at least, A6 was sort-of where it was supposed to be. So, yeah, suspect a stack overflow, somehow.
Looking back up at the stack declarations, the parameter stack is above the return stack (and they are both way too small, but that's good for catching things like this before they get out into production code). So if the parameter stack oveflows, it will kill the return stack.
So now is it time to break out
the stack balancing code
that I wrote while out on that detour?
Let's get a look at our code without the stack balance checks.
Our test "application" code:
***********************************************************************
* simple 8-bit keyboard input for 68000
* using parameter stack,
* with test frame
* Joel Matthew Rees, October 2024
OPT LIST,SYMTAB ; Options we want for the stand-alone assembler.
OPT DEBUG ; We want labels for debugging.
OUTPUT
***********************************************************************
INCLUDE rt_rig03_68K.s
MACHINE MC68000 ; because there are a lot the assembler can do.
****************
* Program code:
*
* Essential BIOS/OS:
conin EQU 2
* INCHAR ; Echo must be handled by user code?
INCHNE BSR.S INCHV
AND.L #$FFFF,D0 ; clear out the scan code
MOVE.L D0,-(A6) ; return the character
RTS
* Wait for input, return character, scan code in D7
INCHV MOVE.W #devscrkbd,-(A7) ; push the device number
MOVE.W #conin,-(A7) ; push the BIOS routine selector
TRAP #BIOSTRAP ; call into the BIOS
ADDQ.L #4,A7 ; deallocate the BIOS
RTS
PROMPT DC.B CR,LF ; Put message at beginning of line
DC.B "Type any key, Q to quit. " ;
DC.B CR,LF ; Put the prompt on a new line
DC.B NUL
KEYCOL DC.B "KEY:"
DC.B NUL
COLBIN DC.B ASCCOL,NUL
COLHEX DC.B ": $"
DC.B NUL
*
*
*
EVEN
PGSTRT LEA PROMPT(PC),A0
MOVE.L A0,-(A6)
BSR.W OUTS
BSR.W INCHNE ; Hold off echo
MOVE.L (A6),-(A6)
LEA KEYCOL(PC),A0
MOVE.L A0,-(A6) ; duplicate
BSR.W OUTS
BSR.W OUTC ; output character
MOVE.L (A6),-(A6) ; duplicate
LEA COLBIN(PC),A0
MOVE.L A0,-(A6)
BSR.W OUTS
BSR.W OUTB8
MOVE.L (A6),-(A6) ; duplicate
LEA COLHEX(PC),A0
MOVE.L A0,-(A6)
BSR.W OUTS
BSR.W OUTHX8
BSR.W OUTNWLN
MOVE.L (A6)+,D1 ; balance stack
CMP.B #ASCQ,D1
BNE.S PGSTRT
RTS
*
END ENTRY
INCHNE and INCHV don't look wrong.
Here's the framework rigging, with the stack initialization fixed:
***********************************************************************
* A simple run-time framework inclusion for 68000
* providing parameter stack and local base
* Version 00.00.03
* Joel Matthew Rees, October 2024
MACHINE MC68000 ; because there are a lot the assembler can do.
***********************************************************************
*
* Essential control codes
LF EQU $0A ; line feed
CR EQU $0D ; carriage return
NUL EQU 0
* Other essential ASCII codes
ASC0 EQU '0' ; Some assemblers won't handle 'c constants well.
ASC9 EQU '9'
ASCA EQU 'A'
ASCXGAP EQU ASCA-ASC9-1 ; Gap between '9' and 'A' for hexadecimal
ASCCOL EQU ':
ASCQ EQU 'Q
* Essential BIOS/OS:
*
* We will not be using these:
*GEMDOSTRAP EQU 1
*GEMprintstr EQU 9 ; PRINT LINE in some docs
*
NATWID EQU 4 ; 4 bytes in the CPU's natural integer
HAFNAT EQU (NATWID/2) ; 2 bytes in the CPU's half natural integer
LOWBYTE EQU (NATWID-1) ; offset of low byte
*********************************
* Per-process local static area:
* ORG $20000 ; TOS just gives you what it gives you.
EVEN
LOCBAS EQU * ; here pointer, local static base starts here.
ENTRY BRA.W START
NOP ; buffers to 4 byte even
NOP
A4SAVE DS.L 1 ; a place to keep A4 to A7 so we can return clean
A5SAVE DS.L 1 ; using it as pseudo-DP
A6SAVE DS.L 1 ; save A6 anyway.
A7SAVE DS.L 1 ; SP
* A7 will, of course, be our return address stack pointer
* A6 will be our parameter stack pointer
* A5 will be our local static base.
* A4 not yet dedicated, should be preserved if used.
* A0, A1, A2 are assumed volatile in BIOS.
* A3 is used here (volatile).
* D6, D7 are used here (volatile).
* D0, D1, D2 are assumed volatile in BIOS.
* D3, D4, and D5 should be preserved if used.
*
* In this code, we handle the parameter stack directly.
* Items on the parameter stack
* are assumed to be full natural width (NATWID bytes),
* as a simplifying assumption.
* room for something
GAP1 DS.L 58 ; to even 256 bytes, not much here yet
*
DS.L 2 ; a little bumper space
SSTKLIM DS.L 16 ; 16 levels of call, max
* ; 68000 is pre-dec (decrement-before-store) push
SSTKBAS DS.L 2 ; a little bumper space
PSTKLIM DS.L 32 ; roughly 16 levels of call at two parameters per call
PSTKBAS DS.L 2 ; bumper space -- parameter stack is pre-dec
******************************
* Maintain local static area:
INITRT MOVE.L (A7)+,A0 ; get the return address
LEA LOCBAS(PC),A3 ; temporary local base (pseudo-DP)
MOVEM.L A4-A7,A4SAVE-LOCBAS(A3) ; Store away what the BIOS gives us.
MOVE.L A3,A5 ; set up our local base (pseudo-DP)
LEA SSTKBAS-LOCBAS(A5),A7 ; set up our return stack
LEA PSTKBAS-LOCBAS(A5),A6 ; set up our parameter stack
JMP (A0) ; return via A0
*****************************************
* Low level I/O:
* This will be a focus point in porting,
* Primarily OUTNWLN, OUTC, INCH
* No need for PPOPD, PPUSHD, PPOPX, or PPUSHX, etc. in 68000 code.
* Output an 8-bit byte in hexadecimal,
* byte as a 16-bit parameter on PSP.
OUTHX8 CLR.L D6
MOVE.B LOWBYTE(A6),D6 ; get the byte
LSR.B #4,D6
BSR.S OUTRAD
MOVE.B LOWBYTE(A6),D6 ; Get it again.
AND.W #$0F,D6 ; mask it off
BSR.S OUTRAD
LEA NATWID(A6),A6
RTS
* Convert the value in B to ASCII numeric,
* including hexadecimals and up to base 36 ('Z'+1)
OUTRAD ADD.W #ASC0,D6 ; Add the ASCII for '0'
CMP.W #ASC9,D6; Greater than '9'?
BLS.S OUTRADD ; no, output as is.
ADD.W #ASCXGAP,D6 ; Adjust it to 'A' to 'Z'
OUTRADD MOVE.L D6,D7
BSR.W OUTCV
RTS
* Output the 8-bit number on the stack in binary (base two).
* For consistency, we are passing the byte in the lowest-order byte
* of a 32-bit word.
* Uses D6, D7.
OUTB8 MOVE.L (A6),D6 ; shift on memory is 16-bit, use register
MOVE.W #8,(A6) ; 8 bits to output, borrow parameter high word.
OUTB8L LSL.B #1,D6 ; Get the leftmost bit of the lowest byte.
BCS.S OUTB81
OUTB80 MOVEQ.L #'0',D7
BRA.S OUTB8D
OUTB81 MOVEQ.L #'1',D7
OUTB8D BSR.S OUTCV
SUBQ.W #1,(A6)
BNE.S OUTB8L ; loop if not Zero
SUBQ.L #NATWID,A6 ; drop parameter character
RTS
* driver level code to output a new line
OUTNWLN MOVEQ.L #CR,D7
BSR.S OUTCV
MOVEQ.L #LF,D7
BSR.S OUTCV
RTS
* Essential BIOS/OS:
BIOSTRAP EQU 13
bconout EQU 3
devscrkbd EQU 2
* OUTC and OUTCV are two entry points to the same routine.
OUTC CLR.W D7 ; clear character high byte.
MOVE.B NATWID-1(A6),D7 ; Get the character, high byte cleared
ADDQ.L #NATWID,A6 ; drop it from parameter stack before falling through.
* Falls through!
* Common low-level hook: call with the character in D7.
* This bit of glue calls the BIOS.
OUTCV MOVE.W D7,-(A7) ; Push the character on A7 where bconout wants it.
MOVE.W #devscrkbd,-(A7) ; push the device number
MOVE.W #bconout,-(A7) ; push the BIOS routine selector
TRAP #BIOSTRAP ; call into the BIOS
ADDQ.L #6,A7 ; deallocate the BIOS parameters when done
RTS
* Return from TRAP is RTE,not RTS.
* Can't rob TRAP's return.
* Output NUL-terminated string of bytes.
* Very low-level, no guard rails on length.
* A3 is preserved by Hatari BIOS.
OUTS MOVE.L (A6)+,A3 ; get the string pointer
OUTSL CLR.L D7 ; Prepare the high bytes.
MOVE.B (A3)+,D7 ; get the byte, update the pointer
BEQ.S OUTDN ; if NUL, leave before outputting the character.
BSR.S OUTCV ; use OUTCV directly
BRA.S OUTSL ; next character
OUTDN RTS
******************************
* intermediate-level library:
*
* We often will not need these, but we'll go ahead and define them:
*
* input parameters:
* 16-bit left, right
* output parameter:
* 16-bit sum
ADD16 MOVE.W (A6)+,D7 ; right (16-bit only)
ADD.W D7,(A6) ; add to left
RTS ; *** all flags valid!! ***
*
* input parameters:
* 16-bit left, right
* output parameter:
* 16-bit difference
SUB16 MOVE.W (A6)+,D7 ; right (16-bit only)
SUB.W D7,(A6) ; subtract from left
RTS ; *** all flags valid!! ***
*
* input parameters:
* 32-bit left, right
* output parameter:
* 32-bit sum
ADD32 MOVE.L (A6)+,D7 ; right
ADD.L D7,(A6) ; add to left
RTS ; *** all flags valid!! ***
*
* input parameters:
* 32-bit left, right
* output parameter:
* 32-bit difference
SUB32 MOVE.L (A6)+,D7 ; right
SUB.L D7,(A6) ; subtract from left
RTS ; *** all flags valid!! ***
*
* input parameters:
* 16-bit unsigned left, right
* output parameter:
* 32-bit sum
ADD16L CLR.L D7
MOVE.W 2(A6),D7 ; left (no sign extension)
CLR.L D6
MOVE.W (A6),D6 ; right (no sign extension)
ADD.L D6,D7 ; 32-bit sum
MOVE.L D7,(A6) ; 32-bit result on stack
RTS ; *** X, N, Z valid ***
*
* input parameters:
* 16-bit left, right
* output parameter:
* 32-bit signed difference
SUB16L CLR.L D7
MOVE.W 2(A6),D7 ; left (no sign extension)
CLR.L D6
MOVE.W (A6),D6 ; right (no sign extension)
SUB.L D6,D7 ; 32-bit difference
MOVE.L D7,(A6) ; 32-bit result on stack
RTS ; *** X, N, Z valid ***
************************************
* Start run-time, call program.
* Expects program to define PGSTRT:
*
START BSR.W INITRT
NOP ; place to set breakpoint
*
BSR.W PGSTRT
*
DONE NOP ; place to set breakpoint
MOVEM.L A4SAVE-LOCBAS(A5),A4-A7 ; restore the monitor's A4-A7
NOP
NOP ; landing pad
NOP
NOP
* One way to return to the OS or other calling program
CLR.W -(A7) ; there should be enough room on the caller's stack
TRAP #1 ; quick exit
That's a lot to look through. But look at each routine and see if you see something suspicious about the way it's handling A6 on return.
I think you see it.
This has become rather long, I'm getting tired, and I don't want to go to the trouble of doing the stack balance check code.
But if we don't we'll lose a good opportunity. So, even if you see it, let's insert the stack balance checks in the next chapter.
No comments:
Post a Comment