diff --git a/Lib/test/test_gdb.py b/Lib/test/test_gdb.py index 2af5800..dff7951 100644 --- a/Lib/test/test_gdb.py +++ b/Lib/test/test_gdb.py @@ -46,7 +46,8 @@ class DebuggerTests(unittest.TestCase): ).communicate() return out, err - def get_stack_trace(self, source, breakpoint='PyObject_Print', + def get_stack_trace(self, source=None, script=None, + breakpoint='PyObject_Print', commands_after_breakpoint=None, import_site=False): ''' @@ -77,7 +78,8 @@ class DebuggerTests(unittest.TestCase): 'run'] if commands_after_breakpoint: commands += commands_after_breakpoint - commands += ['backtrace'] + else: + commands += ['backtrace'] # print commands @@ -91,7 +93,10 @@ class DebuggerTests(unittest.TestCase): # -S suppresses the default 'import site' args += ["-S"] - args += ["-c", source] + if source: + args += ["-c", source] + elif script: + args += [script] # print args @@ -116,14 +121,25 @@ class DebuggerTests(unittest.TestCase): # # For a nested structure, the first time we hit the breakpoint will # give us the top-level structure - gdb_output = self.get_stack_trace(source, 'PyObject_Print', - commands_after_breakpoint, - import_site) + gdb_output = self.get_stack_trace(source, breakpoint='PyObject_Print', + commands_after_breakpoint=commands_after_breakpoint, + import_site=import_site) m = re.match('.*#0 PyObject_Print \(op\=(.*?), fp=.*\).*', gdb_output, re.DOTALL) #print m.groups() return m.group(1), gdb_output + def assertEndsWith(self, actual, exp_end): + '''Ensure that the given "actual" string ends with "exp_end"''' + self.assert_(actual.endswith(exp_end), + msg='%r did not end with %r' % (actual, exp_end)) + + def assertMultilineMatches(self, actual, pattern): + m = re.match(pattern, actual, re.DOTALL) + self.assert_(m, + msg='%r did not match %r' % (actual, pattern)) + +class PrettyPrintTests(DebuggerTests): def test_getting_backtrace(self): gdb_output = self.get_stack_trace('print 42') self.assertTrue('PyObject_Print' in gdb_output) @@ -136,6 +152,7 @@ class DebuggerTests(unittest.TestCase): self.assertEquals(gdb_repr, repr(val), gdb_output) def test_int(self): + 'Verify the pretty-printing of various "int" values' self.assertGdbRepr(42) self.assertGdbRepr(0) self.assertGdbRepr(-7) @@ -143,37 +160,46 @@ class DebuggerTests(unittest.TestCase): self.assertGdbRepr(-sys.maxint) def test_long(self): + 'Verify the pretty-printing of various "long" values' self.assertGdbRepr(0L) self.assertGdbRepr(1000000000000L) self.assertGdbRepr(-1L) self.assertGdbRepr(-1000000000000000L) def test_singletons(self): + 'Verify the pretty-printing of True, False and None' self.assertGdbRepr(True) self.assertGdbRepr(False) self.assertGdbRepr(None) def test_dicts(self): + 'Verify the pretty-printing of dictionaries' self.assertGdbRepr({}) self.assertGdbRepr({'foo': 'bar'}) def test_lists(self): + 'Verify the pretty-printing of lists' self.assertGdbRepr([]) self.assertGdbRepr(range(5)) def test_strings(self): + 'Verify the pretty-printing of strings' self.assertGdbRepr('') self.assertGdbRepr('And now for something hopefully the same') + self.assertGdbRepr('string with embedded NUL here \0 and then some more text') def test_tuples(self): + 'Verify the pretty-printing of tuples' self.assertGdbRepr(tuple()) self.assertGdbRepr((1,)) def test_unicode(self): + 'Verify the pretty-printing of unicode values' self.assertGdbRepr(u'hello world') self.assertGdbRepr(u'\u2620') def test_classic_class(self): + 'Verify the pretty-printing of classic class instances' gdb_repr, gdb_output = self.get_gdb_repr(''' class Foo: pass @@ -185,6 +211,7 @@ print foo''') msg='Unexpected classic-class rendering %r' % gdb_repr) def test_modern_class(self): + 'Verify the pretty-printing of new-style class instances' gdb_repr, gdb_output = self.get_gdb_repr(''' class Foo(object): pass @@ -196,6 +223,7 @@ print foo''') msg='Unexpected new-style class rendering %r' % gdb_repr) def test_subclassing_list(self): + 'Verify the pretty-printing of an instance of a list subclass' gdb_repr, gdb_output = self.get_gdb_repr(''' class Foo(list): pass @@ -208,8 +236,9 @@ print foo''') msg='Unexpected new-style class rendering %r' % gdb_repr) def test_subclassing_tuple(self): - '''This should exercise the negative tp_dictoffset code in the - new-style class support''' + 'Verify the pretty-printing of an instance of a tuple subclass' + # This should exercise the negative tp_dictoffset code in the + # new-style class support gdb_repr, gdb_output = self.get_gdb_repr(''' class Foo(tuple): pass @@ -228,7 +257,7 @@ print foo''') representation''' gdb_repr, gdb_output = \ self.get_gdb_repr(source, - commands_after_breakpoint=[corruption]) + commands_after_breakpoint=[corruption, 'backtrace']) self.assertTrue(re.match('<%s at remote 0x[0-9a-f]+>' % exp_type, gdb_repr), 'Unexpected gdb representation: %r\n%s' % \ @@ -236,27 +265,37 @@ print foo''') def test_NULL_ptr(self): 'Ensure that a NULL PyObject* is handled gracefully' - self.assertSane('print 42', - 'set variable op=0') + gdb_repr, gdb_output = ( + self.get_gdb_repr('print 42', + commands_after_breakpoint=['set variable op=0', + 'backtrace']) + ) + + self.assertEquals(gdb_repr, '0x0') def test_NULL_ob_type(self): + 'Ensure that a PyObject* with NULL ob_type is handled gracefully' self.assertSane('print 42', 'set op->ob_type=0') def test_corrupt_ob_type(self): + 'Ensure that a PyObject* with a corrupt ob_type is handled gracefully' self.assertSane('print 42', 'set op->ob_type=0xDEADBEEF') def test_corrupt_tp_flags(self): + 'Ensure that a PyObject* with a type with corrupt tp_flags is handled' self.assertSane('print 42', 'set op->ob_type->tp_flags=0x0', exp_type='int') def test_corrupt_tp_name(self): + 'Ensure that a PyObject* with a type with corrupt tp_name is handled' self.assertSane('print 42', 'set op->ob_type->tp_name=0xDEADBEEF') def test_NULL_instance_dict(self): + 'Ensure that a PyInstanceObject with with a NULL in_dict is handled' self.assertSane(''' class Foo: pass @@ -267,7 +306,7 @@ print foo''', exp_type='Foo') def test_builtins_help(self): - # Ensure that the new-style class _Helper in site.py can be handled + 'Ensure that the new-style class _Helper in site.py can be handled' # (this was the issue causing tracebacks in # http://bugs.python.org/issue8032#msg100537 ) @@ -280,8 +319,109 @@ print foo''', # frames +class PyListTests(DebuggerTests): + def assertListing(self, expected, actual): + self.assertEndsWith(actual, expected) + + def test_basic_command(self): + 'Verify that the "py-list" command works' + bt = self.get_stack_trace(script='Lib/test/test_gdb_sample.py', + commands_after_breakpoint=['py-list']) + + self.assertListing(''' + 5 + 6 def bar(a, b, c): + 7 baz(a, b, c) + 8 + 9 def baz(*args): + 10 print 42 + 11 + 12 foo(1, 2, 3) +''', + bt) + + def test_one_abs_arg(self): + 'Verify the "py-list" command with one absolute argument' + bt = self.get_stack_trace(script='Lib/test/test_gdb_sample.py', + commands_after_breakpoint=['py-list 9']) + + self.assertListing(''' + 9 def baz(*args): + 10 print 42 + 11 + 12 foo(1, 2, 3) +''', + bt) + + def test_two_abs_args(self): + 'Verify the "py-list" command with two absolute arguments' + bt = self.get_stack_trace(script='Lib/test/test_gdb_sample.py', + commands_after_breakpoint=['py-list 1,3']) + + self.assertListing(''' + 1 # Sample script for use by test_gdb.py + 2 + 3 def foo(a, b, c): +''', + bt) + +class StackNavigationTests(DebuggerTests): + def test_pyup_command(self): + 'Verify that the "py-up" command works' + bt = self.get_stack_trace(script='Lib/test/test_gdb_sample.py', + commands_after_breakpoint=['py-up']) + self.assertMultilineMatches(bt, + r'''^.* +Frame 0x[0-9a-f]+, for file Lib/test/test_gdb_sample.py, line 7, in bar \(a=1, b=2, c=3\) + baz\(a, b, c\) +$''') + + def test_down_at_bottom(self): + 'Verify handling of "py-down" at the bottom of the stack' + bt = self.get_stack_trace(script='Lib/test/test_gdb_sample.py', + commands_after_breakpoint=['py-down']) + self.assertEndsWith(bt, + 'Unable to find a newer python frame\n') + + def test_up_at_top(self): + 'Verify handling of "py-up" at the top of the stack' + bt = self.get_stack_trace(script='Lib/test/test_gdb_sample.py', + commands_after_breakpoint=['py-up'] * 4) + self.assertEndsWith(bt, + 'Unable to find an older python frame\n') + + def test_up_then_down(self): + 'Verify "py-up" followed by "py-down"' + bt = self.get_stack_trace(script='Lib/test/test_gdb_sample.py', + commands_after_breakpoint=['py-up', 'py-down']) + self.assertMultilineMatches(bt, + r'''^.* +Frame 0x[0-9a-f]+, for file Lib/test/test_gdb_sample.py, line 7, in bar \(a=1, b=2, c=3\) + baz\(a, b, c\) +Frame 0x[0-9a-f]+, for file Lib/test/test_gdb_sample.py, line 10, in baz \(args=\(1, 2, 3\)\) + print 42 +$''') + +class PyBtTests(DebuggerTests): + def test_basic_command(self): + 'Verify that the "py-bt" command works' + bt = self.get_stack_trace(script='Lib/test/test_gdb_sample.py', + commands_after_breakpoint=['py-bt']) + self.assertMultilineMatches(bt, + r'''^.* + Frame 0x[0-9a-f]+, for file Lib/test/test_gdb_sample.py, line 7, in bar \(a=1, b=2, c=3\) + baz\(a, b, c\) + Frame 0x[0-9a-f]+, for file Lib/test/test_gdb_sample.py, line 4, in foo \(a=1, b=2, c=3\) + bar\(a, b, c\) + Frame 0x[0-9a-f]+, for file Lib/test/test_gdb_sample.py, line 12, in \(\) +foo\(1, 2, 3\) +''') + def test_main(): - run_unittest(DebuggerTests) + run_unittest(PrettyPrintTests, + PyListTests, + StackNavigationTests, + PyBtTests) if __name__ == "__main__": test_main() diff --git a/Lib/test/test_gdb_sample.py b/Lib/test/test_gdb_sample.py new file mode 100644 index 0000000..e66bad2 --- /dev/null +++ b/Lib/test/test_gdb_sample.py @@ -0,0 +1,12 @@ +# Sample script for use by test_gdb.py + +def foo(a, b, c): + bar(a, b, c) + +def bar(a, b, c): + baz(a, b, c) + +def baz(*args): + print 42 + +foo(1, 2, 3) diff --git a/Tools/gdb/libpython.py b/Tools/gdb/libpython.py index 1ef0e69..204dde3 100644 --- a/Tools/gdb/libpython.py +++ b/Tools/gdb/libpython.py @@ -2,7 +2,9 @@ ''' From gdb 7 onwards, gdb's build can be configured --with-python, allowing gdb to be extended with Python code e.g. for library-specific data visualizations, -such as for the C++ STL types. +such as for the C++ STL types. Documentation on this API can be seen at: +http://sourceware.org/gdb/current/onlinedocs/gdb/Python-API.html + This python module deals with the case when the process being debugged (the "inferior process" in gdb parlance) is itself python, or more specifically, @@ -21,12 +23,12 @@ holding three PyObject* that turn out to be PyStringObject* instances, we can generate a proxy value within the gdb process that is a list of strings: ["foo", "bar", "baz"] -We try to defer all gdb.lookup_type() invocations until as late as possible: -when the /usr/bin/python process starts in the debugger, the libpython.so -hasn't been dynamically loaded yet, so none of the type names are known to -the debugger +We try to defer gdb.lookup_type() invocations for python types until as late as +possible: for a dynamically linked python binary, when the process starts in +the debugger, the libpython.so hasn't been dynamically loaded yet, so none of +the type names are known to the debugger -Tested with both libpython2.6 and libpython3.1 +The module also extends gdb with some python-specific commands. ''' import gdb @@ -162,6 +164,11 @@ class PyObjectPtr(object): self.address = address def __repr__(self): + # For the NULL pointer, we have no way of knowing a type, so + # special-case it as per + # http://bugs.python.org/issue8032#msg100882 + if self.address == 0: + return '0x0' return '<%s at remote 0x%x>' % (self.tp_name, self.address) return FakeRepr(self.safe_tp_name(), @@ -284,8 +291,8 @@ class HeapTypeObjectPtr(PyObjectPtr): ''' Support for new-style classes. - Currently we just locate the dictionary using _PyObject_GetDictPtr, - ignoring descriptors + Currently we just locate the dictionary using a transliteration to + python of _PyObject_GetDictPtr, ignoring descriptors ''' attr_dict = {} @@ -346,6 +353,26 @@ class PyCodeObjectPtr(PyObjectPtr): """ _typename = 'PyCodeObject' + def addr2line(self, addrq): + ''' + Get the line number for a given bytecode offset + + Analogous to PyCode_Addr2Line; translated from pseudocode in + Objects/lnotab_notes.txt + ''' + co_lnotab = PyObjectPtr.from_pyobject_ptr(self.field('co_lnotab')).proxyval() + + # Initialize lineno to co_firstlineno as per PyCode_Addr2Line + # not 0, as lnotab_notes.txt has it: + lineno = int_from_int(self.field('co_firstlineno')) + + addr = 0 + for addr_incr, line_incr in zip(co_lnotab[::2], co_lnotab[1::2]): + addr += ord(addr_incr) + if addr > addrq: + return lineno + lineno += ord(line_incr) + return lineno class PyDictObjectPtr(PyObjectPtr): """ @@ -466,9 +493,10 @@ class PyStringObjectPtr(PyObjectPtr): _typename = 'PyStringObject' def __str__(self): + field_ob_size = self.field('ob_size') field_ob_sval = self.field('ob_sval') char_ptr = field_ob_sval.address.cast(_type_char_ptr) - return char_ptr.string() + return ''.join([chr(field_ob_sval[i]) for i in safe_range(field_ob_size)]) def proxyval(self): return str(self) @@ -537,6 +565,7 @@ class FrameInfo: self.co_name = PyObjectPtr.from_pyobject_ptr(self.co.field('co_name')) self.co_filename = PyObjectPtr.from_pyobject_ptr(self.co.field('co_filename')) self.f_lineno = int_from_int(fval.field('f_lineno')) + self.f_lasti = int_from_int(fval.field('f_lasti')) self.co_nlocals = int_from_int(self.co.field('co_nlocals')) self.co_varnames = PyTupleObjectPtr.from_pyobject_ptr(self.co.field('co_varnames')) self.locals = [] # list of kv pairs @@ -551,10 +580,40 @@ class FrameInfo: #print 'value=%s' % value self.locals.append((str(name), value)) + def filename(self): + '''Get the path of the current Python source file, as a string''' + return self.co_filename.proxyval() + + def current_line_num(self): + '''Get current line number as an integer (1-based) + + Translated from PyFrame_GetLineNumber and PyCode_Addr2Line + + See Objects/lnotab_notes.txt + ''' + f_trace = self.fval.field('f_trace') + if long(f_trace) != 0: + # we have a non-NULL f_trace: + return self.f_lineno + else: + #try: + return self.co.addr2line(self.f_lasti) + #except ValueError: + # return self.f_lineno + + def current_line(self): + '''Get the text of the current source line as a string, with a trailing + newline character''' + with open(self.filename(), 'r') as f: + all_lines = f.readlines() + # Convert from 1-based current_line_num to 0-based list offset: + return all_lines[self.current_line_num()-1] + def __str__(self): - return ('File %s, line %i, in %s (%s)' - % (self.co_filename, - self.f_lineno, + return ('Frame 0x%x, for file %s, line %i, in %s (%s)' + % (long(self.fval._gdbval), + self.co_filename, + self.current_line_num(), self.co_name, ', '.join(['%s=%s' % (k, stringify(v)) for k, v in self.locals])) ) @@ -618,3 +677,172 @@ def register (obj): obj.pretty_printers.append(pretty_printer_lookup) register (gdb.current_objfile ()) + +def get_python_frame(gdb_frame): + try: + f = gdb_frame.read_var('f') + return PyFrameObjectPtr.from_pyobject_ptr(f) + except ValueError: + return None + +def get_selected_python_frame(): + '''Try to obtain a (gdbframe, PyFrameObjectPtr) pair for the + currently-running python code, or (None, None)''' + gdb_frame = gdb.selected_frame() + while gdb_frame: + if (gdb_frame.function() is None or + gdb_frame.function().name != 'PyEval_EvalFrameEx'): + gdb_frame = gdb_frame.older() + continue + + try: + f = gdb_frame.read_var('f') + return gdb_frame, PyFrameObjectPtr.from_pyobject_ptr(f) + except ValueError: + gdb_frame = gdb_frame.older() + return None, None + +class PyList(gdb.Command): + '''List the current Python source code, if any + + Use + py-list START + to list at a different line number within the python source. + + Use + py-list START, END + to list a specific range of lines within the python source. + ''' + + def __init__(self): + gdb.Command.__init__ (self, + "py-list", + gdb.COMMAND_FILES, + gdb.COMPLETE_NONE) + + + def invoke(self, args, from_tty): + import re + + start = None + end = None + + m = re.match(r'\s*(\d+)\s*', args) + if m: + start = int(m.group(0)) + end = start + 10 + + m = re.match(r'\s*(\d+)\s*,\s*(\d+)\s*', args) + if m: + start, end = map(int, m.groups()) + + gdb_frame, py_frame = get_selected_python_frame() + if not py_frame: + print 'Unable to locate python frame' + return + + fi = FrameInfo(py_frame) + filename = fi.filename() + lineno = fi.current_line_num() + + if start is None: + start = lineno - 5 + end = lineno + 5 + + if start<1: + start = 1 + + with open(filename, 'r') as f: + all_lines = f.readlines() + # start and end are 1-based, all_lines is 0-based; + # so [start-1:end] as a python slice gives us [start, end] as a + # closed interval + for i, line in enumerate(all_lines[start-1:end]): + sys.stdout.write('%4s %s' % (i+start, line)) + + +# ...and register the command: +PyList() + +def move_in_stack(move_up): + '''Move up or down the stack (for the py-up/py-down command)''' + gdb_frame, py_frame = get_selected_python_frame() + while gdb_frame: + if move_up: + iter_frame = gdb_frame.older() + else: + iter_frame = gdb_frame.newer() + + if not iter_frame: + break + + if (iter_frame.function() and + iter_frame.function().name == 'PyEval_EvalFrameEx'): + # Result: + iter_frame.select() + py_frame = get_python_frame(iter_frame) + fi = FrameInfo(py_frame) + print fi + sys.stdout.write(fi.current_line()) + return + + gdb_frame = iter_frame + + if move_up: + print 'Unable to find an older python frame' + else: + print 'Unable to find a newer python frame' + +class PyUp(gdb.Command): + 'Select and print the python stack frame that called this one (if any)' + def __init__(self): + gdb.Command.__init__ (self, + "py-up", + gdb.COMMAND_STACK, + gdb.COMPLETE_NONE) + + + def invoke(self, args, from_tty): + move_in_stack(move_up=True) + +PyUp() + +class PyDown(gdb.Command): + 'Select and print the python stack frame called by this one (if any)' + def __init__(self): + gdb.Command.__init__ (self, + "py-down", + gdb.COMMAND_STACK, + gdb.COMPLETE_NONE) + + + def invoke(self, args, from_tty): + move_in_stack(move_up=False) + +PyDown() + +class PyBacktrace(gdb.Command): + 'Display the current python frame and all the frames within its call stack (if any)' + def __init__(self): + gdb.Command.__init__ (self, + "py-bt", + gdb.COMMAND_STACK, + gdb.COMPLETE_NONE) + + + def invoke(self, args, from_tty): + gdb_frame, py_frame = get_selected_python_frame() + while gdb_frame: + gdb_frame = gdb_frame.older() + + if not gdb_frame: + break + + if (gdb_frame.function() and + gdb_frame.function().name == 'PyEval_EvalFrameEx'): + py_frame = get_python_frame(gdb_frame) + fi = FrameInfo(py_frame) + print ' ', fi + sys.stdout.write(fi.current_line()) + +PyBacktrace()