Section C.3. Debugging

C 3 Debugging

The compiler can locate and describe syntax errors. The linker can reveal the existence of inconsistencies among program components and give some help as to how to locate them. One of the most challenging aspects to using C++ is learning how to find and fix various kinds of run-time errors.

Run-time errors are logical errors that can exist in a program that is syntactically correct and contains no undefined objects or functions. Effective use of a debugger, a program specifically designed for tracking down runtime errors, can greatly reduce the amount of time spent dealing with these kinds of errors.

A debugger permits the stepwise execution of your code, as well as the inspection of object values. Since debuggers work with compiled code, the early versions could only be used by programmers who were familiar with assembly language. Modern debuggers are able to step concurrently through the compiled machine code and the original source code. The GNU family of developer tools includes gdb, the source-level GNU debugger, which we can use for C/C++ applications. gdb has been designed with a command-line interface that is quite powerful but not particularly user-friendly. Fortunately, there are several open-source graphical facades for gdb, one of which we will discuss below. Commercial C++ IDEs (e.g., Visual Studio) generally have built-in source-level debuggers.

C.3.1. Building a Debuggable Target

For gdb to work, debugging symbols must be built into the code at compile time. Otherwise, the machine instructions will not be mapped to locations in C++ source files. This is easily accomplished by using the appropriate command-line switch (-g) when invoking the compiler:

g++ -g filename.cpp

This often results in a significantly larger executable file. Generally, the growth is proportional to the size and complexity of the source code files. The expanded executable contains symbol table information that the debugger can use to find source code that corresponds to machine instructions. To get qmake to generate makefiles with the -g switch passed along to g++, add the following line to your qmake project file:

CONFIG += debug

When the Qt library has been built with debugging symbols, you can step through the Qt source code just as easily as your own code. You may need to build Qt with debugging symbols to debug certain programs that contain code directly called from the Qt library.

Building Qt With Debugging Symbols

In Win32, its a menu choice you can click on. On *nix platforms, after unpacking the source code tarball, pass a parameter to the configure script before building and your Qt library will be built with debug symbols.

./configure --enable-debug make make install

Exercise: Building a Debuggable Target

Compare the size of an executable file created with and without the CONFIG += debug line in the project file.

Make a mental note to try this again later with a more complex application.

C.3.2. gdb Quickstart

Imagine you are running a program and, for some mysterious reason, it crashes.

[lazarus] app> ./playlistmgr Segmentation fault [lazarus] app>

When your app aborts, or crashes, it is helpful to know (as quickly as possible) exactly where it happened. We can use gdb to locate the trouble spot quickly and easily.

[lazarus] app> gdb playlistmgr GNU gdb 6.3-debian Copyright 2004 Free Software Foundation, Inc. This GDB was configured as "i386-linux"...Using host libthread_db library "/lib/tls/libthread_db.so.1". (gdb) r[4] Starting program: ftgui/app/playlistmgr [Thread debugging using libthread_db enabled] [New Thread -1227622176 (LWP 17021)] Qt: gdb: -nograb added to command-line options. Use the -dograb option to enforce grabbing. This is a debug message Program received signal SIGSEGV, Segmentation fault. [Switching to Thread -1227622176 (LWP 17021)] 0xb7f03320 in FormDialog::createActions (this=0x80ae2a0) at formdialog.cpp:53 53 delete m_OkAction; (gdb)

[4] r is the command for "run."

gdb shows you not only the filename, and line number, but also the corresponding line in the source code. However, we still might want to get some context for this error. The command list shows you the surrounding source code for the current file:

(gdb) list 51 void FormDialog::createActions() { 52 53 delete m_OkAction; 54 delete m_CancelAction; 55 m_OkAction = new OkAction(m_Model, m_View); 56 m_CancelAction = new CancelAction(m_Model, m_View); 57 QHBoxLayout *buttons = new QHBoxLayout(0); (gdb)

The command where shows you the stack trace, or how we got there.

(gdb) where #0 0xb7f03320 in FormDialog::createActions (this=0x80ae2a0) at formdialog.cpp:53 #1 0xb7f03058 in FormDialog::setModel (this=0x80ae2a0, fmodel=0x80c80d0) at formdialog.cpp:34 #2 0x080664bd in SettingsDialog (this=0x80ae2a0, parent=0x0) at settingsdialog.cpp:14 #3 0x0805f313 in MainWindow (this=0xbfffdec8) at mainwindow.cpp:42 #4 0x08066f14 in Controller (this=0xbfffdec0, argc=1, argv=0xbfffdfe4) at controller.cpp:25 #5 0x0805a8a4 in main (argc=1, argv=0xbfffdfe4) at main.cpp:7 (gdb)

Most open-source IDEs use gdb under the hood. They each offer a user interface that makes certain features easier to learn and use. Four open-source apps that provide a front-end for gdb are: Eclipse, kdevelop, kdbg, and ddd.

Viewing Qstrings Inside the Debugger

QStrings are hard to see inside some debuggers because they are indirect pointers to Unicode data. The debugger needs to know extra things about a QString in order to display it properly.

Download these Qt 4 helper macros from the KDE subversion repository[5] and put this in your ~/.gdbinit:

[5] http://websvn.kde.org/*checkout*/trunk/KDE/kdesdk/scripts/kde-devel-gdb

source /path/to/kde/kde-devel-gdb define pqs printq4string $arg0 end

Now you should be able to print qstrings with the pqs macro.

C.3.3. Finding Memory Errors

Memory errors are very difficult to track down without the aid of a run-time analysis tool. A program that analyzes the running performance of a program is called a profiler. valgrind is an open-source profiling tool for Linux that tracks the memory and CPU usage of your code and detects a variety of errors. These include

Each of the errors just listed can cause catastrophic results in a piece of software. Profilers can also be used for performance tuning and determining which code is responsible for slowing down a program (i.e., finding bottlenecks).

Example C.4 shows a short program that contains a deliberate memory usage error.

Example C.4. src/debugging/wrongdelete.cpp

void badpointer1(int* ip, int n) { ip = new int[n]; delete ip; <-- 1 } int main() { int* iptr; int num(4); badpointer1(iptr, num); }

(1)wrong delete syntax

For the output to be human readable, we compile with debugging symbols (-g).

debugging/wrongdelete> g++ -g -pedantic -Wall wrongdelete.cpp debugging/wrongdelete> ./a.out debugging/wrongdelete>

The compiler didn complain, and even after running the program, no error behavior is exhibited. However, memory is corrupted by this program.

Here is a (slightly abbreviated) look at valgrinds analysis of our program. We have removed the process id of the valgrind job from the beginning of each line. The process id will, of course, be different each time you run valgrind.

src/debugging> valgrind a.out --3332-- DWARF2 CFI reader: unhandled CFI instruction 0:50 --3332-- DWARF2 CFI reader: unhandled CFI instruction 0:50 Mismatched free() / delete / delete [] at 0x401C1CB: operator delete(void*) (vg_replace_malloc.c:246) by 0x80484BD: badpointer1(int*, int) (wrongdelete.cpp:3) by 0x80484F4: main (wrongdelete.cpp:9) Address 0x4277028 is 0 bytes inside a block of size 16 allocd at 0x401BBF4: operator new[](unsigned) (vg_replace_malloc.c:197) by 0x80484AC: badpointer1(int*, int) (wrongdelete.cpp:2) by 0x80484F4: main (wrongdelete.cpp:9)

valgrind found the errors and, with debugging symbols, could point us to the location of the problem code. Example C.5 is a little more interesting because it contains memory leaks and array index errors.

Example C.5. src/debugging/valgrind-test.cpp

#include int badpointer2(int k) { int* ip = new int[3]; ip[0] = k; return ip[3]; <-- 1 } <-- 2 int main() { using namespace std; int* iptr; int num(4), k; <-- 3 /* what is the state of iptr? */ cout << iptr[num-1] << endl; cout << badpointer2(k) << endl; }

(1)out of bounds index

(2)memory leak

(3)k is uninitialized.

Running Example C.5 through valgrind shows us the exact locations of some errors.

For more details, rerun with: -v --2164-- DWARF2 CFI reader: unhandled CFI instruction 0:50 --2164-- DWARF2 CFI reader: unhandled CFI instruction 0:50 Use of uninitialised value of size 4 at 0x80486AF: main (valgrind-test.cpp:17) 68500558 Invalid read of size 4 at 0x804867C: badpointer2(int) (valgrind-test.cpp:8) by 0x80486DD: main (valgrind-test.cpp:18) Address 0x4277034 is 0 bytes after a block of size 12 allocd at 0x401BBF4: operator new[](unsigned) (vg_replace_malloc.c:197) by 0x8048667: badpointer2(int) (valgrind-test.cpp:6) by 0x80486DD: main (valgrind-test.cpp:18) 0 ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 19 from 1) malloc/free: in use at exit: 12 bytes in 1 blocks. malloc/free: 1 allocs, 0 frees, 12 bytes allocated. For counts of detected errors, rerun with: -v searching for pointers to 1 not-freed blocks. checked 120,048 bytes. LEAK SUMMARY: definitely lost: 12 bytes in 1 blocks. possibly lost: 0 bytes in 0 blocks. still reachable: 0 bytes in 0 blocks. suppressed: 0 bytes in 0 blocks. Use --leak-check=full to see details of leaked memory.

If this is not enough information to find where the memory leak is, we can rerun valgrind with the switch --leak-check=full.

Категории