Tuesday, February 7, 2012

Backporting "watch -l" from GDB 7.4+ using Python

GDB 7.4 added a very useful "-l / -location" option to the "watch expr" command[1]:

"Ordinarily a watchpoint respects the scope of variables in expr. The -location argument tells gdb to instead watch the memory referred to by expr. In this case, gdb will evaluate expr, take the address of the result, and watch the memory at that address.".

i.e. If you do "watch p->field_that_gets_corrupted" the watch point will get deleted when local variable p goes out of scope while "watch -l" works the way you want.

For those stuck using GDB 7.2 (as shipped in Ubuntu 11.04) the following GDB Python script ( gdb-watch-location.py) might help:

import gdb

def _watch_location(expr, watch_point_class):
    l = gdb.parse_and_eval(expr).address
    wp_str = '*(%(type)s)(%(address)s)' % dict(type=l.type,
                                               address=l)
    gdb.Breakpoint(wp_str, gdb.BP_WATCHPOINT, watch_point_class)

class _WatchLocationCommand(gdb.Command):
    'Like "watch -l" in gdb 7.4+'
    def __init__(self):
        gdb.Command.__init__(self, 'watch-l',
                             gdb.COMMAND_BREAKPOINTS,
                             gdb.COMPLETE_SYMBOL)

    def invoke(self, arg, from_tty):
        _watch_location(arg, gdb.WP_WRITE)

class _RWatchLocationCommand(gdb.Command):
    'Like "rwatch -l" in gdb 7.4+'
    ...

class _AWatchLocationCommand(gdb.Command):
    'Like "awatch -l" in gdb 7.4+'
    ...

_WatchLocationCommand()
_RWatchLocationCommand()
_AWatchLocationCommand()

Suppose you're working with the following C code snippet:

struct bag {
        int a, b;
};

static void bag_poke(struct bag *p)
{
        p->a = 1;
}

Here's how gdb-watch-locations.py works:

In [1]: gdb.parse_and_eval('p->a')
Out[1]: <gdb.Value at 0x294a070>
  • A gdb.Value has a type and an address
In [2]: v = gdb.parse_and_eval('p->a')

In [3]: print v.type, v.address
int 0x7fffffffdc30
  • We use that to create a GDB watchpoint expression which we later pass to gdb.Breakpoint:
In [4]: '*(%(type)s)(%(address)s)' % dict(type=v.address.type, address=v.address)
u'*(int *)(0x7fffffffdc30)'
  • The code then adds three new GDB commands: "watch-l", "rwatch-l" and "awatch-l". Tells GDB that they should be classified as breakpoint commands in the online help system and use symbols for TAB completion.

Sample session:

$ gdb -x gdb-watch-locations.py data-access
(gdb) break bag_poke
Breakpoint 1 at 0x40050c: file data-access.c, line 9.
(gdb) run
Starting program: /home/scottt/work/scottt-gdb/data-access 

Breakpoint 1, bag_poke (p=0x7fffffffdc40) at data-access.c:9
9  p->a = 1;
(gdb) wa
watch    watch-l  
(gdb) watch-l p->a
Hardware watchpoint 2: *(int *)(0x7fffffffdc40)
(gdb) continue 
Continuing.
Hardware watchpoint 2: *(int *)(0x7fffffffdc40)

Old value = 0
New value = 1
bag_poke (p=0x7fffffffdc40) at data-access.c:10
10 }

See Also

Tuesday, January 31, 2012

Exploring the Gdb Python API with IPython

Exploring the Gdb Python API with IPython

I've found a way to explore GDB's nice Python API in a comfortable programming environment with Python name completion by using IPython.

Save the following content in $HOME/bin/gdbipy:

#!/usr/bin/gdb --python
# vim: set filetype=python:

from IPython.zmq.ipkernel import IPKernelApp

app = IPKernelApp.instance()
app.initialize([])
app.start()

Running gdbipy would start an ipython "kernel" within the gdb process:

$ chmod +x ~/bin/gdbipy
$ gdbipy 
[IPKernelApp] To connect another client to this kernel, use:
[IPKernelApp] --existing kernel-23135.json

In another terminal, run:

$ ipython console --existing kernel-23135.json

In [1]: import gdb

In [2]: gdb.<TAB>
Display all 166 possibilities? (y or n)
gdb.ARCH_FRAME                   gdb.SYMBOL_LOC_REGPARM_ADDR
gdb.BP_ACCESS_WATCHPOINT         gdb.SYMBOL_LOC_STATIC
gdb.BP_BREAKPOINT                gdb.SYMBOL_LOC_TYPEDEF
...

You can quit the ipython console and restart it at anytime without losing state.

Here's an example session:

$ ipython console --existing kernel-23735.json

In [2]: gdb.execute('file /bin/cat')

In [3]: gdb.execute('start')

In [4]: o = gdb.execute('disassemble exit', to_string=True); print o
Dump of assembler code for function __GI_exit:
   0x0000003c902399a0 <+0>: lea    0x375cc1(%rip),%rsi        # 0x3c905af668 <__exit_funcs>
   0x0000003c902399a7 <+7>: sub    $0x8,%rsp
   0x0000003c902399ab <+11>: mov    $0x1,%edx
   0x0000003c902399b0 <+16>: callq  0x3c902398a0 <__run_exit_handlers>
End of assembler dump.

In [5]: print gdb.parse_and_eval('main').type
int (int, char **)

Having an interactive Python environment with name completion makes exploring and learning the gdb.Value API quite a bit easier.

The reason we need to run "ipython console" in a separate process from GDB is so that the two don't fight over the terminal settings. You can experience what that's like by changing the content of the gdbipy script to just "import IPython; IPython.embed()" which embeds an IPython read-eval-print loop in-process. The result is partially garbled terminal output and non functional TAB completion. The two process IPython console solution presented requires IPython 0.12+ and works out of the box on Fedora 16.

See Also

Update Feb 2: changed IPython version requirement from 0.11 to 0.12. After Paul Ivanov pointed it out in the comments.