Index: Tools/gdb/libpython.py =================================================================== --- Tools/gdb/libpython.py (revision 87275) +++ Tools/gdb/libpython.py (working copy) @@ -1,4 +1,10 @@ #!/usr/bin/python + +# NOTE: this file is taken from the Python source distribution +# It can be found under Tools/gdb/libpython.py. It is shipped with Cython +# because it's not installed as a python module, and because changes are only +# merged into new python versions (v3.2+). + ''' 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, @@ -19,10 +25,9 @@ In particular, given a gdb.Value corresponding to a PyObject* in the inferior process, we can generate a "proxy value" within the gdb process. For example, given a PyObject* in the inferior process that is in fact a PyListObject* -holding three PyObject* that turn out to be PyBytesObject* instances, we can -generate a proxy value within the gdb process that is a list of bytes -instances: - [b"foo", b"bar", b"baz"] +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"] Doing so can be expensive for complicated graphs of objects, and could take some time, so we also have a "write_repr" method that writes a representation @@ -41,33 +46,50 @@ The module also extends gdb with some python-specific commands. ''' from __future__ import with_statement -import gdb + +import os +import re +import sys +import struct import locale -import sys +import atexit +import warnings +import tempfile +import textwrap +import itertools +import gdb + +if sys.version_info[0] < 3: + # I think this is the only way to fix this bug :'( + # http://sourceware.org/bugzilla/show_bug.cgi?id=12285 + out, err = sys.stdout, sys.stderr + reload(sys).setdefaultencoding('UTF-8') + sys.stdout = out + sys.stderr = err + # 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_unsigned_char_ptr = gdb.lookup_type('unsigned char').pointer() _type_void_ptr = gdb.lookup_type('void').pointer() # void* -_type_size_t = gdb.lookup_type('size_t') SIZEOF_VOID_P = _type_void_ptr.sizeof - Py_TPFLAGS_HEAPTYPE = (1L << 9) +Py_TPFLAGS_INT_SUBCLASS = (1L << 23) Py_TPFLAGS_LONG_SUBCLASS = (1L << 24) Py_TPFLAGS_LIST_SUBCLASS = (1L << 25) Py_TPFLAGS_TUPLE_SUBCLASS = (1L << 26) +Py_TPFLAGS_STRING_SUBCLASS = (1L << 27) Py_TPFLAGS_BYTES_SUBCLASS = (1L << 27) Py_TPFLAGS_UNICODE_SUBCLASS = (1L << 28) Py_TPFLAGS_DICT_SUBCLASS = (1L << 29) Py_TPFLAGS_BASE_EXC_SUBCLASS = (1L << 30) Py_TPFLAGS_TYPE_SUBCLASS = (1L << 31) +MAX_OUTPUT_LEN = 1024 -MAX_OUTPUT_LEN=1024 - hexdigits = "0123456789abcdef" ENCODING = locale.getpreferredencoding() @@ -135,10 +157,21 @@ def getvalue(self): return self._val + +# pretty printer lookup +all_pretty_typenames = set() + +class PrettyPrinterTrackerMeta(type): + + def __init__(self, name, bases, dict): + super(PrettyPrinterTrackerMeta, self).__init__(name, bases, dict) + all_pretty_typenames.add(self._typename) + + class PyObjectPtr(object): """ Class wrapping a gdb.Value that's a either a (PyObject*) within the - inferior process, or some subclass pointer e.g. (PyBytesObject*) + inferior process, or some subclass pointer e.g. (PyStringObject*) There will be a subclass for every refined PyObject type that we care about. @@ -146,8 +179,11 @@ Note that at every stage the underlying pointer could be NULL, point to corrupt data, etc; this is the debugger, after all. """ + + __metaclass__ = PrettyPrinterTrackerMeta + _typename = 'PyObject' - + def __init__(self, gdbval, cast_to=None): if cast_to: self._gdbval = gdbval.cast(cast_to) @@ -320,7 +356,7 @@ #print 'tp_flags = 0x%08x' % tp_flags #print 'tp_name = %r' % tp_name - + name_map = {'bool': PyBoolObjectPtr, 'classobj': PyClassObjectPtr, 'instance': PyInstanceObjectPtr, @@ -332,26 +368,30 @@ } if tp_name in name_map: return name_map[tp_name] + + if tp_flags & (Py_TPFLAGS_HEAPTYPE|Py_TPFLAGS_TYPE_SUBCLASS): + return PyTypeObjectPtr - if tp_flags & Py_TPFLAGS_HEAPTYPE: - return HeapTypeObjectPtr - + if tp_flags & Py_TPFLAGS_INT_SUBCLASS: + return PyIntObjectPtr if tp_flags & Py_TPFLAGS_LONG_SUBCLASS: return PyLongObjectPtr if tp_flags & Py_TPFLAGS_LIST_SUBCLASS: return PyListObjectPtr if tp_flags & Py_TPFLAGS_TUPLE_SUBCLASS: return PyTupleObjectPtr - if tp_flags & Py_TPFLAGS_BYTES_SUBCLASS: - return PyBytesObjectPtr + if tp_flags & Py_TPFLAGS_STRING_SUBCLASS: + try: + gdb.lookup_type('PyBytesObject') + return PyBytesObjectPtr + except RuntimeError: + return PyStringObjectPtr if tp_flags & Py_TPFLAGS_UNICODE_SUBCLASS: return PyUnicodeObjectPtr if tp_flags & Py_TPFLAGS_DICT_SUBCLASS: return PyDictObjectPtr if tp_flags & Py_TPFLAGS_BASE_EXC_SUBCLASS: return PyBaseExceptionObjectPtr - #if tp_flags & Py_TPFLAGS_TYPE_SUBCLASS: - # return PyTypeObjectPtr # Use the base class: return cls @@ -366,7 +406,7 @@ p = PyObjectPtr(gdbval) cls = cls.subclass_from_type(p.type()) return cls(gdbval, cast_to=cls.get_gdb_type()) - except RuntimeError: + except RuntimeError, exc: # Handle any kind of error e.g. NULL ptrs by simply using the base # class pass @@ -379,6 +419,7 @@ def as_address(self): return long(self._gdbval) + class PyVarObjectPtr(PyObjectPtr): _typename = 'PyVarObject' @@ -439,11 +480,11 @@ nitems * typeobj.field('tp_itemsize') + (SIZEOF_VOID_P - 1) ) & ~(SIZEOF_VOID_P - 1) - ).cast(_type_size_t) + ).cast(gdb.lookup_type('size_t')) -class HeapTypeObjectPtr(PyObjectPtr): - _typename = 'PyObject' - +class PyTypeObjectPtr(PyObjectPtr): + _typename = 'PyTypeObject' + def get_attr_dict(self): ''' Get the PyDictObject ptr representing the attribute dictionary @@ -502,10 +543,17 @@ out.write('<...>') return visited.add(self.as_address()) - + + # try: + # tp_name = self.field('tp_name').string() + # except RuntimeError: + # tp_name = 'unknown' + # + # out.write('' % (tp_name, + # self.as_address())) pyop_attrdict = self.get_attr_dict() - _write_instance_repr(out, visited, - self.safe_tp_name(), pyop_attrdict, self.as_address()) + _write_instance_repr(out, visited, self.safe_tp_name(), pyop_attrdict, + self.as_address()) class ProxyException(Exception): def __init__(self, tp_name, args): @@ -521,7 +569,7 @@ within the process being debugged. """ _typename = 'PyBaseExceptionObject' - + def proxyval(self, visited): # Guard against infinite loops: if self.as_address() in visited: @@ -541,6 +589,7 @@ out.write(self.safe_tp_name()) self.write_field_repr('args', out, visited) + class PyClassObjectPtr(PyObjectPtr): """ Class wrapping a gdb.Value that's a PyClassObject* i.e. a @@ -633,7 +682,7 @@ if not pyop_value.is_null(): pyop_key = PyObjectPtr.from_pyobject_ptr(ep['me_key']) yield (pyop_key, pyop_value) - + def proxyval(self, visited): # Guard against infinite loops: if self.as_address() in visited: @@ -667,7 +716,7 @@ class PyInstanceObjectPtr(PyObjectPtr): _typename = 'PyInstanceObject' - + def proxyval(self, visited): # Guard against infinite loops: if self.as_address() in visited: @@ -703,9 +752,16 @@ _write_instance_repr(out, visited, cl_name, pyop_in_dict, self.as_address()) +class PyIntObjectPtr(PyObjectPtr): + _typename = 'PyIntObject' + + def proxyval(self, visited): + result = int_from_int(self.field('ob_ival')) + return result + class PyListObjectPtr(PyObjectPtr): _typename = 'PyListObject' - + def __getitem__(self, i): # Get the gdb.Value for the (PyObject*) with the given index: field_ob_item = self.field('ob_item') @@ -738,7 +794,7 @@ class PyLongObjectPtr(PyObjectPtr): _typename = 'PyLongObject' - + def proxyval(self, visited): ''' Python's Include/longobjrep.h has this declaration: @@ -756,7 +812,7 @@ where SHIFT can be either: #define PyLong_SHIFT 30 #define PyLong_SHIFT 15 - ''' + ''' ob_size = long(self.field('ob_size')) if ob_size == 0: return 0L @@ -786,12 +842,14 @@ Class wrapping a gdb.Value that's a PyBoolObject* i.e. one of the two instances (Py_True/Py_False) within the process being debugged. """ + _typename = 'PyBoolObject' + def proxyval(self, visited): - if PyLongObjectPtr.proxyval(self, visited): - return True - else: - return False + castto = gdb.lookup_type('PyLongObject').pointer() + self._gdbval = self._gdbval.cast(castto) + return bool(PyLongObjectPtr(self._gdbval).proxyval(visited)) + class PyNoneStructPtr(PyObjectPtr): """ Class wrapping a gdb.Value that's a PyObject* pointing to the @@ -806,7 +864,7 @@ class PyFrameObjectPtr(PyObjectPtr): _typename = 'PyFrameObject' - def __init__(self, gdbval, cast_to): + def __init__(self, gdbval, cast_to=None): PyObjectPtr.__init__(self, gdbval, cast_to) if not self.is_optimized_out(): @@ -997,17 +1055,17 @@ class PyBytesObjectPtr(PyObjectPtr): _typename = 'PyBytesObject' - + 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_unsigned_char_ptr) - return ''.join([chr(char_ptr[i]) for i in safe_range(field_ob_size)]) + return ''.join(struct.pack('b', field_ob_sval[i]) + for i in safe_range(field_ob_size)) def proxyval(self, visited): return str(self) - def write_repr(self, out, visited): + def write_repr(self, out, visited, py3=True): # Write this out as a Python 3 bytes literal, i.e. with a "b" prefix # Get a PyStringObject* within the Python 2 gdb process: @@ -1018,7 +1076,10 @@ quote = "'" if "'" in proxy and not '"' in proxy: quote = '"' - out.write('b') + + if py3: + out.write('b') + out.write(quote) for byte in proxy: if byte == quote or byte == '\\': @@ -1038,6 +1099,12 @@ out.write(byte) out.write(quote) +class PyStringObjectPtr(PyBytesObjectPtr): + _typename = 'PyStringObject' + + def write_repr(self, out, visited): + return super(PyStringObjectPtr, self).write_repr(out, visited, py3=False) + class PyTupleObjectPtr(PyObjectPtr): _typename = 'PyTupleObject' @@ -1074,10 +1141,7 @@ else: out.write(')') -class PyTypeObjectPtr(PyObjectPtr): - _typename = 'PyTypeObject' - def _unichr_is_printable(char): # Logic adapted from Python 3's Tools/unicode/makeunicodedata.py if char == u" ": @@ -1097,7 +1161,6 @@ ch2 = 0xDC00 | (x & 0x3FF) return unichr(ch1) + unichr(ch2) - class PyUnicodeObjectPtr(PyObjectPtr): _typename = 'PyUnicodeObject' @@ -1145,13 +1208,20 @@ return result def write_repr(self, out, visited): - # Write this out as a Python 3 str literal, i.e. without a "u" prefix - # Get a PyUnicodeObject* within the Python 2 gdb process: proxy = self.proxyval(visited) # Transliteration of Python 3's Object/unicodeobject.c:unicode_repr # to Python 2: + try: + gdb.parse_and_eval('PyString_Type') + except RuntimeError: + # Python 3, don't write 'u' as prefix + pass + else: + # Python 2, write the 'u' + out.write('u') + if "'" in proxy and '"' not in proxy: quote = '"' else: @@ -1253,8 +1323,6 @@ out.write(quote) - - def int_from_int(gdbval): return int(str(gdbval)) @@ -1289,8 +1357,7 @@ type = gdbval.type.unqualified() if type.code == gdb.TYPE_CODE_PTR: type = type.target().unqualified() - t = str(type) - if t in ("PyObject", "PyFrameObject", "PyUnicodeObject"): + if str(type) in all_pretty_typenames: return PyObjectPtrPrinter(gdbval) """ @@ -1321,8 +1388,6 @@ register (gdb.current_objfile ()) - - # Unfortunately, the exact API exposed by the gdb module varies somewhat # from build to build # See http://bugs.python.org/issue8279?#msg102276 @@ -1605,13 +1670,7 @@ class PyLocals(gdb.Command): 'Look up the given python variable name, and print it' - def __init__(self): - gdb.Command.__init__ (self, - "py-locals", - gdb.COMMAND_DATA, - gdb.COMPLETE_NONE) - def invoke(self, args, from_tty): name = str(args) @@ -1625,9 +1684,795 @@ print 'Unable to read information on python frame' return - for pyop_name, pyop_value in pyop_frame.iter_locals(): - print ('%s = %s' - % (pyop_name.proxyval(set()), - pyop_value.get_truncated_repr(MAX_OUTPUT_LEN))) + namespace = self.get_namespace(pyop_frame) + namespace = [(name.proxyval(set()), val) for name, val in namespace] + + if namespace: + name, val = max(namespace, key=lambda (name, val): len(name)) + max_name_length = len(name) + + for name, pyop_value in namespace: + value = pyop_value.get_truncated_repr(MAX_OUTPUT_LEN) + print ('%-*s = %s' % (max_name_length, name, value)) -PyLocals() + def get_namespace(self, pyop_frame): + return pyop_frame.iter_locals() + + +class PyGlobals(PyLocals): + 'List all the globals in the currently select Python frame' + + def get_namespace(self, pyop_frame): + return pyop_frame.iter_globals() + + +PyLocals("py-locals", gdb.COMMAND_DATA, gdb.COMPLETE_NONE) +PyGlobals("py-globals", gdb.COMMAND_DATA, gdb.COMPLETE_NONE) + + +class PyNameEquals(gdb.Function): + + def _get_pycurframe_attr(self, attr): + frame = Frame(gdb.selected_frame()) + if frame.is_evalframeex(): + pyframe = frame.get_pyop() + if pyframe is None: + warnings.warn("Use a Python debug build, Python breakpoints " + "won't work otherwise.") + return None + + return getattr(pyframe, attr).proxyval(set()) + + return None + + def invoke(self, funcname): + attr = self._get_pycurframe_attr('co_name') + return attr is not None and attr == funcname.string() + +PyNameEquals("pyname_equals") + + +class PyModEquals(PyNameEquals): + + def invoke(self, modname): + attr = self._get_pycurframe_attr('co_filename') + if attr is not None: + filename, ext = os.path.splitext(os.path.basename(attr)) + return filename == modname.string() + return False + +PyModEquals("pymod_equals") + + +class PyBreak(gdb.Command): + """ + Set a Python breakpoint. Examples: + + Break on any function or method named 'func' in module 'modname' + + py-break modname.func + + Break on any function or method named 'func' + + py-break func + """ + + def invoke(self, funcname, from_tty): + if '.' in funcname: + modname, dot, funcname = funcname.rpartition('.') + cond = '$pyname_equals("%s") && $pymod_equals("%s")' % (funcname, + modname) + else: + cond = '$pyname_equals("%s")' % funcname + + gdb.execute('break PyEval_EvalFrameEx if ' + cond) + +PyBreak("py-break", gdb.COMMAND_RUNNING, gdb.COMPLETE_NONE) + + +class _LoggingState(object): + """ + State that helps to provide a reentrant gdb.execute() function. + """ + + def __init__(self): + self.fd, self.filename = tempfile.mkstemp() + self.file = os.fdopen(self.fd, 'r+') + _execute("set logging file %s" % self.filename) + self.file_position_stack = [] + + atexit.register(os.close, self.fd) + atexit.register(os.remove, self.filename) + + def __enter__(self): + if not self.file_position_stack: + _execute("set logging redirect on") + _execute("set logging on") + _execute("set pagination off") + + self.file_position_stack.append(os.fstat(self.fd).st_size) + return self + + def getoutput(self): + gdb.flush() + self.file.seek(self.file_position_stack[-1]) + result = self.file.read() + return result + + def __exit__(self, exc_type, exc_val, tb): + startpos = self.file_position_stack.pop() + self.file.seek(startpos) + self.file.truncate() + if not self.file_position_stack: + _execute("set logging off") + _execute("set logging redirect off") + _execute("set pagination on") + + +def execute(command, from_tty=False, to_string=False): + """ + Replace gdb.execute() with this function and have it accept a 'to_string' + argument (new in 7.2). Have it properly capture stderr also. Ensure + reentrancy. + """ + if to_string: + with _logging_state as state: + _execute(command, from_tty) + return state.getoutput() + else: + _execute(command, from_tty) + + +_execute = gdb.execute +gdb.execute = execute +_logging_state = _LoggingState() + + +def get_selected_inferior(): + """ + Return the selected inferior in gdb. + """ + # Woooh, another bug in gdb! Is there an end in sight? + # http://sourceware.org/bugzilla/show_bug.cgi?id=12212 + return gdb.inferiors()[0] + + selected_thread = gdb.selected_thread() + + for inferior in gdb.inferiors(): + for thread in inferior.threads(): + if thread == selected_thread: + return inferior + +def source_gdb_script(script_contents, to_string=False): + """ + Source a gdb script with script_contents passed as a string. This is useful + to provide defines for py-step and py-next to make them repeatable (this is + not possible with gdb.execute()). See + http://sourceware.org/bugzilla/show_bug.cgi?id=12216 + """ + fd, filename = tempfile.mkstemp() + f = os.fdopen(fd, 'w') + f.write(script_contents) + f.close() + gdb.execute("source %s" % filename, to_string=to_string) + +def register_defines(): + source_gdb_script(textwrap.dedent("""\ + define py-step + -py-step + end + + define py-next + -py-next + end + + document py-step + %s + end + + document py-next + %s + end + """) % (PyStep.__doc__, PyNext.__doc__)) + + +def stackdepth(frame): + "Tells the stackdepth of a gdb frame." + depth = 0 + while frame: + frame = frame.older() + depth += 1 + + return depth + +class ExecutionControlCommandBase(gdb.Command): + """ + Superclass for language specific execution control. Language specific + features should be implemented by lang_info using the LanguageInfo + interface. 'name' is the name of the command. + """ + + def __init__(self, name, lang_info): + super(ExecutionControlCommandBase, self).__init__( + name, gdb.COMMAND_RUNNING, gdb.COMPLETE_NONE) + self.lang_info = lang_info + + def install_breakpoints(self): + all_locations = itertools.chain( + self.lang_info.static_break_functions(), + self.lang_info.runtime_break_functions()) + + for location in all_locations: + result = gdb.execute('break %s' % location, to_string=True) + yield re.search(r'Breakpoint (\d+)', result).group(1) + + + + def delete_breakpoints(self, breakpoint_list): + for bp in breakpoint_list: + gdb.execute("delete %s" % bp) + + def filter_output(self, result): + output = [] + + match_finish = re.search(r'^Value returned is \$\d+ = (.*)', result, + re.MULTILINE) + if match_finish: + output.append('Value returned: %s' % match_finish.group(1)) + + reflags = re.MULTILINE + regexes = [ + (r'^Program received signal .*', reflags|re.DOTALL), + (r'.*[Ww]arning.*', 0), + (r'^Program exited .*', reflags), + ] + + for regex, flags in regexes: + match = re.search(regex, result, flags) + if match: + output.append(match.group(0)) + + return '\n'.join(output) + + def stopped(self): + return get_selected_inferior().pid == 0 + + def finish_executing(self, result): + """ + After doing some kind of code running in the inferior, print the line + of source code or the result of the last executed gdb command (passed + in as the `result` argument). + """ + result = self.filter_output(result) + + if self.stopped(): + print result.strip() + else: + frame = gdb.selected_frame() + if self.lang_info.is_relevant_function(frame): + raised_exception = self.lang_info.exc_info(frame) + if raised_exception: + print raised_exception + print self.lang_info.get_source_line(frame) or result + else: + print result + + def _finish(self): + """ + Execute until the function returns (or until something else makes it + stop) + """ + if gdb.selected_frame().older() is not None: + return gdb.execute('finish', to_string=True) + else: + # outermost frame, continue + return gdb.execute('cont', to_string=True) + + def _finish_frame(self): + """ + Execute until the function returns to a relevant caller. + """ + while True: + result = self._finish() + + try: + frame = gdb.selected_frame() + except RuntimeError: + break + + hitbp = re.search(r'Breakpoint (\d+)', result) + is_relevant = self.lang_info.is_relevant_function(frame) + if hitbp or is_relevant or self.stopped(): + break + + return result + + def finish(self, *args): + "Implements the finish command." + result = self._finish_frame() + self.finish_executing(result) + + def step(self, stepinto, stepover_command='next'): + """ + Do a single step or step-over. Returns the result of the last gdb + command that made execution stop. + + This implementation, for stepping, sets (conditional) breakpoints for + all functions that are deemed relevant. It then does a step over until + either something halts execution, or until the next line is reached. + + If, however, stepover_command is given, it should be a string gdb + command that continues execution in some way. The idea is that the + caller has set a (conditional) breakpoint or watchpoint that can work + more efficiently than the step-over loop. For Python this means setting + a watchpoint for f->f_lasti, which means we can then subsequently + "finish" frames. + We want f->f_lasti instead of f->f_lineno, because the latter only + works properly with local trace functions, see + PyFrameObjectPtr.current_line_num and PyFrameObjectPtr.addr2line. + """ + if stepinto: + breakpoint_list = list(self.install_breakpoints()) + + beginframe = gdb.selected_frame() + + if self.lang_info.is_relevant_function(beginframe): + # If we start in a relevant frame, initialize stuff properly. If + # we don't start in a relevant frame, the loop will halt + # immediately. So don't call self.lang_info.lineno() as it may + # raise for irrelevant frames. + beginline = self.lang_info.lineno(beginframe) + + if not stepinto: + depth = stackdepth(beginframe) + + newframe = beginframe + + while True: + if self.lang_info.is_relevant_function(newframe): + result = gdb.execute(stepover_command, to_string=True) + else: + result = self._finish_frame() + + if self.stopped(): + break + + newframe = gdb.selected_frame() + is_relevant_function = self.lang_info.is_relevant_function(newframe) + try: + framename = newframe.name() + except RuntimeError: + framename = None + + m = re.search(r'Breakpoint (\d+)', result) + if m: + if is_relevant_function and m.group(1) in breakpoint_list: + # although we hit a breakpoint, we still need to check + # that the function, in case hit by a runtime breakpoint, + # is in the right context + break + + if newframe != beginframe: + # new function + + if not stepinto: + # see if we returned to the caller + newdepth = stackdepth(newframe) + is_relevant_function = (newdepth < depth and + is_relevant_function) + + if is_relevant_function: + break + else: + # newframe equals beginframe, check for a difference in the + # line number + lineno = self.lang_info.lineno(newframe) + if lineno and lineno != beginline: + break + + if stepinto: + self.delete_breakpoints(breakpoint_list) + + self.finish_executing(result) + + def run(self, *args): + self.finish_executing(gdb.execute('run', to_string=True)) + + def cont(self, *args): + self.finish_executing(gdb.execute('cont', to_string=True)) + + +class LanguageInfo(object): + """ + This class defines the interface that ExecutionControlCommandBase needs to + provide language-specific execution control. + + Classes that implement this interface should implement: + + lineno(frame) + Tells the current line number (only called for a relevant frame). + If lineno is a false value it is not checked for a difference. + + is_relevant_function(frame) + tells whether we care about frame 'frame' + + get_source_line(frame) + get the line of source code for the current line (only called for a + relevant frame). If the source code cannot be retrieved this + function should return None + + exc_info(frame) -- optional + tells whether an exception was raised, if so, it should return a + string representation of the exception value, None otherwise. + + static_break_functions() + returns an iterable of function names that are considered relevant + and should halt step-into execution. This is needed to provide a + performing step-into + + runtime_break_functions() -- optional + list of functions that we should break into depending on the + context + """ + + def exc_info(self, frame): + "See this class' docstring." + + def runtime_break_functions(self): + """ + Implement this if the list of step-into functions depends on the + context. + """ + return () + +class PythonInfo(LanguageInfo): + + def pyframe(self, frame): + pyframe = Frame(frame).get_pyop() + if pyframe: + return pyframe + else: + raise gdb.RuntimeError( + "Unable to find the Python frame, run your code with a debug " + "build (configure with --with-pydebug or compile with -g).") + + def lineno(self, frame): + return self.pyframe(frame).current_line_num() + + def is_relevant_function(self, frame): + return Frame(frame).is_evalframeex() + + def get_source_line(self, frame): + try: + pyframe = self.pyframe(frame) + return '%4d %s' % (pyframe.current_line_num(), + pyframe.current_line().rstrip()) + except IOError, e: + return None + + def exc_info(self, frame): + try: + tstate = frame.read_var('tstate').dereference() + if gdb.parse_and_eval('tstate->frame == f'): + # tstate local variable initialized + inf_type = tstate['curexc_type'] + inf_value = tstate['curexc_value'] + if inf_type: + return 'An exception was raised: %s(%s)' % (inf_type, + inf_value) + except (ValueError, RuntimeError), e: + # Could not read the variable tstate or it's memory, it's ok + pass + + def static_break_functions(self): + yield 'PyEval_EvalFrameEx' + + +class PythonStepperMixin(object): + """ + Make this a mixin so CyStep can also inherit from this and use a + CythonCodeStepper at the same time. + """ + + def python_step(self, stepinto): + frame = gdb.selected_frame() + framewrapper = Frame(frame) + + output = gdb.execute('watch f->f_lasti', to_string=True) + watchpoint = int(re.search(r'[Ww]atchpoint (\d+):', output).group(1)) + self.step(stepinto=stepinto, stepover_command='finish') + gdb.execute('delete %s' % watchpoint) + + +class PyStep(ExecutionControlCommandBase, PythonStepperMixin): + "Step through Python code." + + stepinto = True + + def invoke(self, args, from_tty): + self.python_step(stepinto=self.stepinto) + +class PyNext(PyStep): + "Step-over Python code." + + stepinto = False + +class PyFinish(ExecutionControlCommandBase): + "Execute until function returns to a caller." + + invoke = ExecutionControlCommandBase.finish + +class PyRun(ExecutionControlCommandBase): + "Run the program." + + invoke = ExecutionControlCommandBase.run + +class PyCont(ExecutionControlCommandBase): + + invoke = ExecutionControlCommandBase.cont + + +def _pointervalue(gdbval): + """ + Return the value of the pionter as a Python int. + + gdbval.type must be a pointer type + """ + # don't convert with int() as it will raise a RuntimeError + if gdbval.address is not None: + return long(gdbval.address) + else: + # the address attribute is None sometimes, in which case we can + # still convert the pointer to an int + return long(gdbval) + +def pointervalue(gdbval): + pointer = _pointervalue(gdbval) + try: + if pointer < 0: + raise gdb.GdbError("Negative pointer value, presumably a bug " + "in gdb, aborting.") + except RuntimeError: + # work around yet another bug in gdb where you get random behaviour + # and tracebacks + pass + + return pointer + +def get_inferior_unicode_postfix(): + try: + gdb.parse_and_eval('PyUnicode_FromEncodedObject') + except RuntimeError: + try: + gdb.parse_and_eval('PyUnicodeUCS2_FromEncodedObject') + except RuntimeError: + return 'UCS4' + else: + return 'UCS2' + else: + return '' + +class PythonCodeExecutor(object): + + Py_single_input = 256 + Py_file_input = 257 + Py_eval_input = 258 + + def malloc(self, size): + chunk = (gdb.parse_and_eval("(void *) malloc((size_t) %d)" % size)) + + pointer = pointervalue(chunk) + if pointer == 0: + raise gdb.GdbError("No memory could be allocated in the inferior.") + + return pointer + + def alloc_string(self, string): + pointer = self.malloc(len(string)) + get_selected_inferior().write_memory(pointer, string) + + return pointer + + def alloc_pystring(self, string): + stringp = self.alloc_string(string) + PyString_FromStringAndSize = 'PyString_FromStringAndSize' + + try: + gdb.parse_and_eval(PyString_FromStringAndSize) + except RuntimeError: + # Python 3 + PyString_FromStringAndSize = ('PyUnicode%s_FromStringAndSize' % + (get_inferior_unicode_postfix,)) + + try: + result = gdb.parse_and_eval( + '(PyObject *) %s((char *) %d, (size_t) %d)' % ( + PyString_FromStringAndSize, stringp, len(string))) + finally: + self.free(stringp) + + pointer = pointervalue(result) + if pointer == 0: + raise gdb.GdbError("Unable to allocate Python string in " + "the inferior.") + + return pointer + + def free(self, pointer): + gdb.parse_and_eval("free((void *) %d)" % pointer) + + def incref(self, pointer): + "Increment the reference count of a Python object in the inferior." + gdb.parse_and_eval('Py_IncRef((PyObject *) %d)' % pointer) + + def decref(self, pointer): + "Decrement the reference count of a Python object in the inferior." + # Py_DecRef is like Py_XDECREF, but a function. So we don't have + # to check for NULL. This should also decref all our allocated + # Python strings. + gdb.parse_and_eval('Py_DecRef((PyObject *) %d)' % pointer) + + def evalcode(self, code, input_type, global_dict=None, local_dict=None): + """ + Evaluate python code `code` given as a string in the inferior and + return the result as a gdb.Value. Returns a new reference in the + inferior. + + Of course, executing any code in the inferior may be dangerous and may + leave the debuggee in an unsafe state or terminate it alltogether. + """ + if '\0' in code: + raise gdb.GdbError("String contains NUL byte.") + + code += '\0' + + pointer = self.alloc_string(code) + + globalsp = pointervalue(global_dict) + localsp = pointervalue(local_dict) + + if globalsp == 0 or localsp == 0: + raise gdb.GdbError("Unable to obtain or create locals or globals.") + + code = """ + PyRun_String( + (char *) %(code)d, + (int) %(start)d, + (PyObject *) %(globals)s, + (PyObject *) %(locals)d) + """ % dict(code=pointer, start=input_type, + globals=globalsp, locals=localsp) + + with FetchAndRestoreError(): + try: + self.decref(gdb.parse_and_eval(code)) + finally: + self.free(pointer) + + +class FetchAndRestoreError(PythonCodeExecutor): + """ + Context manager that fetches the error indicator in the inferior and + restores it on exit. + """ + + def __init__(self): + self.sizeof_PyObjectPtr = gdb.lookup_type('PyObject').pointer().sizeof + self.pointer = self.malloc(self.sizeof_PyObjectPtr * 3) + + type = self.pointer + value = self.pointer + self.sizeof_PyObjectPtr + traceback = self.pointer + self.sizeof_PyObjectPtr * 2 + + self.errstate = type, value, traceback + + def __enter__(self): + gdb.parse_and_eval("PyErr_Fetch(%d, %d, %d)" % self.errstate) + + def __exit__(self, *args): + if gdb.parse_and_eval("(int) PyErr_Occurred()"): + gdb.parse_and_eval("PyErr_Print()") + + pyerr_restore = ("PyErr_Restore(" + "(PyObject *) *%d," + "(PyObject *) *%d," + "(PyObject *) *%d)") + + try: + gdb.parse_and_eval(pyerr_restore % self.errstate) + finally: + self.free(self.pointer) + + +class FixGdbCommand(gdb.Command): + + def __init__(self, command, actual_command): + super(FixGdbCommand, self).__init__(command, gdb.COMMAND_DATA, + gdb.COMPLETE_NONE) + self.actual_command = actual_command + + def fix_gdb(self): + """ + So, you must be wondering what the story is this time! Yeeees, indeed, + I have quite the story for you! It seems that invoking either 'cy exec' + and 'py-exec' work perfectly fine, but after this gdb's python API is + entirely broken. Some unset exception value is still set? + sys.exc_clear() didn't help. A demonstration: + + (gdb) cy exec 'hello' + 'hello' + (gdb) python gdb.execute('cont') + RuntimeError: Cannot convert value to int. + Error while executing Python code. + (gdb) python gdb.execute('cont') + [15148 refs] + + Program exited normally. + """ + warnings.filterwarnings('ignore', r'.*', RuntimeWarning, + re.escape(__name__)) + try: + long(gdb.parse_and_eval("(void *) 0")) == 0 + except RuntimeError: + pass + # warnings.resetwarnings() + + def invoke(self, args, from_tty): + self.fix_gdb() + try: + gdb.execute('%s %s' % (self.actual_command, args)) + except RuntimeError, e: + raise gdb.GdbError(str(e)) + self.fix_gdb() + + +class PyExec(gdb.Command): + + def readcode(self, expr): + if expr: + return expr, PythonCodeExecutor.Py_single_input + else: + lines = [] + while True: + try: + line = raw_input('>') + except EOFError: + break + else: + if line.rstrip() == 'end': + break + + lines.append(line) + + return '\n'.join(lines), Py_file_input + + def invoke(self, expr, from_tty): + expr, input_type = self.readcode(expr) + + executor = PythonCodeExecutor() + global_dict = gdb.parse_and_eval('PyEval_GetGlobals()') + local_dict = gdb.parse_and_eval('PyEval_GetLocals()') + + if pointervalue(global_dict) == 0 or pointervalue(local_dict) == 0: + raise gdb.GdbError("Unable to find the locals or globals of the " + "most recent Python function (relative to the " + "selected frame).") + + executor.evalcode(expr, input_type, global_dict, local_dict) + + +gdb.execute('set breakpoint pending on') + +if hasattr(gdb, 'GdbError'): + # Wrap py-step and py-next in gdb defines to make them repeatable. + py_step = PyStep('-py-step', PythonInfo()) + py_next = PyNext('-py-next', PythonInfo()) + register_defines() + py_finish = PyFinish('py-finish', PythonInfo()) + py_run = PyRun('py-run', PythonInfo()) + py_cont = PyCont('py-cont', PythonInfo()) + + py_exec = FixGdbCommand('py-exec', '-py-exec') + _py_exec = PyExec("-py-exec", gdb.COMMAND_DATA, gdb.COMPLETE_NONE) +else: + warnings.warn("Use gdb 7.2 or higher to use the py-exec command.") Index: Lib/test/gdb_sample2.py =================================================================== --- Lib/test/gdb_sample2.py (revision 0) +++ Lib/test/gdb_sample2.py (revision 0) @@ -0,0 +1,42 @@ +# 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): + a = 1 + b = 2 + id(42) + +foo(1, 2, 3) +# 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): + a = 1 + b = 2 + id(42) + +foo(1, 2, 3) +# 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): + a = 1 + b = 2 + id(42) + +foo(1, 2, 3) \ No newline at end of file Index: Lib/test/test_gdb.py =================================================================== --- Lib/test/test_gdb.py (revision 87275) +++ Lib/test/test_gdb.py (working copy) @@ -19,11 +19,12 @@ # This is what "no gdb" looks like. There may, however, be other # errors that manifest this way too. raise unittest.SkipTest("Couldn't find gdb on the path") -gdb_version_number = re.search(b"^GNU gdb [^\d]*(\d+)\.", gdb_version) -if int(gdb_version_number.group(1)) < 7: +gdb_version_number = re.search(b"^GNU gdb [^\d]*(\d+)\.(\d+)", gdb_version) +gdb_version_info = (int(gdb_version_number.group(1)), + int(gdb_version_number.group(2))) +if gdb_version_info[0] < 7: raise unittest.SkipTest("gdb versions before 7.0 didn't support python embedding" " Saw:\n" + gdb_version.decode('ascii', 'replace')) - # Verify that "gdb" was built with the embedded python support enabled: cmd = "--eval-command=python import sys; print sys.version_info" p = subprocess.Popen(["gdb", "--batch", cmd], @@ -60,6 +61,7 @@ out, err = subprocess.Popen( args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ).communicate() + return out.decode('utf-8', 'replace'), err.decode('utf-8', 'replace') def get_stack_trace(self, source=None, script=None, @@ -100,10 +102,15 @@ # print commands # Use "commands" to generate the arguments with which to invoke "gdb": + + # os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + args = ["gdb", "--batch"] + # "--eval-command=python execfile(%r)" % libpython] args += ['--eval-command=%s' % cmd for cmd in commands] args += ["--args", - sys.executable] + sys.executable, + "-u"] if not import_site: # -S suppresses the default 'import site' @@ -156,7 +163,12 @@ if not m: self.fail('Unexpected gdb output: %r\n%s' % (gdb_output, gdb_output)) return m.group(1), gdb_output - + + def run_sample2(self, *commands): + return self.get_stack_trace(script=self.get_sample_script2(), + breakpoint='main', + cmds_after_breakpoint=commands) + def assertEndsWith(self, actual, exp_end): '''Ensure that the given "actual" string ends with "exp_end"''' self.assertTrue(actual.endswith(exp_end), @@ -165,10 +177,13 @@ def assertMultilineMatches(self, actual, pattern): m = re.match(pattern, actual, re.DOTALL) if not m: - self.fail(msg='%r did not match %r' % (actual, pattern)) + self.fail(msg='%s did not match %s' % (actual, pattern)) def get_sample_script(self): return findfile('gdb_sample.py') + + def get_sample_script2(self): + return os.path.join(os.path.dirname(__file__), 'gdb_sample2.py') class PrettyPrintTests(DebuggerTests): def test_getting_backtrace(self): @@ -663,6 +678,85 @@ self.assertMultilineMatches(bt, r".*\na = 1\nb = 2\nc = 3\n.*") +skip_unless_7_2_decorator = unittest.skipUnless( + gdb_version_info >= (7, 2), "test requires gdb 7.2 or higher.") + +class PyBreakTest(DebuggerTests): + + @skip_unless_7_2_decorator + def test_break(self): + output = self.run_sample2( + 'py-break foo', + 'py-break bar', + 'py-break baz', + 'py-cont', + 'py-cont', + 'py-cont', + ) + + assert 'def foo(a, b, c):' in output, repr(output) + assert 'def bar(a, b, c):' in output, repr(output) + assert 'def baz(*args):' in output, repr(output) + + +class PyStepNextTests(DebuggerTests): + + @skip_unless_7_2_decorator + def test_step(self): + output = self.run_sample2( + 'py-break bar', + 'py-cont', + 'py-step', + 'py-step', + 'py-step', + 'py-step', + 'py-step') + + assert 'def bar(a, b, c):' in output, repr(output) + assert 'def baz(*args):' in output, repr(output) + assert 'a = 1' in output, repr(output) + assert 'b = 2' in output, repr(output) + assert 'id(42)' in output, repr(output) + + @skip_unless_7_2_decorator + def test_next(self): + output = self.run_sample2( + 'py-break bar', + 'py-cont', + 'py-next', + 'py-next', + 'py-next', + ) + + assert 'def bar(a, b, c):' in output, repr(output) + assert 'baz(a, b, c)' in output, repr(output) + assert 'def baz(*args):' not in output, repr(output) + assert 'foo(1, 2, 3)' in output, repr(output) + + +class PyExecTests(DebuggerTests): + + @skip_unless_7_2_decorator + def test_exec(self): + output = self.run_sample2( + 'py-break foo', + 'py-cont', + 'py-step', + 'py-exec import sys', + 'py-exec sys.stdout.write("py-exec: %d %d %d\\n" % (a, b, c))', + 'py-exec "py-exec2:", a, b, c', + 'py-exec import sys; ' + 'sys.stderr = sys.stdout; ' + 'raise NameError("somevariable does not exist")', + ) + + assert 'py-exec: 1 2 3' in output, output + tupstr = "('py-exec2:', 1, 2, 3)" + assert (tupstr in output or + tupstr.replace("'", '"') in output), (tupstr, output) + assert "somevariable does not exist" in output, repr(output) + + def test_main(): if python_is_optimized(): raise unittest.SkipTest("Python was compiled with optimizations") @@ -671,7 +765,10 @@ StackNavigationTests, PyBtTests, PyPrintTests, - PyLocalsTests + PyLocalsTests, + PyBreakTest, + PyStepNextTests, + PyExecTests, ) if __name__ == "__main__":