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" ; 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.