""" GDB pretty-printers for the PIP (Platform-Independent Primitives) library. Covered types: PIString, PIByteArray, PIChar, PIVector, PIDeque, PIStringList, PIMap, PISet, PIPair, 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 ``/share/pip/gdb/pip_pp.py`` and an auto-load stub ``/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 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 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* 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* pid_data, size_t pid_size, size_t pid_rsize, size_t pid_start. Also used for PIStringList (derived from PIDeque). """ 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: PIVector pim_content + PIDeque 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: derived from PIMap. 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: 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