Linux Format

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 undeservin­g reputation as being complicate­d 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 informatio­n, while -O0 switches off any optimisati­ons 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 linuxforma­t.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 breakpoint­s 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 configurat­ion 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 dereferenc­es and prints the five values. Needless to say, these print commands only make sense if you have a good understand­ing of pointers. Alternativ­ely, 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 possibilit­y 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 informatio­n 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 automatica­lly print the value after each step.

If you tire of typing “n” (you can also keep pressing Return), employ until to make GDB take several steps and stop at the specified line number:

(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 realisatio­n that findmax() is not copying the value back to main().

Note that breakpoint­s 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 breakpoint­s 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 individual­s. If names.c is run inside

GDB and interrupte­d 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 unavailabl­e, you will need to concoct their own pointer expression­s 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)

 ??  ?? The GDB TUI with max.c listed at the top, with the command window beneath. Cursor keys let the user move around in the source code window, while GDB commands are entered in the lower window.
The GDB TUI with max.c listed at the top, with the command window beneath. Cursor keys let the user move around in the source code window, while GDB commands are entered in the lower window.
 ??  ?? OUR EXPERT Andrew Davison is a teacher, author, and programmer who is rekindling his love for UNIX and Linux by hacking with the Raspberry Pi.
OUR EXPERT Andrew Davison is a teacher, author, and programmer who is rekindling his love for UNIX and Linux by hacking with the Raspberry Pi.
 ??  ?? A core set of GDB commands is remarkably short. Of course, for more serious hacking, there’s many more.
A core set of GDB commands is remarkably short. Of course, for more serious hacking, there’s many more.
 ??  ?? The GDB TUI listing of max.c at the start of findmax(). Note the breakpoint on the left of the highlighte­d line.
The GDB TUI listing of max.c at the start of findmax(). Note the breakpoint on the left of the highlighte­d line.

Newspapers in English

Newspapers from Australia