
Debugging Amiga Software

by Carolyn Scheppner

This article presents some general techniques for debugging software on the 
Amiga.  Before you start programming the Amiga, it's a good idea to read the 
development guidelines in the introductions of the the Addison-Wesley Hardware 
Manual (ISBN 0-201-18157-6) and ROM Kernel Reference Manual: Libraries and 
Devices (ISBN 0-201-18187-8).  These guidelines contain important rules 
which are applicable to all Amiga programs, configurations, and operating 
system releases.  Additional information can be found in the Troubleshooting 
Guide published in Amiga Mail and in the Libraries and Devices manual.  
These documents cover the most common Amiga programming problems.

Preventing Bugs

The best way to debug software is to prevent bugs in the first place.  
Accordingly, here are seven basic rules you should always follow when 
writing Amiga software:

1. Use Enforcer abd Mungwall while developing your code.

2. Read the latest autodocs and the include file comments for the functions 
  and structures you are using.

3. Always check return values from system functions.  Provide a clean way 
  out and useful messages if something fails!   Assembler programmers -
  remember to TST.L D0 after system calls, before branching on condition codes.

4. C Programmers - Use function prototypes for system functions and your own
  functions.  It's a little extra work but will save you time in the long run 
  by immediately catching most types of improper function calls (missing
  arguments, swapped args, etc.)

5. Keep a version number in your code and update the version number whenever
  changes are made. The 2.0 VERSION command can print out the version
  of any executable which contains a specially formatted version string:

   In C:UBYTE *vers="\0$VER: programname 36.10";
   In Asm:vers DC.B 0,'$VER: programname 36.10',0


6. Document the code changes for each version.  This can be done manually
   or by using a document control system such as RCS.

7. Test your code!  Test on different configurations, under low memory
   and error conditions, and in conjunction with various watchdog tools.
   Test your product with MungWall, if possible in conjunction with Enforcer
   to catch uses of null pointers and freed memory.


Finding and Fixing Bugs

It is hard to generalize about debugging because different kinds of bugs 
often require very different approaches.  A bug report from a user is quite 
different from a bug that you've just introduced in new code!  However, all 
debugging requires some common steps:

1. Define the problem
2. Narrow the search and find the bug
3. Understand, and fix the bug
4. Make sure you didn't just break something else

Steps 3 and 4 are the same for all types of bugs, so we'll cover those last.  
Steps 1 and 2 require different approaches for different kinds of bugs.  
Here are some examples.

You've added or written new code and something is broken.

1. Define the problem.  Make sure you can reproduce the problem so you'll 
know when it's gone.  Define as "when I do xxx the program does (or doesn't 
do) do yyy."

2. Narrow the Search.  If you just added a couple of lines of code, and have 
the same development environment as before, check your source code first.  
Check for misuse of existing variables, improper error checking, improper 
use of system or internal functions, and possible changes to conditional 
program flow.

If you can't spot the problem, it's time to slow it down and see what's going 
on.  Use a source-level or symbolic debugger, or print/kprint/dprint 
debugging, with delays added if necessary.  One particularly useful type of 
debugging statement is:

printf("About to do xxx.  k =%ld Ptr1=$%lx...\n",k,Ptr1); 
Delay(50);

The delay gives the debugging line time to be output and gives you a chance 
to read it before the action is taken.  See mydebug.h on the 1991 DevCon 
disks for easy ways to add conditional debugging statements like this to 
your code.

By stepping through or printing out your actions and variables, you will 
generally be able to isolate the bug.  If you have isolated the area but 
still can't find the bug, re-read the autodocs for the routines you are 
using.  Check the Troubleshooting Guide in the Addison-Wesley Libraries and 
Devices manual.  Check all other uses of the variables in the problem area.  
If all else fails, isolate the problem code by writing the smallest possible 
example that demonstrates the problem.

If the problem is not present in the smallest possible example, then go back 
and check your code.  If the problem is still present, contact CATS for 
assistance or upload the example to BIX (note - one of the quickest ways to 
find bugs in a small source code example is to upload the source to BIX 
amiga.dev/main and ask what's wrong with it).


B. Your code has intermittent problems that you can't pin down, or appears to 
trash something under certain conditions.

1. Define the problem.  It is difficult to reproduce intermittent problems, 
so try to force the problem to show itself.  First try running your program 
with Enforcer and MungWall.  If you don't have an MMU, 
use WatchMem and MungWall, but be prepared to crash a lot.  If you don't get 
any hits, try the same thing during low-memory situations, heavy multitasking 
and device IO, etc.  If you are doing Exec device IO, try IO_Torture to catch 
premature reuse of IORequests.  Hopefully, you will pick up a hit.

2. Narrow the Search.  If you have no MungWall/Enforcer hits, try some 
debugging statements or source-level debugging to follow the values of your 
variables.  Use TStat to see if your stack usage is high.  Check all possible 
areas where you might be overwriting the end of an array or otherwise 
trashing memory.  Re-read the autodocs for the system functions you are using.

If you are reusing an IORequest too soon, check your source code (debugging 
would just slow down your execution and might give the IORequest a chance to 
complete, masking the problem).

If you have Enforcer hits, use debugging statements or a debugger to step 
through your code WHILE running MungWall and Enforcer (or WatchMem).
This will allow you to pinpoint where the problem occurs.



C. Your code works fine on one system but not on another.  Or you've received 
a bug report from a user.

1. Define the problem.  First find out the exact configuration of the system 
the problem occurred on.  Important elements include memory configuration and 
addresses, amount of free Chip and Fast RAM, processor type, custom chip 
version, expansion peripherals, OS version, and other software in use when 
the problem occurred.  The Config program on the 1991 DevCon disks is useful 
for printing out much of this information.

The memory address ranges can be particularly important now that machines are 
available with memory beyond the 24 bit address limit.  For example, 
overwriting a byte array by one byte now has a good chance of trashing a 
32-bit address variable, or even your routine's return address on the stack.

If a user reports the problem, find out the exact version of your software 
they are running, how they launched the program, and what their stack is set 
to (if launched from CLI).  Try to get them to reproduce the problem in a 
known environment (ie. after booting with a release Workbench diskette).  
Get their phone number and keep it with a record of all of the information 
you can get on the problem.  Keep bug reports in an organized form.  If you 
get two reports on the same problem, you can be pretty sure that the problem 
really exists, and the combined information may help you track it down.

2. Narrow the Search.  Attempt to reproduce the problem.  If you can't 
reproduce it immediately, try stepping through the problem area while using 
Enforcer and MungWall.  If you don't get any hits, try again with less 
memory available and other tasks running.  Try to reproduce the user's 
configuration and environment.  If you still can not reproduce the problem, 
ask the user to come up with a simple repeatable sequence which causes the 
problem on a system booted with a normal release Workbench disk.

Read the Troubleshooting guide in the Addison-Wesley Libraries and Devices 
manual or Amiga Mail Technotes for information on the causes for many 
problems that only show up in certain configurations or environments.

If all else fails, look carefully at your code for misuse of variables or 
system functions, and for improper error-checking or cleanup after any 
allocation or open.  Check that all cleanups are done in the proper order.




D. Your program loses memory.

1. Define the problem.  First make sure that you are actually losing memory.  
Use Flush (from the 1991 DevCon disks) and Avail to check for actual memory 
loss.

Set up your system so you have a shell window available and can start your 
program without moving any windows (re-arranging windows causes memory 
fluctuations).  Test for memory loss as follows.  First, try Flush and Avail 
a few times to make sure nothing else in your system is causing memory to 
fluctuate.  Then perform the following steps.

1. FLUSH
2. AVAIL  (write down the Fast, Chip, and total memory free)
3. Start your program and use its features
4. Exit your program
5. FLUSH
6. AVAIL  (compare the fast, chip, and total free to previous figures)
7. If you have a loss, go back to step 2.

Testing such as shown above will flush out all properly closed devices,
libraries, and fonts which have been loaded from disk by your program and
other programs.  This allows you to check for actual memory loss.

Note that under 2.0, since the audio.device is ROM-resident but
not initialized by the system until it is opened by someone, the
first program to use the audio device or speech capabilities will
appear to cause a small but permanent memory loss.  This is the
memory allocated for the audio device's base structures.  If your program
uses audio or speech, first use the SAY program or SPEECH: before performing
the above memory loss test so that the audio device's initial memory
usage will not interfere with your tests.

One special memory loss problems is a continual loss of memory while
a program is running.  This is generally caused by not keeping up
with IntuiMessages, or not freeing Locks.

 2. Narrow the search.  Try the above test again, but this time just start 
your program and exit immediately.  If you do not lose memory, try several 
times more, using some of your program's features, and attempt to determine 
which part of your program causes the memory loss.  Check your source code 
for all opens and allocations and check for matching frees and closes, in 
the proper order, for each of them.



The size of a memory loss can also be a clue to the cause.  For example, a 
loss of exactly 24 bytes is probably a Lock() which has not been UnLock()'d.  
Knowing the exact size of the loss (as determined with Flush and Avail) is 
important when you try determine which allocation is not being freed.
 
Some additional tools on the 1991 DevCon disks can help determine where 
memory losses occur. You can use MemMon to record the relative memory usage 
as you test various parts of your program.  Snoop can be used to record all 
memory allocations and frees on a remote terminal, after which SnoopStrip 
can strip out all matching pairs.  MungWall contains an enhanced snoop
option for tracking only the memory allocations of a particular task.
MemList, which outputs the system memory list, can also be useful when
debugging memory loss and fragmentation.

The Wedge program, which can restrict its reporting to the function calls 
made by a single task or list of tasks, can also be used to monitor the 
allocations and frees done by your task.  By inserting debugging statements, 
you can mix status messages  ("About to do xxx") with Wedge or MungWall
SNOOP output. Examine the output for an allocation which matches the size
of your loss.  Use LVO's WEDGELINE option to generate command lines
for Wedge.


Removing Bugs

I mentioned steps 3 and 4 earlier.  This is the easy part (finding the bug is 
the hard part).  These steps are the same for most debugging problems. 

3. Understand, and Fix the Bug.  When you find the bug, make sure you 
understand it.  Don't just try something else.  If you are having a problem 
with a system routine, read the autodocs and chapter text for that routine.  
Check the Troubleshooting Guide in the 1.3 Addison- Wesley Libraries and 
Devices manual.  

When you understand what is wrong, fix the problem, being especially careful 
not to affect the behavior of any other parts of your program.  Carefully 
document the changes that you make and bump the revision number of the 
program.  Note your changes in the initial comments of the program, and in 
the area where the changes were made.


4. Make Sure You Didn't Break Anything New.  Try to reproduce the problem 
several times and make sure it is gone.  Thoroughly test the rest of your 
program and make sure that nothing else has been broken by your fix.  Test 
your program in combination with watchdog tools such as MungWall and Enforcer.




Debugging Tools

1. Software Watchdog and Stressing Tools

The MMU-based Amiga debugging tool "Enforcer" provides debugging and quality 
assurance capabilities far beyond what was previously possible.  It is now 
possible to find bugs even in code that appears to be working perfectly - 
the kinds of bugs that could cause serious problems on different 
configurations.  Enforcer is able to trap improper low memory accesses, 
writes to ROM, and accesses of non-existent memory - problems which are 
generally caused by use of freed or improperly initialized pointers or 
structures.

When used in conjunction with a free memory invalidation tool such
as MungWall, additional illegal memory uses are forced out into
areas trappable by Enforcer.

Another extremely useful testing tool, especially for assembler programmers,
is "Scratch" by Bill Hawes.  One of the most surprising compatibility
problems we have seen is improper use or dependence on scratch registers
(D1,A0,A1) after a system call.  Scratch allows you to invalidate the
contents of these scratch registers after system calls so that improper
usage of these registers in your code may be brought out.

It is also useful to test your software with stressing tools such
as EatMem, Memoration, and EatCycles, in conjunction with Enforcer
because Enforcer will help to catch use of unsuccessful allocations
immediately.

All software should be tested with these tools during development, and 
should be required to pass a test with Enforcer in conjunction with 
MungWall, Scratcher, and IO_Torture before being released and distributed.


2. Symbolic and source-level debuggers

Symbolic debuggers allow you to trace and single step through your code, and 
examine or change your variables and structures.  The source-level debuggers 
which are provided with some compilers allow you to trace and single step 
your code at the source-level after compiling with special flags.  Debuggers 
can often be used in combination with other tools such as MungWall and
Enforcer to detect exactly where a problem is occurring.

3. Printf() and kprintf() / dprintf() debugging

This simple method of debugging allows you to monitor where you are, what 
your variables contain, and anything else you care to print out.  Printf 
debugging is suitable for any process code that is not in a Forbid or 
Disable (printf breaks a Forbid or Disable).  Kprintf (serial) and dprintf 
(parallel) debugging is more flexible and can be used in process, task, or 
interrupt code.  The kprintf function is provided in the debug.lib linker 
library.  The parallel version, dprintf, is provided in the ddebug.lib 
linker library.  See the debug.lib kprintf autodocs for more information on 
the types of formats handled by kprintf and dprintf.

Kprintf outputs to the serial port at whatever baud rate the port is 
currently set to.  Generally, kprintf is done at 9600 baud with a terminal, 
or another Amiga running a terminal package, connected to your serial port 
with a null modem serial cable.



However, it is possible to kprintf to yourself (ie. to a terminal package 
running on your own machine) if you have a modem attached to your serial 
port, and your terminal package set to the baud rate of your modem.  
Obviously, if the problem you are debugging causes you to crash, a remote 
terminal is a better choice.  The ASCII capture feature of your terminal 
package can be used to capture the kprintf debugging output for later 
examination.

Remote (kprintf/dprintf) debugging is extremely useful when combined with 
other remote debugging tools such as Enforcer and IO_Torture because your own 
debugging statements will be interspersed with the remote output of the 
other debugging tools, allowing you to track what your program is doing 
when problems occur.

Printf/kprintf/dprint debugging can be conditionally coded more conveniently 
by using an include file such as mydebug.h (see the DevCon disks).  Mydebug.h 
eliminates the need for messy #ifdef and #endif lines around your debugging 
statements by providing the conditional macros D(bug()), D2(bug()), and 
DQ(bug()) which take printf-style format strings and arguments in their 
inner parens.  One handy feature of these macros is that your debugging 
statements can be be quickly changed from printf's to kprintf's or dprintf's 
by simply setting a flag in mydebug.h and recompiling.

Example:  D(bug("I'm here now and a=%ld",a));

4. Other ways to debug low -level code

If you can't link with debug.lib, low level code can also be debugged by 
inserting visual or audio cues to let you know where you are.  DebTones.asm 
(in AmigaMail and on the 1991 DevCon disks) demonstrates a small audio tone 
macro suitable for debugging low level code.  Another common method is 
flashing the power LED (see togl_led.asm), or doing an Intuition DisplayBeep() 
to flash the screen.

5. Specialized debugging tools

A variety of specialized debugging tools are available for monitoring and 
debugging such things as system function calls, device IO, process status, 
memory usage, and software errors.  These tools can be used without 
recompiling your program and can provide valuable debugging information.  
See the list of tools accompanying this article in the DevCon notes.



How to Use MMU Watchdogs and Other Remote Debugging Tools

Remote Serial Debugging

Hardware Setup:  When hooking two Amigas together, use a straight RS-232 
cable with a null-modem adaptor, or use a null-modem cable.  When hooking 
an Amiga up to another type of computer or terminal, you may or may not need 
the null-modem (crossed lines) depending on whether the other machine's 
RS-232 port is designed to be basically a sender or a receiver.  Avoid 
connecting lines which are not directly related to RS-232 because different 
computers have various power supplies and grounds on these other lines.  My 
null modem debugging cable is wired as follows:

Amiga		Terminal
1		1
2		2
3		3
7		7
5, 6, 8		5, 6, 8
20		20

Software:  For remote debugging at 9600 baud, set the sending machine's 
Preferences to 9600 baud, and use a 9600 baud terminal or an Amiga running 
a 9600 baud terminal package (preferably with ASCII capture capability) as 
the receiving machine. Note that other baud rates can also be used for most 
serial debugging because normal serial kprintf's do not modify the serial 
SERPER register and are therefore output at the last baud rate your serial 
hardware was set to.  Test your setup by copying a small text file to SER: 
or try the ktest program from the 1900 DevCon disks.  The output should show 
up on the remote terminal.

Applications can output serial debugging statements by using kprintf from C 
or KPrintF from assembler and linking with amiga.lib and debug.lib.  See the 
debug.lib autodocs for more information.  Serial input functions are also 
available.  Also see the mydebug.h conditional debugging macros on the 1991 
DevCon disks.

Watchdog software setup:  Make sure your test machine is set to the same 
baud rate as the remote terminal you are connected to.  Turn on the ASCII 
capture of your remote terminal.


Serial Watchdog software setup:

For machines with 68030 or 68020+MMU:
[RUN] MungWall (removable with CTRL-C, or BREAK n if RUN)
[RUN] IO_Torture
Enforcer ON

For non-MMU machines    (warning - encourages bad software to crash!)
[RUN] MungWall (removable with CTRL-C, or BREAK n if RUN)
[RUN] IO_Torture
[RUN] WatchMem


Setup for Local Serial Debugging

Hardware:  If you have a modem attached to your serial port, it is possible 
to capture your own serial debugging output locally.  This setup can be 
useful as long as the problem you are debugging is not one which crashes 
the machine.

Software:  Run a terminal package at your modem's baud rate to capture the 
kprintfs.  You probably won't be able to test this setup by copying a file 
to SER: (since the terminal package probably has an exclusive open on the 
serial device).  Instead, use a small program like ktest (on the 1991 DevCon 
disks) to test your setup, or, if you already have an MMU watchdog installed, 
try an illegal memory accessor such as Lawbreaker.  Use the terminal package's 
ASCII capture feature to capture your debugging output.

Watchdog software setup:  Same as for remote serial debugging, but first 
start up a terminal package on the test machine, at the baud rate of the 
attached modem, with ASCII capture turned on.


Setup for Parallel Debugging

Hardware:  To set up for parallel debugging, attach a parallel printer to 
the Amiga's parallel port and turn the printer on.  Note - if no device is 
attached to the parallel port, parallel debugging statements will hang 
waiting for the port hardware.

Software:  Some debugging commands have options for parallel rather than 
serial output.  Examples include Enforcer.par, MungWall.par, IO_Torture.par,
and Wedge with the 'p' option.  Also, your can send your own debugging
statements to the parallel port by using dprintf from C, or DPutFmt from
assembler, and linking with ddebug.lib and amiga.lib.  On the 1991 DevCon
disks, see dtest.asm for an example of calling DPutFmt from assembler, and
mydebug.h for debugging macros which can use printf, kprintf, or dprintf.


Parallel Watchdog software setup:

For machines with 68030 or 68020+MMU:
[RUN] MungWall.par (removable with CTRL-C, or BREAK n if RUN)
[RUN] IO_Torture.par
Enforcer.par ON

For non-MMU machines    (warning - encourages bad software to crash!)
[RUN] MungWall.par (removable with CTRL-C, or BREAK n if RUN)
[RUN] IO_Torture.par
[RUN] WatchMem

