summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Lib/test/test_gdb.py101
-rw-r--r--Misc/NEWS9
-rw-r--r--Tools/gdb/libpython.py101
3 files changed, 200 insertions, 11 deletions
diff --git a/Lib/test/test_gdb.py b/Lib/test/test_gdb.py
index fd1275c..27fccd6 100644
--- a/Lib/test/test_gdb.py
+++ b/Lib/test/test_gdb.py
@@ -11,6 +11,12 @@ import sysconfig
import unittest
import locale
+# Is this Python configured to support threads?
+try:
+ import _thread
+except ImportError:
+ _thread = None
+
from test.support import run_unittest, findfile, python_is_optimized
try:
@@ -151,7 +157,6 @@ class DebuggerTests(unittest.TestCase):
# Ensure no unexpected error messages:
self.assertEqual(err, '')
-
return out
def get_gdb_repr(self, source,
@@ -172,7 +177,7 @@ class DebuggerTests(unittest.TestCase):
# gdb can insert additional '\n' and space characters in various places
# in its output, depending on the width of the terminal it's connected
# to (using its "wrap_here" function)
- m = re.match('.*#0\s+builtin_id\s+\(self\=.*,\s+v=\s*(.*?)\)\s+at\s+Python/bltinmodule.c.*',
+ m = re.match('.*#0\s+builtin_id\s+\(self\=.*,\s+v=\s*(.*?)\)\s+at\s+\S*Python/bltinmodule.c.*',
gdb_output, re.DOTALL)
if not m:
self.fail('Unexpected gdb output: %r\n%s' % (gdb_output, gdb_output))
@@ -671,6 +676,98 @@ Traceback \(most recent call first\):
foo\(1, 2, 3\)
''')
+ @unittest.skipUnless(_thread,
+ "Python was compiled without thread support")
+ def test_threads(self):
+ 'Verify that "py-bt" indicates threads that are waiting for the GIL'
+ cmd = '''
+from threading import Thread
+
+class TestThread(Thread):
+ # These threads would run forever, but we'll interrupt things with the
+ # debugger
+ def run(self):
+ i = 0
+ while 1:
+ i += 1
+
+t = {}
+for i in range(4):
+ t[i] = TestThread()
+ t[i].start()
+
+# Trigger a breakpoint on the main thread
+id(42)
+
+'''
+ # Verify with "py-bt":
+ gdb_output = self.get_stack_trace(cmd,
+ cmds_after_breakpoint=['thread apply all py-bt'])
+ self.assertIn('Waiting for the GIL', gdb_output)
+
+ # Verify with "py-bt-full":
+ gdb_output = self.get_stack_trace(cmd,
+ cmds_after_breakpoint=['thread apply all py-bt-full'])
+ self.assertIn('Waiting for the GIL', gdb_output)
+
+ @unittest.skipIf(python_is_optimized(),
+ "Python was compiled with optimizations")
+ # Some older versions of gdb will fail with
+ # "Cannot find new threads: generic error"
+ # unless we add LD_PRELOAD=PATH-TO-libpthread.so.1 as a workaround
+ @unittest.skipUnless(_thread,
+ "Python was compiled without thread support")
+ def test_gc(self):
+ 'Verify that "py-bt" indicates if a thread is garbage-collecting'
+ cmd = ('from gc import collect\n'
+ 'id(42)\n'
+ 'def foo():\n'
+ ' collect()\n'
+ 'def bar():\n'
+ ' foo()\n'
+ 'bar()\n')
+ # Verify with "py-bt":
+ gdb_output = self.get_stack_trace(cmd,
+ cmds_after_breakpoint=['break update_refs', 'continue', 'py-bt'],
+ )
+ self.assertIn('Garbage-collecting', gdb_output)
+
+ # Verify with "py-bt-full":
+ gdb_output = self.get_stack_trace(cmd,
+ cmds_after_breakpoint=['break update_refs', 'continue', 'py-bt-full'],
+ )
+ self.assertIn('Garbage-collecting', gdb_output)
+
+ @unittest.skipIf(python_is_optimized(),
+ "Python was compiled with optimizations")
+ # Some older versions of gdb will fail with
+ # "Cannot find new threads: generic error"
+ # unless we add LD_PRELOAD=PATH-TO-libpthread.so.1 as a workaround
+ @unittest.skipUnless(_thread,
+ "Python was compiled without thread support")
+ def test_pycfunction(self):
+ 'Verify that "py-bt" displays invocations of PyCFunction instances'
+ cmd = ('from time import sleep\n'
+ 'def foo():\n'
+ ' sleep(1)\n'
+ 'def bar():\n'
+ ' foo()\n'
+ 'bar()\n')
+ # Verify with "py-bt":
+ gdb_output = self.get_stack_trace(cmd,
+ breakpoint='time_sleep',
+ cmds_after_breakpoint=['bt', 'py-bt'],
+ )
+ self.assertIn('<built-in method sleep', gdb_output)
+
+ # Verify with "py-bt-full":
+ gdb_output = self.get_stack_trace(cmd,
+ breakpoint='time_sleep',
+ cmds_after_breakpoint=['py-bt-full'],
+ )
+ self.assertIn('#0 <built-in method sleep', gdb_output)
+
+
class PyPrintTests(DebuggerTests):
@unittest.skipIf(python_is_optimized(),
"Python was compiled with optimizations")
diff --git a/Misc/NEWS b/Misc/NEWS
index ee9520a..20a2407 100644
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -22,6 +22,15 @@ Extension Modules
- Issue #15194: Update libffi to the 3.0.11 release.
+Tools/Demos
+-----------
+
+- Issue #12605: The gdb hooks for debugging CPython (within Tools/gdb) have
+ been enhanced to show information on more C frames relevant to CPython within
+ the "py-bt" and "py-bt-full" commands:
+ * C frames that are waiting on the GIL
+ * C frames that are garbage-collecting
+ * C frames that are due to the invocation of a PyCFunction
What's New in Python 3.3.0 Beta 1?
==================================
diff --git a/Tools/gdb/libpython.py b/Tools/gdb/libpython.py
index cf67cf8..2cdc3da 100644
--- a/Tools/gdb/libpython.py
+++ b/Tools/gdb/libpython.py
@@ -1390,6 +1390,23 @@ class Frame(object):
iter_frame = iter_frame.newer()
return index
+ # We divide frames into:
+ # - "python frames":
+ # - "bytecode frames" i.e. PyEval_EvalFrameEx
+ # - "other python frames": things that are of interest from a python
+ # POV, but aren't bytecode (e.g. GC, GIL)
+ # - everything else
+
+ def is_python_frame(self):
+ '''Is this a PyEval_EvalFrameEx frame, or some other important
+ frame? (see is_other_python_frame for what "important" means in this
+ context)'''
+ if self.is_evalframeex():
+ return True
+ if self.is_other_python_frame():
+ return True
+ return False
+
def is_evalframeex(self):
'''Is this a PyEval_EvalFrameEx frame?'''
if self._gdbframe.name() == 'PyEval_EvalFrameEx':
@@ -1406,6 +1423,49 @@ class Frame(object):
return False
+ def is_other_python_frame(self):
+ '''Is this frame worth displaying in python backtraces?
+ Examples:
+ - waiting on the GIL
+ - garbage-collecting
+ - within a CFunction
+ If it is, return a descriptive string
+ For other frames, return False
+ '''
+ if self.is_waiting_for_gil():
+ return 'Waiting for the GIL'
+ elif self.is_gc_collect():
+ return 'Garbage-collecting'
+ else:
+ # Detect invocations of PyCFunction instances:
+ older = self.older()
+ if older and older._gdbframe.name() == 'PyCFunction_Call':
+ # Within that frame:
+ # "func" is the local containing the PyObject* of the
+ # PyCFunctionObject instance
+ # "f" is the same value, but cast to (PyCFunctionObject*)
+ # "self" is the (PyObject*) of the 'self'
+ try:
+ # Use the prettyprinter for the func:
+ func = older._gdbframe.read_var('func')
+ return str(func)
+ except RuntimeError:
+ return 'PyCFunction invocation (unable to read "func")'
+
+ # This frame isn't worth reporting:
+ return False
+
+ def is_waiting_for_gil(self):
+ '''Is this frame waiting on the GIL?'''
+ # This assumes the _POSIX_THREADS version of Python/ceval_gil.h:
+ name = self._gdbframe.name()
+ if name:
+ return name.startswith('pthread_cond_timedwait')
+
+ def is_gc_collect(self):
+ '''Is this frame "collect" within the the garbage-collector?'''
+ return self._gdbframe.name() == 'collect'
+
def get_pyop(self):
try:
f = self._gdbframe.read_var('f')
@@ -1435,8 +1495,22 @@ class Frame(object):
@classmethod
def get_selected_python_frame(cls):
- '''Try to obtain the Frame for the python code in the selected frame,
- or None'''
+ '''Try to obtain the Frame for the python-related code in the selected
+ frame, or None'''
+ frame = cls.get_selected_frame()
+
+ while frame:
+ if frame.is_python_frame():
+ return frame
+ frame = frame.older()
+
+ # Not found:
+ return None
+
+ @classmethod
+ def get_selected_bytecode_frame(cls):
+ '''Try to obtain the Frame for the python bytecode interpreter in the
+ selected GDB frame, or None'''
frame = cls.get_selected_frame()
while frame:
@@ -1460,7 +1534,11 @@ class Frame(object):
else:
sys.stdout.write('#%i (unable to read python frame information)\n' % self.get_index())
else:
- sys.stdout.write('#%i\n' % self.get_index())
+ info = self.is_other_python_frame()
+ if info:
+ sys.stdout.write('#%i %s\n' % (self.get_index(), info))
+ else:
+ sys.stdout.write('#%i\n' % self.get_index())
def print_traceback(self):
if self.is_evalframeex():
@@ -1474,7 +1552,11 @@ class Frame(object):
else:
sys.stdout.write(' (unable to read python frame information)\n')
else:
- sys.stdout.write(' (not a python frame)\n')
+ info = self.is_other_python_frame()
+ if info:
+ sys.stdout.write(' %s\n' % info)
+ else:
+ sys.stdout.write(' (not a python frame)\n')
class PyList(gdb.Command):
'''List the current Python source code, if any
@@ -1510,9 +1592,10 @@ class PyList(gdb.Command):
if m:
start, end = map(int, m.groups())
- frame = Frame.get_selected_python_frame()
+ # py-list requires an actual PyEval_EvalFrameEx frame:
+ frame = Frame.get_selected_bytecode_frame()
if not frame:
- print 'Unable to locate python frame'
+ print 'Unable to locate gdb frame for python bytecode interpreter'
return
pyop = frame.get_pyop()
@@ -1564,7 +1647,7 @@ def move_in_stack(move_up):
if not iter_frame:
break
- if iter_frame.is_evalframeex():
+ if iter_frame.is_python_frame():
# Result:
if iter_frame.select():
iter_frame.print_summary()
@@ -1618,7 +1701,7 @@ class PyBacktraceFull(gdb.Command):
def invoke(self, args, from_tty):
frame = Frame.get_selected_python_frame()
while frame:
- if frame.is_evalframeex():
+ if frame.is_python_frame():
frame.print_summary()
frame = frame.older()
@@ -1637,7 +1720,7 @@ class PyBacktrace(gdb.Command):
sys.stdout.write('Traceback (most recent call first):\n')
frame = Frame.get_selected_python_frame()
while frame:
- if frame.is_evalframeex():
+ if frame.is_python_frame():
frame.print_traceback()
frame = frame.older()