
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 linereverse-next
, step back to the previous source line, skipping function callsreverse-finish
, step back to the moment just before the current function is calledreverse-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
