Optimized code, especially globally optimized code, is tougher to debug since the object program may not match the source code line for line. Also, some source-level debuggers won't even allow optimized code to be debugged, forcing us to retreat to our assemblylevel debugger for support.
The most common optimizer bug is caused by memory-mapped I/O. Consider an I/O device memory-mapped at address data_port. The function in Listing 4 waits for ready status, writes a sequence of characters to the device, then returns the new ready status to the caller.
View the full-size image
An optimizer could just have a ball with this. First, it might consider that the while loop was a redundant read of the same address and replace it with a single read of the location. Next, it might determine that there was nothing to do in the body of the while anyway and decide that the entire while statement wasn't even needed. It would probably assume that the A and C assigned to the address were dead stores and replace them with the single assignment *data_port = 'K'. As for the return, it might assume that the last thing written to the address, a K, should still be there and simply return K instead of reading the status. Now the code, if represented as C source, looks like this:
int sendack()
{
extern char *data_port;
*data_port = 'K';
return((int)('K'));
}
where *data_port is the address of the device.
Because optimizers may rearrange the code to minimize computation, the only safe way to avoid optimizer errors when dealing with memory-mapped I/O is to compile the driver with optimization turned off. This practice is usually safe but in special situations will cause bugs. When optimizing the function in Listing 5, for example, an optimizer might make two assumptions: that the ratio x/y could be done one time, and hence calculated before the loop, and that the multiply operation in the array index computation i*4 could be avoided if the loop were written differently. The optimizer may produce something equivalent to Listing 6.
View the full-size image
View the full-size image
The function now has two bugs! First, the loop will never terminate because i is an unsigned character and can't reach 256; second, a divide error will occur if y==0. Some compiler optimizers are smart enough to detect these situations and avoid producing code with these bugs.
SUGGESTIONS FOR WRITING BETTER C
The quickest way to debug a program is to write a program that has no bugs. Software that's modular and nicely layered will usually have fewer integration bugs. Here are some suggestions for producing code with a minimum of problems.
- Be extremely careful when using pointers; uninitialized-pointer bugs and boundary bugs can be very time consuming to correct.
- Look for typematch bugs by using lint or other utilities, or simply do a "paper debug" to check each function call for proper argument and return types.
- Be careful when using macros, especially those containing parameter substitutions. Capitalizing all macro names can serve as a reminder that they're macros, not function calls.
- Use parentheses liberally to guarantee associativity.
- Think about portability while writing code. If necessary, include a header file containing typedefs for basic types (BYTE, WORD, etc.), then use these instead of char and int. Programs can then be ported to another machine or compiler by modifying the typedefs in the header file.
- Use casting when converting types; don't expect the compiler to do it for you.
- Avoid global variables.
- Use header files for function prototyping and argument definition.
- Use return codes for modules that interface with each other.
- Above all, design the tests to exercise all branches in the program. Include tests for boundary conditions, especially for code using pointers. If possible, keep a test suite for future use, should the module ever be modified or ported to another environment.
Writing software is a complicated and tedious puzzle. Even with a concerted effort by the programmer, it's nearly impossible to produce a nontrivial program that's bug-free the first time. Knowing the potential causes of bugs allows us to adopt disciplines to minimize their occurrence and guides our efforts to stabilize, localize, and correct them.
Robin Knoke cofounded Applied Micro-systems Corp. When this article was written, he was involved in the specification and design of productivity tools for creating and debugging embedded systems software. He left AMC in 1990 and is now running the White Salmon Group, Inc. a small product-development company. ("Small and I like it that way," he says.). White Salmon Group (www.whitesalmongroup.com) designs embedded microprocessors for several clients--and Knoke still does a lot of C coding. You can reach him at knoke@whitesalmon.com.