python-gdb.py: enhance py-bt command
* Add py-bt-full command * py-bt now gives an output similar to a regular Python traceback * py-bt indicates: - if the garbage collector is running - if the thread is waiting for the GIL - detect PyCFunction_Call to get the name of the builtin function
This commit is contained in:
parent
3c5ce404a0
commit
cc1db4bf85
|
@ -13,6 +13,12 @@ import sysconfig
|
|||
from test import test_support
|
||||
from test.test_support import run_unittest, findfile
|
||||
|
||||
# Is this Python configured to support threads?
|
||||
try:
|
||||
import thread
|
||||
except ImportError:
|
||||
thread = None
|
||||
|
||||
def get_gdb_version():
|
||||
try:
|
||||
proc = subprocess.Popen(["gdb", "-nx", "--version"],
|
||||
|
@ -728,20 +734,133 @@ $''')
|
|||
class PyBtTests(DebuggerTests):
|
||||
@unittest.skipIf(python_is_optimized(),
|
||||
"Python was compiled with optimizations")
|
||||
def test_basic_command(self):
|
||||
def test_bt(self):
|
||||
'Verify that the "py-bt" command works'
|
||||
bt = self.get_stack_trace(script=self.get_sample_script(),
|
||||
cmds_after_breakpoint=['py-bt'])
|
||||
self.assertMultilineMatches(bt,
|
||||
r'''^.*
|
||||
#[0-9]+ Frame 0x[0-9a-f]+, for file .*gdb_sample.py, line 7, in bar \(a=1, b=2, c=3\)
|
||||
Traceback \(most recent call first\):
|
||||
File ".*gdb_sample.py", line 10, in baz
|
||||
print\(42\)
|
||||
File ".*gdb_sample.py", line 7, in bar
|
||||
baz\(a, b, c\)
|
||||
#[0-9]+ Frame 0x[0-9a-f]+, for file .*gdb_sample.py, line 4, in foo \(a=1, b=2, c=3\)
|
||||
File ".*gdb_sample.py", line 4, in foo
|
||||
bar\(a, b, c\)
|
||||
#[0-9]+ Frame 0x[0-9a-f]+, for file .*gdb_sample.py, line 12, in <module> \(\)
|
||||
File ".*gdb_sample.py", line 12, in <module>
|
||||
foo\(1, 2, 3\)
|
||||
''')
|
||||
|
||||
@unittest.skipIf(python_is_optimized(),
|
||||
"Python was compiled with optimizations")
|
||||
def test_bt_full(self):
|
||||
'Verify that the "py-bt-full" command works'
|
||||
bt = self.get_stack_trace(script=self.get_sample_script(),
|
||||
cmds_after_breakpoint=['py-bt-full'])
|
||||
self.assertMultilineMatches(bt,
|
||||
r'''^.*
|
||||
#[0-9]+ Frame 0x-?[0-9a-f]+, for file .*gdb_sample.py, line 7, in bar \(a=1, b=2, c=3\)
|
||||
baz\(a, b, c\)
|
||||
#[0-9]+ Frame 0x-?[0-9a-f]+, for file .*gdb_sample.py, line 4, in foo \(a=1, b=2, c=3\)
|
||||
bar\(a, b, c\)
|
||||
#[0-9]+ Frame 0x-?[0-9a-f]+, for file .*gdb_sample.py, line 12, in <module> \(\)
|
||||
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
|
||||
print 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'
|
||||
'print 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'
|
||||
# Tested function must not be defined with METH_NOARGS or METH_O,
|
||||
# otherwise call_function() doesn't call PyCFunction_Call()
|
||||
cmd = ('from time import gmtime\n'
|
||||
'def foo():\n'
|
||||
' gmtime(1)\n'
|
||||
'def bar():\n'
|
||||
' foo()\n'
|
||||
'bar()\n')
|
||||
# Verify with "py-bt":
|
||||
gdb_output = self.get_stack_trace(cmd,
|
||||
breakpoint='time_gmtime',
|
||||
cmds_after_breakpoint=['bt', 'py-bt'],
|
||||
)
|
||||
self.assertIn('<built-in function gmtime', gdb_output)
|
||||
|
||||
# Verify with "py-bt-full":
|
||||
gdb_output = self.get_stack_trace(cmd,
|
||||
breakpoint='time_gmtime',
|
||||
cmds_after_breakpoint=['py-bt-full'],
|
||||
)
|
||||
self.assertIn('#0 <built-in function gmtime', gdb_output)
|
||||
|
||||
|
||||
class PyPrintTests(DebuggerTests):
|
||||
@unittest.skipIf(python_is_optimized(),
|
||||
"Python was compiled with optimizations")
|
||||
|
|
|
@ -45,6 +45,7 @@ The module also extends gdb with some python-specific commands.
|
|||
|
||||
from __future__ import print_function, with_statement
|
||||
import gdb
|
||||
import locale
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
@ -76,6 +77,8 @@ Py_TPFLAGS_TYPE_SUBCLASS = (1 << 31)
|
|||
|
||||
MAX_OUTPUT_LEN=1024
|
||||
|
||||
ENCODING = locale.getpreferredencoding()
|
||||
|
||||
class NullPyObjectPtr(RuntimeError):
|
||||
pass
|
||||
|
||||
|
@ -92,6 +95,18 @@ def safe_range(val):
|
|||
# threshold in case the data was corrupted
|
||||
return xrange(safety_limit(int(val)))
|
||||
|
||||
if sys.version_info[0] >= 3:
|
||||
def write_unicode(file, text):
|
||||
file.write(text)
|
||||
else:
|
||||
def write_unicode(file, text):
|
||||
# Write a byte or unicode string to file. Unicode strings are encoded to
|
||||
# ENCODING encoding with 'backslashreplace' error handler to avoid
|
||||
# UnicodeEncodeError.
|
||||
if isinstance(text, unicode):
|
||||
text = text.encode(ENCODING, 'backslashreplace')
|
||||
file.write(text)
|
||||
|
||||
|
||||
class StringTruncated(RuntimeError):
|
||||
pass
|
||||
|
@ -903,7 +918,12 @@ class PyFrameObjectPtr(PyObjectPtr):
|
|||
newline character'''
|
||||
if self.is_optimized_out():
|
||||
return '(frame information optimized out)'
|
||||
with open(self.filename(), 'r') as f:
|
||||
filename = self.filename()
|
||||
try:
|
||||
f = open(filename, 'r')
|
||||
except IOError:
|
||||
return None
|
||||
with f:
|
||||
all_lines = f.readlines()
|
||||
# Convert from 1-based current_line_num to 0-based list offset:
|
||||
return all_lines[self.current_line_num()-1]
|
||||
|
@ -914,9 +934,9 @@ class PyFrameObjectPtr(PyObjectPtr):
|
|||
return
|
||||
out.write('Frame 0x%x, for file %s, line %i, in %s ('
|
||||
% (self.as_address(),
|
||||
self.co_filename,
|
||||
self.co_filename.proxyval(visited),
|
||||
self.current_line_num(),
|
||||
self.co_name))
|
||||
self.co_name.proxyval(visited)))
|
||||
first = True
|
||||
for pyop_name, pyop_value in self.iter_locals():
|
||||
if not first:
|
||||
|
@ -929,6 +949,16 @@ class PyFrameObjectPtr(PyObjectPtr):
|
|||
|
||||
out.write(')')
|
||||
|
||||
def print_traceback(self):
|
||||
if self.is_optimized_out():
|
||||
sys.stdout.write(' (frame information optimized out)\n')
|
||||
return
|
||||
visited = set()
|
||||
sys.stdout.write(' File "%s", line %i, in %s\n'
|
||||
% (self.co_filename.proxyval(visited),
|
||||
self.current_line_num(),
|
||||
self.co_name.proxyval(visited)))
|
||||
|
||||
class PySetObjectPtr(PyObjectPtr):
|
||||
_typename = 'PySetObject'
|
||||
|
||||
|
@ -1222,6 +1252,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':
|
||||
|
@ -1238,6 +1285,50 @@ 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 ('PyThread_acquire_lock' in name
|
||||
and 'lock_PyThread_acquire_lock' not in name)
|
||||
|
||||
def is_gc_collect(self):
|
||||
'''Is this frame "collect" within the garbage-collector?'''
|
||||
return self._gdbframe.name() == 'collect'
|
||||
|
||||
def get_pyop(self):
|
||||
try:
|
||||
f = self._gdbframe.read_var('f')
|
||||
|
@ -1267,8 +1358,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:
|
||||
|
@ -1283,14 +1388,38 @@ class Frame(object):
|
|||
if self.is_evalframeex():
|
||||
pyop = self.get_pyop()
|
||||
if pyop:
|
||||
sys.stdout.write('#%i %s\n' % (self.get_index(), pyop.get_truncated_repr(MAX_OUTPUT_LEN)))
|
||||
line = pyop.get_truncated_repr(MAX_OUTPUT_LEN)
|
||||
write_unicode(sys.stdout, '#%i %s\n' % (self.get_index(), line))
|
||||
if not pyop.is_optimized_out():
|
||||
line = pyop.current_line()
|
||||
sys.stdout.write(' %s\n' % line.strip())
|
||||
if line is not None:
|
||||
sys.stdout.write(' %s\n' % line.strip())
|
||||
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():
|
||||
pyop = self.get_pyop()
|
||||
if pyop:
|
||||
pyop.print_traceback()
|
||||
if not pyop.is_optimized_out():
|
||||
line = pyop.current_line()
|
||||
if line is not None:
|
||||
sys.stdout.write(' %s\n' % line.strip())
|
||||
else:
|
||||
sys.stdout.write(' (unable to read python frame information)\n')
|
||||
else:
|
||||
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
|
||||
|
@ -1326,9 +1455,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()
|
||||
|
@ -1346,7 +1476,13 @@ class PyList(gdb.Command):
|
|||
if start<1:
|
||||
start = 1
|
||||
|
||||
with open(filename, 'r') as f:
|
||||
try:
|
||||
f = open(filename, 'r')
|
||||
except IOError as err:
|
||||
sys.stdout.write('Unable to open %s: %s\n'
|
||||
% (filename, err))
|
||||
return
|
||||
with f:
|
||||
all_lines = f.readlines()
|
||||
# start and end are 1-based, all_lines is 0-based;
|
||||
# so [start-1:end] as a python slice gives us [start, end] as a
|
||||
|
@ -1374,7 +1510,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()
|
||||
|
@ -1416,6 +1552,24 @@ if hasattr(gdb.Frame, 'select'):
|
|||
PyUp()
|
||||
PyDown()
|
||||
|
||||
class PyBacktraceFull(gdb.Command):
|
||||
'Display the current python frame and all the frames within its call stack (if any)'
|
||||
def __init__(self):
|
||||
gdb.Command.__init__ (self,
|
||||
"py-bt-full",
|
||||
gdb.COMMAND_STACK,
|
||||
gdb.COMPLETE_NONE)
|
||||
|
||||
|
||||
def invoke(self, args, from_tty):
|
||||
frame = Frame.get_selected_python_frame()
|
||||
while frame:
|
||||
if frame.is_python_frame():
|
||||
frame.print_summary()
|
||||
frame = frame.older()
|
||||
|
||||
PyBacktraceFull()
|
||||
|
||||
class PyBacktrace(gdb.Command):
|
||||
'Display the current python frame and all the frames within its call stack (if any)'
|
||||
def __init__(self):
|
||||
|
@ -1426,10 +1580,11 @@ class PyBacktrace(gdb.Command):
|
|||
|
||||
|
||||
def invoke(self, args, from_tty):
|
||||
sys.stdout.write('Traceback (most recent call first):\n')
|
||||
frame = Frame.get_selected_python_frame()
|
||||
while frame:
|
||||
if frame.is_evalframeex():
|
||||
frame.print_summary()
|
||||
if frame.is_python_frame():
|
||||
frame.print_traceback()
|
||||
frame = frame.older()
|
||||
|
||||
PyBacktrace()
|
||||
|
|
Loading…
Reference in New Issue