Debugging C code
Andrew Davison describes using the text-based user interface (TUI) of GDB, the GNU debugger for C, and looks at ways to configure and extend it.
Andrew Davison explains using GDB, the GNU Debugger for C, and looks at ways to configure and extend it.
GDB has an undeserving reputation as being complicated to use, mostly because of its oldstyle command-line interface. In fact, there are numerous GUI frontends for the tool, including DDD
(www.gnu.org/software/ddd), CGDB (https://github. com/cgdb/cgdb), GDB dashboard (https://github. com/cyrus-and/gdb-dashboard), and gdbgui (www. gdbgui.com). However, its text-based interface (TUI) is built in, simple to use and understand, especially when debugging C code. Faulty code should be compiled by
GCC with the necessary flags, and loaded into GDB: gcc -std=c99 -ggdb3 -O0 -o max max.c gdb -tui -q ./max
-ggdb3 makes GCC save the maximum amount of debugging information, while -O0 switches off any optimisations that might affect that data. -std=c99 indicates that the code follows the C99 standard. GDB’S
-tui flag switches on the TUI, and -q disables the printing of GDB’S licensing preamble.
Debugging to the max
The code max.c (all the code can be found on the DVD or linuxformat.com/archives) is meant to find the largest integer in an array by calling findmax(): int findmax(int *arr, int len, int max)
{ if(!arr || (len <= 0)) return -1;
max = arr[0]; for(int i=1; i <= len; i++) { if(max < arr[i]) max = arr[i]; } return 0;
} // end of findmax()
int main(void)
{ int a[5] = {17, 21, 44, 2, 60}; int max = a[0]; printf(“array: “); printarr(a, 5);
if (findmax(a, 5, max) == -1) printf(“error\n”); else printf(“max value: %d\n”, max); return 0;
}
Note the program contains a printarr() function for printing an array. Neither GCC nor cppcheck found any errors in the code (see the boxout opposite), but the program prints the wrong answer:
./max
Array: { 17 21 44 2 60 }
Max value: 17
After loading a program into GDB, the usual first step is to sprinkle breakpoints among the code. Execution will stop at these places so you can examine data. A breakpoint is put on line 44 of max.c, so the array can be checked before findmax() is called. It’s visually indicated in the GDB source window by a b+ tag placed
to the left of line 44. The program is then started with the run command:
(gdb) b 44
(gdb) run
The array can be printed out in a few ways:
(gdb) p a
$1 = {[0] = 17,
[1] = 21,
[2] = 44,
[3] = 2,
[4] = 60}
(gdb) p a[2]
$2 = 44
(gdb) call printarr(a,5)
{ 17 21 44 2 60 }
(gdb)
The print (p) command can report on any variable that’s in scope at that point in the program. The code above prints the entire array and its third element. It’s also possible to call a function defined in the program; in this case, max.c’s printarr(). The output of print may be less pretty when you try things, since it depends on several set print settings in GDB’S configuration file,
.gdbinit, in the home directory. The relevant lines are:
set print pretty on set print array on set print array-indexes on
A complete .gdbinit file is included with the other source code for this article. GDB can execute the C code line by line using either next (n) or step (s), but differ if that line is a function call. If next is utilised, findmax()
will be completely executed, and GDB will then move on to the next line in main(). If step is employed than GDB steps into findmax() and stops at its first line. The following GDB snippet does the latter, and then the user prints its array argument in several different ways:
(gdb) step findmax (arr=0xbefff110, len=5, max=17) at max.c:17 (gdb) info args arr = 0xbefff110 len = 5 max = 17
(gdb) p arr
$3 = (int *) 0xbefff110
(gdb) p *arr
$4 = 17
(gdb) p arr@5
$5 = {[0] = 0xbefff110,
[1] = 0xbefff12c,
[2] = 0x10378 <_start>,
[3] = 0xbefff12c,
[4] = 0xbefff12c}
(gdb) p *arr@5
$6 = {[0] = 17,
[1] = 21,
[2] = 44,
[3] = 2,
[4] = 60}
(gdb) call printarr(arr, 5)
{ 17 21 44 2 60 }
(gdb)
The step call also causes the source window to be redrawn to highlight the first line of findmax(). The info args command lists all the function’s arguments. info locals is also useful for printing local variables.
The array was passed to findmax() as a pointer, so p arr only prints the pointer value. p *arr is a little better, but only prints the value in arr[0]. p arr@5 prints the first five pointers starting at the arr address. The most useful command is print *arr@5, which dereferences and prints the five values. Needless to say, these print commands only make sense if you have a good understanding of pointers. Alternatively, calling the program’s printarr() hides that complexity.
Another way is to use the DUEL printing command (dl), which is part of gdb-tools (see boxout on page 95).
This sets a breakpoint at the start of findmax(), starts the program, and uses DUEL’S notation to print the array: (gdb) b findmax
Breakpoint 1 at 0x104b8: file max.c, line 17.
(gdb) run
Starting program: /home/pi/code/debug/max Breakpoint 1, findmax (arr=0xbefff110, len=5, max=17) at max.c:17 (gdb) dl arr[..5] arr[0] = 17 arr[1] = 21 arr[2] = 44 arr[3] = 2 arr[4] = 60 (gdb)
arr[..5] DUEL hides the fact that arr is an array pointer. Another possibility to print a subrange of the array is:
(gdb) dl arr[1..3] arr[1] = 21 arr[2] = 44 arr[3] = 2
Typing dl help prints information on DUEL’S common uses, and there’s dl examples to show a few examples.
The programmer will probably want to monitor how the max variable changes as findmax() progresses. This could be done by typing next and p max repeatedly, but a better way is to enter display max, which will automatically print the value after each step.
If you tire of typing “n” (you can also keep pressing Return), employ until
(gdb) display max
1: max = 17
(gdb) n
1: max = 17
(gdb) n
1: max = 17
(gdb) until 25 findmax (arr=0xbefff110, len=5, max=60) at max.c:25 1: max = 60
(gdb)
The max value is now 60, the largest value in the array. Typing “n” a few times takes the execution back to main() and into the if statement where the result is printed.
Rather than typing lots of “n”s, two larger stepping operations are finish and continue – finish steps the execution until it returns from the enclosing function, and continue progresses until the next breakpoint is reached or the debugged program terminates.
A drawback of the TUI interface is that it doesn’t nicely present the output from the debugged program. The output of max.c’s printf() is sent to the command window and can sometimes overwrite the bottom of the source window. The result can be messy and confusing.
The solution is to redirect max.c’s output to a different window. Create a second terminal window on your desktop and type tty to find its name (something like /dev/pts/1). In GDB, make sure to start max.c
running with its output redirected to that terminal: (gdb) run > /dev/pts/1
GDB output will still appear in the command window, but any program output is sent to the other terminal.
max.c’s printf() in main() shows the max variable is assigned 17, not the 60 it held at the end of findmax().
This should be enough to trigger the realisation that findmax() is not copying the value back to main().
Note that breakpoints and display variables will remain in place while GDB is running. This is even true when the debugged program (e.g. max.c) has finished. A new run call will reuse previously set breakpoints and display variables. To actually leave GDB, type quit.
Printing pointers
One headache of debugging C programs is dealing with pointer-based data structures. The names.c example constructs a linked list of nodes, pointing to person structs. The relevant data structures are: struct person { char *name; int age; char *ssn;
};
struct node { struct person *person; struct node *next;
} *head, *tail;
The head and tail globals will point to the beginning and end of the list. The figure (top right) is a list containing details about three individuals. If names.c is run inside
GDB and interrupted after the end of the list-building stage, then how can the list be examined? The simplest way is to ensure that the program includes useful print functions. For example, names.c contains displayall() and display(), which can be called from inside GDB:
(gdb) call displayall() {ad,18,xx1}--{tw,22,zx3}--{cz,19,db4}
(gdb) call display(0)
{ad,18,xx1}
(gdb) call display(2)
{cz,19,db4}
(gdb)
If functions like these are unavailable, you will need to concoct their own pointer expressions for print, such as:
(gdb) p head->person->name
$1 = 0x22828 “ad”
(gdb) p *head->person
$2 = { name = 0x22828 “ad”, age = 18, ssn = 0x22890 “xx1”
}
(gdb) p *tail->person
$3 = { name = 0x22a08 “cz”, age = 19, ssn = 0x22a70 “db4”
}
(gdb)