Debugging Machine Language Programs
by John L 2/11/88
This paper is presented in two parts.
- Part 1 covers basic ideas and procedures used in debugging: Code reading; using a monitor program; breakpoints; testing; and patching.
- Part 2 deals with special problems: Debugging interrupt routines; emulation; symbolic debuggers; and debug code.
PART 1. Programmers' Workshop Conference, Feb 11, 1988, SYSOP JL
Consider first what you will need. A monitor program. The C128 has one built in or you may have a cartridge with one built in. Otherwise load up a monitor program and initialize it before loading your M/L program to work on. A listing of the program you are working on. If you don't have a printer then have your handwritten code handy, or at least write down starting addresses of routines and locations used for variables and data. If you don't have an assembler listing (preferred) then disassemble the code to a printer.
For working on M/L you will want a reset button on your computer. If you don't have one then build one, use a cartridge with one or purchase a plug in reset button. M/L programs often lock up the keyboard when they crash. Re-loading programs, remaking patches and re-starting tests is time consuming. Also if you have to power down to recover you have lost important information you will need to find the problem.
Pencil and paper, a calculator, reference books, patience and a logical mind are also helpful. Practice at having a part of your mind doing *only* what the program tells you to do. In doing this don't make assumptions - interpret instructions literally. For example, the code sequence CMP #3 : BMI LOOP does NOT mean "if the accumulator is less than 3 then loop". What it DOES mean is "set carry, subtract with carry 3 from the accumulator and branch if bit 7 of the result is set". So in tracking the problem down if you used this code sequence you would find that if the accumulator contains 135 ($87) then the branch would be taken in addition to cases like accumulator equal to 2. (For what is intended here, BCC LOOP would be correct instead of BMI LOOP)
Where to start... First find out what you see the program do when it runs. You will then want to start following your code from its entry point until you get to a point just past where it does what you see it do correctly. Paying close attention to what the program does before it crashes or does something wrong can give you clues to where to start looking. Then you can start using one or more of the following methods to isolate the problem.
Code reading
Look over the code in the area where you suspect a problem to be. Use the "dumb computer" thought process I mentioned earlier. Most times if you KNOW a program section does something wrong you can see the problem in your code. Besides possibly finding the problem without a great deal of effort this exercise will thoroughly familiarize you with the program logic in the area where there is a problem.
Check variables/data areas
Use a monitor program to check memory locations used by your program. Don't forget to look at locations used for temporary storage of registers or data. You may get a clue as to what values it was using when it messed up. Check memory locations used for inputs. You may get a clue but even if you don't, write the value down. You may want to later try testing part of the routine with special values you find. If an input is outside of the range which your program expects then look back to where the input is passed to it. One common bug found in programs is to expect a memory location to contain an input value or temporary storage and find out that some other routine or a system routine overwrites your saved value with something different. More on this subject later involving interrupt routines.
Breakpoints
If you still don't find anything then you will have to go to active bug swatting. Some monitors have a feature that allows you to set a BREAKPOINT. This is a handy feature of a monitor but it is not necessary, only convenient. Make sure you pick a point in your program with an executable instruction to set a breakpoint at. Make a note of the location and put a zero byte (BRK instruction) at that location. Put several in your program at first so that you can get a better idea of where it is messing up. The best place to put a breakpoint is at a branch instruction. Branch instructions are always 2 bytes long and you don't need to keep the second byte if you use a BRK at that location. Then when the program halts you can look at the registers and the processor status for expected values, then continue the program either at the instruction following the branch or at the instruction to which the branch would go to if the branch condition is satisfied. All branch instructions use the processor status register bits to determine if a branch is to be taken or not. I keep the following written on the front of my monitor shelf in large black letters... NV-BDIZC. This is the meaning of the processor status register bits that are tested when a branch instruction is executed. They have the following meaning when a branch decision is made:
N (Negative) = '1' BMI will branch
= '0' BPL will branch
V (Overflow) = '1' BVS will branch
= '0' BVC will branch
Z (Zero) = '1' BEQ will branch
= '0' BNE will branch
C (Carry) = '1' BCS will branch
= '0' BCC will branch
D (Decimal mode) '1' decimal mode set
B (Break) '1' cause of an interrupt was BRK
I (Interrupt) '1' Interrupts are disabled
After setting the breakpoints then start the program in the normal way. When one of the BRK instructions is executed the monitor will display the instruction address counter and registers. Look at the address displayed to see which of your breakpoints was executed. The C128 monitor displays the location of the BRK plus 2 so remember to look 2 bytes earlier than the address shown. Now you can look at the registers and memory locations to figure out what is going on at that point in the program. Then continue the program with a G XXXX with the address of the next instruction or the target instruction of the branch. To reset a breakpoint use the monitor to change the byte you set to zero back to the byte that is in the listing.
Patching
There are two reasons for patching. One is to make temporary changes correcting an error in your program. I say temporary because you will want to go back and correct your source code and re- assemble the program to make the change permanent. If you leave the source alone and just make the patch then later when you add another feature or change your source code for another reason you will still have the bug in it. The second reason for patching is to insert temporary code into the program to help you find a bug. The principles are the same either way so I will just talk about the temporary debugging patch here.
Put in a piece of code in some spare memory that does something visible. I like to set the border color to different values at different major areas of code. Then you can see the different sections of code as they are executed. Also, if a program locks up the keyboard you can see if it is in a loop that flashes the border colors or you can tell what the last major section of code was that was executed before the lockup by what color the border is. Some other things that you can do in a patch are... increment a memory location each time the patch is executed; save a register value at a spare memory location; etc... This way you can leave tracks as your program runs. End your patch code with a duplicate of the code you will replace in the next step of the process and a RTS instruction.
After setting up the patch code then go into your program that has the problem and put a JSR to your patch code in place of some instruction. It is easiest to replace a 3 byte instruction with the JSR. Then you will only have to duplicate the one instruction in your patch and the RTS will return to the very next instruction in your listing. If you replace a one or two byte instruction in your program with the JSR then make sure you fix up the code following the JSR so that it is executable (put enough NOP instructions in to pad out the remainder of any messed up instructions). You should NEVER replace an RTS with the JSR. It will mess up memory in other routines.
Testing
Testing is a special variation on the above methods. Typically what you will do is to set a breakpoint at the end of a routine (replace an RTS with a BRK) first. Then from the monitor you set up all expected input values (registers, pointers, absolute memory locations, etc) to the values you want to test with. Pick values that represent limit values as well as nominal values. If a register can contain any value when the routine is entered then test it with 0, $ff, $80, $1 and some other value or values as appropriate for what the routine does. Do the same for any other memory locations used by the routine. After setting all the variables used to what you want to test with, do a G XXXX from the monitor to the start of the routine. When the routine completes you will exit to the monitor and can check for the expected values. You should have an idea ahead of time from the program design what the expected values are that it will return so that you can check to see if it worked correctly. Or if not, how the results differ. If you are using a C128 monitor then you don't need to replace the RTS with a BRK... use the monitor command J XXXX instead of G XXXX and it will return to the monitor when the RTS instruction is executed.
PART 2. Programmers' Workshop Conference, Feb 18, 1988, SYSOP JL
Debug Code
Using debug code in your source program is similar to patching methods except that it is preplanned and is easier to do in source code then to do while debugging a program. There are three ways that you can incorporate debug code into your source programs. Each is discussed below.
First you can use conditional assembly if your assembler has options that will allow it. Some assemblers have compiler directives (pseudo- ops) that can identify a block of code to be assembled if a condition is satisfied. For example, MAE-64 has pseudo-ops IFE, IFN, IFP, IFM and *** for this purpose. A debug code block coded for MAE might look like this...
debug .de $ff .
other statements.... .
ifn debug
php
pha
lda #2
sta border
pla
plp
***
program continues...
Now when you assemble the program you will have code built in that sets the screen border color to red when this section of your code is executed. When you are done debugging the program and are ready to try it full speed and without the debug options then it is a simple matter to change the equate "debug .de $ff" to be "debug .de 0" and reassemble the program. When you do that the debug code will not be assembled because the condition tested by the ifn is no longer true.
Similarly, Power Assembler from Spinnaker has conditional assembly using pseudo ops .IF, .ELSE and .IFE. If the label expression on the .IF statement is not zero then continue assembly, else skip to the .ELSE or .IFE and continue assembly. For inserting debug code you would probably not use the .ELSE statement. If your assembler does not have a conditional assembly option you can still do the same things except that you will have to do the operation of removing the debug code manually. Just code the debug code inline with your program and when you are done with the debugging you can simply comment out the statements that you no longer want to run. I recommend not deleting them from your source but just insert a ; as the first character on the line so that they will be treated as comments by your assembler. Then if you later have to re-insert the debug code you can just delete the ;'s from these lines and reassemble. When you code debug code inline this way, use comment lines to flag and highlight the debug code so that it is easy to find later by looking at the listing.
A third method is to code the debug code inline except put a JMP just ahead of the inline debug code that jumps around it. Then to turn on the debug code while you are actively debugging you can simply NOP out the JMP instruction and the debug code is activated. Again, in order to later clean up your program for release you should remove the debug code by commenting it out or deleting it. So highlight it in your source code so you can easily find it later.
Debugging Interrupt routines
Use debug code or patches. During the interrupt you cannot just break out of a routine to the monitor. You may get all kinds of problems. Patch code can be set up so that when you exit through it it redirects the interrupts back to the normal location and saves information... registers, interrupt flags, important memory locations, counters, etc. In setting up debug code for interrupt routines, use counters or save values in spare memory locations. It is better to allow the interrupt routines to run at near normal speed and just provide the minimum necessary tracks on where it is with debug code. This means that you will have to rely more on testing and code reading to find program bugs, but that is about the best you can do with interrupt routines. Code testing is probably the best method you have of finding the problems in interrupt driven routines. This of course depends on what the code is that you are debugging, but if it does any amount of data manipulation you should check out the program logic before trying it with live interrupts. For example, if you are doing split screen interrupts make sure by testing the routine first that it calculates the correct raster line to set up the next raster interrupt.
With regard to interrupt routine problems, perhaps one of the most common problems to watch out for is the read-modify-write problem. This problem occurs when you have a memory location that is being read, modified then re-written by the main part of your program and is also being read, modified and re-written in an interrupt routine. If you have a condition like this, then you will either have to disable interrupts in the main part of the program during the read-modify-write or you will have to set up a shadow location for the main program to use. What happens is that if the main program reads the location and then the interrupt hits, the main program will modify the original value that it retrieved prior to the interrupt and rewrite it, while the interrupt routine will have meanwhile changed the value. Another problem is timing... you have to make sure that the condition which causes the interrupt does not recur until you are completely done with handling the first occurrence of it and have returned to the main program. You may either miss interrupts or you may just continually operate in your interrupt routine and never get anything else done. On the 64 and 128 the timer or VIC chip interrupt registers must be read following an interrupt to reset them. If you don't reset the interrupt your interrupt handler will just run continuously. Every time it re- enables interrupts the next instruction will be interrupted, immediately restarting your routine. This means that a code sequence of CLI then RTI will result in the RTI being executed, but then immediately jumping back into the interrupt routine. Also, watch out to make sure you always restore the correct number of parameters off of the stack and in the correct order. Again, this is something you can check with testing by verifying that the SP register value is the same at the end of a routine as it was at the start.
Emulation
Use a machine language emulator to follow the program along. Some monitors have a step-by-step execution mode or a trace mode. This is very useful for following a program. Use it for assisting your code reading step in debugging. If your monitor does not have this feature then you can use a program like "6510 sim. rev", available in the Programmers' Workshop, Assemblers and Monitors library, to follow it along. A trace mode does a trace disassembly of the source code. Each time you step the trace it disassembles the next instruction. When you encounter a branch instruction you can select to follow the branch or you can select to continue with the next instruction. When you encounter a JSR you can select to continue with the next instruction that will be executed after returning or you can follow the subroutine. The 6510 sim. rev program is similar to a step-by-step execution with a monitor but it does it by EMULATING the instructions rather than executing them. It has its own simulated registers that it keeps track of rather than using the actual 6510 registers.
Symbolic Debuggers
Symbolic debuggers are the ultimate in convenience and speed for debugging machine language programs. Two examples are...
geoProgrammer -- $69.95 from Berkely Softworks
(415) 644-0980
PTD-6510 Symbolic Debugger -- $49.95 from Schnedler Systems
(704) 274-4646
Symbolic Debuggers are similar to the best monitors but have more features. They also read in the symbol table from your assembler source so that you can reference memory locations and code entry points by your source code labels. Breakpoints, reverse disassembly, break after N times, memory display in byte, word or ascii, and many other useful features make the symbolic debugger a powerful tool if you are going to be doing a lot of ML programming.
disclaimer
The above document is the sole work of the author and is for informational and educational purposes. It is intended as a review and should be used as such. I except no money, royalties, or gratuities for its contents. I also will not be liable for misuse or any damages, either direct or consequential, from use of any information found here. ALL INFO IS USE AT YOUR OWN RISK !!!