diff --git a/Lib/test/test_gdb.py b/Lib/test/test_gdb.py index dff7951..3e54776 100644 --- a/Lib/test/test_gdb.py +++ b/Lib/test/test_gdb.py @@ -48,7 +48,7 @@ class DebuggerTests(unittest.TestCase): def get_stack_trace(self, source=None, script=None, breakpoint='PyObject_Print', - commands_after_breakpoint=None, + cmds_after_breakpoint=None, import_site=False): ''' Run 'python -c SOURCE' under gdb with a breakpoint. @@ -57,7 +57,7 @@ class DebuggerTests(unittest.TestCase): Returns the stdout from gdb - commands_after_breakpoint: if provided, a list of strings: gdb commands + cmds_after_breakpoint: if provided, a list of strings: gdb commands ''' # We use "set breakpoint pending yes" to avoid blocking with a: # Function "foo" not defined. @@ -76,8 +76,8 @@ class DebuggerTests(unittest.TestCase): commands = ['set breakpoint pending yes', 'break %s' % breakpoint, 'run'] - if commands_after_breakpoint: - commands += commands_after_breakpoint + if cmds_after_breakpoint: + commands += cmds_after_breakpoint else: commands += ['backtrace'] @@ -99,7 +99,8 @@ class DebuggerTests(unittest.TestCase): args += [script] # print args - + # print ' '.join(args) + # Use "args" to invoke gdb, capturing stdout, stderr: out, err = self.run_gdb(*args) @@ -112,7 +113,7 @@ class DebuggerTests(unittest.TestCase): return out def get_gdb_repr(self, source, - commands_after_breakpoint=None, + cmds_after_breakpoint=None, import_site=False): # Given an input python source representation of data, # run "python -c'print DATA'" under gdb with a breakpoint on @@ -122,7 +123,7 @@ 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, breakpoint='PyObject_Print', - commands_after_breakpoint=commands_after_breakpoint, + cmds_after_breakpoint=cmds_after_breakpoint, import_site=import_site) m = re.match('.*#0 PyObject_Print \(op\=(.*?), fp=.*\).*', gdb_output, re.DOTALL) @@ -144,11 +145,11 @@ class PrettyPrintTests(DebuggerTests): gdb_output = self.get_stack_trace('print 42') self.assertTrue('PyObject_Print' in gdb_output) - def assertGdbRepr(self, val, commands_after_breakpoint=None): + def assertGdbRepr(self, val, cmds_after_breakpoint=None): # Ensure that gdb's rendering of the value in a debugged process # matches repr(value) in this process: gdb_repr, gdb_output = self.get_gdb_repr('print ' + repr(val), - commands_after_breakpoint) + cmds_after_breakpoint) self.assertEquals(gdb_repr, repr(val), gdb_output) def test_int(self): @@ -187,6 +188,7 @@ class PrettyPrintTests(DebuggerTests): self.assertGdbRepr('') self.assertGdbRepr('And now for something hopefully the same') self.assertGdbRepr('string with embedded NUL here \0 and then some more text') + self.assertGdbRepr('this is byte 255:\xff and byte 128:\x80') def test_tuples(self): 'Verify the pretty-printing of tuples' @@ -198,6 +200,47 @@ class PrettyPrintTests(DebuggerTests): self.assertGdbRepr(u'hello world') self.assertGdbRepr(u'\u2620') + def test_sets(self): + 'Verify the pretty-printing of sets' + self.assertGdbRepr(set()) + self.assertGdbRepr(set(['a','b','c'])) + self.assertGdbRepr(set([4, 5, 6])) + + # Ensure that we handled sets containing the "dummy" key value, + # which happens on deletion: + gdb_repr, gdb_output = self.get_gdb_repr('''s = set(['a','b']) +s.pop() +print s''') + self.assertEquals(gdb_repr, "set(['b'])") + + def test_frozensets(self): + 'Verify the pretty-printing of frozensets' + self.assertGdbRepr(frozenset()) + self.assertGdbRepr(frozenset(['a','b','c'])) + self.assertGdbRepr(frozenset([4, 5, 6])) + + def test_exceptions(self): + # Test a RuntimeError + gdb_repr, gdb_output = self.get_gdb_repr(''' +try: + raise RuntimeError("I am an error") +except RuntimeError, e: + print e +''') + self.assertEquals(gdb_repr, + "exceptions.RuntimeError('I am an error',)") + + + # Test division by zero: + gdb_repr, gdb_output = self.get_gdb_repr(''' +try: + a = 1 / 0 +except ZeroDivisionError, e: + print e +''') + self.assertEquals(gdb_repr, + "exceptions.ZeroDivisionError('integer division or modulo by zero',)") + def test_classic_class(self): 'Verify the pretty-printing of classic class instances' gdb_repr, gdb_output = self.get_gdb_repr(''' @@ -255,9 +298,14 @@ print foo''') Verify that the variable's representation is the expected failsafe representation''' + if corruption: + cmds_after_breakpoint=[corruption, 'backtrace'] + else: + cmds_after_breakpoint=['backtrace'] + gdb_repr, gdb_output = \ self.get_gdb_repr(source, - commands_after_breakpoint=[corruption, 'backtrace']) + cmds_after_breakpoint=cmds_after_breakpoint) self.assertTrue(re.match('<%s at remote 0x[0-9a-f]+>' % exp_type, gdb_repr), 'Unexpected gdb representation: %r\n%s' % \ @@ -267,7 +315,7 @@ print foo''') 'Ensure that a NULL PyObject* is handled gracefully' gdb_repr, gdb_output = ( self.get_gdb_repr('print 42', - commands_after_breakpoint=['set variable op=0', + cmds_after_breakpoint=['set variable op=0', 'backtrace']) ) @@ -315,6 +363,67 @@ print foo''', self.assertTrue(m, msg='Unexpected rendering %r' % gdb_repr) + def test_selfreferential_list(self): + '''Ensure that a reference loop involving a list doesn't lead proxyval + into an infinite loop:''' + gdb_repr, gdb_output = \ + self.get_gdb_repr("a = [3, 4, 5] ; a.append(a) ; print a") + + self.assertEquals(gdb_repr, '[3, 4, 5, [...]]') + + gdb_repr, gdb_output = \ + self.get_gdb_repr("a = [3, 4, 5] ; b = [a] ; a.append(b) ; print a") + + self.assertEquals(gdb_repr, '[3, 4, 5, [[...]]]') + + def test_selfreferential_dict(self): + '''Ensure that a reference loop involving a dict doesn't lead proxyval + into an infinite loop:''' + gdb_repr, gdb_output = \ + self.get_gdb_repr("a = {} ; b = {'bar':a} ; a['foo'] = b ; print a") + + self.assertEquals(gdb_repr, "{'foo': {'bar': {...}}}") + + def test_selfreferential_old_style_instance(self): + gdb_repr, gdb_output = \ + self.get_gdb_repr(''' +class Foo: + pass +foo = Foo() +foo.an_attr = foo +print foo''') + self.assertTrue(re.match('\) at remote 0x[0-9a-f]+>', + gdb_repr), + 'Unexpected gdb representation: %r\n%s' % \ + (gdb_repr, gdb_output)) + + def test_selfreferential_new_style_instance(self): + gdb_repr, gdb_output = \ + self.get_gdb_repr(''' +class Foo(object): + pass +foo = Foo() +foo.an_attr = foo +print foo''') + self.assertTrue(re.match('\) at remote 0x[0-9a-f]+>', + gdb_repr), + 'Unexpected gdb representation: %r\n%s' % \ + (gdb_repr, gdb_output)) + + gdb_repr, gdb_output = \ + self.get_gdb_repr(''' +class Foo(object): + pass +a = Foo() +b = Foo() +a.an_attr = b +b.an_attr = a +print a''') + self.assertTrue(re.match('\) at remote 0x[0-9a-f]+>\) at remote 0x[0-9a-f]+>', + gdb_repr), + 'Unexpected gdb representation: %r\n%s' % \ + (gdb_repr, gdb_output)) + # TODO: # frames @@ -326,7 +435,7 @@ class PyListTests(DebuggerTests): 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']) + cmds_after_breakpoint=['py-list']) self.assertListing(''' 5 @@ -334,7 +443,7 @@ class PyListTests(DebuggerTests): 7 baz(a, b, c) 8 9 def baz(*args): - 10 print 42 + 10 print(42) 11 12 foo(1, 2, 3) ''', @@ -343,11 +452,11 @@ class PyListTests(DebuggerTests): 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']) + cmds_after_breakpoint=['py-list 9']) self.assertListing(''' 9 def baz(*args): - 10 print 42 + 10 print(42) 11 12 foo(1, 2, 3) ''', @@ -356,7 +465,7 @@ class PyListTests(DebuggerTests): 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']) + cmds_after_breakpoint=['py-list 1,3']) self.assertListing(''' 1 # Sample script for use by test_gdb.py @@ -369,7 +478,7 @@ 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']) + cmds_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\) @@ -379,34 +488,34 @@ $''') 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']) + cmds_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) + cmds_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']) + cmds_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 + 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']) + cmds_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\) diff --git a/Lib/test/test_gdb_sample.py b/Lib/test/test_gdb_sample.py index e66bad2..a732b25 100644 --- a/Lib/test/test_gdb_sample.py +++ b/Lib/test/test_gdb_sample.py @@ -7,6 +7,6 @@ def bar(a, b, c): baz(a, b, c) def baz(*args): - print 42 + print(42) foo(1, 2, 3) diff --git a/Tools/gdb/libpython.py b/Tools/gdb/libpython.py index 204dde3..44099a9 100644 --- a/Tools/gdb/libpython.py +++ b/Tools/gdb/libpython.py @@ -35,6 +35,7 @@ import gdb # Look up the gdb.Type for some standard types: _type_char_ptr = gdb.lookup_type('char').pointer() # char* +_type_unsigned_char_ptr = gdb.lookup_type('unsigned char').pointer() # unsigned char* _type_void_ptr = gdb.lookup_type('void').pointer() # void* _type_size_t = gdb.lookup_type('size_t') @@ -140,7 +141,7 @@ class PyObjectPtr(object): # Can't even read the object at all? return 'unknown' - def proxyval(self): + def proxyval(self, visited): ''' Scrape a value from the inferior process, and try to represent it within the gdb process, whilst (hopefully) avoiding crashes when @@ -150,6 +151,11 @@ class PyObjectPtr(object): For example, a PyIntObject* with ob_ival 42 in the inferior process should result in an int(42) in this process. + + visited: a set of all gdb.Value pyobject pointers already visited + whilst generating this value (to guard against infinite recursion when + visiting object graphs with loops). Analogous to Py_ReprEnter and + Py_ReprLeave ''' class FakeRepr(object): @@ -209,6 +215,8 @@ class PyObjectPtr(object): 'instance': PyInstanceObjectPtr, 'NoneType': PyNoneStructPtr, 'frame': PyFrameObjectPtr, + 'set' : PySetObjectPtr, + 'frozenset' : PySetObjectPtr, } if tp_name in name_map: return name_map[tp_name] @@ -230,8 +238,8 @@ class PyObjectPtr(object): return PyUnicodeObjectPtr if tp_flags & Py_TPFLAGS_DICT_SUBCLASS: return PyDictObjectPtr - #if tp_flags & Py_TPFLAGS_BASE_EXC_SUBCLASS: - # return something + if tp_flags & Py_TPFLAGS_BASE_EXC_SUBCLASS: + return PyBaseExceptionObjectPtr #if tp_flags & Py_TPFLAGS_TYPE_SUBCLASS: # return PyTypeObjectPtr @@ -258,6 +266,22 @@ class PyObjectPtr(object): def get_gdb_type(cls): return gdb.lookup_type(cls._typename).pointer() + def as_address(self): + return long(self._gdbval) + + +class ProxyAlreadyVisited(object): + ''' + Placeholder proxy to use when protecting against infinite recursion due to + loops in the object graph. + + Analogous to the values emitted by the users of Py_ReprEnter and Py_ReprLeave + ''' + def __init__(self, rep): + self._rep = rep + + def __repr__(self): + return self._rep class InstanceProxy(object): @@ -287,15 +311,19 @@ def _PyObject_VAR_SIZE(typeobj, nitems): class HeapTypeObjectPtr(PyObjectPtr): _typename = 'PyObject' - def proxyval(self): + def proxyval(self, visited): ''' Support for new-style classes. Currently we just locate the dictionary using a transliteration to python of _PyObject_GetDictPtr, ignoring descriptors ''' - attr_dict = {} + # Guard against infinite loops: + if self.as_address() in visited: + return ProxyAlreadyVisited('<...>') + visited.add(self.as_address()) + attr_dict = {} try: typeobj = self.type() dictoffset = int_from_int(typeobj.field('tp_dictoffset')) @@ -313,16 +341,39 @@ class HeapTypeObjectPtr(PyObjectPtr): dictptr = self._gdbval.cast(_type_char_ptr) + dictoffset PyObjectPtrPtr = PyObjectPtr.get_gdb_type().pointer() dictptr = dictptr.cast(PyObjectPtrPtr) - attr_dict = PyObjectPtr.from_pyobject_ptr(dictptr.dereference()).proxyval() + attr_dict = PyObjectPtr.from_pyobject_ptr(dictptr.dereference()).proxyval(visited) except RuntimeError: # Corrupt data somewhere; fail safe - pass + pass tp_name = self.safe_tp_name() # New-style class: return InstanceProxy(tp_name, attr_dict, long(self._gdbval)) +class ProxyException(Exception): + def __init__(self, tp_name, args): + self.tp_name = tp_name + self.args = args + + def __repr__(self): + return '%s%r' % (self.tp_name, self.args) + +class PyBaseExceptionObjectPtr(PyObjectPtr): + """ + Class wrapping a gdb.Value that's a PyBaseExceptionObject* i.e. an exception + within the process being debugged. + """ + _typename = 'PyBaseExceptionObject' + + def proxyval(self, visited): + # Guard against infinite loops: + if self.as_address() in visited: + return ProxyAlreadyVisited('(...)') + visited.add(self.as_address()) + arg_proxy = PyObjectPtr.from_pyobject_ptr(self.field('args')).proxyval(visited) + return ProxyException(self.safe_tp_name(), + arg_proxy) class PyBoolObjectPtr(PyObjectPtr): """ @@ -331,7 +382,7 @@ class PyBoolObjectPtr(PyObjectPtr): """ _typename = 'PyBoolObject' - def proxyval(self): + def proxyval(self, visited): if int_from_int(self.field('ob_ival')): return True else: @@ -360,7 +411,7 @@ class PyCodeObjectPtr(PyObjectPtr): Analogous to PyCode_Addr2Line; translated from pseudocode in Objects/lnotab_notes.txt ''' - co_lnotab = PyObjectPtr.from_pyobject_ptr(self.field('co_lnotab')).proxyval() + co_lnotab = PyObjectPtr.from_pyobject_ptr(self.field('co_lnotab')).proxyval(set()) # Initialize lineno to co_firstlineno as per PyCode_Addr2Line # not 0, as lnotab_notes.txt has it: @@ -381,27 +432,37 @@ class PyDictObjectPtr(PyObjectPtr): """ _typename = 'PyDictObject' - def proxyval(self): + def proxyval(self, visited): + # Guard against infinite loops: + if self.as_address() in visited: + return ProxyAlreadyVisited('{...}') + visited.add(self.as_address()) + result = {} for i in safe_range(self.field('ma_mask') + 1): ep = self.field('ma_table') + i pvalue = PyObjectPtr.from_pyobject_ptr(ep['me_value']) if not pvalue.is_null(): pkey = PyObjectPtr.from_pyobject_ptr(ep['me_key']) - result[pkey.proxyval()] = pvalue.proxyval() + result[pkey.proxyval(visited)] = pvalue.proxyval(visited) return result class PyInstanceObjectPtr(PyObjectPtr): _typename = 'PyInstanceObject' - def proxyval(self): + def proxyval(self, visited): + # Guard against infinite loops: + if self.as_address() in visited: + return ProxyAlreadyVisited('<...>') + visited.add(self.as_address()) + # Get name of class: in_class = PyObjectPtr.from_pyobject_ptr(self.field('in_class')) - cl_name = PyObjectPtr.from_pyobject_ptr(in_class.field('cl_name')).proxyval() + cl_name = PyObjectPtr.from_pyobject_ptr(in_class.field('cl_name')).proxyval(visited) # Get dictionary of instance attributes: - in_dict = PyObjectPtr.from_pyobject_ptr(self.field('in_dict')).proxyval() + in_dict = PyObjectPtr.from_pyobject_ptr(self.field('in_dict')).proxyval(visited) # Old-style class: return InstanceProxy(cl_name, in_dict, long(self._gdbval)) @@ -410,11 +471,10 @@ class PyInstanceObjectPtr(PyObjectPtr): class PyIntObjectPtr(PyObjectPtr): _typename = 'PyIntObject' - def proxyval(self): + def proxyval(self, visited): result = int_from_int(self.field('ob_ival')) return result - class PyListObjectPtr(PyObjectPtr): _typename = 'PyListObject' @@ -423,8 +483,13 @@ class PyListObjectPtr(PyObjectPtr): field_ob_item = self.field('ob_item') return field_ob_item[i] - def proxyval(self): - result = [PyObjectPtr.from_pyobject_ptr(self[i]).proxyval() + def proxyval(self, visited): + # Guard against infinite loops: + if self.as_address() in visited: + return ProxyAlreadyVisited('[...]') + visited.add(self.as_address()) + + result = [PyObjectPtr.from_pyobject_ptr(self[i]).proxyval(visited) for i in safe_range(int_from_int(self.field('ob_size')))] return result @@ -432,7 +497,7 @@ class PyListObjectPtr(PyObjectPtr): class PyLongObjectPtr(PyObjectPtr): _typename = 'PyLongObject' - def proxyval(self): + def proxyval(self, visited): ''' Python's Include/longobjrep.h has this declaration: struct _longobject { @@ -477,7 +542,7 @@ class PyNoneStructPtr(PyObjectPtr): """ _typename = 'PyObject' - def proxyval(self): + def proxyval(self, visited): return None @@ -489,16 +554,39 @@ class PyFrameObjectPtr(PyObjectPtr): return str(fi) +class PySetObjectPtr(PyObjectPtr): + _typename = 'PySetObject' + + def proxyval(self, visited): + # Guard against infinite loops: + if self.as_address() in visited: + return ProxyAlreadyVisited('%s(...)' % self.safe_tp_name()) + visited.add(self.as_address()) + + members = [] + table = self.field('table') + for i in safe_range(self.field('mask')+1): + setentry = table[i] + key = setentry['key'] + if key != 0: + key_proxy = PyObjectPtr.from_pyobject_ptr(key).proxyval(visited) + if key_proxy != '': + members.append(key_proxy) + if self.safe_tp_name() == 'frozenset': + return frozenset(members) + else: + return set(members) + 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 ''.join([chr(field_ob_sval[i]) for i in safe_range(field_ob_size)]) + char_ptr = field_ob_sval.address.cast(_type_unsigned_char_ptr) + return ''.join([chr(char_ptr[i]) for i in safe_range(field_ob_size)]) - def proxyval(self): + def proxyval(self, visited): return str(self) @@ -510,8 +598,13 @@ class PyTupleObjectPtr(PyObjectPtr): field_ob_item = self.field('ob_item') return field_ob_item[i] - def proxyval(self): - result = tuple([PyObjectPtr.from_pyobject_ptr(self[i]).proxyval() + def proxyval(self, visited): + # Guard against infinite loops: + if self.as_address() in visited: + return ProxyAlreadyVisited('(...)') + visited.add(self.as_address()) + + result = tuple([PyObjectPtr.from_pyobject_ptr(self[i]).proxyval(visited) for i in safe_range(int_from_int(self.field('ob_size')))]) return result @@ -523,7 +616,7 @@ class PyTypeObjectPtr(PyObjectPtr): class PyUnicodeObjectPtr(PyObjectPtr): _typename = 'PyUnicodeObject' - def proxyval(self): + def proxyval(self, visited): # From unicodeobject.h: # Py_ssize_t length; /* Length of raw Unicode data in buffer */ # Py_UNICODE *str; /* Raw Unicode buffer */ @@ -576,13 +669,13 @@ class FrameInfo: if not value.is_null(): name = PyObjectPtr.from_pyobject_ptr(self.co_varnames[i]) #print 'name=%s' % name - value = value.proxyval() + value = value.proxyval(set()) #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() + return self.co_filename.proxyval(set()) def current_line_num(self): '''Get current line number as an integer (1-based) @@ -626,7 +719,7 @@ class PyObjectPtrPrinter: self.gdbval = gdbval def to_string (self): - proxyval = PyObjectPtr.from_pyobject_ptr(self.gdbval).proxyval() + proxyval = PyObjectPtr.from_pyobject_ptr(self.gdbval).proxyval(set()) return stringify(proxyval)