Debugging Pep/6 Programs

Some CISC221 students, having begun to work on their second assignments may have begun to realize that debugging assembler level code is pretty tricky. Here are some hints.

  1. As in a high-level language, you can put in debugging 'print statements', in the form of DECO or HEXO or CHARO instructions. Unfortunately a single instruction like that doesn't do much, but you can put in several. Another possibility would be to add a little tiny Pep/6 debugging subroutine to your program, like this:
    debug:     CHARO c#/!/,i   ; debugging line
               CHARO c#/n/,i   ; show value of n
               CHARO c#/=/,i
               HEXO  n,d
             etc. 
             etc. putting in whichever debugging outputs you need
               CHARO d#13,i    ; newline at end of debugging line
               RTS

    and then you can just drop in JSR debug wherever you need a debugging output. If there are different debugging outputs you need, you could write different debugging subroutines. The subroutine doesn't have any parameters, so there is no need to worry about parameter passing conventions, just JSR and RTS! If the debugging print routine doesn't do anything except output, then it will have no effect on status registers or anything else in the state of the Pep/6 machine.

  2. In general, execution debugging requires knowing the contents of memory and of registers, at certain key points during execution. This can be done as above, but a more systematic way to examine the state of the computation is to use tracing with breakpoints. To trace a program is to examine the change of state of the machine as instructions are executed. There are basically two ways to do this with the Pep/6 emulator software:
    1. You can single-step through execution, which means that the emulator executes exactly one instruction; and then you can examine the contents of memory and registers, and then single-step again; or
    2. You can set breakpoints. A breakpoint is the address of an instruction, and if that instruction's about to be executed interruption occurs. When interruption has occurred you can exmaine the contents of registers and memory; and you can continue the execution, or single step, from that point.

    When interruption takes place, you want to know what's in the registers and what's in memory. You ought know what your algorithm is supposed to be doing, because you have to be able to compute (in your head or on paper) the expected bit patterns, or hex, or decimal, or character values in each register and in the memory locations you're interested in.

    Good places to put breakpoints are:

    1. At the end of variable initialization
    2. At the top of a loop
    3. At the end of a complicated sequence of if-then-else's
    4. Just before a JSR (to check that the stack has been loaded properly)

    The placement of breakpoints is greatly eased by using a consistent and structured programming style. Addresses for breakpoints and data (variables) can be obtained from the Assembler Listing.

  3. In days gone by, the system would give you a complete print-out of the whole of memory, in hexadecimal, when an unexpected error occurred. This was called a 'dump'. Reading dump files was a skill assembler programmers cultivated! Fortunately we have debugger tools these days. A dump of say 100K words might be readable, but how about 100Mb?