
The GNU Debugger, commonly referred to by its command gdb, is an interactive console that helps you browse source code and analyze what is happening during execution. Essentially, it is a reverse engineering tool for problems that occur in malfunctioning applications.
Troubleshooting can be complicated. The GNU Debugger is not a particularly complex application, but if you don’t know where to start or even when and why you might need to turn to GDB for troubleshooting, it can be overwhelming. If you have been using print, echo, or printf statements to debug your code and are starting to wonder if there is something more powerful, then this tutorial is for you.
Code with Errors
To start using GDB, you need some code. Here is a sample application written in C++ (it’s okay if you don’t usually program in C++, the principles are the same across all languages), sourced from an example in the Guessing Game series.
#include <iostream>
#include <stdlib.h> //srand
#include <stdio.h> //printf
using namespace std;
int main () {
srand (time(NULL));
int alpha = rand() % 8;
cout << "Hello world." << endl;
int beta = 2;
printf("alpha is set to is %s\n", alpha);
printf("kiwi is set to is %s\n", beta);
return 0;
} // main
This code example contains a bug, but it does compile (at least with GCC 5). If you are familiar with C++, you may have already spotted it, but it is a simple issue that can help new GDB users understand the debugging process. Compile and run it to see the error:
$ g++ -o buggy example.cpp
$ ./buggy
Hello world.
Segmentation fault
Excluding Segmentation Faults
From this output, you can infer that the setting of the variable alpha
is correct, because otherwise you wouldn’t see the line of code that comes after it execute. Of course, this is not always true, but it is a good working theory, and if you use printf
as logging and debugging, you would basically come to the same conclusion. From here, you can assume the bug is in the line after the successfully printed line. However, it is unclear whether the error is in the next line or several lines later.
The GNU Debugger is an interactive troubleshooting tool, so you can use the gdb command to run the buggy code. For better results, you should recompile your buggy application from the source code with debug symbols. First, see what information GDB can provide without recompiling:
$ gdb ./buggy
Reading symbols from ./buggy...done.
(gdb) start
Temporary breakpoint 1 at 0x400a44
Starting program: /home/seth/demo/buggy
Temporary breakpoint 1, 0x0000000000400a44 in main ()
(gdb)
When you start GDB with a binary executable as an argument, GDB loads the application and waits for your instructions. Since this is your first time running GDB on this executable, it makes sense to try to reproduce the error in hopes that GDB can provide further insight. Intuitively, the command GDB uses to start the loaded application is start
. By default, GDB has a built-in breakpoint, so when it encounters the main
function of your application, it pauses execution. To let GDB continue execution, use the command continue
:
(gdb) continue
Continuing.
Hello world.
Program received signal SIGSEGV, Segmentation fault.
0x00007ffff71c0c0b in vfprintf () from /lib64/libc.so.6
(gdb)
No surprise: the application crashed shortly after printing “Hello world,” but GDB can provide the function call that was happening when the crash occurred. This may be enough for you to find the bug causing the crash, but to better understand GDB’s capabilities and the general debugging process, imagine if the issue is still unclear and you want to dig deeper into what is happening in the code.
Compile Code with Debug Symbols
To make the most of GDB, you need to compile your executable file with debug symbols. You can use the -g
option in GCC to generate these symbols:
$ g++ -g -o debuggy example.cpp
$ ./debuggy
Hello world.
Segmentation fault
Compiling debug symbols into an executable results in a much larger file, so they are typically not distributed for convenience. However, if you are debugging open-source code, it makes sense to recompile tests with debug symbols:
$ ls -l *buggy* *cpp
-rw-r--r-- 310 Feb 19 08:30 debug.cpp
-rwxr-xr-x 11624 Feb 19 10:27 buggy*
-rwxr-xr-x 22952 Feb 19 10:53 debuggy*
Debugging with GDB
Load the new executable file (in this case, debuggy
) to start GDB:
$ gdb ./debuggy
Reading symbols from ./debuggy...done.
(gdb) start
Temporary breakpoint 1 at 0x400a44
Starting program: /home/seth/demo/debuggy
Temporary breakpoint 1, 0x0000000000400a44 in main ()
(gdb)
As mentioned earlier, use the start
command:
(gdb) start
Temporary breakpoint 1 at 0x400a48: file debug.cpp, line 9.
Starting program: /home/sek/demo/debuggy
Temporary breakpoint 1, main () at debug.cpp:9
9 srand (time(NULL));
(gdb)
This time, the automatic main
breakpoint can indicate the line number where GDB paused and the code contained in that line. You can use continue
to resume normal operation, but you already know the application will crash before it completes, so you can use the next
keyword to step through your code line by line:
(gdb) next
10 int alpha = rand() % 8;
(gdb) next
11 cout << "Hello world." << endl;
(gdb) next
Hello world.
12 int beta = 2;
(gdb) next
14 printf("alpha is set to is %s\n", alpha);
(gdb) next
Program received signal SIGSEGV, Segmentation fault.
0x00007ffff71c0c0b in vfprintf () from /lib64/libc.so.6
(gdb)
From this process, it can be confirmed that the crash did not occur when setting the beta
variable, but rather when executing the printf
line. This bug has been exposed several times in this article (culprit: providing the wrong data type to printf
), but for now, let’s assume the solution is still unclear and needs further investigation.
Setting Breakpoints
Once your code is loaded into GDB, you can query GDB for the data produced by the code so far. To try data introspection, restart your application by issuing the start
command again, and proceed to line 11. A quick way to reach line 11 is to set a breakpoint at that specific line number:
(gdb) start
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Temporary breakpoint 2 at 0x400a48: file debug.cpp, line 9.
Starting program: /home/sek/demo/debuggy
Temporary breakpoint 2, main () at debug.cpp:9
9 srand (time(NULL));
(gdb) break 11
Breakpoint 3 at 0x400a74: file debug.cpp, line 11.
After setting the breakpoint, continue execution with continue
:
(gdb) continue
Continuing.
Breakpoint 3, main () at debug.cpp:11
11 cout << "Hello world." << endl;
(gdb)
Now paused at line 11, just after the alpha
variable is set and before beta
is set.
Using GDB for Variable Introspection
To view the value of a variable, use the print command. In this sample code, the value of alpha
is random, so your actual result may differ from mine:
(gdb) print alpha
$1 = 3
(gdb)
Of course, you cannot see the value of a variable that has not yet been established:
(gdb) print beta
$2 = 0
Using Control Flow
To continue, you can step through the code to reach the point where beta
is set to a value:
(gdb) next
Hello world.
12 int beta = 2;
(gdb) next
14 printf("alpha is set to is %s\n", alpha);
(gdb) print beta
$3 = 2
Additionally, you can set a watchpoint, which is like a breakpoint and a way to control the flow of execution in GDB. In this case, you know that the beta
variable should be set to 2
, so you can set a watchpoint to notify you when beta
‘s value changes:
(gdb) watch beta > 0
Hardware watchpoint 5: beta > 0
(gdb) continue
Continuing.
Breakpoint 3, main () at debug.cpp:11
11 cout << "Hello world." << endl;
(gdb) continue
Continuing.
Hello world.
Hardware watchpoint 5: beta > 0
Old value = false
New value = true
main () at debug.cpp:14
14 printf("alpha is set to is %s\n", alpha);
(gdb)
You can manually step through the execution of the code with next
, or you can control the execution of the code with breakpoints, watchpoints, and catchpoints.
Using GDB to Analyze Data
You can view data in different formats. For example, view the value of beta
in octal:
(gdb) print /o beta
$4 = 02
To see its address in memory:
(gdb) print /o &beta
$5 = 0x2
You can also see the data type of a variable:
(gdb) whatis beta
type = int
Using GDB to Solve Errors
This introspection not only helps you better understand what code is executing but also how it is executing. In this example, running the whatis
command on the variable gave you a clue that both your alpha
and beta
variables are integers, which might jog your memory about the printf
syntax, making you realize that in your printf
statement, you must use %d
instead of %s
. Making this change allows the application to run as expected, with no more obvious errors present.
It is especially frustrating to discover bugs when the code compiles, but the trickiest bugs are often like this; if they were easy to spot, they wouldn’t be bugs. Using GDB is one way to hunt them down and eliminate them.
Download Our Cheat Sheet
The truth of life is that even the most basic programming can have bugs. Not all errors lead to applications failing to run (or even compile), and not all errors are caused by incorrect code. Sometimes bugs occur intermittently based on a particularly creative combination of choices made by a user. Sometimes programmers inherit bugs from libraries they use in their own code. Whatever the reason, bugs are essentially everywhere, and it is the programmer’s job to discover and eliminate them.
The GNU Debugger is a useful tool for finding bugs. There is much more you can do with it than I have demonstrated in this article. You can read about its many features through the GNU Info reader:
$ info gdb
Whether you are just starting to learn GDB or are a professional, it is helpful to remind yourself of what commands are available and what the syntax for those commands is.
via: https://opensource.com/article/21/3/debug-code-gdb
Author: Seth Kenlon, Topic: lujun9972, Translator: wxy, Proofreader: wxy
This article is originally compiled by LCTT and honorably launched by Linux China.