diff --git a/gdb/qt5printers/__init__.py b/gdb/qt5printers/__init__.py index 5046a82..49735f6 100644 --- a/gdb/qt5printers/__init__.py +++ b/gdb/qt5printers/__init__.py @@ -1,33 +1,33 @@ # Copyright 2014 Alex Merry # # Permission to use, copy, modify, and distribute this software # and its documentation for any purpose and without fee is hereby # granted, provided that the above copyright notice appear in all # copies and that both that the copyright notice and this # permission notice and warranty disclaimer appear in supporting # documentation, and that the name of the author not be used in # advertising or publicity pertaining to distribution of the # software without specific, written prior permission. # # The author disclaims all warranties with regard to this # software, including all implied warranties of merchantability # and fitness. In no event shall the author be liable for any # special, indirect or consequential damages or any damages # whatsoever resulting from loss of use, data or profits, whether # in an action of contract, negligence or other tortious action, # arising out of or in connection with the use or performance of # this software. import gdb.printing -import core +from qt5printers import core """Qt5 Pretty Printers for GDB. The printers are split into submodules, one for each Qt library. Each submodule has a "printer" attribute that contains a pretty-printer for that library. """ def register_printers(obj): """Registers all known Qt5 pretty-printers.""" gdb.printing.register_pretty_printer(obj, core.printer) diff --git a/gdb/qt5printers/core.py b/gdb/qt5printers/core.py index 93471e0..1f7cfb2 100644 --- a/gdb/qt5printers/core.py +++ b/gdb/qt5printers/core.py @@ -1,900 +1,900 @@ # Copyright 2014 Alex Merry # # Permission to use, copy, modify, and distribute this software # and its documentation for any purpose and without fee is hereby # granted, provided that the above copyright notice appear in all # copies and that both that the copyright notice and this # permission notice and warranty disclaimer appear in supporting # documentation, and that the name of the author not be used in # advertising or publicity pertaining to distribution of the # software without specific, written prior permission. # # The author disclaims all warranties with regard to this # software, including all implied warranties of merchantability # and fitness. In no event shall the author be liable for any # special, indirect or consequential damages or any damages # whatsoever resulting from loss of use, data or profits, whether # in an action of contract, negligence or other tortious action, # arising out of or in connection with the use or performance of # this software. import gdb.printing import itertools -import typeinfo +from qt5printers import typeinfo try: import urlparse except ImportError: # Python 3 import urllib.parse as urlparse """Qt5Core pretty printer for GDB.""" # NB: no QPair printer: the default should be fine def _format_jd(jd): """Format a Julian Day in YYYY-MM-DD format.""" # maths from http://www.tondering.dk/claus/cal/julperiod.php a = jd + 32044 b = (4 * a + 3) // 146097 c = a - ( (146097 * b) // 4 ) d = (4 * c + 3) // 1461 e = c - ( (1461 * d) // 4 ) m = (5 * e + 2) // 153 day = e - ( (153 * m + 2) // 5 ) + 1 month = m + 3 - 12 * ( m // 10 ) year = 100 * b + d - 4800 + ( m // 10 ) return '{:0=4}-{:0=2}-{:0=2}'.format(year, month, day) def _jd_is_valid(jd): """Return whether QDate would consider a given Julian Day valid.""" return jd >= -784350574879 and jd <= 784354017364 def _format_time_ms(msecs): """Format a number of milliseconds since midnight in HH:MM:SS.ssss format.""" secs = msecs // 1000 mins = secs // 60 hours = mins // 60 return '{:0=2}:{:0=2}:{:0=2}.{:0=3}'.format( hours % 24, mins % 60, secs % 60, msecs % 1000) def _ms_is_valid(msecs): """Return whether QTime would consider a ms since midnight valid.""" return msecs >= 0 and msecs <= 86400000 class ArrayIter: """Iterates over a fixed-size array.""" def __init__(self, array, size): self.array = array self.i = -1 self.size = size def __iter__(self): return self def __next__(self): if self.i + 1 >= self.size: raise StopIteration self.i += 1 return ('[%d]' % self.i, self.array[self.i]) def next(self): return self.__next__() class StructReader: """Reads entries from a struct.""" def __init__(self, data): self.data = data.reinterpret_cast(gdb.lookup_type('char').pointer()) self.ptr_t = gdb.lookup_type('void').pointer() def next_aligned_val(self, typ): ptr_val = int(str(self.data.reinterpret_cast(self.ptr_t)), 16) misalignment = ptr_val % self.ptr_t.sizeof if misalignment > 0: self.data += self.ptr_t.sizeof - misalignment val = self.data.reinterpret_cast(typ.pointer()) self.data += typ.sizeof return val.referenced_value() def next_val(self, typ): val = self.data.reinterpret_cast(typ.pointer()) self.data += typ.sizeof return val.referenced_value() class QBitArrayPrinter: """Print a Qt5 QBitArray""" class Iter: def __init__(self, data, size): self.data = data self.i = -1 self.size = size def __iter__(self): return self def __next__(self): if self.i + 1 >= self.size: raise StopIteration self.i += 1 if self.data[1 + (self.i >> 3)] & (1 << (self.i&7)): return (str(self.i), 1) else: return (str(self.i), 0) def next(self): return self.__next__() def __init__(self, val): self.val = val def children(self): d = self.val['d']['d'] data = d.reinterpret_cast(gdb.lookup_type('char').pointer()) + d['offset'] size = (int(d['size']) << 3) - int(data[0]) return self.Iter(data, size) def to_string(self): d = self.val['d']['d'] data = d.reinterpret_cast(gdb.lookup_type('char').pointer()) + d['offset'] size = (int(d['size']) << 3) - int(data[0]) if size == 0: return '' return None def display_hint(self): return 'array' class QByteArrayPrinter: """Print a Qt5 QByteArray""" def __init__(self, val): self.val = val def children(self): d = self.val['d'] data = d.reinterpret_cast(gdb.lookup_type('char').pointer()) + d['offset'] return ArrayIter(data, d['size']) def to_string(self): d = self.val['d'] data = d.reinterpret_cast(gdb.lookup_type('char').pointer()) + d['offset'] return data.string('', 'replace', d['size']) def display_hint(self): return 'string' class QCharPrinter: """Print a Qt5 QChar""" def __init__(self, val): self.val = val def to_string(self): ucs = self.val['ucs'] data = ucs.address.reinterpret_cast(gdb.lookup_type('char').pointer()) unicode_str = data.string('utf-16', 'replace', 2) uch = unicode_str[0] if uch == unichr(0x27): return "'\\''" # this actually gives us Python escapes, but they should all be # valid C escapes as well return "'" + uch.encode('unicode_escape') + "'" def display_hint(self): # this is not recognized by gdb, hence the manual escaping and quoting # we do above return 'char' class QDatePrinter: """Print a Qt5 QDate""" def __init__(self, val): self.val = val def to_string(self): jd = int(self.val['jd']) if not _jd_is_valid(jd): return '' return _format_jd(jd) def display_hint(self): return 'date' class QDateTimePrinter: """Print a Qt5 QDateTime""" def __init__(self, val): self.val = val _unix_epoch_jd = 2440588 _ms_per_day = 86400000 # status field _validDate = 0x04 _validTime = 0x08 _validDateTime = 0x10 _timeZoneCached = 0x20 # time spec _localTime = 0 _UTC = 1 _offsetFromUTC = 2 _timeZone = 3 def to_string(self): d = self.val['d']['d'] if not d: return '' try: qshareddata_t = gdb.lookup_type('QSharedData') except gdb.error: try: # well, it only has a QAtomicInt in it qshareddata_t = gdb.lookup_type('QAtomicInt') except gdb.error: # let's hope it's the same size as an int qshareddata_t = gdb.lookup_type('int') try: timespec_t = gdb.lookup_type('Qt::TimeSpec') except gdb.error: # probably an int timespec_t = gdb.lookup_type('int') reader = StructReader(d) reader.next_val(qshareddata_t) m_msecs = reader.next_aligned_val(gdb.lookup_type('qint64')) spec = int(reader.next_val(timespec_t)) m_offsetFromUtc = reader.next_val(gdb.lookup_type('int')) m_timeZone = reader.next_val(gdb.lookup_type('QTimeZone')) status = int(reader.next_val(gdb.lookup_type('int'))) if spec == self._timeZone: timeZoneStr = QTimeZonePrinter(m_timeZone).to_string() if timeZoneStr == '': return '' if spec == self._localTime or (spec == self._timeZone and not status & self._timeZoneCached): # Because QDateTime delays timezone calculations as far as # possible, the ValidDateTime flag may not be set even if # it is a valid DateTime. if not status & self._validDate or not status & self._validTime: return '' elif not (status & self._validDateTime): return '' # actually fetch: m_msecs = int(m_msecs) jd = self._unix_epoch_jd # UNIX epoch jd += m_msecs // self._ms_per_day msecs = m_msecs % self._ms_per_day if msecs < 0: # need to adjust back to the previous day jd -= 1 msecs += self._ms_per_day result = _format_jd(jd) + ' ' + _format_time_ms(msecs) if spec == self._localTime: result += ' (Local)' elif spec == self._UTC: result += ' (UTC)' elif spec == self._offsetFromUTC: offset = int(m_offsetFromUtc) if offset == 0: diffstr = '' else: hours = abs(offset // 3600) mins = abs((offset % 3600) // 60) secs = abs(offset % 60) sign = '+' if offset > 0 else '-' diffstr = '{:}{:0=2d}:{:0=2d}'.format(sign, hours, mins) if secs > 0: diffstr += ':{:0=2d}'.format(secs) result += ' (UTC{:})'.format(diffstr) elif spec == self._timeZone: result += ' ({:})'.format(timeZoneStr) return result def display_hint(self): return 'datetime' class QHashPrinter: """Print a Qt5 QHash""" class Iter: def __init__(self, d, e): self.buckets_left = d['numBuckets'] self.node_type = e.type # set us up at the end of a "dummy bucket" self.current_bucket = d['buckets'] - 1 self.current_node = None self.i = -1 self.waiting_for_value = False def __iter__(self): return self def __next__(self): if self.waiting_for_value: self.waiting_for_value = False node = self.current_node.reinterpret_cast(self.node_type) return ('value' + str(self.i), node['value']) if self.current_node: self.current_node = self.current_node['next'] # the dummy node that terminates a bucket is distinguishable # by not having its 'next' value set if not self.current_node or not self.current_node['next']: while self.buckets_left: self.current_bucket += 1 self.buckets_left -= 1 self.current_node = self.current_bucket.referenced_value() if self.current_node['next']: break else: raise StopIteration self.i += 1 self.waiting_for_value = True node = self.current_node.reinterpret_cast(self.node_type) return ('key' + str(self.i), node['key']) def next(self): return self.__next__() def __init__(self, val): self.val = val def children(self): d = self.val['d'] if d['size'] == 0: return [] return self.Iter(d, self.val['e']) def to_string(self): # if we return an empty list from children, gdb doesn't print anything if self.val['d']['size'] == 0: return '' return None def display_hint(self): return 'map' class QLatin1StringPrinter: """Print a Qt5 QLatin1String""" def __init__(self, val): self.val = val def to_string(self): return self.val['m_data'].string('', 'replace', self.val['m_size']) def display_hint(self): return 'string' class QLinkedListPrinter: """Print a Qt5 QLinkedList""" class Iter: def __init__(self, tail, size): self.current = tail self.i = -1 self.size = size def __iter__(self): return self def __next__(self): if self.i + 1 >= self.size: raise StopIteration self.i += 1 self.current = self.current['n'] return (str(self.i), self.current['t']) def next(self): return self.__next__() def __init__(self, val): self.val = val def children(self): size = int(self.val['d']['size']) if size == 0: return [] return self.Iter(self.val['e'], size) def to_string(self): # if we return an empty list from children, gdb doesn't print anything if self.val['d']['size'] == 0: return '' return None def display_hint(self): return 'array' class QListPrinter: """Print a Qt5 QList""" class Iter: def __init__(self, array, begin, end, typ): self.array = array self.end = end self.begin = begin self.offset = 0 if typ.name == 'QStringList': self.el_type = gdb.lookup_type('QString') else: self.el_type = typ.template_argument(0) if ((self.el_type.sizeof > gdb.lookup_type('void').pointer().sizeof) or typeinfo.type_is_known_static(self.el_type)): self.is_pointer = True elif (typeinfo.type_is_known_movable(self.el_type) or typeinfo.type_is_known_primitive(self.el_type)): self.is_pointer = False else: raise ValueError("Could not determine whether QList stores " + self.el_type.name + " directly or as a pointer: to fix " + "this, add it to one of the variables in the "+ "qt5printers.typeinfo module") self.node_type = gdb.lookup_type(typ.name + '::Node').pointer() def __iter__(self): return self def __next__(self): if self.begin + self.offset >= self.end: raise StopIteration node = self.array[self.begin + self.offset].reinterpret_cast(self.node_type) if self.is_pointer: p = node['v'] else: p = node self.offset += 1 return ((str(self.offset), p.cast(self.el_type))) def next(self): return self.__next__() def __init__(self, val): self.val = val def children(self): d = self.val['d'] begin = int(d['begin']) end = int(d['end']) if begin == end: return [] return self.Iter(d['array'], begin, end, self.val.type.strip_typedefs()) def to_string(self): # if we return an empty list from children, gdb doesn't print anything if self.val['d']['begin'] == self.val['d']['end']: return '' return None def display_hint(self): return 'array' class QMapPrinter: """Print a Qt5 QMap""" class Iter: def __init__(self, root, node_p_type): self.root = root self.current = None self.node_p_type = node_p_type self.next_is_key = True self.i = -1 # we store the path here to avoid keeping re-fetching # values from the inferior (also, skips the pointer # arithmetic involved in using the parent pointer) self.path = [] def __iter__(self): return self def moveToNextNode(self): if self.current is None: # find the leftmost node if not self.root['left']: return False self.current = self.root while self.current['left']: self.path.append(self.current) self.current = self.current['left'] elif self.current['right']: self.path.append(self.current) self.current = self.current['right'] while self.current['left']: self.path.append(self.current) self.current = self.current['left'] else: last = self.current self.current = self.path.pop() while self.current['right'] == last: last = self.current self.current = self.path.pop() # if there are no more parents, we are at the root if len(self.path) == 0: return False return True def __next__(self): if self.next_is_key: if not self.moveToNextNode(): raise StopIteration self.current_typed = self.current.reinterpret_cast(self.node_p_type) self.next_is_key = False self.i += 1 return ('key' + str(self.i), self.current_typed['key']) else: self.next_is_key = True return ('value' + str(self.i), self.current_typed['value']) def next(self): return self.__next__() def __init__(self, val): self.val = val def children(self): d = self.val['d'] size = int(d['size']) if size == 0: return [] realtype = self.val.type.strip_typedefs() keytype = realtype.template_argument(0) valtype = realtype.template_argument(1) node_type = gdb.lookup_type('QMapData<' + keytype.name + ',' + valtype.name + '>::Node') return self.Iter(d['header'], node_type.pointer()) def to_string(self): # if we return an empty list from children, gdb doesn't print anything if self.val['d']['size'] == 0: return '' return None def display_hint(self): return 'map' class QSetPrinter: """Print a Qt5 QSet""" def __init__(self, val): self.val = val def children(self): hashPrinter = QHashPrinter(self.val['q_hash']) # the keys of the hash are the elements of the set, so select # every other item (starting with the first) return itertools.islice(hashPrinter.children(), 0, None, 2) def to_string(self): # if we return an empty list from children, gdb doesn't print anything if self.val['q_hash']['d']['size'] == 0: return '' return None def display_hint(self): return 'array' class QStringPrinter: """Print a Qt5 QString""" def __init__(self, val): self.val = val def to_string(self): d = self.val['d'] data = d.reinterpret_cast(gdb.lookup_type('char').pointer()) + d['offset'] data_len = d['size'] * gdb.lookup_type('unsigned short').sizeof return data.string('utf-16', 'replace', data_len) def display_hint(self): return 'string' class QTimePrinter: """Print a Qt5 QTime""" def __init__(self, val): self.val = val def to_string(self): msecs = int(self.val['mds']) if not _ms_is_valid(msecs): return '' return _format_time_ms(msecs) def display_hint(self): return 'time' class QTimeZonePrinter: """Print a Qt5 QTimeZone""" def __init__(self, val): self.val = val def to_string(self): d = self.val['d']['d'] if not d: return '' try: # Accessing the private data is error-prone, # so try just calling the id() method. # This should be reasonably safe, as all it will # do is create a QByteArray that references the # same internal data as the stored one. However, # it will only work with an attached process. m_id = gdb.parse_and_eval('((QTimeZone*){:})->id()'.format(self.val.address)) except: ptr_size = gdb.lookup_type('void').pointer().sizeof try: qshareddata_t = gdb.lookup_type('QSharedData') except gdb.error: try: # well, it only has a QAtomicInt in it qshareddata_t = gdb.lookup_type('QAtomicInt') except gdb.error: # let's hope it's the same size as an int qshareddata_t = gdb.lookup_type('int') reader = StructReader(d) reader.next_val(gdb.lookup_type('void').pointer()) # vtable reader.next_val(qshareddata_t) m_id = reader.next_aligned_val(gdb.lookup_type('QByteArray')) return QByteArrayPrinter(m_id).to_string() def display_hint(self): return 'string' class QVariantPrinter: """Print a Qt5 QVariant""" _varmap = { 'char': 'c', 'uchar': 'uc', 'short': 's', 'signed char': 'sc', 'ushort': 'us', 'int': 'i', 'uint': 'u', 'long': 'l', 'ulong': 'ul', 'bool': 'b', 'double': 'd', 'float': 'f', 'qreal': 'real', 'qlonglong': 'll', 'qulonglong': 'ull', 'QObject*': 'o', 'void*': 'ptr' } def __init__(self, val): self.val = val def children(self): d = self.val['d'] typ = int(d['type']) if typ == typeinfo.meta_type_unknown: return [('type', 'invalid')] data = d['data'] if typ in typeinfo.meta_type_names: typename = typeinfo.meta_type_names[typ] if typename in self._varmap: field = self._varmap[typename] return [('type', typename), ('data', data[field])] try: if typename.endswith('*'): gdb_type = gdb.lookup_type(typename[0:-1]).pointer() else: gdb_type = gdb.lookup_type(typename) except gdb.error: # couldn't find any type information return [('type', typename), ('data', data)] if gdb_type.sizeof > data.type.sizeof: is_pointer = True elif (typeinfo.type_is_known_movable(gdb_type) or typeinfo.type_is_known_primitive(gdb_type)): is_pointer = False elif gdb_type.tag == 'enum': is_pointer = False else: # couldn't figure out how the type is stored return [('type', typename), ('data', data)] if is_pointer: value = data['shared']['ptr'].reinterpret_cast(gdb_type.pointer()) else: void_star = gdb.lookup_type('void').pointer() data_void = data['c'].address.reinterpret_cast(void_star) value = data_void.reinterpret_cast(gdb_type.pointer()) return [('type', typename), ('data', value.referenced_value())] else: # custom type? return [('type', typ), ('data', data)] def to_string(self): return None class QVarLengthArrayPrinter: """Print a Qt5 QVarLengthArray""" def __init__(self, val): self.val = val def children(self): size = int(self.val['s']) if size == 0: return [] return ArrayIter(self.val['ptr'], size) def to_string(self): # if we return an empty list from children, gdb doesn't print anything if self.val['s'] == 0: return '' return None def display_hint(self): return 'array' class QVectorPrinter: """Print a Qt5 QVector""" def __init__(self, val): self.val = val def children(self): d = self.val['d'] el_type = self.val.type.template_argument(0) data_len = int(d['size']) if data_len == 0: return [] data_char = d.reinterpret_cast(gdb.lookup_type('char').pointer()) + d['offset'] data = data_char.reinterpret_cast(el_type.pointer()) return ArrayIter(data, data_len) def to_string(self): # if we return an empty list from children, gdb doesn't print anything if self.val['d']['size'] == 0: return '' return None def display_hint(self): return 'array' class QUrlPrinter: """Print a Qt5 QUrl""" def __init__(self, val): self.val = val def to_string(self): d = self.val['d'] if not d: return '' int_t = gdb.lookup_type('int') try: atomicint_t = gdb.lookup_type('QAtomicInt') except gdb.error: # let's hope it's the same size as an int atomicint_t = int_t qstring_t = gdb.lookup_type('QString') uchar_t = gdb.lookup_type('uchar') reader = StructReader(d) # These fields (including order) are unstable, and # may change between even patch-level Qt releases reader.next_val(atomicint_t) port = int(reader.next_val(int_t)) scheme = reader.next_val(qstring_t) userName = reader.next_val(qstring_t) password = reader.next_val(qstring_t) host = reader.next_val(qstring_t) path = reader.next_val(qstring_t) query = reader.next_val(qstring_t) fragment = reader.next_val(qstring_t) reader.next_val(gdb.lookup_type('void').pointer()) sections = int(reader.next_val(uchar_t)) flags = int(reader.next_val(uchar_t)) # isLocalFile and no query and no fragment if flags & 0x01 and not (sections & 0x40) and not (sections & 0x80): # local file return path def qs_to_s(qstring): return QStringPrinter(qstring).to_string() # QUrl::toString() is way more complicated than what we do here, # but this is good enough for debugging result = '' if sections & 0x01: result += qs_to_s(scheme) + ':' if sections & (0x02 | 0x04 | 0x08 | 0x10) or flags & 0x01: result += '//' if sections & 0x02 or sections & 0x04: result += qs_to_s(userName) if sections & 0x04: # this may appear in backtraces that will be sent to other # people result += ':' result += '@' if sections & 0x08: result += qs_to_s(host) if port != -1: result += ':' + str(port) result += qs_to_s(path) if sections & 0x40: result += '?' + qs_to_s(query) if sections & 0x80: result += '#' + qs_to_s(fragment) return result def display_hint(self): return 'string' def build_pretty_printer(): """Builds the pretty printer for Qt5Core.""" pp = gdb.printing.RegexpCollectionPrettyPrinter("Qt5Core") pp.add_printer('QBitArray', '^QBitArray$', QBitArrayPrinter) pp.add_printer('QByteArray', '^QByteArray$', QByteArrayPrinter) pp.add_printer('QChar', '^QChar$', QCharPrinter) pp.add_printer('QDate', '^QDate$', QDatePrinter) pp.add_printer('QDateTime', '^QDateTime$', QDateTimePrinter) pp.add_printer('QLatin1String', '^QLatin1String$', QLatin1StringPrinter) pp.add_printer('QLinkedList', '^QLinkedList<.*>$', QLinkedListPrinter) pp.add_printer('QList', '^QList<.*>$', QListPrinter) pp.add_printer('QMap', '^QMap<.*>$', QMapPrinter) pp.add_printer('QHash', '^QHash<.*>$', QHashPrinter) pp.add_printer('QQueue', '^QQueue<.*>$', QListPrinter) pp.add_printer('QSet', '^QSet<.*>$', QSetPrinter) pp.add_printer('QStack', '^QStack<.*>$', QVectorPrinter) pp.add_printer('QString', '^QString$', QStringPrinter) pp.add_printer('QStringList', '^QStringList$', QListPrinter) pp.add_printer('QTime', '^QTime$', QTimePrinter) pp.add_printer('QTimeZone', '^QTimeZone$', QTimeZonePrinter) pp.add_printer('QVariant', '^QVariant$', QVariantPrinter) pp.add_printer('QVariantList', '^QVariantList$', QListPrinter) pp.add_printer('QVariantMap', '^QVariantMap$', QMapPrinter) pp.add_printer('QVector', '^QVector<.*>$', QVectorPrinter) pp.add_printer('QVarLengthArray', '^QVarLengthArray<.*>$', QVarLengthArrayPrinter) pp.add_printer('QUrl', '^QUrl$', QUrlPrinter) return pp printer = build_pretty_printer() """The pretty printer for Qt5Core. This can be registered using gdb.printing.register_pretty_printer(). """