From dcd7f2a74c039da1b767345364d4fba5f6e04f5f Mon Sep 17 00:00:00 2001 From: Mark Florisson Date: Sat, 13 Nov 2010 01:21:57 +0100 Subject: [PATCH] Added 'cy exec' and 'py-exec' commands --- Cython/Debugger/libcython.py | 88 ++++++++++++++- Cython/Debugger/libpython.py | 209 ++++++++++++++++++++++++++++++++++- 2 files changed, 289 insertions(+), 8 deletions(-) diff --git a/Cython/Debugger/libcython.py b/Cython/Debugger/libcython.py index d54545b1..dc4ccb99 100644 --- a/Cython/Debugger/libcython.py +++ b/Cython/Debugger/libcython.py @@ -346,11 +346,7 @@ class CythonBase(object): selected_frame.select() - def get_cython_globals_dict(self): - """ - Get the Cython globals dict where the remote names are turned into - local strings. - """ + def get_remote_cython_globals_dict(self): m = gdb.parse_and_eval('__pyx_m') try: @@ -361,7 +357,16 @@ class CythonBase(object): with debugging support (-g)?""")) m = m.cast(PyModuleObject.pointer()) - pyobject_dict = libpython.PyObjectPtr.from_pyobject_ptr(m['md_dict']) + return m['md_dict'] + + + def get_cython_globals_dict(self): + """ + Get the Cython globals dict where the remote names are turned into + local strings. + """ + remote_dict = self.get_remote_cython_globals_dict() + pyobject_dict = libpython.PyObjectPtr.from_pyobject_ptr(remote_dict) result = {} seen = set() @@ -598,6 +603,8 @@ class CyCy(CythonCommand): print_ = CyPrint.register(), locals = CyLocals.register(), globals = CyGlobals.register(), + exec_ = libpython.FixGdbCommand('cy exec', '-cy-exec'), + _exec = CyExec.register(), cy_cname = CyCName('cy_cname'), cy_cvalue = CyCValue('cy_cvalue'), cy_lineno = CyLine('cy_lineno'), @@ -1092,6 +1099,75 @@ class CyGlobals(CyLocals): prefix=' ') +class CyExec(CythonCommand): + name = '-cy-exec' + command_class = gdb.COMMAND_STACK + completer_class = gdb.COMPLETE_NONE + + def _fill_locals_dict(self, executor, local_dict_pointer): + "Fill a remotely allocated dict with values from the Cython C stack" + cython_func = self.get_cython_function() + + for name, cyvar in cython_func.locals.iteritems(): + if cyvar.type == PythonObject: + # skip unitialized Cython variables + try: + val = gdb.parse_and_eval(cyvar.cname) + except RuntimeError: + continue + else: + # Fortunately, Cython initializes all local (automatic) + # variables to NULL + if libpython.pointervalue(val) == 0: + continue + + pystringp = executor.alloc_pystring(name) + code = ''' + PyDict_SetItem( + (PyObject *) %d, + (PyObject *) %d, + (PyObject *) %s) + ''' % (local_dict_pointer, pystringp, cyvar.cname) + + # PyDict_SetItem doesn't steal our reference + executor.decref(pystringp) + + if gdb.parse_and_eval(code) < 0: + gdb.parse_and_eval('PyErr_Print()') + raise gdb.GdbError("Unable to execute Python code.") + + def _find_first_cython_or_python_frame(self): + frame = gdb.selected_frame() + while frame: + if (self.is_cython_function(frame) or + self.is_python_function(frame)): + return frame + + frame = frame.older() + + raise gdb.GdbError("There is no Cython or Python frame on the stack.") + + def invoke(self, expr, from_tty): + frame = self._find_first_cython_or_python_frame() + if self.is_python_function(frame): + libpython.py_exec.invoke(expr, from_tty) + return + + executor = libpython.PythonCodeExecutor() + + # get the dict of Cython globals and construct a dict in the inferior + # with Cython locals + global_dict = gdb.parse_and_eval( + '(PyObject *) PyModule_GetDict(__pyx_m)') + local_dict = gdb.parse_and_eval('(PyObject *) PyDict_New()') + + try: + self._fill_locals_dict(executor, libpython.pointervalue(local_dict)) + executor.evalcode(expr, global_dict, local_dict) + finally: + executor.decref(libpython.pointervalue(local_dict)) + + # Functions class CyCName(gdb.Function, CythonBase): diff --git a/Cython/Debugger/libpython.py b/Cython/Debugger/libpython.py index 7a5572e0..c4173b64 100644 --- a/Cython/Debugger/libpython.py +++ b/Cython/Debugger/libpython.py @@ -49,6 +49,7 @@ import os import re import sys import atexit +import warnings import tempfile import itertools @@ -1591,6 +1592,22 @@ 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 @@ -1687,7 +1704,7 @@ class GenericCodeStepper(gdb.Command): """ def stopped(self): - return gdb.inferiors()[0].pid == 0 + return get_selected_inferior().pid == 0 def _stackdepth(self, frame): depth = 0 @@ -1881,4 +1898,192 @@ py_finish = PyFinish('py-finish') py_run = PyRun('py-run') py_cont = PyCont('py-cont') -py_step.init_breakpoints() \ No newline at end of file +py_step.init_breakpoints() + +Py_single_input = 256 +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) + + +class PythonCodeExecutor(object): + + def malloc(self, size): + chunk = (gdb.parse_and_eval("(void *) malloc(%d)" % size)) + + pointer = pointervalue(chunk) + if pointer == 0: + err("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) + try: + result = gdb.parse_and_eval( + 'PyString_FromStringAndSize((char *) %d, (size_t) %d)' % + (stringp, len(string))) + finally: + self.free(stringp) + + pointer = pointervalue(result) + if pointer == 0: + err("Unable to allocate Python string in " + "the inferior.") + + return pointer + + def free(self, pointer): + gdb.parse_and_eval("free((void *) %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, 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: + err("String contains NUL byte.") + + code += '\0' + + pointer = self.alloc_string(code) + + globalsp = pointervalue(global_dict) + localsp = pointervalue(local_dict) + + if globalp == 0 or localp == 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=Py_single_input, + 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() + gdb.execute('%s %s' % (self.actual_command, args)) + self.fix_gdb() + + +class PyExec(gdb.Command): + + def invoke(self, expr, from_tty): + 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, 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 -- 2.26.2