Files
pip/tools/gdb/pip_pp.py
T

376 lines
11 KiB
Python

"""
GDB pretty-printers for the PIP (Platform-Independent Primitives) library.
Covered types:
PIString, PIByteArray, PIChar, PIVector<T>, PIDeque<T>,
PIStringList, PIMap<K,V>, PISet<T>, PIPair<K,V>, PIVariant,
PINetworkAddress
Usage
-----
1. Manual (works for any build, requires debug info -g):
(gdb) source /path/to/pip_pp.py
Load the file after the executable (or library) is loaded, e.g. in a
session started as ``gdb ./program`` or after the ``file`` command.
2. Automatic: CMake install places this file at
``<prefix>/share/pip/gdb/pip_pp.py`` and an auto-load stub
``<libdir>/libpip.so-gdb.py`` next to the library. GDB evaluates the
stub automatically when libpip.so is loaded (see GDB manual,
"Auto-loading extensions"). If the library is installed outside of the
default auto-load safe path, run once in gdb:
(gdb) add-auto-load-safe-path /your/lib/dir
All printers are read-only and defensive: on any error they return None
and GDB falls back to its default (raw) printing.
Long containers are truncated: only the first _MAX_ELEMENTS elements are
printed (default 32; change the constant at the top of this file), the
tail is shown as `..., ... (N total)`.
Output examples:
(gdb) p s
$1 = "hello"
(gdb) p v
$2 = {1, 2, 3}
(gdb) p m
$3 = {"a": 1, "b": 2}
(gdb) p &s
$4 = (PIString *) 0x7fffffffdac0 "hello"
"""
import re
import gdb
# Absolute safety cap: never try to read more elements than this
# (protects against corrupted size fields).
_MAX_READ = 10000000
# Maximum number of elements printed per container. Change to taste.
_MAX_ELEMENTS = 32
def _max_elements():
return max(1, _MAX_ELEMENTS)
# ---------------------------------------------------------------------------
# helpers
# ---------------------------------------------------------------------------
def _c_type_name(t):
"""Build a C-style name for a type (handles pointers recursively)."""
t = t.unqualified()
if t.code == gdb.TYPE_CODE_PTR:
return _c_type_name(t.target()) + " *"
return t.tag if t.tag is not None else t.name
def _escape_char(c):
"""Escape a single character for a GDB C-style string literal."""
o = ord(c)
if c == '"':
return '\\"'
if c == "\\":
return "\\\\"
if c == "\n":
return "\\n"
if c == "\t":
return "\\t"
if c == "\r":
return "\\r"
if o < 0x20 or o == 0x7F:
return "\\x%02x" % o
return c
def _utf16_to_string(units):
"""Build a GDB string literal from a list of UTF-16 code units."""
out = []
i = 0
n = len(units)
while i < n:
u = units[i]
# combine surrogate pairs
if 0xD800 <= u <= 0xDBFF and i + 1 < n and 0xDC00 <= units[i + 1] <= 0xDFFF:
u = 0x10000 + ((u - 0xD800) << 10) + (units[i + 1] - 0xDC00)
i += 2
else:
i += 1
if 0 <= u <= 0x10FFFF:
out.append(_escape_char(chr(u)))
else:
out.append("?")
return '"' + "".join(out) + '"'
# ---------------------------------------------------------------------------
# printers
# ---------------------------------------------------------------------------
class _Printer(gdb.ValuePrinter):
"""Base class for PIP pretty-printers.
Subclasses implement _total() and _item(i) for element containers.
"""
def __init__(self, val, prefix=""):
self._val = val
self._prefix = prefix
def _total(self):
raise NotImplementedError
def _item(self, i):
raise NotImplementedError
def to_string(self):
try:
total = int(self._total())
if total < 0 or total > _MAX_READ:
return None
if total == 0:
return self._prefix + "{}"
lim = _max_elements()
count = min(total, lim)
items = [self._item(i) for i in range(count)]
tail = "" if total <= lim else ", ... (%d total)" % total
return self._prefix + "{" + ", ".join(items) + tail + "}"
except Exception:
return None
class _PIStringPrinter(_Printer):
"""PIString: PIDeque<PIChar> d + cache members."""
def to_string(self):
try:
d = self._val["d"]
n = int(d["pid_size"])
if n < 0 or n > _MAX_READ:
return None
start = int(d["pid_start"])
data = d["pid_data"]
units = [int(data[start + i]["ch"]) for i in range(n)]
return self._prefix + _utf16_to_string(units)
except Exception:
return None
class _PIByteArrayPrinter(_Printer):
"""PIByteArray: PIDeque<uchar> d. Printed as a hex byte list."""
def _total(self):
return self._val["d"]["pid_size"]
def _item(self, i):
d = self._val["d"]
return "0x%02x" % int(d["pid_data"][int(d["pid_start"]) + i])
class _PICharPrinter(_Printer):
"""PIChar: single UTF-16 code unit (ushort ch)."""
def to_string(self):
try:
ch = int(self._val["ch"])
if 0x20 <= ch <= 0x7E and ch not in (0x22, 0x5C):
return self._prefix + "'%s'" % chr(ch)
return self._prefix + "U+%04x" % ch
except Exception:
return None
class _PIVectorPrinter(_Printer):
"""PIVector<T>: T* piv_data, size_t piv_size, size_t piv_rsize."""
def _total(self):
return self._val["piv_size"]
def _item(self, i):
return str(self._val["piv_data"][i])
class _PIDequePrinter(_Printer):
"""PIDeque<T>: T* pid_data, size_t pid_size, size_t pid_rsize, size_t pid_start.
Also used for PIStringList (derived from PIDeque<PIString>).
"""
def _total(self):
return self._val["pid_size"]
def _item(self, i):
return str(self._val["pid_data"][int(self._val["pid_start"]) + i])
class _PIMapPrinter(_Printer):
"""PIMap<K,V>: PIVector<V> pim_content + PIDeque<MapIndex> pim_index.
MapIndex{ Key key; size_t index; } is kept sorted by key.
"""
def _total(self):
return self._val["pim_index"]["pid_size"]
def _item(self, i):
idx = self._val["pim_index"]
mi = idx["pid_data"][int(idx["pid_start"]) + i]
key = str(mi["key"])
value = str(self._val["pim_content"]["piv_data"][int(mi["index"])])
return "%s: %s" % (key, value)
class _PISetPrinter(_Printer):
"""PISet<T>: derived from PIMap<T,uchar>. Printed as key list."""
def _total(self):
return self._val["pim_index"]["pid_size"]
def _item(self, i):
idx = self._val["pim_index"]
mi = idx["pid_data"][int(idx["pid_start"]) + i]
return str(mi["key"])
class _PIPairPrinter(_Printer):
"""PIPair<K,V>: public first, second members."""
def to_string(self):
try:
return (self._prefix + "{" + str(self._val["first"])
+ ", " + str(self._val["second"]) + "}")
except Exception:
return None
class _PINetworkAddressPrinter(_Printer):
"""PINetworkAddress: union {uint ip_; uchar ip_b[4]}; ushort port_.
The IP bytes are stored in ip_b[0..3] in octet order, so they are
printed as-is (no byte-swap, works for any endianness).
Printed as "i.i.i.i:p".
"""
def to_string(self):
try:
# Mask against a signed char type: octets must be 0..255.
b = [int(self._val["ip_b"][i]) & 0xFF for i in range(4)]
port = int(self._val["port_"])
return self._prefix + "%d.%d.%d.%d:%d" % (b[0], b[1], b[2], b[3], port)
except Exception:
return None
class _PIVariantPrinter(_Printer):
"""PIVariant: PIByteArray _content (serialized), Type _type (enum).
The content is a serialized blob, so the printer shows the type name
and the content size.
"""
def to_string(self):
try:
t = str(self._val["_type"])
if "::" in t:
t = t.split("::", 1)[1]
d = self._val["_content"]["d"]
n = int(d["pid_size"])
return self._prefix + "PIVariant(%s, %d bytes)" % (t, n)
except Exception:
return None
# ---------------------------------------------------------------------------
# lookup and registration
# ---------------------------------------------------------------------------
_PATTERNS = [
(re.compile(r"^PIString$"), lambda v, p: _PIStringPrinter(v, p)),
(re.compile(r"^PIByteArray$"), lambda v, p: _PIByteArrayPrinter(v, p)),
(re.compile(r"^PIChar$"), lambda v, p: _PICharPrinter(v, p)),
(re.compile(r"^PIStringList$"), lambda v, p: _PIDequePrinter(v, p)),
(re.compile(r"^PIDeque<.*>$"), lambda v, p: _PIDequePrinter(v, p)),
(re.compile(r"^PIVector<.*>$"), lambda v, p: _PIVectorPrinter(v, p)),
(re.compile(r"^PIMap<.*>$"), lambda v, p: _PIMapPrinter(v, p)),
(re.compile(r"^PISet<.*>$"), lambda v, p: _PISetPrinter(v, p)),
(re.compile(r"^PIPair<.*>$"), lambda v, p: _PIPairPrinter(v, p)),
(re.compile(r"^PIVariant$"), lambda v, p: _PIVariantPrinter(v, p)),
(re.compile(r"^PINetworkAddress$"), lambda v, p: _PINetworkAddressPrinter(v, p)),
]
def _pip_pp_lookup(val):
"""Pretty-printer lookup function for PIP types.
Handles both values and pointers to values:
p s -> "hello"
p &s -> (PIString *) 0x7fffffffdac0 "hello"
"""
try:
t = val.type.unqualified()
prefix = ""
if t.code == gdb.TYPE_CODE_PTR:
target = t.target()
if target is None:
return None
addr = int(val)
if addr == 0:
return None
prefix = "(%s) 0x%x " % (_c_type_name(t), addr)
val = val.dereference()
t = target.unqualified()
name = t.tag if t.tag is not None else t.name
if name is None:
return None
for pattern, factory in _PATTERNS:
if pattern.match(name):
return factory(val, prefix)
except Exception:
return None
return None
def register_printers(objfile=None):
"""Register PIP pretty printers.
If an objfile is given (or the current objfile is available) the
printers are registered for it (GDB checks the pretty-printer lists
of all objfiles of the current program space when printing values).
Otherwise the printers are registered globally, so they are used
for all values.
Safe to call multiple times.
"""
if objfile is None:
try:
objfile = gdb.current_objfile()
except gdb.error:
objfile = None
if objfile is not None:
try:
if _pip_pp_lookup not in objfile.pretty_printers:
objfile.pretty_printers.append(_pip_pp_lookup)
return
except Exception:
pass
try:
if _pip_pp_lookup not in gdb.pretty_printers:
gdb.pretty_printers.append(_pip_pp_lookup)
except Exception:
pass
# Register for the current objfile when loaded (e.g. `source pip_pp.py`
# in a session with a loaded executable). The auto-load stub calls
# register_printers() explicitly with the objfile of libpip.so.
try:
register_printers()
except Exception:
pass