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