Title: Debug Diversion
Date: December 13, 2020
Tags: altair programming
========================================

As I was finishing up my assembler, I knew I was going to need a way to save
assembled programs off the Altair so I starting working on a new bootloader-type
program.  Writing, assembling, and testing revealed a new problem of trying to
debug programs that I don't have the hand assembled version of.

Without hand assembling, I don't easily know where instructions will land in
memory or what the value of the instructions will be.  Before, when I had a
problem, I could look at my assembled program and use the front panel to go to
where I though the problem was.  Usually I'd replace an instruction with HLT and
execute the program to that point.  Then remove the HLT and put the original
instruction back and single step from there.

But, now that I have an assembler doing all the work for me, it was suddenly
really easy to insert a CALL to a debug subroutine.  I can put the CALL where I
think the code goes awry, reassemble and execute it just like putting the HLT in
via the front panel.

I wrote the subroutine into the monitor I was working on so it would get
assembled with it and I could CALL it by a label.  The subroutine could also
reuse the existing serial IO routines.

The debug subroutine prints out the Stack Pointer and Program Counter from
before the CALL to debug, and all of the CPU registers.  Upon RET, the program
can continue where it left off.  I basically rediscovered breakpoints.

Using the front panel for debugging doesn't even give you access to see the
Stack Pointer (except during a stack operation) or the CPU registers.

With the source code in the text editor on my laptop (which is also my terminal)
and the symbol table output from the assembler showing the addresses of the
labels, I can easily keep track of where I am in the code and in memory and
what's expected to be in the registers while debugging a program.

I added a couple additional features for a good first pass of the debug
subroutine.  I print the flag register as a string instead of just the octal
byte so I don't have to remember which bit is the Zero flag or Carry flag, etc.
I also added the ability to examine the byte at any memory address.

I could add some extras like changing memory or even registers but I haven't
needed to change registers before and the assembler can change memory for me.
And I still have access to the front panel for anything I was doing before.


# Software Debuggers #

If you've used a software debugger, say in an IDE, you might already see what's
left to build a (more or less) fully featured debugger.  Instead of adding a
CALL and reassembling, I'd need to go back to my original process of replacing
instructions.  By saving the bytes of the assembled program and replacing them
with the CALL to the debug subroutine directly in memory I could dynamically
place breakpoints.  And, of course, remembering to replace the original bytes
and fixing up the program counter when continuing execution.

Setting dynamic breakpoints can be a bit tricky, though.  You don't want to have
to set it by address, because you don't know the addresses of the code anymore,
and you don't want to put the CALL in the middle of data or an immediate value
or over the address part of another CALL or a JMP.

The debugger would need to be aware of the source code so it can set breakpoints
on a line of executable code.  Couple the source with the symbol table (you've
heard of needing debug symbols when using a C debugger?) and you have a map of
addresses of where certain things are and could do some quick counting to get to
the specific instruction the breakpoint needs to go on.

Dynamically adding breakpoints opens up the possibility for single stepping,
stepping into a subroutine or stepping over a subroutine or other such features.
It can be done by dynamically adding or moving the breakpoint CALL.

When it has access to the symbol table, you can also view variables by name.  In
assembly, that would be an EQU, a SET or a labeled address used for data
storage.  An extra feature would be to be able to tell the debugger if the
variable is a byte, a word, or even a string so it can display the full multi-
byte value.

Of course, there is a lot of detail to get a reliable debugger that can
understand the source and the symbol table and the assembler has to be written
with that in mind.  I haven't done all of that work.  I have my laptop with the
source and I can save the symbol table output there as well.

I also make some assumptions here about the program being debugged like that
it's using serial IO, has set up the Stack Pointer and has enough stack room for
the debug subroutine to use, and is not using interrupts.  I don't know when I'd
not have serial IO or a stack but at least the interrupts would need to be
worked around.

What I've done for now is leave the debug subroutine in my new monitor located
at an easy to remember address so I can add a CALL to that address from any
program I am writing and reassemble it.


# Debug Subroutine #

Getting the Stack Pointer, Program Counter, and Registers was a fun project.  It
took me a couple rewrites to get it to do what I wanted and be reasonably
compact.  I'm sure smarter people than I could shave some more bytes off (which
goes for all my assembly, I'm not exactly brilliant at this, I'm just getting
by).

When executing a CALL, what happens is that the Program Counter gets pushed to
the stack and gets set to the address CALLed to where execution continues.  So
keep track of that.  If we want to print the Program Counter from before the
CALL, it just got pushed to the stack for us at the location the Stack Pointer
was at before the CALL.  That's 2 pieces of the information we want to output.

To preserve the CPU registers, we need to PUSH each pair to the stack before
doing anything else.  The value of the Stack Pointer continues to decrement by 2
for each PUSH.


DEBUG   PUSH    PSW     ; save registers for resetting
       PUSH    BC
       PUSH    DE
       PUSH    HL


Now everything is safe and we just need to pull it back out to print it, but
also to save it so we can restore the registers back into the CPU, reset the
Stack Pointer, and then RET will bring us back to where the Program Counter was
pointing.

We need to move up the stack without moving the Stack Pointer because we use
CALLs while in the DEBUG subroutine which would overwrite whatever was on the
stack which we are trying to preserve.  We need our own Stack Pointer which we
can create by setting HL to 000000Q and then adding the current value of the
Stack Pointer to it.  This is the only way I know to get the value of the Stack
Pointer.  In C, and probably other languages, the stack is used to pass
variables and is called the stack frame so I'll call my copy of the Stack
Pointer the frame pointer.  I think a CALL or a RST (which is just a CALL to a
fixed address) and reading off the stack is the only way to get the Program
Counter.

So now we have a frame pointer and we know it was at Program Counter + 2 bytes
per Register down the stack.  So increment the frame pointer back up by that
amount.  The value is now what the Stack Pointer was before we CALLed debug.
Print the value of our frame pointer.


       LXI     HL,000Q         ; get SP for frame pointer
       DAD     SP
       LXI     DE,012Q         ; add 10 to get to top of stack
       DAD     DE
       XCHG                    ; save frame pointer in DE
;SP
       LXI     HL,SPSTR        ; load string pointer
       CALL    PRNTSTR         ; print string
       MOV     A,D             ; high byte
       CALL    PRNTOCT         ; print octal byte as ascii to terminal
       MVI     B,' '
       CALL    PRNTCHR
       MOV     A,E             ; low byte
       CALL    PRNTOCT


Now we move back down the stack.  The next 2 bytes is the Program Counter that
the CALL to debug saved for us and we can print that.  Then each of the next 2
bytes are the register pairs.  Print as we go.


;PC
       LXI     HL,PCSTR        ; load string pointer
       CALL    PRNTSTR         ; print string
       LXI     BC,177775Q      ; -3
       DCX     DE
       LDAX    DE              ; get high byte
       MOV     H,A
       DCX     DE
       LDAX    DE              ; get low byte
       MOV     L,A
       DAD     BC              ; subtract 3 because we inserted CALL DEBUG
       MOV     A,H
       CALL    PRNTOCT         ; print an octal byte as ascii to terminal
       MVI     B,' '
       CALL    PRNTCHR
       MOV     A,L             ; L is safe from PRNTOCT
       CALL    PRNTOCT
;PSW
       LXI     HL,AREGSTR
       CALL    PRNTREG
       LXI     HL,FREGSTR
       CALL    PRNTSTR
       DCX     DE
       LDAX    DE              ; get F
       CALL    PRNTFLG         ; special print the flags
;BC
       LXI     HL,BREGSTR
       CALL    PRNTREG
       LXI     HL,CREGSTR
       CALL    PRNTREG
;DE
       LXI     HL,DREGSTR
       CALL    PRNTREG
       LXI     HL,EREGSTR
       CALL    PRNTREG
;HL
       LXI     HL,HREGSTR
       CALL    PRNTREG
       LXI     HL,LREGSTR
       CALL    PRNTREG


Meanwhile. the real Stack Pointer remained at the bottom of the stack below all
the registers and we can CALL and RET all day long without overwriting them.

When we're done debugging, we can POP the registers back into place which will
use the real Stack Pointer and then RET back to where we left off in the program
and it has no idea that we were gone.


;continue
DASK    LXI     HL,CONTSTR
       CALL    PRNTSTR
DLOOP2  CALL    GETCHR
       MOV     A,B             ; copy to A
       CPI     003Q            ; ^C
       JZ      DCONT
       CPI     033Q            ; esc
       JZ      RESET
       CPI     'R'
       JZ      DRMEM
       CPI     'r'
       JZ      DRMEM
       JMP     DLOOP2
DCONT   POP     HL              ; restore registers
       POP     DE
       POP     BC
       POP     PSW
       RET                     ; go back


And here are the helper subroutines, most notably how I print the flag register.


;print reg
; HL = prefix string pointer
PRNTREG
       CALL    PRNTSTR
       DCX     DE
       LDAX    DE              ; get reg value
       CALL    PRNTOCT
       RET

;read mem address
; TODO on error, drop back to DEBUG, not monitor
DRMEM   LXI     HL,ADDRSTR
       CALL    PRNTSTR
       CALL    READADDR
       MVI     B,':'
       CALL    PRNTCHR
       MVI     B,' '
       CALL    PRNTCHR
       MOV     A,M             ; read byte
       CALL    PRNTOCT
       JMP     DASK

;print flags
; A: register
; SZ0Ac0P1C
; Sign Zero (0) Aux carry (0) Parity (1) Carry
PRNTFLG LXI     HL,FLGSTR       ; flag string
       MOV     C,A             ; save flag reg
PFLOOP  MOV     A,M             ; get char
       CPI     000Q            ; check for \0
       RZ
       MOV     B,A             ; save char
       MOV     A,C             ; restore flag reg
       RLC                     ; flag into carry
       MOV     C,A             ; save flag reg
       JC      PFPRNT
       MOV     A,B             ; restore char
       ADI     040Q            ; lower case
       MOV     B,A
PFPRNT  CALL    PRNTCHR
       INX     HL
       JMP     PFLOOP

FLGSTR  DB      "SZ"
       DB      020Q            ; needed because the assembler doesn't let us
       DB      "A"             ; input lowercase. :(
       DB      020Q            ; TODO, use lowercase and SUI in subroutine so
       DB      "P1C\0"</pre>   ;   we can use all printable chars here


I left out the strings other than for printing the flags but you can see what
they are from the output when we CALL the debug subroutine.


SP: 370 002
PC: 003 073
A: 000 F: sZ0A0P1c
B: 060 C: 001
D: 000 E: 370
H: 374 L: 000
^C: CONT, R: READ MEM, ESC: QUIT TO MONITOR
ADDR? 200 000: 061
^C: CONT, R: READ MEM, ESC: QUIT TO MONITOR


There are some TODOs in the code and there are things to fix in general.  It
assumes a number of things and if you mistype when entering an address to view,
it uses the monitor's error subroutine (as it's borrowing the monitor's GETADDR
subroutine) and will drop you to the monitor resetting the stack and basically
blowing up your debugging.

I've already burned it into a PROM chip so, eh, I'll fix it eventually.