= = = = = =

Learn how to use some of GDB’s lesser-known features to examine and fix code.

The GNU Debugger (GDB) is an invaluable tool for checking running processes and resolving problems while developing programs.

You can set breakpoints at specific locations (by function name, line number, and so on), enable and disable them, display and alter values, and perform all the standard actions that the debugger wants to do. But it also has many other features that you may not have tried. Here are five you can try.

Conditional breakpoints

Setting breakpoints is the first step in learning to use the GNU debugger. The program stops when it reaches a breakpoint, you can run GDB commands to check it or change variables, and then allow the program to continue.

For example, you might know that a frequently called function sometimes crashes, but only when it gets a certain parameter value. You can set a breakpoint at the beginning of the function and run the program. Function arguments are displayed each time the breakpoint is hit, and if no value is provided for the argument that triggered the crash, operations can continue until the function is called again. When the troublesome parameter triggers a crash, you can step through the code to see where the problem lies.

(gdb) breaksometimes_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
Copy the code

To make this method more repeatable, you can count the number of times the function has been called before the particular call you are interested in, and set a counter at that breakpoint (for example, continue 30 to ignore the breakpoint for the next 29 times it is reached).

But the real power of breakpoints is their ability to evaluate expressions at run time, which allows you to automate such tests.

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)Copy the code

Conditional breakpoints allow you to avoid having GDB ask you what to do every time the function is called. Instead, conditional breakpoints allow GDB to stop at that location only if the value of a particular expression is true. If execution reaches the conditional breakpoint, but the expression evaluates to false, the debugger automatically lets the program continue without asking the user what to do.

Breakpoint commands

An even more complex feature of GNU debugger breakpoints is the ability to write scripts that respond to the arrival of breakpoints. The breakpoint command allows you to write a series of GNU debugger commands to run when the breakpoint is reached.

We can use it to circumvent the bugs we know about in the sometimes_crashes function and make it return harmlessly 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 a breakpoint is hit, the stack frame is displayed, even before the breakpoint command is run.

(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)
Copy the code

Dump binary memory

The GNU debugger has built-in support for checking memory in various formats, including octal, hexadecimal, and so on, using the X command. But I like to see two formats side by side: hexadecimal bytes on the left and ASCII characters represented by the same bytes on the right.

I often use hexdump -c (hexdump comes from the util-Linux package) when I want to view the contents of a file byte by byte. Here are the hexadecimal bytes shown by GDB’s x command:

(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
Copy the code

What if you want GDB to display memory like hexdump? This is fine, in fact, you can use this method in any format you like.

By using the dump command to store bytes in a file, combined with the shell command to run hexdump on the 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
Copy the code

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

(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.. 00 | 00000020 | | 00000021Copy the code

Inline disassembly

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

The disAssemble command lets you see the CPU instructions that implement the function. But sometimes the output can be hard to track. In general, I want to look at the instructions that correspond to a particular part of the function’s source code. To do this, include the source line in disassembly using the /s modifier.

(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...]
Copy the code

Here, using the INFO register to see the current values of all CPU registers and executing one instruction at a time with commands like stepi gives you a more detailed view of the program.

Reverse debugging

Sometimes, you wish you could turn back time. Imagine that you have reached the monitoring point of variables. A monitor is like a breakpoint, but instead of being set somewhere in the program, it is set on an expression (using the watch command). Whenever the value of the expression changes, execution stops and the debugger gains control.

Imagine that you have reached this monitoring point, and the memory used by that variable has changed its value. As it turns out, it may have been caused by something that happened earlier. For example, memory has been freed and is now being reused. But when and where was it released?

The GNU debugger even solves this problem because you can run the program in reverse!

It does this by carefully recording the state of the program at each step so that the previously recorded state can be restored, creating the illusion of going back in time.

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

  • reverse-stepGo back to the previous source code line
  • *reverse-next, it goes backwards to the previous source code line, skipping function calls backwards
  • reverse-finish, back to the time when the current function was about to be called
  • reverse-continue, which returns to the previous state in the program that will (now) trigger the breakpoint (or other state that causes the breakpoint to stop)

Here is 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. 0x0000000000401154in 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);
Copy the code

These are just some of the useful things the GNU debugger can do. Much more remains to be discovered. What’s your favorite hidden, little-known, or surprising feature of GDB? Please share in the comments.


Via: opensource.com/article/19/…

By Tim Waugh, lujun9972

This article is originally compiled by LCTT and released in Linux China