The GDB Debugger in Unix/Linux

The GNU Debugger (GDB) (Debugging with GDB 2002; GDB 2017) is an interactive debugger, which can debug programs written in C, C++ and several other languages. In Linux, the command man gdb displays the manual pages of gdb, which provides a brief description of how to use GDB. The reader may find more detailed information on GDB in the listed references. In this section, we shall cover GDB basics and show how to use GDB to debug C programs in the Integrated Development Environment (IDE) of EMACS under X-windows, which is available in all Linux systems. Since the Graphic User Interface (GUI) part of different Linux distributions may differ, the following discussions are specific to Ubuntu Linux Version 15.10 or later, but it should also be applicable to other Linux distributions, e.g. Slackware Linux 14.2, etc.

1. Use GDB in Emacs IDE

  1. Source Code: Under X-window, open a pseudo-terminal. Use EMACS to create a Makefile, as shown below.

Makefile:

t: t.c

gcc -g -o t t.c

Then use EMACS to edit a C source file. Since the objective here is to show GDB usage, we shall use a very simple C program.

/******** Source file: t.c Code ********/

#include <stdio.h> int sub();

int g, h; // globals

int main()

{

int a, b, c;

printf(“enter main\n”);

a = 1 ;

b = 2;

c = 3;

g = 123;

h = 456;

c = sub(a, b);

printf(“c = %d\n”, c);

printf(“main exit\n”);

}

int sub(int x, int y)

{

int u,v;

printf(“enter sub\n”);

u = 4;

v = 5;

printf(“sub return\n”);

return x+y+u+v+g+h;

}

  1. Compile Source Code: When EMACS is running, it displays a menu and a tool bar at the top of the edit window (Fig. 2.17).

Each menu can be opened to display a table of submenus. Open EMACS Tools menu and select Compile. EMACS will show a prompt line at the bottom of the edit window

make -k

and waits for user response. EMACS normally compile-link the source code by a makefile. If the reader already has a makefile in the same directory as shown above, press the Enter key to let EMACS continue. In instead of a makefile, the reader may also enter the command line manually.

gcc -g -o t t.c

In order to generate a binary executable for GDB to debug, the -g flag is required. With the -g flag, the GCC compiler-linker will build a symbol table in the binary executable file for GDB to access variables and functions during execution. Without the -g flag, the resulting executable file can not be debugged by GDB. After compilation finishes, EMACS will show the compile results, including warning or error messages, if any, in a separate window below the source code window.

  1. Start up GDB: Open EMACS Tools menu and select Debugger.

EMACS will show a prompt line at the bottom of the edit window and wait for user response.

gdb -i=mi t

Press Enter to start up the GDB debugger. GDB will run in the upper window and display a menu and a tool bar at the top of the EMACS edit window, as shown in Fig. 2.18.

The user may now enter GDB commands to debug the program. For example, to set break points, enter the GDB commands

b main       # set break  point at main

b sub        # set break  point at  sub

b 10        # set break  point at line  10 in program

When the user enters the Run (r) command (or choose Run in the tool bar), GDB will display the program code in the same GDB window. Other frames/windows can be activated through the submenu GDB-Frames or GDB-Windows. The following steps demonstrate the debugging process by using both commands and tool bar in the multi-windows layout of GDB.

  1. GDB in Multi-Windows: From the GDB menu, choose Gud => GDB-MI => Display Other Windows, where => means follow a submenu. GDB will display GDB buffers in different windows, as shown in Fig.2.19.

Figure 2.19 shows six (6) GDB windows, each displays a specific GDB buffer.

Gud-t: GDB buffer for user commands and GDB messages

t.c: Program source code to show progress of execution

Stack frames: show stack frames of function calling sequence

Local Registers: show local variables in current executing function

Input/output: for program I/O

Breakpoints: display current break points settings

It also shows some of the commonly used GDB commands in a tool bar, e.g. Run, Continue, Next line, Step line, which allows the user to choose an action instead of entering commands.

While the program is executing, GDB shows the execution progress by a dark triangular mark, which points to the next line of program code to be executed. Execution will stop at each break point, providing a break for the user to interact with GDB. Since we have set main as a break point, execution will stop at main when GDB starts to run. While execution stops at a break point, the user may interact with GDB by entering commands in the GDB window, such as set/clear break points, display/change variables, etc. Then, enter the Continue (c) command or choose Continue in the tool bar to continue program execution. Enter Next (n) command or choose Next line or Step line in the GDB tool bar to execute in single line mode.

Figure 2.19 shows that execution in main() has already completed executing several lines of the program code, and the next line mark is at the statement

g = 123;

At the moment, the local variables a, b, c are already assigned values, which are shown in the Locals registers windows as a=1, b=2, c=3. Global variables are not shown in any window, but the user may enter Print (p) commands

p g

p h

to print the global variables g and h, both of which should still be 0 since they are not assigned any values yet.

. input/output window shows the outputs of printf statements of main().

. stack frames window shows execution is now inside the main() function.

. breakpoints window shows the current breakpoints settings, etc.

This multi-windows layout provides the user with a complete set of information about the status of the executing program.

The user may enter Continue or choose Continue in the tool bar to continue the execution. When control reaches the sub() function, it will stop again at the break point. Figure 2.20 shows that the program execution is now inside sub() and the execution already passed the statements before

printf(“return from sub\n”);

At this moment, the Locals Registers window shows the local variables of sub() as u=4 and v=5. The input/output window shows the print results of both main() and sub(). The Stack frames window shows sub() is the top frame and main() is the next frame, which is consistent with the function calling sequence.

(5). Additional GDB Commands: At each break point or while executing in single line mode, the user may enter GDB commands either manually, by the GDB tool bar or by choosing submenu items in the Gud menu, which includes all the commands in the GDB tool bar. The following lists some additional GDB commands and their meanings.

Clear Break Points:

clear line# : clear bp at line#

clear name : clear bp at function name

Change Variable Values

set var a=100 : set variable a to 100

set var b=200 : set b to 200, etc.

Watch Variable Changes:

watch c : watch for changes in variable c; whenever c changes, it will display its old value and new value.

Back trace (bt):

bt stackFrame# to back trace stack frames

2. Advices on Using Debugging Tools

GDB is a powerful debugger, which is fairly easy to use. However, the reader should keep in mind that all debugging tools can only offer limited help. In some cases, even a powerful debugger like the GDB is of little use. The best approach to program development is to design the program’s algorithm carefully and then write program code in accordance with the algorithm. Many beginning programmers tend to write program code without any planning, just hoping their program would work, which most likely would not. When their program fails to work or does not produce the right results, they would immediately turn to a debugger, trying to trace the program executions to find out the problem. Relying too much on debugging tools is often counter-productive as it may waste more time than necessary. In the following, we shall point out some common programming errors and show how to avoid them in C programs.

3. Common Errors in C programs

A program in execution may encounter many types of run-time errors, such as illegal instruction, privilege violation, divide by zero, invalid address, etc. Such errors are recognized by the CPU as exceptions, which trap the process to the operating system kernel. If the user has not made any provision to handle such errors, the process will terminate by a signal number, which indicates the cause of the exception. If the program is written in C, which is executed by a process in user mode, exceptions such as illegal instruction and privilege violation should never occur. In system program­ming, programs seldom use divide operations, so divide by zero exceptions are also rare. The predominate type of run-time errors are due to invalid addresses, which cause memory access exceptions, resulting in the dreadful and familiar message of segmentation fault. In the following, we list some of the most probable causes in C programs that lead to memory access exceptions at run-time.

(1). Uninitialized pointers or pointers with wrong values: Consider the following code segments, with line numbers for ease of reference.

1. int *p; // global, initial value = 0

int main()
{

2.       int *q; // q is local on stack, can be any value
3.       *p = 1; // dereference a NULL pointer
4.      *q = 2; // dereference a pointer with unknown value

}

Line 1 defines a global integer pointer p, which is in the BSS section of the run-time image with an initial value 0. So it’s a NULL pointer. Line 3 tries to dereference a NULL pointer, which will cause a segmentation fault.

Line 2 defines a local integer pointer q, which is on the stack, so it can be any value. Line 4 tries to dereference the pointer q, which points at an unknown memory location. If the location is outside of the program’s writable memory area, it will cause a segmentation fault due to memory access violation. If the location is within the program’s writable memory area, it may not cause an immediate error but it may lead to other errors later due to corrupted data or stack contents. The latter kind of run-time error is extremely difficult to diagnose because the errors may have propagated through the program execu­tion. The following shows the correct ways of using these pointers. Modify the above code segment as shown below.

int x, *p; int main()            // or int *p = &x; 

{

int *q;

p = &x;                   // let p point at x

*p = 1;

q = (int *)malloc(sizeof(int); // q point at allocate memory

*q = 2;

}

The principle is very simple. When using any pointer, the programmer must ensure the pointer is not NULL or has been set to point to a valid memory address.

(2). Array index out of bounds: In C programs, each array is defined with a finite number of N elements. The index of the array must be in the range of [0, N-1]. If the array index exceeds the range at run-time, it may cause invalid memory access, which either corrupt the program data area or result in a segmentation fault. We illustrate this by an example. Consider the following code segment.

Line 1 defines an array of N elements, which is followed by the index variable i. Line 2 represents the proper usage of the array index, which is within the bounds [0, N-1]. Line 3.sets a[N] to a large value, which actually changes the variable i because a[N] and i are in the same memory location. Line 4 prints the current value of i, which is no longer N but the large value. Line 5 will most likely to cause a segmentation fault because a[123456789] tries to access a memory location outside of the program’s data area.

(3). Improper use of string pointers and char arrays: Many string operation functions in the C library are defined with char * parameters. As a specific example, consider the strcpy() function, which is defined as

char * strcpy(char *dest, char *src)

It copies a string from src to dest. The Linux man page on strcpy() clearly specifies that the dest string must be large enough to receive the copy. Many programmers, including some ‘experienced’ graduate students, often overlook the specification and try to use strcpy() as follows.

char *s;  // s is a char pointer

strcpy(s, ” this is a string”);

The code segment is wrong because s is not pointing at any memory location with enough space to receive the src string. If s is global, it is a NULL pointer. In this case, strcpy() will cause a segmentation fault immediately. If s is local, it may point to an arbitrary memory location. In this case, strcpy() may not cause an immediate error but it may lead to other errors later due to corrupted memory contents. Such errors are very subtle and difficult to diagnose even with a debugger such as GDB. The correct way of using strcpy() is to ensure dest is NOT just a string pointer but a real memory area with enough space to receive the copied string, as in

char s[128];   // s is a char array

strcpy(s, ” this is a string”);

Although the same s variable in both char *s and char s[128] can be used as an address, the reader must beware there is a fundamental difference between them.

(4). The assert macro: Most Unix-like systems, including Linux, support an assert(condition) macro, which can be used in C programs to check whether a specified condition is met or not. If the condition expression evaluates to FALSE (0), the program will abort with an error message. As an example, consider the following code segments, in which the mysum() function is designed to return the sum of an integer array (pointed by int *ptr) of n<= 128 elements. Since the function is called from other code of a program, which may pass in invalid parameters, we must ensure the pointer ptr is not NULL and the array size n does not exceed the LIMIT. These can be done by including assert() statements at the entry point of a function, as shown below.

#define LIMIT 128

int mysum(int *ptr, int n)

{

int i = 0, sum = 0;

assert(ptr != NULL);      // assert ptr not NULL

assert(n <= LIMIT);       // assert n <= LIMIT

while(i++ < n)

sum += *ptr++

}

When execution enters the mysum() function, if either assert(condition) fails, the function will abort with an error message.

(5). Use fprintf() and getchar() in Program Code: When writing C programs, it is often very useful to include fprintf(stderr, message) statements at key places in the program code to display expected results. Since fprintf() to stderr is unbuffered, the printed message or results will show up immediately before the next C statement is executed. If desired, the programmer may also user getchar () to stop the program flow, allowing the user to examine the execution results at that moment before continuing. We cite a simple program task to illustrate the point. A priority queue is a singly link list ordered by priority, with high priority entries in front. Entries with the same priority are ordered First-in-First-out (FIFO). Write an enqueue() function which insert an item into a priority queue by priority.

typedef struct entry{

struct entry *next;

char name[64];      // entry name

int priority;       // entry priority

} ENTRY;

void printQ(ENTRY *queue) // print queue contents

{

while(queue){

printf(“[%s %d]-> “, queue->name, queue->priority);

queue = queue->next;

}

printf(“\n”);

}

void enqueue(ENTRY **queue, ENTRY *p)

{

ENTRY *q = *queue;

printQ(q);          // show queue before insertion

if (q==0 || p->priority > q->priority){ // first in queue

*queue = p;

p->next = q;

}

else{ // not first in queue; insert to the right spot

while (q->next && p->priority <= q->priority)

q = q->next;

p->next = q->next;

q->next = p;

}

printQ(q);          // show queue after insertion

}

In the above example code, if the enqueue() function code is incorrect, it may insert an entry to the wrong spot in the queue, which will cause other program code to fail if they rely on the queue contents being correct. In this case, using either assert() statements or relying on a debugger may offer very little help, since all the pointers are valid and it would be too tedious to trace a long link list in a debugger.

Instead, we print the queue before inserting a new entry and print it again after the insertion. These allow the user to see directly whether or not the queue is maintained correctly. After verifying the code works, the user may comment out the printQ() statements.

Source: Wang K.C. (2018), Systems Programming in Unix/Linux, Springer; 1st ed. 2018 edition.

Leave a Reply

Your email address will not be published. Required fields are marked *