5 Lesser-Known GDB Debugger Tips

5 Lesser-Known GDB Debugger Tips

Learn how to use some lesser-known features of gdb to inspect and fix your code.

— Tim Waugh

GNU Debugger (gdb) is a valuable tool for inspecting running processes and troubleshooting while developing programs.

You can set breakpoints at specific locations (by function name, line number, etc.), enable and disable these breakpoints, display and change variable values, and perform all the standard operations that a debugger is expected to perform. However, it also has many other features you might not have tried. Here are five that you can explore.

Conditional Breakpoints

Setting breakpoints is the first step in learning to use the GNU Debugger. The program stops when it reaches a breakpoint, allowing you to run gdb commands to inspect or change variables before allowing the program to continue running.

For example, you might know that a frequently called function sometimes crashes, but only when it receives a certain parameter value. You can set a breakpoint at the start of that function and run the program. Each time the breakpoint is hit, the function parameters will be displayed, and if the triggering parameter value is not provided, you can continue until the function is called again. When the troublesome parameter triggers a crash, you can step through the code to see what went wrong.

(gdb) break sometimes_crashes
Breakpoint 1 at 0x40110e: file prog.c, line 5.
(gdb) run
[...]
Breakpoint 1, sometimes_crashes (f=0x7fffffffd1bc) at prog.c:5
5      fprintf(stderr,
(gdb) continue
Breakpoint 1, sometimes_crashes (f=0x7fffffffd1bc) at prog.c:5
5      fprintf(stderr,
(gdb) continue

To make this method more repeatable, you can count how many times the function is called before the specific call you are interested in and set a counter at that breakpoint (for example, continue 30 to ignore it for the next 29 hits).

But the real power of breakpoints lies in their ability to evaluate expressions at runtime, allowing you to automate this kind of testing.

break [LOCATION] if CONDITION

(gdb) break sometimes_crashes if !f
Breakpoint 1 at 0x401132: file prog.c, line 5.
(gdb) run
[...]
Breakpoint 1, sometimes_crashes (f=0x0) at prog.c:5
5      fprintf(stderr,
(gdb)

Conditional breakpoints allow you to let gdb stop at that location only when the value of a specific expression is true. If execution reaches the conditional breakpoint but the expression evaluates to false, the debugger will automatically continue running the program without asking the user what to do.

Breakpoint Commands

An even more complex feature of breakpoints in the GNU Debugger is the ability to write scripts that respond to hitting a breakpoint. Breakpoint commands allow you to write a series of gdb commands to execute when that breakpoint is reached.

We can use it to bypass known bugs in the sometimes_crashes function and return safely from that function when it provides a null pointer.

We can use silent as the first line to better control the output. Otherwise, every time the breakpoint is hit, the stack frame will be displayed even before running the breakpoint commands.

(gdb) break sometimes_crashes
Breakpoint 1 at 0x401132: file prog.c, line 5.
(gdb) commands 1
Type commands for breakpoint(s) 1, one per line.
End with a line saying just "end".
>silent
>if !f
 >frame
 >printf "Skipping call\n"
 >return 0
 >continue
 >end
>printf "Continuing\n"
>continue
>end
(gdb) run
Starting program: /home/twaugh/Documents/GDB/prog
warning: Loadable section ".note.gnu.property" outside of ELF segments
Continuing
Continuing
Continuing
#0  sometimes_crashes (f=0x0) at prog.c:5
5      fprintf(stderr,
Skipping call
[Inferior 1 (process 9373) exited normally]
(gdb)

Dumping Binary Memory

The GNU Debugger has built-in support for checking memory in various formats with the x command, including octal, hexadecimal, etc. However, I like to see both formats side by side: hexadecimal bytes on the left and the ASCII characters represented by those bytes on the right.

I often use hexdump -C (from the util-linux package) when I want to view the contents of a file byte by byte. Here’s how the hexadecimal bytes displayed by gdb’s x command look:

(gdb) x/33xb mydata
0x404040 <mydata>   :    0x02    0x01    0x00    0x02    0x00    0x00    0x00    0x01
0x404048 <mydata+8> :    0x01    0x47    0x00    0x12    0x61    0x74    0x74    0x72
0x404050 <mydata+16>:    0x69    0x62    0x75    0x74    0x65    0x73    0x2d    0x63
0x404058 <mydata+24>:    0x68    0x61    0x72    0x73    0x65    0x75    0x00    0x05
0x404060 <mydata+32>:    0x00

What if you want gdb to display memory like hexdump? This is possible; in fact, you can use this method for any format you prefer.

By using the dump command to store bytes in a file, combined with the shell command to run hexdump on that file and the define command, we can create our own new hexdump command to display memory contents using hexdump.

(gdb) define hexdump
Type commands for definition of "hexdump".
End with a line saying just "end".
>dump binary memory /tmp/dump.bin $arg0 $arg0+$arg1
>shell hexdump -C /tmp/dump.bin
>end

These commands can even be placed in the ~/.gdbinit file to permanently define the hexdump command. Here’s an example of how it runs:

(gdb) hexdump mydata sizeof(mydata)
00000000  02 01 00 02 00 00 00 01  01 47 00 12 61 74 74 72  |.........G..attr|
00000010  69 62 75 74 65 73 2d 63  68 61 72 73 65 75 00 05  |ibutes-charseu..|
00000020  00                                                |.|
00000021

Inline Disassembly

Sometimes you want to know more about what caused a crash, and the source code isn’t enough. You want to see what happened at the CPU instruction level.

The disassemble command lets you see the CPU instructions that implement a function. However, the output can sometimes be difficult to follow. Often, I want to see the instructions corresponding to a specific part of the function’s source code. To do this, use the /s modifier to include source code lines in the disassembly.

(gdb) disassemble/s main
Dump of assembler code for function main:
prog.c:
11    {
   0x0000000000401158 <+0>:    push   %rbp
   0x0000000000401159 <+1>:    mov      %rsp,%rbp
   0x000000000040115c <+4>:    sub      $0x10,%rsp

12      int n = 0;
   0x0000000000401160 <+8>:    movl   $0x0,-0x4(%rbp)

13      sometimes_crashes(&n);
   0x0000000000401167 <+15>:    lea     -0x4(%rbp),%rax
   0x000000000040116b <+19>:    mov     %rax,%rdi
   0x000000000040116e <+22>:    callq  0x401126 <sometimes_crashes>
[...snipped...]

Here, using info registers to view the current values of all CPU registers, along with commands like stepi to execute one instruction at a time, can give you a more detailed understanding of the program.

Reverse Debugging

Sometimes, you wish you could reverse time. Imagine you’ve hit a watchpoint. A watchpoint is like a breakpoint but is set on an expression (using the watch command) rather than a specific location in the program. Execution stops every time the value of the expression changes, and the debugger takes control.

Imagine you’ve hit this watchpoint, and the memory used by that variable has changed its value. It turns out this may have been caused by something that happened earlier. For example, memory was freed and is now being reused. But when and where was it freed?

The GNU Debugger can even solve this problem because you can run the program backward!

It achieves this by carefully recording the program’s state at each step, allowing you to restore a previously recorded state, creating the illusion of time reversal.

To enable this state recording, use the target record-full command. Then, you can use some commands that sound less feasible, such as:

reverse-step, step back to the previous source line
reverse-next, step back to the previous source line, skipping function calls
reverse-finish, step back to the moment just before the current function is called
reverse-continue, return to a previous state in the program that would (now) trigger a breakpoint (or other state that causes a breakpoint to stop)

Here’s an example of reverse debugging in action:

(gdb) b main
Breakpoint 1 at 0x401160: file prog.c, line 12.
(gdb) r
Starting program: /home/twaugh/Documents/GDB/prog
[...]
Breakpoint 1, main () at prog.c:12
12      int n = 0;
(gdb) target record-full
(gdb) c
Continuing.

Program received signal SIGSEGV, Segmentation fault.
0x0000000000401154 in sometimes_crashes (f=0x0) at prog.c:7
7      return *f;
(gdb) reverse-finish
Run back to call of #0  0x0000000000401154 in sometimes_crashes (f=0x0)
        at prog.c:7
0x0000000000401190 in main () at prog.c:16
16      sometimes_crashes(0);

These are just some useful things that the GNU Debugger can do. There’s much more to discover. What is your favorite hidden, lesser-known, or surprising feature of gdb? Please share in the comments.

via: https://opensource.com/article/19/9/tips-gnu-debugger

Author: Tim Waugh Edited by: lujun9972 Translator: wxy Proofreader: wxy

This article is a LCTT original compilation, proudly presented by Linux China

5 Lesser-Known GDB Debugger Tips
😻: Still watching?

Leave a Comment