  Debugging Machine Language Programs

                       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 thouroughly
     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 displayes 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
     visable. 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 informaiton... 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 occurance 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 !!!

     ----------------------------------------------------------------------

   This page was created using...

   Hot Dog HTML Editor Version 1.0
   NeoPaint Version 3.1c
   Paint Shop Pro 3.1

     ----------------------------------------------------------------------

   Author Email:F.Yarra fyarra@juno.com - comments, suggestions are welcomed.

                     +-----------------------------------+
                     | What's New | About | Links | Home |
                     +-----------------------------------+

     ----------------------------------------------------------------------

      (c)1998 FYARRA
      Last modified April 8, 1998

