Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Professional C++ [eng].pdf
Скачиваний:
1715
Добавлен:
16.08.2013
Размер:
11.09 Mб
Скачать

Chapter 20

Bug Taxonomies

A bug in a computer program is incorrect run-time behavior. This undesirable behavior includes both catastrophic bugs that cause program death, data corruption, operating system panics, or some other similarly horrific outcome and noncatastrophic bugs that cause the program to behave incorrectly in more subtle ways. For example, a Web browser might return the wrong Web page, or a spreadsheet application might calculate the standard deviation of a column incorrectly. The underlying cause, or root cause, of the bug is the mistake in the program that causes this incorrect behavior. The process of debugging a program includes both determining the root cause of the bug and fixing the code so that the bug will not occur again.

Programmers often use the term root-cause as a verb, as in “Have you root-caused that core dump yet?”

Avoiding Bugs

The powerful features of C++ make it an especially error-prone language, so debugging skills are even more important when coding in C++ than when using most other languages. Here are a few tips for avoiding bugs in your programs:

Read this book from cover to cover. Learn the C++ language intimately, especially pointers and memory management. Then, recommend this book to your friends and coworkers so they avoid bugs too!

Follow the style guidelines in this book, specifically those described in Chapter 7. They will lead to fewer bugs because you, and other people, will be able to understand your programs.

Design before you code. Designing while you code tends to lead to convoluted designs that are harder to understand and are more error-prone. It also makes you more likely to omit possible edge cases and error conditions.

Utilize code reviews: At least two people should look at every line of code that you write. Sometimes it takes a fresh perspective to notice problems.

Test, test, and test again. Follow the guidelines in Chapter 19.

Expect error conditions, and handle them appropriately. In particular, plan for and handle out- of-memory conditions. They will occur. See Chapter 15.

Last, and probably most importantly, use smart pointers to avoid memory leaks. See Chapters 13, 15, and 25 for details.

Planning for Bugs

Your programs should contain features that enable easier debugging when the inevitable bugs arise. This section describes these features and presents sample implementations that you can incorporate into your own programs.

Error Logging

Imagine this scenario: You have just released a new version of your flagship product, and one of the first users reports that the program “stopped working.” You attempt to pry more information from the user,

528

Conquering Debugging

and eventually discover that the program died in the middle of an operation. The user can’t quite remember what he was doing, or if there were any error messages. How will you debug this problem?

Now imagine the same scenario, but in addition to the limited information from the user, you are also able to examine the error log on the user’s computer. In the log you see a message from your program that says “Error: unable to allocate memory.” Looking at the code near the spot where that error message was generated, you find a line in which you dereferenced a pointer without checking for NULL.

You’ve found the root cause of your bug!

Error logging is the process of writing error messages to persistent storage so that they will be available following an application, or even machine, death. Despite the example scenario, you might still have doubts about this strategy. Won’t it be obvious by your program’s behavior if it encounters errors? Won’t the user notice if something goes wrong? As the preceding example shows, user reports are not always accurate or complete. In addition, many programs, such as the operating system kernel and long-running daemons like inetd or syslogd on Unix, are not interactive and run unattended on a machine. The only way these programs can communicate with users is through error logging.

Thus, your program should log errors as it encounters them. That way, if a user reports a bug, you will be able to examine the log files on the machine to see if your program reported any errors prior to encountering the bug. Unfortunately, error logging is platform dependent: C++ does not contain a standard logging mechanism. Examples of platform-specific logging mechanisms include the syslog facility in Unix and the event reporting API in Windows. You should consult the documentation for your development platform. There are also some open-source implementations of cross-platform logging classes, including log4cpp (available at http://sourceforge.net).

Now that you’re convinced that error logging is a great feature to add to your programs, you might be tempted to log error messages every few lines in your code, so that, in the event of any bug, you’ll be able to trace the code path that was executing. These types of error messages are appropriately called “traces.” However, you should not write these traces to error logs for two reasons. First, writing to persistent storage is slow. Even on systems that write the logs asynchronously, logging that much information will slow down your program. Second, and most importantly, most of the information that you would put in your traces is not appropriate for the end user to see. It will just confuse the user, leading to unwarranted service calls. That said, tracing is an important debugging technique under the correct circumstances, as described in the next section.

Here are some specific guidelines for the types of errors your programs should log:

Unrecoverable errors, such as an inability to allocate memory or a system call failing unexpectedly. These errors will usually directly precede an application exit or memory core dump.

Errors for which an administrator can take action, such as low memory, an incorrectly formatted data file, an inability to write to disk, or a network connection being down.

Unexpected errors such as a code path that you never expected to take or variables with unexpected values. Note that your code should “expect” users to enter invalid data and should handle it appropriately. An unexpected error would represent a bug in your program.

Security breaches such as a network connection attempted from an unauthorized address or too many network connections attempted (denial of service).

Additionally, most APIs allow you to specify a log level or error level. You can log nonerror conditions under a log level that is less severe than “error.” For example, you might want to log significant state

529

Chapter 20

changes in your application, or startup and shutdown of the program. You also might consider giving your users a way to adjust the log level of your program at run time so that they can customize the amount of logging that occurs.

Debug Traces

When debugging complicated problems, public error messages generally do not contain enough information. You often need a complete trace of the code path taken or values of variables before the bug showed up. In addition to basic messages, it’s sometimes helpful to include the following information in debug traces:

The thread ID, if a multithreaded program.

The name of the function that generated the trace.

The name of the source file in which the code that generates this trace lives.

You can add this tracing to your program through a special debug mode, or via a ring buffer. Both of these methods are explained in detail below.

Debug Mode

The first technique to add debug traces is to provide a debug mode for your program. In debug mode, the program writes trace output to standard error or to a file, and perhaps does extra checking during execution. There are several ways to add a debug mode to your program.

Compile-Time Debug Mode

You can use preprocessor #ifdefs to selectively compile the debug code into your program. The advantage of this method is that your debug code is not compiled into the “release” binary, and so does not increase its size. The disadvantages are that there is no way to enable debugging at a customer site for testing or following the discovery of a bug, and your code starts to look cluttered and indecipherable.

The rest of this section shows an example of a simple program instrumented with a compile-time debug mode. This program doesn’t do anything useful: it is only for demonstrating the technique.

In order to generate a debug version of the program, it should be compiled with the symbol DEBUG_MODE defined. Your compiler should allow you to specify symbols to define during compilation; consult your documentation for details. For example, g++ allows you to specify –Dsymbol on the compile command.

Note that this example uses a global variable for the ofstream object. This example is one of the only times that we recommend using global variables! It’s acceptable here because debug mode should not interfere with the rest of the program. If the ofstream object were not global, you would have to pass it to each function, requiring changes to all the function prototypes in the whole program.

// CTDebug.cpp #include <exception> #include <fstream> #include <iostream> using namespace std;

530

Conquering Debugging

#ifdef DEBUG_MODE ofstream debugOstr;

const char* debugFileName = “debugfile.out”; #endif

class ComplicatedClass

{

public: ComplicatedClass() {}

// Class details omitted for brevity

};

class UserCommand

{

public: UserCommand() {}

// Class details not shown for brevity

};

ostream& operator<<(ostream& ostr, const ComplicatedClass& src); ostream& operator<<(ostream& ostr, const UserCommand& src); UserCommand getNextCommand(ComplicatedClass* obj);

void processUserCommand(UserCommand& cmd);

void trickyFunction(ComplicatedClass* obj) throw(exception);

int main(int argc, char** argv)

{

#ifdef DEBUG_MODE

//Open the output stream. debugOstr.open(debugFileName); if (debugOstr.fail()) {

cout << “Unable to open debug file!\n”; return (1);

}

//Print the command-line arguments to the trace. for (int i = 0; i < argc; i++) {

debugOstr << argv[i] << “ “; debugOstr << endl;

}

#endif

// Rest of the function not shown return (0);

}

ostream& operator<<(ostream& ostr, const ComplicatedClass& src)

{

ostr << “ComplicatedClass”; return (ostr);

}

531

Chapter 20

ostream& operator<<(ostream& ostr, const UserCommand& src)

{

ostr << “UserCommand”; return (ostr);

}

UserCommand getNextCommand(ComplicatedClass* obj)

{

UserCommand cmd; return (cmd);

}

void processUserCommand(UserCommand& cmd)

{

// Details omitted for brevity

}

void trickyFunction(ComplicatedClass* obj) throw(exception)

{

#ifdef DEBUG_MODE

// If in debug mode, print the values with which this function starts debugOstr << “trickyFunction(): given argument: “ << *obj << endl;

#endif

while (true) {

UserCommand cmd = getNextCommand(obj);

#ifdef DEBUG_MODE

debugOstr << “trickyFunction(): retrieved cmd “ << cmd << endl;

#endif

try { processUserCommand(cmd);

} catch (exception& e) { #ifdef DEBUG_MODE

debugOstr << “trickyFunction(): “

<<“ received exception from procesUserCommand(): “

<<e.what() << endl;

#endif

throw;

}

}

}

Start-Time Debug Mode

Start-time debug mode is an alternative to #ifdefs that is just as simple to implement. A command-line argument to the program can specify whether it should run in debug mode. Unlike compile-time debug mode, this strategy includes the debug code in the “release” binary, and allows debug mode to be enabled at a customer site. However, it still requires users to restart the program in order to run it in debug mode, which is not always an attractive alternative for customers, and which may prevent you from obtaining useful information about bugs.

532

Conquering Debugging

The following example of start-time debug mode uses the same program as that shown for compile-time debug mode so that you can directly compare the differences. This version of the program again uses global variables: this time for the ofstream and the Boolean specifying whether the program is in debug mode. This choice is acceptable here to avoid imposing extra debug arguments on all the function prototypes.

One aspect of this program needs further comment: there is no standard library functionality in C++ for parsing command-line arguments. This program uses a simple function isDebugSet() to check for the debug flag among all the command-line arguments, but a function to parse all command-line arguments would need to be more sophisticated.

// STDebug.cpp #include <exception> #include <fstream> #include <iostream> using namespace std;

ofstream debugOstr; bool debug = false;

const char* debugFileName = “debugfile.out”;

class ComplicatedClass

{

public: ComplicatedClass() {} ~ComplicatedClass() {}

};

class UserCommand

{

public: UserCommand() {}

};

bool isDebugSet(int argc, char** argv);

ostream& operator<<(ostream& ostr, const ComplicatedClass& src); ostream& operator<<(ostream& ostr, const UserCommand& src); UserCommand getNextCommand(ComplicatedClass* obj);

void processUserCommand(UserCommand& cmd);

void trickyFunction(ComplicatedClass* obj) throw(exception);

int main(int argc, char** argv)

{

debug = isDebugSet(argc, argv);

if (debug) {

//Open the output stream. debugOstr.open(debugFileName); if (debugOstr.fail()) {

cout << “Unable to open debug file!\n”; return (1);

}

//Print the command-line arguments.

533

Chapter 20

for (int i = 0; i < argc; i++) { debugOstr << argv[i] << “ “; debugOstr << endl;

}

}

// Rest of the function not shown return (0);

}

bool isDebugSet(int argc, char** argv)

{

for (int i = 0; i < argc; i++) {

if (strcmp(argv[i], “-d”) == 0) { return (true);

}

}

return (false);

}

ostream& operator<<(ostream& ostr, const ComplicatedClass& src)

{

ostr << “ComplicatedClass”; return (ostr);

}

ostream& operator<<(ostream& ostr, const UserCommand& src)

{

ostr << “UserCommand”; return (ostr);

}

UserCommand getNextCommand(ComplicatedClass* obj)

{

UserCommand cmd; return (cmd);

}

void processUserCommand(UserCommand& cmd)

{

// Details omitted for brevity

}

void trickyFunction(ComplicatedClass* obj) throw(exception)

{

if (debug) {

// If in debug mode, print the values with which this function starts debugOstr << “trickyFunction(): given argument: “ << *obj << endl;

}

while (true) {

UserCommand cmd = getNextCommand(obj); if (debug) {

debugOstr << “trickyFunction(): retrieved cmd “ << cmd << endl;

}

534

Conquering Debugging

try { processUserCommand(cmd);

} catch (exception& e) { if (debug) {

debugOstr << “trickyFunction(): “

<<“ received exception from procesUserCommand(): “

<<e.what() << endl;

}

throw;

}

}

}

Run-Time Debug Mode

The most flexible way to provide a debug mode is to allow it to be enabled or disabled at run time. One way to provide this feature is to supply an asynchronous interface that controls debug mode on the fly. In a GUI program, this interface could take the form of a menu command. In a CLI program, this interface could be an asynchronous command that makes an interprocess call into the program (using sockets, signals, or remote procedure calls for example). C++ provides no standard way to perform interprocess communication or GUIs, so we do not show an example of this technique.

Ring Buffers

Debug mode is useful for debugging reproducible problems and for running tests. However, bugs often appear when the program is running in nondebug mode, and by the time you or the customer enables debug mode, it is too late to gain any information about the bug. One solution to this problem is to enable tracing in your program at all times. You usually need only the most recent traces to debug a program, so you should store only the most recent traces at any point in a program’s execution. One way to provide this limitation is through careful use of log file rotations.

However, in order to avoid the problems with logging traces described earlier in the “Error Logging” section, it is better if your program doesn’t log these traces at all; it should store them in memory. Then, it should provide a mechanism to dump all the trace messages to standard error or to a log file if the need arises. A common technique is to use a ring buffer to store a fixed number of messages, or messages in a fixed amount of memory. When the buffer fills up, it starts writing messages at the beginning of the buffer again, overwriting the older messages. This cycle can repeat indefinitely. The following sections provide an implementation of a ring buffer and show you how you can use it in your programs.

Ring Buffer Interface

#include <vector> #include <string> #include <fstream>

using std::string; using std::vector; using std::ostream;

//

//class RingBuffer

//Provides a simple debug buffer. The client specifies the number

//of entries in the constructor and adds messages with the addEntry()

535

Chapter 20

//method. Once the number of entries exceeds the number allowed, new

//entries overwrite the oldest entries in the buffer.

//

//The buffer also provides the option to print entries as they

//are added to the buffer. The client can specify an output stream

//in the constructor, and can reset it with the setOutput() method.

//Finally, the buffer supports streaming to an output stream.

//

class RingBuffer

{

public:

//

//Constructs a ring buffer with space for numEntries.

//Entries are written to *ostr as they are queued.

RingBuffer(int numEntries = kDefaultNumEntries, ostream* ostr = NULL); ~RingBuffer();

//

//Adds the string to the ring buffer, possibly overwriting the

//oldest string in the buffer (if the buffer is full).

//

void addEntry(const string& entry);

//

// Streams the buffer entries, separated by newlines, to ostr.

//

friend ostream& operator<<(ostream& ostr, const RingBuffer& rb);

//

//Sets the output stream to which entries are streamed as they are added.

//Returns the old output stream.

//

ostream* setOutput(ostream* newOstr);

protected:

vector<string> mEntries; ostream* mOstr;

int mNumEntries, mNext; bool mWrapped;

static const int kDefaultNumEntries = 500;

private:

// Prevent assignment and pass-by-value. RingBuffer(const RingBuffer& src); RingBuffer& operator=(const RingBuffer& rhs);

};

Ring Buffer Implementation

This implementation of the ring buffer stores a fixed number of strings. Each of these strings must be copied into the ring buffer, requiring dynamic allocation of memory. This approach certainly is not the

536

Conquering Debugging

most efficient solution. Other possibilities would be to provide a fixed number of bytes of memory for the buffer. However, that requires mucking with low-level C-strings and memory management, which you should avoid whenever possible. This implementation should be sufficient unless you’re writing a high-performance application.

This ring buffer uses the STL vector to store the string entries. You could also use a standard C-style array. The use of the STL is straightforward except for the implementation of operator<< for the RingBuffer, which employs some fancy iterators. Consult Chapters 21, 22, and 23 for the details of iterators and the copy algorithm.

#include <algorithm> #include <iterator> #include <iostream> #include “RingBuffer.h”

using namespace std;

const int RingBuffer::kDefaultNumEntries;

//

//Initialize the vector to hold exactly numEntries. The vector size

//does not need to change during the lifetime of the object.

//

// Initialize the other members.

//

RingBuffer::RingBuffer(int numEntries, ostream* ostr) : mEntries(numEntries), mOstr(ostr), mNumEntries(numEntries), mNext(0), mWrapped(false)

{

}

RingBuffer::~RingBuffer()

{

}

//

//The algorithm is pretty simple: add the entry to the next

//free spot, then reset mNext to indicate the free spot after

//that. If mNext reaches the end of the vector, it starts over at 0.

//The buffer needs to know if the buffer has wrapped or not so

//that it knows whether to print the entries past mNext in operator<<

void RingBuffer::addEntry(const string& entry)

{

//Add the entry to the next free spot and increment

//mNext to point to the free spot after that.

mEntries[mNext++] = entry;

//Check if we’ve reached the end of the buffer. If so, we need to wrap. if (mNext >= mNumEntries) {

mNext = 0; mWrapped = true;

}

//If there is a valid ostream, write this entry to it.

537

Chapter 20

if (mOstr != NULL) {

*mOstr << entry << endl;

}

}

ostream* RingBuffer::setOutput(ostream* newOstr)

{

ostream* ret = mOstr; mOstr = newOstr; return (ret);

}

//

// This function uses an ostream_iterator to “copy” entries directly

//from the vector to the output stream.

//This function must print the entries in order. If the buffer has wrapped,

//the earliest entry is one past the most recent entry, which is the entry

//indicated by mNext. So first print from entry mNext to the end.

//

// Then (even if the buffer hasn’t wrapped) print from the beginning to mNext - 1.

//

ostream& operator<<(ostream& ostr, const RingBuffer& rb)

{

if (rb.mWrapped) {

//

//If the buffer has wrapped, print the elements from

//the earliest entry to the end.

//

copy (rb.mEntries.begin() + rb.mNext, rb.mEntries.end(), ostream_iterator<string>(ostr, “\n”));

}

//

//Now print up to the most recent entry.

//Go up to begin() + mNext because the range is not inclusive on the

//right side.

//

copy (rb.mEntries.begin(), rb.mEntries.begin() + rb.mNext, ostream_iterator<string>(ostr, “\n”));

return (ostr);

}

Using the Ring Buffer

In order to use the ring buffer, you can simply declare an object and start adding messages to it. When you want to print the buffer, just use operator<< to print it to the appropriate ostream. Here is the earlier start-time debug mode program modified to show use of a ring buffer instead:

#include “RingBuffer.h” #include <exception> #include <fstream> #include <iostream> #include <cassert>

538

Conquering Debugging

#include <sstream> using namespace std;

RingBuffer debugBuf;

class ComplicatedClass

{

public: ComplicatedClass() {} ~ComplicatedClass() {}

};

class UserCommand

{

public: UserCommand() {}

};

ostream& operator<<(ostream& ostr, const ComplicatedClass& src); ostream& operator<<(ostream& ostr, const UserCommand& src); UserCommand getNextCommand(ComplicatedClass* obj);

void processUserCommand(UserCommand& cmd);

void trickyFunction(ComplicatedClass* obj) throw(exception);

int main(int argc, char** argv)

{

// Print the command-line arguments. for (int i = 0; i < argc; i++) {

debugBuf.addEntry(argv[i]);

}

trickyFunction(new ComplicatedClass());

// Print the current contents of the debug buffer to cout.

cout << debugBuf;

return (0);

}

ostream& operator<<(ostream& ostr, const ComplicatedClass& src)

{

ostr << “ComplicatedClass”; return (ostr);

}

ostream& operator<<(ostream& ostr, const UserCommand& src)

{

ostr << “UserCommand”; return (ostr);

}

UserCommand getNextCommand(ComplicatedClass* obj)

{

UserCommand cmd; return (cmd);

}

539