SourceXR

C/C++ Cross-Reference Tool

Reverse Debugging with the GNU Debugger

Many times when a program is being debugged, the faulty behavior occurs once or we easily pass the point where the bug is occurring. The dream of many software developers is to be able to go backward in time and examine closely what happened.

A new feature added in gdb 7 is the reverse debugging support: the ability to go backward.

We describe here a debugging session with reverse debugging. Reverse debugging is available on the i386 and amd64 platforms.

Support is being added for additional platforms. Internally it works by storing the machine state while the program is being executed. Some parameters control the number of instructions recorded, etc.

Faulty Program

First the source code of a faulty program. The bug is pretty obvious!

#include <string.h>

void g (const char *&s) {
    s = 0;
}

int f (const char *s) {

    g (s);
    int i = strlen (s);
    return i;
}

int main () {

    const char *s = "string";

    int i = f (s);
    return 0;
}

If we run the program it crashes at expected. So let's start our favorite debugger and find out what is going on.

Reverse Debugging Commands

The usual continue, next, step, etc. commands have their counterpart prefixed by reverse:

  • reverse-continue
  • reverse-finish
  • reverse-next
  • reverse-nexti
  • reverse-search
  • reverse-step
  • reverse-stepi

They perform the expected function, but in reverse: the program runs backward.

Reverse Debugging Session

Set a breakpoint at the beginning of the program (before the expected bug)

Run the program, and when it hits the breakpoint issue the followingcommand to log the program execution (otherwise you'll get this error message: "Target child does not support this command."):

target record

Then you can continue, and let the program fails with a segfault:

Program received signal SIGSEGV, Segmentation fault.

Then, it is easy to spot why the program is in segfault (strlen of a null pointer) but when a null pointer was passed to this function? If your program has many execution paths it can take time to find out. Or it can save time if the bug occurrence is depending on external condition or is difficult to reproduce. You just need one failed execution to be able to understand what happened. This is were reverse debugging comes to play.

Using the reverse-* commands you can go backward in the execution and find out when the pointer was set to null.

First of all the stack:

#0  __strlen_sse42 () at ../sysdeps/x86_64/multiarch/strlen-sse4.S:32
#1  0x0000000000400605 in f (s=0x0) at gdbreverse.cpp:11
#2  0x0000000000400629 in main () at gdbreverse.cpp:19

Then we go backward several times (rn is a shortcut for reverse-next):

(gdb) rn
32      in ../sysdeps/x86_64/multiarch/strlen-sse4.S
(gdb)
31      in ../sysdeps/x86_64/multiarch/strlen-sse4.S
(gdb)
30      in ../sysdeps/x86_64/multiarch/strlen-sse4.S
(gdb)
29      in ../sysdeps/x86_64/multiarch/strlen-sse4.S
(gdb)
28      in ../sysdeps/x86_64/multiarch/strlen-sse4.S
(gdb)
27      in ../sysdeps/x86_64/multiarch/strlen-sse4.S
(gdb)
f (s=0x0) at gdbreverse.cpp:11
11          int i = strlen (s);
(gdb) p s
$1 = 0x0
(gdb) rn
10          g (s);
(gdb) p s
$2 = 0x4006ec "string"

Notice that before the call to g, the s pointer was fine. The bug is occurring in g.

We jump now in g (with a step command) and find out the bug.

Conclusion

These gdb commands add a new way to ease debugging and no longer waste time trying to reproduce many times a bug. The first time the bug is noticed, just stop the process and run it backwards to find out where the bug occurred. That's awesome!

Comments !