Thursday, October 24, 2024

ALPP 03-13 -- Keyboard Input Routines and Character Code Output on the 68000 (Debug Session -- Init Error)

Keyboard Input Routines
and Character Coded Output
on the 68000
(Debug Session -- Init Error)

(Title Page/Index)

 

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.


(Title Page/Index)

 

 

 

 

No comments:

Post a Comment