Index: Tools/gdb/libpython.py =================================================================== --- Tools/gdb/libpython.py (revision 86828) +++ Tools/gdb/libpython.py (working copy) @@ -1,4 +1,8 @@ #!/usr/bin/python + +# NOTE: this file is taken from the Python source distribution +# It can be found under Tools/gdb/libpython.py + ''' 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 +23,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,24 +44,33 @@ The module also extends gdb with some python-specific commands. ''' from __future__ import with_statement -import gdb + +import os +import re +import sys import locale -import sys +import atexit +import warnings +import tempfile +import itertools +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') 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) @@ -138,7 +150,7 @@ 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. @@ -320,7 +332,7 @@ #print 'tp_flags = 0x%08x' % tp_flags #print 'tp_name = %r' % tp_name - + name_map = {'bool': PyBoolObjectPtr, 'classobj': PyClassObjectPtr, 'instance': PyInstanceObjectPtr, @@ -336,14 +348,20 @@ 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: @@ -366,7 +384,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 @@ -439,7 +457,7 @@ 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' @@ -541,6 +559,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 +652,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: @@ -703,6 +722,13 @@ _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' @@ -786,12 +812,11 @@ 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. """ + def proxyval(self, visited): - if PyLongObjectPtr.proxyval(self, visited): - return True - else: - return False + return bool(PyLongObjectPtr.proxyval(self, visited)) + class PyNoneStructPtr(PyObjectPtr): """ Class wrapping a gdb.Value that's a PyObject* pointing to the @@ -806,7 +831,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(): @@ -1038,6 +1063,10 @@ out.write(byte) out.write(quote) +class PyStringObjectPtr(PyBytesObjectPtr): + _typename = 'PyStringObject' + + class PyTupleObjectPtr(PyObjectPtr): _typename = 'PyTupleObject' @@ -1097,7 +1126,6 @@ ch2 = 0xDC00 | (x & 0x3FF) return unichr(ch1) + unichr(ch2) - class PyUnicodeObjectPtr(PyObjectPtr): _typename = 'PyUnicodeObject' @@ -1285,12 +1313,16 @@ proxyval = pyop.proxyval(set()) return stringify(proxyval) + def pretty_printer_lookup(gdbval): type = gdbval.type.unqualified() if type.code == gdb.TYPE_CODE_PTR: type = type.target().unqualified() - t = str(type) - if t in ("PyObject", "PyFrameObject", "PyUnicodeObject"): + # do this every time to allow new subclasses to "register" + # alternatively, we could use a metaclass to register all the typenames + classes = [PyObjectPtr] + classes.extend(PyObjectPtr.__subclasses__()) + if str(type) in [cls._typename for cls in classes]: return PyObjectPtrPrinter(gdbval) """ @@ -1605,13 +1637,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 +1651,726 @@ 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: + 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 + + +class GenericCodeStepper(gdb.Command): + """ + Superclass for code stepping. Subclasses must implement the following + methods: + + lineno(frame) + tells the current line number (only called for a relevant frame) + + 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 + + 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 + list of functions that we should break into depending on the + context + + This class provides an 'invoke' method that invokes a 'step' or 'step-over' + depending on the 'stepinto' argument. + """ + + stepper = False + static_breakpoints = {} + runtime_breakpoints = {} + + def __init__(self, name, stepinto=False): + super(GenericCodeStepper, self).__init__(name, + gdb.COMMAND_RUNNING, + gdb.COMPLETE_NONE) + self.stepinto = stepinto + + def _break_func(self, funcname): + result = gdb.execute('break %s' % funcname, to_string=True) + return re.search(r'Breakpoint (\d+)', result).group(1) + + def init_breakpoints(self): + """ + Keep all breakpoints around and simply disable/enable them each time + we are stepping. We need this because if you set and delete a + breakpoint, gdb will not repeat your command (this is due to 'delete'). + Why? I'm buggered if I know. To further annoy us, we can't use the + breakpoint API because there's no option to make breakpoint setting + silent. + So now! We may have an insane amount of breakpoints to list when the + user does 'info breakpoints' :( + + This method must be called whenever the list of functions we should + step into changes. It can be called on any GenericCodeStepper instance. + """ + break_funcs = set(self.static_break_functions()) + + for funcname in break_funcs: + if funcname not in self.static_breakpoints: + try: + gdb.Breakpoint('', gdb.BP_BREAKPOINT, internal=True) + except (AttributeError, TypeError): + # gdb.Breakpoint does not take an 'internal' argument, or + # gdb.Breakpoint does not exist. + breakpoint = self._break_func(funcname) + except RuntimeError: + # gdb.Breakpoint does take an 'internal' argument, use it + # and hide output + result = gdb.execute( + "python bp = gdb.Breakpoint(%r, gdb.BP_BREAKPOINT, internal=True); " + "print bp.number", + to_string=True) + + breakpoint = int(result) + + self.static_breakpoints[funcname] = breakpoint + + for bp in set(self.static_breakpoints) - break_funcs: + gdb.execute("delete " + self.static_breakpoints[bp]) + + self.disable_breakpoints() + + def enable_breakpoints(self): + for bp in self.static_breakpoints.itervalues(): + gdb.execute('enable ' + bp) + + runtime_break_functions = self.runtime_break_functions() + if runtime_break_functions is None: + return + + for funcname in runtime_break_functions: + if (funcname not in self.static_breakpoints and + funcname not in self.runtime_breakpoints): + self.runtime_breakpoints[funcname] = self._break_func(funcname) + elif funcname in self.runtime_breakpoints: + gdb.execute('enable ' + self.runtime_breakpoints[funcname]) + + def disable_breakpoints(self): + chain = itertools.chain(self.static_breakpoints.itervalues(), + self.runtime_breakpoints.itervalues()) + for bp in chain: + gdb.execute('disable ' + bp) + + def runtime_break_functions(self): + """ + Implement this if the list of step-into functions depends on the + context. + """ + + def stopped(self, result): + match = re.search('^Program received signal .*', result, re.MULTILINE) + if match: + return match.group(0) + elif get_selected_inferior().pid == 0: + return result + else: + return None + + def _stackdepth(self, frame): + depth = 0 + while frame: + frame = frame.older() + depth += 1 + + return depth + + 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.stopped(result) + if result: + print result.strip() + # check whether the program was killed by a signal, it should still + # have a stack. + try: + frame = gdb.selected_frame() + except RuntimeError: + pass + else: + print self.get_source_line(frame) + else: + frame = gdb.selected_frame() + output = None + + if self.is_relevant_function(frame): + output = self.get_source_line(frame) + + if output is None: + pframe = getattr(self, 'print_stackframe', None) + if pframe: + pframe(frame, index=0) + else: + print result.strip() + else: + print output + + 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(self, *args): + """ + 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_relavant = self.is_relevant_function(frame) + if hitbp or is_relavant or self.stopped(result): + break + + self.finish_executing(result) + + def _step(self): + """ + Do a single step or step-over. Returns the result of the last gdb + command that made execution stop. + """ + if self.stepinto: + self.enable_breakpoints() + + beginframe = gdb.selected_frame() + beginline = self.lineno(beginframe) + if not self.stepinto: + depth = self._stackdepth(beginframe) + + newframe = beginframe + result = '' + + while True: + if self.is_relevant_function(newframe): + result = gdb.execute('next', to_string=True) + else: + result = self._finish() + + if self.stopped(result): + break + + newframe = gdb.selected_frame() + is_relevant_function = self.is_relevant_function(newframe) + try: + framename = newframe.name() + except RuntimeError: + framename = None + + m = re.search(r'Breakpoint (\d+)', result) + if m: + bp = self.runtime_breakpoints.get(framename) + if bp is None or (m.group(1) == bp and is_relevant_function): + # 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 self.stepinto: + # see if we returned to the caller + newdepth = self._stackdepth(newframe) + is_relevant_function = (newdepth < depth and + is_relevant_function) + + if is_relevant_function: + break + else: + if self.lineno(newframe) > beginline: + break + + if self.stepinto: + self.disable_breakpoints() + + return result + + def step(self, *args): + return self.finish_executing(self._step()) + + 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 PythonCodeStepper(GenericCodeStepper): + + def pyframe(self, frame): + pyframe = Frame(frame).get_pyop() + if pyframe: + return pyframe + else: + raise gdb.GdbError( + "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 static_break_functions(self): + yield 'PyEval_EvalFrameEx' + + +class PyStep(PythonCodeStepper): + "Step through Python code." + + invoke = PythonCodeStepper.step + +class PyNext(PythonCodeStepper): + "Step-over Python code." + + invoke = PythonCodeStepper.step + +class PyFinish(PythonCodeStepper): + "Execute until function returns to a caller." + + invoke = PythonCodeStepper.finish + +class PyRun(PythonCodeStepper): + "Run the program." + + invoke = PythonCodeStepper.run + +class PyCont(PythonCodeStepper): + + invoke = PythonCodeStepper.cont + + +py_step = PyStep('py-step', stepinto=True) +py_next = PyNext('py-next', stepinto=False) +py_finish = PyFinish('py-finish') +py_run = PyRun('py-run') +py_cont = PyCont('py-cont') + +gdb.execute('set breakpoint pending on') +py_step.init_breakpoints() + +Py_single_input = 256 +Py_file_input = 257 +Py_eval_input = 258 + +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 + +class PythonCodeExecutor(object): + + 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: + try: + gdb.parse_and_eval('PyUnicode_FromStringAndSize') + except RuntimeError: + PyString_FromStringAndSize = 'PyUnicodeUCS2_FromStringAndSize' + else: + PyString_FromStringAndSize = 'PyUnicode_FromStringAndSize' + + 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( + (PyObject *) %(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, 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) + +py_exec = FixGdbCommand('py-exec', '-py-exec') +_py_exec = PyExec("-py-exec", gdb.COMMAND_DATA, gdb.COMPLETE_NONE) \ No newline at end of file Index: Lib/test/gdb_sample2.py =================================================================== --- Lib/test/gdb_sample2.py (revision 0) +++ Lib/test/gdb_sample2.py (revision 0) @@ -0,0 +1,14 @@ +# 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 86828) +++ 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__":