python | |
# GDB dashboard - Modular visual interface for GDB in Python. | |
# | |
# https://github.com/cyrus-and/gdb-dashboard | |
# License ---------------------------------------------------------------------- | |
# Copyright (c) 2015-2021 Andrea Cardaci <[email protected]> | |
# | |
# Permission is hereby granted, free of charge, to any person obtaining a copy | |
# of this software and associated documentation files (the "Software"), to deal | |
# in the Software without restriction, including without limitation the rights | |
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
# copies of the Software, and to permit persons to whom the Software is | |
# furnished to do so, subject to the following conditions: | |
# | |
# The above copyright notice and this permission notice shall be included in all | |
# copies or substantial portions of the Software. | |
# | |
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
# SOFTWARE. | |
# Imports ---------------------------------------------------------------------- | |
import ast | |
import io | |
import itertools | |
import math | |
import os | |
import re | |
import struct | |
import traceback | |
# Common attributes ------------------------------------------------------------ | |
class R(): | |
@staticmethod | |
def attributes(): | |
return { | |
# miscellaneous | |
'ansi': { | |
'doc': 'Control the ANSI output of the dashboard.', | |
'default': True, | |
'type': bool | |
}, | |
'syntax_highlighting': { | |
'doc': '''Pygments style to use for syntax highlighting. | |
Using an empty string (or a name not in the list) disables this feature. The | |
list of all the available styles can be obtained with (from GDB itself): | |
python from pygments.styles import * | |
python for style in get_all_styles(): print(style)''', | |
'default': 'monokai' | |
}, | |
'discard_scrollback': { | |
'doc': '''Discard the scrollback buffer at each redraw. | |
This makes scrolling less confusing by discarding the previously printed | |
dashboards but only works with certain terminals.''', | |
'default': True, | |
'type': bool | |
}, | |
# values formatting | |
'compact_values': { | |
'doc': 'Display complex objects in a single line.', | |
'default': True, | |
'type': bool | |
}, | |
'max_value_length': { | |
'doc': 'Maximum length of displayed values before truncation.', | |
'default': 100, | |
'type': int | |
}, | |
'value_truncation_string': { | |
'doc': 'String to use to mark value truncation.', | |
'default': '…', | |
}, | |
'dereference': { | |
'doc': 'Annotate pointers with the pointed value.', | |
'default': True, | |
'type': bool | |
}, | |
# prompt | |
'prompt': { | |
'doc': '''GDB prompt. | |
This value is used as a Python format string where `{status}` is expanded with | |
the substitution of either `prompt_running` or `prompt_not_running` attributes, | |
according to the target program status. The resulting string must be a valid GDB | |
prompt, see the command `python print(gdb.prompt.prompt_help())`''', | |
'default': '{status}' | |
}, | |
'prompt_running': { | |
'doc': '''Define the value of `{status}` when the target program is running. | |
See the `prompt` attribute. This value is used as a Python format string where | |
`{pid}` is expanded with the process identifier of the target program.''', | |
'default': '\[\e[1;35m\]>>>\[\e[0m\]' | |
}, | |
'prompt_not_running': { | |
'doc': '''Define the value of `{status}` when the target program is running. | |
See the `prompt` attribute. This value is used as a Python format string.''', | |
'default': '\[\e[90m\]>>>\[\e[0m\]' | |
}, | |
# divider | |
'omit_divider': { | |
'doc': 'Omit the divider in external outputs when only one module is displayed.', | |
'default': False, | |
'type': bool | |
}, | |
'divider_fill_char_primary': { | |
'doc': 'Filler around the label for primary dividers', | |
'default': '─' | |
}, | |
'divider_fill_char_secondary': { | |
'doc': 'Filler around the label for secondary dividers', | |
'default': '─' | |
}, | |
'divider_fill_style_primary': { | |
'doc': 'Style for `divider_fill_char_primary`', | |
'default': '36' | |
}, | |
'divider_fill_style_secondary': { | |
'doc': 'Style for `divider_fill_char_secondary`', | |
'default': '90' | |
}, | |
'divider_label_style_on_primary': { | |
'doc': 'Label style for non-empty primary dividers', | |
'default': '1;33' | |
}, | |
'divider_label_style_on_secondary': { | |
'doc': 'Label style for non-empty secondary dividers', | |
'default': '1;37' | |
}, | |
'divider_label_style_off_primary': { | |
'doc': 'Label style for empty primary dividers', | |
'default': '33' | |
}, | |
'divider_label_style_off_secondary': { | |
'doc': 'Label style for empty secondary dividers', | |
'default': '90' | |
}, | |
'divider_label_skip': { | |
'doc': 'Gap between the aligning border and the label.', | |
'default': 3, | |
'type': int, | |
'check': check_ge_zero | |
}, | |
'divider_label_margin': { | |
'doc': 'Number of spaces around the label.', | |
'default': 1, | |
'type': int, | |
'check': check_ge_zero | |
}, | |
'divider_label_align_right': { | |
'doc': 'Label alignment flag.', | |
'default': False, | |
'type': bool | |
}, | |
# common styles | |
'style_selected_1': { | |
'default': '1;32' | |
}, | |
'style_selected_2': { | |
'default': '32' | |
}, | |
'style_low': { | |
'default': '90' | |
}, | |
'style_high': { | |
'default': '1;37' | |
}, | |
'style_error': { | |
'default': '31' | |
}, | |
'style_critical': { | |
'default': '0;41' | |
} | |
} | |
# Common ----------------------------------------------------------------------- | |
class Beautifier(): | |
def __init__(self, hint, tab_size=4): | |
self.tab_spaces = ' ' * tab_size | |
self.active = False | |
if not R.ansi or not R.syntax_highlighting: | |
return | |
# attempt to set up Pygments | |
try: | |
import pygments | |
from pygments.lexers import GasLexer, NasmLexer | |
from pygments.formatters import Terminal256Formatter | |
if hint == 'att': | |
self.lexer = GasLexer() | |
elif hint == 'intel': | |
self.lexer = NasmLexer() | |
else: | |
from pygments.lexers import get_lexer_for_filename | |
self.lexer = get_lexer_for_filename(hint, stripnl=False) | |
self.formatter = Terminal256Formatter(style=R.syntax_highlighting) | |
self.active = True | |
except ImportError: | |
# Pygments not available | |
pass | |
except pygments.util.ClassNotFound: | |
# no lexer for this file or invalid style | |
pass | |
def process(self, source): | |
# convert tabs anyway | |
source = source.replace('\t', self.tab_spaces) | |
if self.active: | |
import pygments | |
source = pygments.highlight(source, self.lexer, self.formatter) | |
return source.rstrip('\n') | |
def run(command): | |
return gdb.execute(command, to_string=True) | |
def ansi(string, style): | |
if R.ansi: | |
return '\x1b[{}m{}\x1b[0m'.format(style, string) | |
else: | |
return string | |
def divider(width, label='', primary=False, active=True): | |
if primary: | |
divider_fill_style = R.divider_fill_style_primary | |
divider_fill_char = R.divider_fill_char_primary | |
divider_label_style_on = R.divider_label_style_on_primary | |
divider_label_style_off = R.divider_label_style_off_primary | |
else: | |
divider_fill_style = R.divider_fill_style_secondary | |
divider_fill_char = R.divider_fill_char_secondary | |
divider_label_style_on = R.divider_label_style_on_secondary | |
divider_label_style_off = R.divider_label_style_off_secondary | |
if label: | |
if active: | |
divider_label_style = divider_label_style_on | |
else: | |
divider_label_style = divider_label_style_off | |
skip = R.divider_label_skip | |
margin = R.divider_label_margin | |
before = ansi(divider_fill_char * skip, divider_fill_style) | |
middle = ansi(label, divider_label_style) | |
after_length = width - len(label) - skip - 2 * margin | |
after = ansi(divider_fill_char * after_length, divider_fill_style) | |
if R.divider_label_align_right: | |
before, after = after, before | |
return ''.join([before, ' ' * margin, middle, ' ' * margin, after]) | |
else: | |
return ansi(divider_fill_char * width, divider_fill_style) | |
def check_gt_zero(x): | |
return x > 0 | |
def check_ge_zero(x): | |
return x >= 0 | |
def to_unsigned(value, size=8): | |
# values from GDB can be used transparently but are not suitable for | |
# being printed as unsigned integers, so a conversion is needed | |
mask = (2 ** (size * 8)) - 1 | |
return int(value.cast(gdb.Value(mask).type)) & mask | |
def to_string(value): | |
# attempt to convert an inferior value to string; OK when (Python 3 || | |
# simple ASCII); otherwise (Python 2.7 && not ASCII) encode the string as | |
# utf8 | |
try: | |
value_string = str(value) | |
except UnicodeEncodeError: | |
value_string = unicode(value).encode('utf8') | |
except gdb.error as e: | |
value_string = ansi(e, R.style_error) | |
return value_string | |
def format_address(address): | |
pointer_size = gdb.parse_and_eval('$pc').type.sizeof | |
return ('0x{{:0{}x}}').format(pointer_size * 2).format(address) | |
def format_value(value, compact=None): | |
# format references as referenced values | |
# (TYPE_CODE_RVALUE_REF is not supported by old GDB) | |
if value.type.code in (getattr(gdb, 'TYPE_CODE_REF', None), | |
getattr(gdb, 'TYPE_CODE_RVALUE_REF', None)): | |
try: | |
value = value.referenced_value() | |
except gdb.error as e: | |
return ansi(e, R.style_error) | |
# format the value | |
out = to_string(value) | |
# dereference up to the actual value if requested | |
if R.dereference and value.type.code == gdb.TYPE_CODE_PTR: | |
while value.type.code == gdb.TYPE_CODE_PTR: | |
try: | |
value = value.dereference() | |
except gdb.error as e: | |
break | |
else: | |
formatted = to_string(value) | |
out += '{} {}'.format(ansi(':', R.style_low), formatted) | |
# compact the value | |
if compact is not None and compact or R.compact_values: | |
out = re.sub(r'$\s*', '', out, flags=re.MULTILINE) | |
# truncate the value | |
if R.max_value_length > 0 and len(out) > R.max_value_length: | |
out = out[0:R.max_value_length] + ansi(R.value_truncation_string, R.style_critical) | |
return out | |
# XXX parsing the output of `info breakpoints` is apparently the best option | |
# right now, see: https://sourceware.org/bugzilla/show_bug.cgi?id=18385 | |
# XXX GDB version 7.11 (quire recent) does not have the pending field, so | |
# fall back to the parsed information | |
def fetch_breakpoints(watchpoints=False, pending=False): | |
# fetch breakpoints addresses | |
parsed_breakpoints = dict() | |
for line in run('info breakpoints').split('\n'): | |
# just keep numbered lines | |
if not line or not line[0].isdigit(): | |
continue | |
# extract breakpoint number, address and pending status | |
fields = line.split() | |
number = int(fields[0].split('.')[0]) | |
try: | |
if len(fields) >= 5 and fields[1] == 'breakpoint': | |
# multiple breakpoints have no address yet | |
is_pending = fields[4] == '<PENDING>' | |
is_multiple = fields[4] == '<MULTIPLE>' | |
address = None if is_multiple or is_pending else int(fields[4], 16) | |
is_enabled = fields[3] == 'y' | |
address_info = address, is_enabled | |
parsed_breakpoints[number] = [address_info], is_pending | |
elif len(fields) >= 3 and number in parsed_breakpoints: | |
# add this address to the list of multiple locations | |
address = int(fields[2], 16) | |
is_enabled = fields[1] == 'y' | |
address_info = address, is_enabled | |
parsed_breakpoints[number][0].append(address_info) | |
else: | |
# watchpoints | |
parsed_breakpoints[number] = [], False | |
except ValueError: | |
pass | |
# fetch breakpoints from the API and complement with address and source | |
# information | |
breakpoints = [] | |
# XXX in older versions gdb.breakpoints() returns None | |
for gdb_breakpoint in gdb.breakpoints() or []: | |
# skip internal breakpoints | |
if gdb_breakpoint.number < 0: | |
continue | |
addresses, is_pending = parsed_breakpoints[gdb_breakpoint.number] | |
is_pending = getattr(gdb_breakpoint, 'pending', is_pending) | |
if not pending and is_pending: | |
continue | |
if not watchpoints and gdb_breakpoint.type != gdb.BP_BREAKPOINT: | |
continue | |
# add useful fields to the object | |
breakpoint = dict() | |
breakpoint['number'] = gdb_breakpoint.number | |
breakpoint['type'] = gdb_breakpoint.type | |
breakpoint['enabled'] = gdb_breakpoint.enabled | |
breakpoint['location'] = gdb_breakpoint.location | |
breakpoint['expression'] = gdb_breakpoint.expression | |
breakpoint['condition'] = gdb_breakpoint.condition | |
breakpoint['temporary'] = gdb_breakpoint.temporary | |
breakpoint['hit_count'] = gdb_breakpoint.hit_count | |
breakpoint['pending'] = is_pending | |
# add addresses and source information | |
breakpoint['addresses'] = [] | |
for address, is_enabled in addresses: | |
if address: | |
sal = gdb.find_pc_line(address) | |
breakpoint['addresses'].append({ | |
'address': address, | |
'enabled': is_enabled, | |
'file_name': sal.symtab.filename if address and sal.symtab else None, | |
'file_line': sal.line if address else None | |
}) | |
breakpoints.append(breakpoint) | |
return breakpoints | |
# Dashboard -------------------------------------------------------------------- | |
class Dashboard(gdb.Command): | |
'''Redisplay the dashboard.''' | |
def __init__(self): | |
gdb.Command.__init__(self, 'dashboard', gdb.COMMAND_USER, gdb.COMPLETE_NONE, True) | |
# setup subcommands | |
Dashboard.ConfigurationCommand(self) | |
Dashboard.OutputCommand(self) | |
Dashboard.EnabledCommand(self) | |
Dashboard.LayoutCommand(self) | |
# setup style commands | |
Dashboard.StyleCommand(self, 'dashboard', R, R.attributes()) | |
# main terminal | |
self.output = None | |
# used to inhibit redisplays during init parsing | |
self.inhibited = None | |
# enabled by default | |
self.enabled = None | |
self.enable() | |
def on_continue(self, _): | |
# try to contain the GDB messages in a specified area unless the | |
# dashboard is printed to a separate file (dashboard -output ...) | |
# or there are no modules to display in the main terminal | |
enabled_modules = list(filter(lambda m: not m.output and m.enabled, self.modules)) | |
if self.is_running() and not self.output and len(enabled_modules) > 0: | |
width, _ = Dashboard.get_term_size() | |
gdb.write(Dashboard.clear_screen()) | |
gdb.write(divider(width, 'Output/messages', True)) | |
gdb.write('\n') | |
gdb.flush() | |
def on_stop(self, _): | |
if self.is_running(): | |
self.render(clear_screen=False) | |
def on_exit(self, _): | |
if not self.is_running(): | |
return | |
# collect all the outputs | |
outputs = set() | |
outputs.add(self.output) | |
outputs.update(module.output for module in self.modules) | |
outputs.remove(None) | |
# reset the terminal status | |
for output in outputs: | |
try: | |
with open(output, 'w') as fs: | |
fs.write(Dashboard.reset_terminal()) | |
except: | |
# skip cleanup for invalid outputs | |
pass | |
def enable(self): | |
if self.enabled: | |
return | |
self.enabled = True | |
# setup events | |
gdb.events.cont.connect(self.on_continue) | |
gdb.events.stop.connect(self.on_stop) | |
gdb.events.exited.connect(self.on_exit) | |
def disable(self): | |
if not self.enabled: | |
return | |
self.enabled = False | |
# setup events | |
gdb.events.cont.disconnect(self.on_continue) | |
gdb.events.stop.disconnect(self.on_stop) | |
gdb.events.exited.disconnect(self.on_exit) | |
def load_modules(self, modules): | |
self.modules = [] | |
for module in modules: | |
info = Dashboard.ModuleInfo(self, module) | |
self.modules.append(info) | |
def redisplay(self, style_changed=False): | |
# manually redisplay the dashboard | |
if self.is_running() and not self.inhibited: | |
self.render(True, style_changed) | |
def inferior_pid(self): | |
return gdb.selected_inferior().pid | |
def is_running(self): | |
return self.inferior_pid() != 0 | |
def render(self, clear_screen, style_changed=False): | |
# fetch module content and info | |
all_disabled = True | |
display_map = dict() | |
for module in self.modules: | |
# fall back to the global value | |
output = module.output or self.output | |
# add the instance or None if disabled | |
if module.enabled: | |
all_disabled = False | |
instance = module.instance | |
else: | |
instance = None | |
display_map.setdefault(output, []).append(instance) | |
# process each display info | |
for output, instances in display_map.items(): | |
try: | |
buf = '' | |
# use GDB stream by default | |
fs = None | |
if output: | |
fs = open(output, 'w') | |
fd = fs.fileno() | |
fs.write(Dashboard.setup_terminal()) | |
else: | |
fs = gdb | |
fd = 1 # stdout | |
# get the terminal size (default main terminal if either the | |
# output is not a file) | |
try: | |
width, height = Dashboard.get_term_size(fd) | |
except: | |
width, height = Dashboard.get_term_size() | |
# clear the "screen" if requested for the main terminal, | |
# auxiliary terminals are always cleared | |
if fs is not gdb or clear_screen: | |
buf += Dashboard.clear_screen() | |
# show message if all the modules in this output are disabled | |
if not any(instances): | |
# skip the main terminal | |
if fs is gdb: | |
continue | |
# write the error message | |
buf += divider(width, 'Warning', True) | |
buf += '\n' | |
if self.modules: | |
buf += 'No module to display (see `dashboard -layout`)' | |
else: | |
buf += 'No module loaded' | |
buf += '\n' | |
fs.write(buf) | |
continue | |
# process all the modules for that output | |
for n, instance in enumerate(instances, 1): | |
# skip disabled modules | |
if not instance: | |
continue | |
try: | |
# ask the module to generate the content | |
lines = instance.lines(width, height, style_changed) | |
except Exception as e: | |
# allow to continue on exceptions in modules | |
stacktrace = traceback.format_exc().strip() | |
lines = [ansi(stacktrace, R.style_error)] | |
# create the divider if needed | |
div = [] | |
if not R.omit_divider or len(instances) > 1 or fs is gdb: | |
div = [divider(width, instance.label(), True, lines)] | |
# write the data | |
buf += '\n'.join(div + lines) | |
# write the newline for all but last unless main terminal | |
if n != len(instances) or fs is gdb: | |
buf += '\n' | |
# write the final newline and the terminator only if it is the | |
# main terminal to allow the prompt to display correctly (unless | |
# there are no modules to display) | |
if fs is gdb and not all_disabled: | |
buf += divider(width, primary=True) | |
buf += '\n' | |
fs.write(buf) | |
except Exception as e: | |
cause = traceback.format_exc().strip() | |
Dashboard.err('Cannot write the dashboard\n{}'.format(cause)) | |
finally: | |
# don't close gdb stream | |
if fs and fs is not gdb: | |
fs.close() | |
# Utility methods -------------------------------------------------------------- | |
@staticmethod | |
def start(): | |
# save the instance for customization convenience | |
global dashboard | |
# initialize the dashboard | |
dashboard = Dashboard() | |
Dashboard.set_custom_prompt(dashboard) | |
# parse Python inits, load modules then parse GDB inits | |
dashboard.inhibited = True | |
Dashboard.parse_inits(True) | |
modules = Dashboard.get_modules() | |
dashboard.load_modules(modules) | |
Dashboard.parse_inits(False) | |
dashboard.inhibited = False | |
# GDB overrides | |
run('set pagination off') | |
# display if possible (program running and not explicitly disabled by | |
# some configuration file) | |
if dashboard.enabled: | |
dashboard.redisplay() | |
@staticmethod | |
def get_term_size(fd=1): # defaults to the main terminal | |
try: | |
if sys.platform == 'win32': | |
import curses | |
# XXX always neglects the fd parameter | |
height, width = curses.initscr().getmaxyx() | |
curses.endwin() | |
return int(width), int(height) | |
else: | |
import termios | |
import fcntl | |
# first 2 shorts (4 byte) of struct winsize | |
raw = fcntl.ioctl(fd, termios.TIOCGWINSZ, ' ' * 4) | |
height, width = struct.unpack('hh', raw) | |
return int(width), int(height) | |
except (ImportError, OSError): | |
# this happens when no curses library is found on windows or when | |
# the terminal is not properly configured | |
return 80, 24 # hardcoded fallback value | |
@staticmethod | |
def set_custom_prompt(dashboard): | |
def custom_prompt(_): | |
# render thread status indicator | |
if dashboard.is_running(): | |
pid = dashboard.inferior_pid() | |
status = R.prompt_running.format(pid=pid) | |
else: | |
status = R.prompt_not_running | |
# build prompt | |
prompt = R.prompt.format(status=status) | |
prompt = gdb.prompt.substitute_prompt(prompt) | |
return prompt + ' ' # force trailing space | |
gdb.prompt_hook = custom_prompt | |
@staticmethod | |
def parse_inits(python): | |
# paths where the .gdbinit.d directory might be | |
search_paths = [ | |
'/etc/gdb-dashboard', | |
'{}/gdb-dashboard'.format(os.getenv('XDG_CONFIG_HOME', '~/.config')), | |
'~/Library/Preferences/gdb-dashboard', | |
'~/.gdbinit.d' | |
] | |
# expand the tilde and walk the paths | |
inits_dirs = (os.walk(os.path.expanduser(path)) for path in search_paths) | |
# process all the init files in order | |
for root, dirs, files in itertools.chain.from_iterable(inits_dirs): | |
dirs.sort() | |
for init in sorted(files): | |
path = os.path.join(root, init) | |
_, ext = os.path.splitext(path) | |
# either load Python files or GDB | |
if python == (ext == '.py'): | |
gdb.execute('source ' + path) | |
@staticmethod | |
def get_modules(): | |
# scan the scope for modules | |
modules = [] | |
for name in globals(): | |
obj = globals()[name] | |
try: | |
if issubclass(obj, Dashboard.Module): | |
modules.append(obj) | |
except TypeError: | |
continue | |
# sort modules alphabetically | |
modules.sort(key=lambda x: x.__name__) | |
return modules | |
@staticmethod | |
def create_command(name, invoke, doc, is_prefix, complete=None): | |
Class = type('', (gdb.Command,), {'invoke': invoke, '__doc__': doc}) | |
Class(name, gdb.COMMAND_USER, complete or gdb.COMPLETE_NONE, is_prefix) | |
@staticmethod | |
def err(string): | |
print(ansi(string, R.style_error)) | |
@staticmethod | |
def complete(word, candidates): | |
return filter(lambda candidate: candidate.startswith(word), candidates) | |
@staticmethod | |
def parse_arg(arg): | |
# encode unicode GDB command arguments as utf8 in Python 2.7 | |
if type(arg) is not str: | |
arg = arg.encode('utf8') | |
return arg | |
@staticmethod | |
def clear_screen(): | |
# ANSI: move the cursor to top-left corner and clear the screen | |
# (optionally also clear the scrollback buffer if supported by the | |
# terminal) | |
return '\x1b[H\x1b[J' + '\x1b[3J' if R.discard_scrollback else '' | |
@staticmethod | |
def setup_terminal(): | |
# ANSI: enable alternative screen buffer and hide cursor | |
return '\x1b[?1049h\x1b[?25l' | |
@staticmethod | |
def reset_terminal(): | |
# ANSI: disable alternative screen buffer and show cursor | |
return '\x1b[?1049l\x1b[?25h' | |
# Module descriptor ------------------------------------------------------------ | |
class ModuleInfo: | |
def __init__(self, dashboard, module): | |
self.name = module.__name__.lower() # from class to module name | |
self.enabled = True | |
self.output = None # value from the dashboard by default | |
self.instance = module() | |
self.doc = self.instance.__doc__ or '(no documentation)' | |
self.prefix = 'dashboard {}'.format(self.name) | |
# add GDB commands | |
self.add_main_command(dashboard) | |
self.add_output_command(dashboard) | |
self.add_style_command(dashboard) | |
self.add_subcommands(dashboard) | |
def add_main_command(self, dashboard): | |
module = self | |
def invoke(self, arg, from_tty, info=self): | |
arg = Dashboard.parse_arg(arg) | |
if arg == '': | |
info.enabled ^= True | |
if dashboard.is_running(): | |
dashboard.redisplay() | |
else: | |
status = 'enabled' if info.enabled else 'disabled' | |
print('{} module {}'.format(module.name, status)) | |
else: | |
Dashboard.err('Wrong argument "{}"'.format(arg)) | |
doc_brief = 'Configure the {} module, with no arguments toggles its visibility.'.format(self.name) | |
doc = '{}\n\n{}'.format(doc_brief, self.doc) | |
Dashboard.create_command(self.prefix, invoke, doc, True) | |
def add_output_command(self, dashboard): | |
Dashboard.OutputCommand(dashboard, self.prefix, self) | |
def add_style_command(self, dashboard): | |
Dashboard.StyleCommand(dashboard, self.prefix, self.instance, self.instance.attributes()) | |
def add_subcommands(self, dashboard): | |
for name, command in self.instance.commands().items(): | |
self.add_subcommand(dashboard, name, command) | |
def add_subcommand(self, dashboard, name, command): | |
action = command['action'] | |
doc = command['doc'] | |
complete = command.get('complete') | |
def invoke(self, arg, from_tty, info=self): | |
arg = Dashboard.parse_arg(arg) | |
if info.enabled: | |
try: | |
action(arg) | |
except Exception as e: | |
Dashboard.err(e) | |
return | |
# don't catch redisplay errors | |
dashboard.redisplay() | |
else: | |
Dashboard.err('Module disabled') | |
prefix = '{} {}'.format(self.prefix, name) | |
Dashboard.create_command(prefix, invoke, doc, False, complete) | |
# GDB commands ----------------------------------------------------------------- | |
# handler for the `dashboard` command itself | |
def invoke(self, arg, from_tty): | |
arg = Dashboard.parse_arg(arg) | |
# show messages for checks in redisplay | |
if arg != '': | |
Dashboard.err('Wrong argument "{}"'.format(arg)) | |
elif not self.is_running(): | |
Dashboard.err('Is the target program running?') | |
else: | |
self.redisplay() | |
class ConfigurationCommand(gdb.Command): | |
'''Dump or save the dashboard configuration. | |
With an optional argument the configuration will be written to the specified | |
file. | |
This command allows to configure the dashboard live then make the changes | |
permanent, for example: | |
dashboard -configuration ~/.gdbinit.d/init | |
At startup the `~/.gdbinit.d/` directory tree is walked and files are evaluated | |
in alphabetical order but giving priority to Python files. This is where user | |
configuration files must be placed.''' | |
def __init__(self, dashboard): | |
gdb.Command.__init__(self, 'dashboard -configuration', | |
gdb.COMMAND_USER, gdb.COMPLETE_FILENAME) | |
self.dashboard = dashboard | |
def invoke(self, arg, from_tty): | |
arg = Dashboard.parse_arg(arg) | |
if arg: | |
with open(os.path.expanduser(arg), 'w') as fs: | |
fs.write('# auto generated by GDB dashboard\n\n') | |
self.dump(fs) | |
self.dump(gdb) | |
def dump(self, fs): | |
# dump layout | |
self.dump_layout(fs) | |
# dump styles | |
self.dump_style(fs, R) | |
for module in self.dashboard.modules: | |
self.dump_style(fs, module.instance, module.prefix) | |
# dump outputs | |
self.dump_output(fs, self.dashboard) | |
for module in self.dashboard.modules: | |
self.dump_output(fs, module, module.prefix) | |
def dump_layout(self, fs): | |
layout = ['dashboard -layout'] | |
for module in self.dashboard.modules: | |
mark = '' if module.enabled else '!' | |
layout.append('{}{}'.format(mark, module.name)) | |
fs.write(' '.join(layout)) | |
fs.write('\n') | |
def dump_style(self, fs, obj, prefix='dashboard'): | |
attributes = getattr(obj, 'attributes', lambda: dict())() | |
for name, attribute in attributes.items(): | |
real_name = attribute.get('name', name) | |
default = attribute.get('default') | |
value = getattr(obj, real_name) | |
if value != default: | |
fs.write('{} -style {} {!r}\n'.format(prefix, name, value)) | |
def dump_output(self, fs, obj, prefix='dashboard'): | |
output = getattr(obj, 'output') | |
if output: | |
fs.write('{} -output {}\n'.format(prefix, output)) | |
class OutputCommand(gdb.Command): | |
'''Set the output file/TTY for the whole dashboard or single modules. | |
The dashboard/module will be written to the specified file, which will be | |
created if it does not exist. If the specified file identifies a terminal then | |
its geometry will be used, otherwise it falls back to the geometry of the main | |
GDB terminal. | |
When invoked without argument on the dashboard, the output/messages and modules | |
which do not specify an output themselves will be printed on standard output | |
(default). | |
When invoked without argument on a module, it will be printed where the | |
dashboard will be printed. | |
An overview of all the outputs can be obtained with the `dashboard -layout` | |
command.''' | |
def __init__(self, dashboard, prefix=None, obj=None): | |
if not prefix: | |
prefix = 'dashboard' | |
if not obj: | |
obj = dashboard | |
prefix = prefix + ' -output' | |
gdb.Command.__init__(self, prefix, gdb.COMMAND_USER, gdb.COMPLETE_FILENAME) | |
self.dashboard = dashboard | |
self.obj = obj # None means the dashboard itself | |
def invoke(self, arg, from_tty): | |
arg = Dashboard.parse_arg(arg) | |
# reset the terminal status | |
if self.obj.output: | |
try: | |
with open(self.obj.output, 'w') as fs: | |
fs.write(Dashboard.reset_terminal()) | |
except: | |
# just do nothing if the file is not writable | |
pass | |
# set or open the output file | |
if arg == '': | |
self.obj.output = None | |
else: | |
self.obj.output = arg | |
# redisplay the dashboard in the new output | |
self.dashboard.redisplay() | |
class EnabledCommand(gdb.Command): | |
'''Enable or disable the dashboard. | |
The current status is printed if no argument is present.''' | |
def __init__(self, dashboard): | |
gdb.Command.__init__(self, 'dashboard -enabled', gdb.COMMAND_USER) | |
self.dashboard = dashboard | |
def invoke(self, arg, from_tty): | |
arg = Dashboard.parse_arg(arg) | |
if arg == '': | |
status = 'enabled' if self.dashboard.enabled else 'disabled' | |
print('The dashboard is {}'.format(status)) | |
elif arg == 'on': | |
self.dashboard.enable() | |
self.dashboard.redisplay() | |
elif arg == 'off': | |
self.dashboard.disable() | |
else: | |
msg = 'Wrong argument "{}"; expecting "on" or "off"' | |
Dashboard.err(msg.format(arg)) | |
def complete(self, text, word): | |
return Dashboard.complete(word, ['on', 'off']) | |
class LayoutCommand(gdb.Command): | |
'''Set or show the dashboard layout. | |
Accepts a space-separated list of directive. Each directive is in the form | |
"[!]<module>". Modules in the list are placed in the dashboard in the same order | |
as they appear and those prefixed by "!" are disabled by default. Omitted | |
modules are hidden and placed at the bottom in alphabetical order. | |
Without arguments the current layout is shown where the first line uses the same | |
form expected by the input while the remaining depict the current status of | |
output files. | |
Passing `!` as a single argument resets the dashboard original layout.''' | |
def __init__(self, dashboard): | |
gdb.Command.__init__(self, 'dashboard -layout', gdb.COMMAND_USER) | |
self.dashboard = dashboard | |
def invoke(self, arg, from_tty): | |
arg = Dashboard.parse_arg(arg) | |
directives = str(arg).split() | |
if directives: | |
# apply the layout | |
if directives == ['!']: | |
self.reset() | |
else: | |
if not self.layout(directives): | |
return # in case of errors | |
# redisplay or otherwise notify | |
if from_tty: | |
if self.dashboard.is_running(): | |
self.dashboard.redisplay() | |
else: | |
self.show() | |
else: | |
self.show() | |
def reset(self): | |
modules = self.dashboard.modules | |
modules.sort(key=lambda module: module.name) | |
for module in modules: | |
module.enabled = True | |
def show(self): | |
global_str = 'Dashboard' | |
default = '(default TTY)' | |
max_name_len = max(len(module.name) for module in self.dashboard.modules) | |
max_name_len = max(max_name_len, len(global_str)) | |
fmt = '{{}}{{:{}s}}{{}}'.format(max_name_len + 2) | |
print((fmt + '\n').format(' ', global_str, self.dashboard.output or default)) | |
for module in self.dashboard.modules: | |
mark = ' ' if module.enabled else '!' | |
style = R.style_high if module.enabled else R.style_low | |
line = fmt.format(mark, module.name, module.output or default) | |
print(ansi(line, style)) | |
def layout(self, directives): | |
modules = self.dashboard.modules | |
# parse and check directives | |
parsed_directives = [] | |
selected_modules = set() | |
for directive in directives: | |
enabled = (directive[0] != '!') | |
name = directive[not enabled:] | |
if name in selected_modules: | |
Dashboard.err('Module "{}" already set'.format(name)) | |
return False | |
if next((False for module in modules if module.name == name), True): | |
Dashboard.err('Cannot find module "{}"'.format(name)) | |
return False | |
parsed_directives.append((name, enabled)) | |
selected_modules.add(name) | |
# reset visibility | |
for module in modules: | |
module.enabled = False | |
# move and enable the selected modules on top | |
last = 0 | |
for name, enabled in parsed_directives: | |
todo = enumerate(modules[last:], start=last) | |
index = next(index for index, module in todo if name == module.name) | |
modules[index].enabled = enabled | |
modules.insert(last, modules.pop(index)) | |
last += 1 | |
return True | |
def complete(self, text, word): | |
all_modules = (m.name for m in self.dashboard.modules) | |
return Dashboard.complete(word, all_modules) | |
class StyleCommand(gdb.Command): | |
'''Access the stylable attributes. | |
Without arguments print all the stylable attributes. | |
When only the name is specified show the current value. | |
With name and value set the stylable attribute. Values are parsed as Python | |
literals and converted to the proper type. ''' | |
def __init__(self, dashboard, prefix, obj, attributes): | |
self.prefix = prefix + ' -style' | |
gdb.Command.__init__(self, self.prefix, gdb.COMMAND_USER, gdb.COMPLETE_NONE, True) | |
self.dashboard = dashboard | |
self.obj = obj | |
self.attributes = attributes | |
self.add_styles() | |
def add_styles(self): | |
this = self | |
for name, attribute in self.attributes.items(): | |
# fetch fields | |
attr_name = attribute.get('name', name) | |
attr_type = attribute.get('type', str) | |
attr_check = attribute.get('check', lambda _: True) | |
attr_default = attribute['default'] | |
# set the default value (coerced to the type) | |
value = attr_type(attr_default) | |
setattr(self.obj, attr_name, value) | |
# create the command | |
def invoke(self, arg, from_tty, | |
name=name, | |
attr_name=attr_name, | |
attr_type=attr_type, | |
attr_check=attr_check): | |
new_value = Dashboard.parse_arg(arg) | |
if new_value == '': | |
# print the current value | |
value = getattr(this.obj, attr_name) | |
print('{} = {!r}'.format(name, value)) | |
else: | |
try: | |
# convert and check the new value | |
parsed = ast.literal_eval(new_value) | |
value = attr_type(parsed) | |
if not attr_check(value): | |
msg = 'Invalid value "{}" for "{}"' | |
raise Exception(msg.format(new_value, name)) | |
except Exception as e: | |
Dashboard.err(e) | |
else: | |
# set and redisplay | |
setattr(this.obj, attr_name, value) | |
this.dashboard.redisplay(True) | |
prefix = self.prefix + ' ' + name | |
doc = attribute.get('doc', 'This style is self-documenting') | |
Dashboard.create_command(prefix, invoke, doc, False) | |
def invoke(self, arg, from_tty): | |
# an argument here means that the provided attribute is invalid | |
if arg: | |
Dashboard.err('Invalid argument "{}"'.format(arg)) | |
return | |
# print all the pairs | |
for name, attribute in self.attributes.items(): | |
attr_name = attribute.get('name', name) | |
value = getattr(self.obj, attr_name) | |
print('{} = {!r}'.format(name, value)) | |
# Base module ------------------------------------------------------------------ | |
# just a tag | |
class Module(): | |
'''Base class for GDB dashboard modules. | |
Modules are instantiated once at initialization time and kept during the | |
whole the GDB session. | |
The name of a module is automatically obtained by the class name. | |
Optionally, a module may include a description which will appear in the | |
GDB help system by specifying a Python docstring for the class. By | |
convention the first line should contain a brief description.''' | |
def label(self): | |
'''Return the module label which will appear in the divider.''' | |
pass | |
def lines(self, term_width, term_height, style_changed): | |
'''Return a list of strings which will form the module content. | |
When a module is temporarily unable to produce its content, it | |
should return an empty list; its divider will then use the styles | |
with the "off" qualifier. | |
term_width and term_height are the dimension of the terminal where | |
this module will be displayed. If `style_changed` is `True` then | |
some attributes have changed since the last time so the | |
implementation may want to update its status.''' | |
pass | |
def attributes(self): | |
'''Return the dictionary of available attributes. | |
The key is the attribute name and the value is another dictionary | |
with items: | |
- `default` is the initial value for this attribute; | |
- `doc` is the optional documentation of this attribute which will | |
appear in the GDB help system; | |
- `name` is the name of the attribute of the Python object (defaults | |
to the key value); | |
- `type` is the Python type of this attribute defaulting to the | |
`str` type, it is used to coerce the value passed as an argument | |
to the proper type, or raise an exception; | |
- `check` is an optional control callback which accept the coerced | |
value and returns `True` if the value satisfies the constraint and | |
`False` otherwise. | |
Those attributes can be accessed from the implementation using | |
instance variables named `name`.''' | |
return {} | |
def commands(self): | |
'''Return the dictionary of available commands. | |
The key is the attribute name and the value is another dictionary | |
with items: | |
- `action` is the callback to be executed which accepts the raw | |
input string from the GDB prompt, exceptions in these functions | |
will be shown automatically to the user; | |
- `doc` is the documentation of this command which will appear in | |
the GDB help system; | |
- `completion` is the optional completion policy, one of the | |
`gdb.COMPLETE_*` constants defined in the GDB reference manual | |
(https://sourceware.org/gdb/onlinedocs/gdb/Commands-In-Python.html).''' | |
return {} | |
# Default modules -------------------------------------------------------------- | |
class Source(Dashboard.Module): | |
'''Show the program source code, if available.''' | |
def __init__(self): | |
self.file_name = None | |
self.source_lines = [] | |
self.ts = None | |
self.highlighted = False | |
self.offset = 0 | |
def label(self): | |
return 'Source' | |
def lines(self, term_width, term_height, style_changed): | |
# skip if the current thread is not stopped | |
if not gdb.selected_thread().is_stopped(): | |
return [] | |
# try to fetch the current line (skip if no line information) | |
sal = gdb.selected_frame().find_sal() | |
current_line = sal.line | |
if current_line == 0: | |
return [] | |
# try to lookup the source file | |
candidates = [ | |
sal.symtab.fullname(), | |
sal.symtab.filename, | |
# XXX GDB also uses absolute filename but it is harder to implement | |
# properly and IMHO useless | |
os.path.basename(sal.symtab.filename)] | |
for candidate in candidates: | |
file_name = candidate | |
ts = None | |
try: | |
ts = os.path.getmtime(file_name) | |
break | |
except: | |
# try another or delay error check to open() | |
continue | |
# style changed, different file name or file modified in the meanwhile | |
if style_changed or file_name != self.file_name or ts and ts > self.ts: | |
try: | |
# reload the source file if changed | |
with io.open(file_name, errors='replace') as source_file: | |
highlighter = Beautifier(file_name, self.tab_size) | |
self.highlighted = highlighter.active | |
source = highlighter.process(source_file.read()) | |
self.source_lines = source.split('\n') | |
# store file name and timestamp only if success to have | |
# persistent errors | |
self.file_name = file_name | |
self.ts = ts | |
except IOError as e: | |
msg = 'Cannot display "{}"'.format(file_name) | |
return [ansi(msg, R.style_error)] | |
# compute the line range | |
height = self.height or (term_height - 1) | |
start = current_line - 1 - int(height / 2) + self.offset | |
end = start + height | |
# extra at start | |
extra_start = 0 | |
if start < 0: | |
extra_start = min(-start, height) | |
start = 0 | |
# extra at end | |
extra_end = 0 | |
if end > len(self.source_lines): | |
extra_end = min(end - len(self.source_lines), height) | |
end = len(self.source_lines) | |
else: | |
end = max(end, 0) | |
# return the source code listing | |
breakpoints = fetch_breakpoints() | |
out = [] | |
number_format = '{{:>{}}}'.format(len(str(end))) | |
for number, line in enumerate(self.source_lines[start:end], start + 1): | |
# properly handle UTF-8 source files | |
line = to_string(line) | |
if int(number) == current_line: | |
# the current line has a different style without ANSI | |
if R.ansi: | |
if self.highlighted and not self.highlight_line: | |
line_format = '{}' + ansi(number_format, R.style_selected_1) + ' {}' | |
else: | |
line_format = '{}' + ansi(number_format + ' {}', R.style_selected_1) | |
else: | |
# just show a plain text indicator | |
line_format = '{}' + number_format + '> {}' | |
else: | |
line_format = '{}' + ansi(number_format, R.style_low) + ' {}' | |
# check for breakpoint presence | |
enabled = None | |
for breakpoint in breakpoints: | |
addresses = breakpoint['addresses'] | |
is_root_enabled = addresses[0]['enabled'] | |
for address in addresses: | |
# note, despite the lookup path always use the relative | |
# (sal.symtab.filename) file name to match source files with | |
# breakpoints | |
if address['file_line'] == number and address['file_name'] == sal.symtab.filename: | |
enabled = enabled or (address['enabled'] and is_root_enabled) | |
if enabled is None: | |
breakpoint = ' ' | |
else: | |
breakpoint = ansi('!', R.style_critical) if enabled else ansi('-', R.style_low) | |
out.append(line_format.format(breakpoint, number, line.rstrip('\n'))) | |
# return the output along with scroll indicators | |
if len(out) <= height: | |
extra = [ansi('~', R.style_low)] | |
return extra_start * extra + out + extra_end * extra | |
else: | |
return out | |
def commands(self): | |
return { | |
'scroll': { | |
'action': self.scroll, | |
'doc': 'Scroll by relative steps or reset if invoked without argument.' | |
} | |
} | |
def attributes(self): | |
return { | |
'height': { | |
'doc': '''Height of the module. | |
A value of 0 uses the whole height.''', | |
'default': 10, | |
'type': int, | |
'check': check_ge_zero | |
}, | |
'tab-size': { | |
'doc': 'Number of spaces used to display the tab character.', | |
'default': 4, | |
'name': 'tab_size', | |
'type': int, | |
'check': check_gt_zero | |
}, | |
'highlight-line': { | |
'doc': 'Decide whether the whole current line should be highlighted.', | |
'default': False, | |
'name': 'highlight_line', | |
'type': bool | |
} | |
} | |
def scroll(self, arg): | |
if arg: | |
self.offset += int(arg) | |
else: | |
self.offset = 0 | |
class Assembly(Dashboard.Module): | |
'''Show the disassembled code surrounding the program counter. | |
The instructions constituting the current statement are marked, if available.''' | |
def __init__(self): | |
self.offset = 0 | |
self.cache_key = None | |
self.cache_asm = None | |
def label(self): | |
return 'Assembly' | |
def lines(self, term_width, term_height, style_changed): | |
# skip if the current thread is not stopped | |
if not gdb.selected_thread().is_stopped(): | |
return [] | |
# flush the cache if the style is changed | |
if style_changed: | |
self.cache_key = None | |
# prepare the highlighter | |
try: | |
flavor = gdb.parameter('disassembly-flavor') | |
except: | |
flavor = 'att' # not always defined (see #36) | |
highlighter = Beautifier(flavor) | |
# fetch the assembly code | |
line_info = None | |
frame = gdb.selected_frame() # PC is here | |
height = self.height or (term_height - 1) | |
try: | |
# disassemble the current block | |
asm_start, asm_end = self.fetch_function_boundaries() | |
asm = self.fetch_asm(asm_start, asm_end, False, highlighter) | |
# find the location of the PC | |
pc_index = next(index for index, instr in enumerate(asm) | |
if instr['addr'] == frame.pc()) | |
# compute the instruction range | |
start = pc_index - int(height / 2) + self.offset | |
end = start + height | |
# extra at start | |
extra_start = 0 | |
if start < 0: | |
extra_start = min(-start, height) | |
start = 0 | |
# extra at end | |
extra_end = 0 | |
if end > len(asm): | |
extra_end = min(end - len(asm), height) | |
end = len(asm) | |
else: | |
end = max(end, 0) | |
# fetch actual interval | |
asm = asm[start:end] | |
# if there are line information then use it, it may be that | |
# line_info is not None but line_info.last is None | |
line_info = gdb.find_pc_line(frame.pc()) | |
line_info = line_info if line_info.last else None | |
except (gdb.error, RuntimeError, StopIteration): | |
# if it is not possible (stripped binary or the PC is not present in | |
# the output of `disassemble` as per issue #31) start from PC | |
try: | |
extra_start = 0 | |
extra_end = 0 | |
# allow to scroll down nevertheless | |
clamped_offset = min(self.offset, 0) | |
asm = self.fetch_asm(frame.pc(), height - clamped_offset, True, highlighter) | |
asm = asm[-clamped_offset:] | |
except gdb.error as e: | |
msg = '{}'.format(e) | |
return [ansi(msg, R.style_error)] | |
# fetch function start if available (e.g., not with @plt) | |
func_start = None | |
if self.show_function and frame.function(): | |
func_start = to_unsigned(frame.function().value()) | |
# compute the maximum offset size | |
if asm and func_start: | |
max_offset = max(len(str(abs(asm[0]['addr'] - func_start))), | |
len(str(abs(asm[-1]['addr'] - func_start)))) | |
# return the machine code | |
breakpoints = fetch_breakpoints() | |
max_length = max(instr['length'] for instr in asm) if asm else 0 | |
inferior = gdb.selected_inferior() | |
out = [] | |
for index, instr in enumerate(asm): | |
addr = instr['addr'] | |
length = instr['length'] | |
text = instr['asm'] | |
addr_str = format_address(addr) | |
if self.show_opcodes: | |
# fetch and format opcode | |
region = inferior.read_memory(addr, length) | |
opcodes = (' '.join('{:02x}'.format(ord(byte)) for byte in region)) | |
opcodes += (max_length - len(region)) * 3 * ' ' + ' ' | |
else: | |
opcodes = '' | |
# compute the offset if available | |
if self.show_function: | |
if func_start: | |
offset = '{:+d}'.format(addr - func_start) | |
offset = offset.ljust(max_offset + 1) # sign | |
func_info = '{}{}'.format(frame.function(), offset) | |
else: | |
func_info = '?' | |
else: | |
func_info = '' | |
format_string = '{}{}{}{}{}{}' | |
indicator = ' ' | |
text = ' ' + text | |
if addr == frame.pc(): | |
if not R.ansi: | |
indicator = '> ' | |
addr_str = ansi(addr_str, R.style_selected_1) | |
indicator = ansi(indicator, R.style_selected_1) | |
opcodes = ansi(opcodes, R.style_selected_1) | |
func_info = ansi(func_info, R.style_selected_1) | |
if not highlighter.active or self.highlight_line: | |
text = ansi(text, R.style_selected_1) | |
elif line_info and line_info.pc <= addr < line_info.last: | |
if not R.ansi: | |
indicator = ': ' | |
addr_str = ansi(addr_str, R.style_selected_2) | |
indicator = ansi(indicator, R.style_selected_2) | |
opcodes = ansi(opcodes, R.style_selected_2) | |
func_info = ansi(func_info, R.style_selected_2) | |
if not highlighter.active or self.highlight_line: | |
text = ansi(text, R.style_selected_2) | |
else: | |
addr_str = ansi(addr_str, R.style_low) | |
func_info = ansi(func_info, R.style_low) | |
# check for breakpoint presence | |
enabled = None | |
for breakpoint in breakpoints: | |
addresses = breakpoint['addresses'] | |
is_root_enabled = addresses[0]['enabled'] | |
for address in addresses: | |
if address['address'] == addr: | |
enabled = enabled or (address['enabled'] and is_root_enabled) | |
if enabled is None: | |
breakpoint = ' ' | |
else: | |
breakpoint = ansi('!', R.style_critical) if enabled else ansi('-', R.style_low) | |
out.append(format_string.format(breakpoint, addr_str, indicator, opcodes, func_info, text)) | |
# return the output along with scroll indicators | |
if len(out) <= height: | |
extra = [ansi('~', R.style_low)] | |
return extra_start * extra + out + extra_end * extra | |
else: | |
return out | |
def commands(self): | |
return { | |
'scroll': { | |
'action': self.scroll, | |
'doc': 'Scroll by relative steps or reset if invoked without argument.' | |
} | |
} | |
def attributes(self): | |
return { | |
'height': { | |
'doc': '''Height of the module. | |
A value of 0 uses the whole height.''', | |
'default': 10, | |
'type': int, | |
'check': check_ge_zero | |
}, | |
'opcodes': { | |
'doc': 'Opcodes visibility flag.', | |
'default': False, | |
'name': 'show_opcodes', | |
'type': bool | |
}, | |
'function': { | |
'doc': 'Function information visibility flag.', | |
'default': True, | |
'name': 'show_function', | |
'type': bool | |
}, | |
'highlight-line': { | |
'doc': 'Decide whether the whole current line should be highlighted.', | |
'default': False, | |
'name': 'highlight_line', | |
'type': bool | |
} | |
} | |
def scroll(self, arg): | |
if arg: | |
self.offset += int(arg) | |
else: | |
self.offset = 0 | |
def fetch_function_boundaries(self): | |
frame = gdb.selected_frame() | |
# parse the output of the disassemble GDB command to find the function | |
# boundaries, this should handle cases in which a function spans | |
# multiple discontinuous blocks | |
disassemble = run('disassemble') | |
for block_start, block_end in re.findall(r'Address range 0x([0-9a-f]+) to 0x([0-9a-f]+):', disassemble): | |
block_start = int(block_start, 16) | |
block_end = int(block_end, 16) | |
if block_start <= frame.pc() < block_end: | |
return block_start, block_end - 1 # need to be inclusive | |
# if function information is available then try to obtain the | |
# boundaries by looking at the superblocks | |
block = frame.block() | |
if frame.function(): | |
while block and (not block.function or block.function.name != frame.function().name): | |
block = block.superblock | |
block = block or frame.block() | |
return block.start, block.end - 1 | |
def fetch_asm(self, start, end_or_count, relative, highlighter): | |
# fetch asm from cache or disassemble | |
if self.cache_key == (start, end_or_count): | |
asm = self.cache_asm | |
else: | |
kwargs = { | |
'start_pc': start, | |
'count' if relative else 'end_pc': end_or_count | |
} | |
asm = gdb.selected_frame().architecture().disassemble(**kwargs) | |
self.cache_key = (start, end_or_count) | |
self.cache_asm = asm | |
# syntax highlight the cached entry | |
for instr in asm: | |
instr['asm'] = highlighter.process(instr['asm']) | |
return asm | |
class Variables(Dashboard.Module): | |
'''Show arguments and locals of the selected frame.''' | |
def label(self): | |
return 'Variables' | |
def lines(self, term_width, term_height, style_changed): | |
return Variables.format_frame( | |
gdb.selected_frame(), self.show_arguments, self.show_locals, self.compact, self.align, self.sort) | |
def attributes(self): | |
return { | |
'arguments': { | |
'doc': 'Frame arguments visibility flag.', | |
'default': True, | |
'name': 'show_arguments', | |
'type': bool | |
}, | |
'locals': { | |
'doc': 'Frame locals visibility flag.', | |
'default': True, | |
'name': 'show_locals', | |
'type': bool | |
}, | |
'compact': { | |
'doc': 'Single-line display flag.', | |
'default': True, | |
'type': bool | |
}, | |
'align': { | |
'doc': 'Align variables in column flag (only if not compact).', | |
'default': False, | |
'type': bool | |
}, | |
'sort': { | |
'doc': 'Sort variables by name.', | |
'default': False, | |
'type': bool | |
} | |
} | |
@staticmethod | |
def format_frame(frame, show_arguments, show_locals, compact, align, sort): | |
out = [] | |
# fetch frame arguments and locals | |
decorator = gdb.FrameDecorator.FrameDecorator(frame) | |
separator = ansi(', ', R.style_low) | |
if show_arguments: | |
def prefix(line): | |
return Stack.format_line('arg', line) | |
frame_args = decorator.frame_args() | |
args_lines = Variables.fetch(frame, frame_args, compact, align, sort) | |
if args_lines: | |
if compact: | |
args_line = separator.join(args_lines) | |
single_line = prefix(args_line) | |
out.append(single_line) | |
else: | |
out.extend(map(prefix, args_lines)) | |
if show_locals: | |
def prefix(line): | |
return Stack.format_line('loc', line) | |
frame_locals = decorator.frame_locals() | |
locals_lines = Variables.fetch(frame, frame_locals, compact, align, sort) | |
if locals_lines: | |
if compact: | |
locals_line = separator.join(locals_lines) | |
single_line = prefix(locals_line) | |
out.append(single_line) | |
else: | |
out.extend(map(prefix, locals_lines)) | |
return out | |
@staticmethod | |
def fetch(frame, data, compact, align, sort): | |
lines = [] | |
name_width = 0 | |
if align and not compact: | |
name_width = max(len(str(elem.sym)) for elem in data) if data else 0 | |
# import ipdb; ipdb.set_trace | |
for elem in data or []: | |
name = ansi(elem.sym, R.style_high) + ' ' * (name_width - len(str(elem.sym))) | |
equal = ansi('=', R.style_low) | |
value = format_value(elem.sym.value(frame), compact) | |
lines.append('{} {} {}'.format(name, equal, value)) | |
if sort: | |
lines.sort() | |
return lines | |
class Stack(Dashboard.Module): | |
'''Show the current stack trace including the function name and the file location, if available. | |
Optionally list the frame arguments and locals too.''' | |
def label(self): | |
return 'Stack' | |
def lines(self, term_width, term_height, style_changed): | |
# skip if the current thread is not stopped | |
if not gdb.selected_thread().is_stopped(): | |
return [] | |
# find the selected frame (i.e., the first to display) | |
selected_index = 0 | |
frame = gdb.newest_frame() | |
while frame: | |
if frame == gdb.selected_frame(): | |
break | |
frame = frame.older() | |
selected_index += 1 | |
# format up to "limit" frames | |
frames = [] | |
number = selected_index | |
more = False | |
while frame: | |
# the first is the selected one | |
selected = (len(frames) == 0) | |
# fetch frame info | |
style = R.style_selected_1 if selected else R.style_selected_2 | |
frame_id = ansi(str(number), style) | |
info = Stack.get_pc_line(frame, style) | |
frame_lines = [] | |
frame_lines.append('[{}] {}'.format(frame_id, info)) | |
# add frame arguments and locals | |
variables = Variables.format_frame( | |
frame, self.show_arguments, self.show_locals, self.compact, self.align, self.sort) | |
frame_lines.extend(variables) | |
# add frame | |
frames.append(frame_lines) | |
# next | |
frame = frame.older() | |
number += 1 | |
# check finished according to the limit | |
if self.limit and len(frames) == self.limit: | |
# more frames to show but limited | |
if frame: | |
more = True | |
break | |
# format the output | |
lines = [] | |
for frame_lines in frames: | |
lines.extend(frame_lines) | |
# add the placeholder | |
if more: | |
lines.append('[{}]'.format(ansi('+', R.style_selected_2))) | |
return lines | |
def attributes(self): | |
return { | |
'limit': { | |
'doc': 'Maximum number of displayed frames (0 means no limit).', | |
'default': 10, | |
'type': int, | |
'check': check_ge_zero | |
}, | |
'arguments': { | |
'doc': 'Frame arguments visibility flag.', | |
'default': False, | |
'name': 'show_arguments', | |
'type': bool | |
}, | |
'locals': { | |
'doc': 'Frame locals visibility flag.', | |
'default': False, | |
'name': 'show_locals', | |
'type': bool | |
}, | |
'compact': { | |
'doc': 'Single-line display flag.', | |
'default': False, | |
'type': bool | |
}, | |
'align': { | |
'doc': 'Align variables in column flag (only if not compact).', | |
'default': False, | |
'type': bool | |
}, | |
'sort': { | |
'doc': 'Sort variables by name.', | |
'default': False, | |
'type': bool | |
} | |
} | |
@staticmethod | |
def format_line(prefix, line): | |
prefix = ansi(prefix, R.style_low) | |
return '{} {}'.format(prefix, line) | |
@staticmethod | |
def get_pc_line(frame, style): | |
frame_pc = ansi(format_address(frame.pc()), style) | |
info = 'from {}'.format(frame_pc) | |
# if a frame function symbol is available then use it to fetch the | |
# current function name and address, otherwise fall back relying on the | |
# frame name | |
if frame.function(): | |
name = ansi(frame.function(), style) | |
func_start = to_unsigned(frame.function().value()) | |
offset = ansi(str(frame.pc() - func_start), style) | |
info += ' in {}+{}'.format(name, offset) | |
elif frame.name(): | |
name = ansi(frame.name(), style) | |
info += ' in {}'.format(name) | |
sal = frame.find_sal() | |
if sal and sal.symtab: | |
file_name = ansi(sal.symtab.filename, style) | |
file_line = ansi(str(sal.line), style) | |
info += ' at {}:{}'.format(file_name, file_line) | |
return info | |
class History(Dashboard.Module): | |
'''List the last entries of the value history.''' | |
def label(self): | |
return 'History' | |
def lines(self, term_width, term_height, style_changed): | |
out = [] | |
# fetch last entries | |
for i in range(-self.limit + 1, 1): | |
try: | |
value = format_value(gdb.history(i)) | |
value_id = ansi('$${}', R.style_high).format(abs(i)) | |
equal = ansi('=', R.style_low) | |
line = '{} {} {}'.format(value_id, equal, value) | |
out.append(line) | |
except gdb.error: | |
continue | |
return out | |
def attributes(self): | |
return { | |
'limit': { | |
'doc': 'Maximum number of values to show.', | |
'default': 3, | |
'type': int, | |
'check': check_gt_zero | |
} | |
} | |
class Memory(Dashboard.Module): | |
'''Allow to inspect memory regions.''' | |
DEFAULT_LENGTH = 16 | |
class Region(): | |
def __init__(self, expression, length, module): | |
self.expression = expression | |
self.length = length | |
self.module = module | |
self.original = None | |
self.latest = None | |
def reset(self): | |
self.original = None | |
self.latest = None | |
def format(self, per_line): | |
# fetch the memory content | |
try: | |
address = Memory.parse_as_address(self.expression) | |
inferior = gdb.selected_inferior() | |
memory = inferior.read_memory(address, self.length) | |
# set the original memory snapshot if needed | |
if not self.original: | |
self.original = memory | |
except gdb.error as e: | |
msg = 'Cannot access {} bytes starting at {}: {}' | |
msg = msg.format(self.length, self.expression, e) | |
return [ansi(msg, R.style_error)] | |
# format the memory content | |
out = [] | |
for i in range(0, len(memory), per_line): | |
region = memory[i:i + per_line] | |
pad = per_line - len(region) | |
address_str = format_address(address + i) | |
# compute changes | |
hexa = [] | |
text = [] | |
for j in range(len(region)): | |
rel = i + j | |
byte = memory[rel] | |
hexa_byte = '{:02x}'.format(ord(byte)) | |
text_byte = self.module.format_byte(byte) | |
# differences against the latest have the highest priority | |
if self.latest and memory[rel] != self.latest[rel]: | |
hexa_byte = ansi(hexa_byte, R.style_selected_1) | |
text_byte = ansi(text_byte, R.style_selected_1) | |
# cumulative changes if enabled | |
elif self.module.cumulative and memory[rel] != self.original[rel]: | |
hexa_byte = ansi(hexa_byte, R.style_selected_2) | |
text_byte = ansi(text_byte, R.style_selected_2) | |
# format the text differently for clarity | |
else: | |
text_byte = ansi(text_byte, R.style_high) | |
hexa.append(hexa_byte) | |
text.append(text_byte) | |
# output the formatted line | |
hexa_placeholder = ' {}'.format(self.module.placeholder[0] * 2) | |
text_placeholder = self.module.placeholder[0] | |
out.append('{} {}{} {}{}'.format( | |
ansi(address_str, R.style_low), | |
' '.join(hexa), ansi(pad * hexa_placeholder, R.style_low), | |
''.join(text), ansi(pad * text_placeholder, R.style_low))) | |
# update the latest memory snapshot | |
self.latest = memory | |
return out | |
def __init__(self): | |
self.table = {} | |
def label(self): | |
return 'Memory' | |
def lines(self, term_width, term_height, style_changed): | |
out = [] | |
for expression, region in self.table.items(): | |
out.append(divider(term_width, expression)) | |
out.extend(region.format(self.get_per_line(term_width))) | |
return out | |
def commands(self): | |
return { | |
'watch': { | |
'action': self.watch, | |
'doc': '''Watch a memory region by expression and length. | |
The length defaults to 16 bytes.''', | |
'complete': gdb.COMPLETE_EXPRESSION | |
}, | |
'unwatch': { | |
'action': self.unwatch, | |
'doc': 'Stop watching a memory region by expression.', | |
'complete': gdb.COMPLETE_EXPRESSION | |
}, | |
'clear': { | |
'action': self.clear, | |
'doc': 'Clear all the watched regions.' | |
} | |
} | |
def attributes(self): | |
return { | |
'cumulative': { | |
'doc': 'Highlight changes cumulatively, watch again to reset.', | |
'default': False, | |
'type': bool | |
}, | |
'full': { | |
'doc': 'Take the whole horizontal space.', | |
'default': False, | |
'type': bool | |
}, | |
'placeholder': { | |
'doc': 'Placeholder used for missing items and unprintable characters.', | |
'default': '·' | |
} | |
} | |
def watch(self, arg): | |
if arg: | |
expression, _, length_str = arg.partition(' ') | |
length = Memory.parse_as_address(length_str) if length_str else Memory.DEFAULT_LENGTH | |
# keep the length when the memory is watched to reset the changes | |
region = self.table.get(expression) | |
if region and not length_str: | |
region.reset() | |
else: | |
self.table[expression] = Memory.Region(expression, length, self) | |
else: | |
raise Exception('Specify a memory location') | |
def unwatch(self, arg): | |
if arg: | |
try: | |
del self.table[arg] | |
except KeyError: | |
raise Exception('Memory expression not watched') | |
else: | |
raise Exception('Specify a matched memory expression') | |
def clear(self, arg): | |
self.table.clear() | |
def format_byte(self, byte): | |
# `type(byte) is bytes` in Python 3 | |
if 0x20 < ord(byte) < 0x7f: | |
return chr(ord(byte)) | |
else: | |
return self.placeholder[0] | |
def get_per_line(self, term_width): | |
if self.full: | |
padding = 3 # two double spaces separator (one is part of below) | |
elem_size = 4 # HH + 1 space + T | |
address_length = gdb.parse_and_eval('$pc').type.sizeof * 2 + 2 # 0x | |
return max(int((term_width - address_length - padding) / elem_size), 1) | |
else: | |
return Memory.DEFAULT_LENGTH | |
@staticmethod | |
def parse_as_address(expression): | |
value = gdb.parse_and_eval(expression) | |
return to_unsigned(value) | |
class Registers(Dashboard.Module): | |
'''Show the CPU registers and their values.''' | |
def __init__(self): | |
self.table = {} | |
def label(self): | |
return 'Registers' | |
def lines(self, term_width, term_height, style_changed): | |
# skip if the current thread is not stopped | |
if not gdb.selected_thread().is_stopped(): | |
return [] | |
# obtain the registers to display | |
if style_changed: | |
self.table = {} | |
if self.register_list: | |
register_list = self.register_list.split() | |
else: | |
register_list = Registers.fetch_register_list() | |
# fetch registers status | |
registers = [] | |
for name in register_list: | |
# exclude registers with a dot '.' or parse_and_eval() will fail | |
if '.' in name: | |
continue | |
value = gdb.parse_and_eval('${}'.format(name)) | |
string_value = Registers.format_value(value) | |
# exclude unavailable registers (see #255) | |
if string_value == '<unavailable>': | |
continue | |
changed = self.table and (self.table.get(name, '') != string_value) | |
self.table[name] = string_value | |
registers.append((name, string_value, changed)) | |
# compute lengths considering an extra space between and around the | |
# entries (hence the +2 and term_width - 1) | |
max_name = max(len(name) for name, _, _ in registers) | |
max_value = max(len(value) for _, value, _ in registers) | |
max_width = max_name + max_value + 2 | |
columns = min(int((term_width - 1) / max_width) or 1, len(registers)) | |
rows = int(math.ceil(float(len(registers)) / columns)) | |
# build the registers matrix | |
if self.column_major: | |
matrix = list(registers[i:i + rows] for i in range(0, len(registers), rows)) | |
else: | |
matrix = list(registers[i::columns] for i in range(columns)) | |
# compute the lengths column wise | |
max_names_column = list(max(len(name) for name, _, _ in column) for column in matrix) | |
max_values_column = list(max(len(value) for _, value, _ in column) for column in matrix) | |
line_length = sum(max_names_column) + columns + sum(max_values_column) | |
extra = term_width - line_length | |
# compute padding as if there were one more column | |
base_padding = int(extra / (columns + 1)) | |
padding_column = [base_padding] * columns | |
# distribute the remainder among columns giving the precedence to | |
# internal padding | |
rest = extra % (columns + 1) | |
while rest: | |
padding_column[rest % columns] += 1 | |
rest -= 1 | |
# format the registers | |
out = [''] * rows | |
for i, column in enumerate(matrix): | |
max_name = max_names_column[i] | |
max_value = max_values_column[i] | |
for j, (name, value, changed) in enumerate(column): | |
name = ' ' * (max_name - len(name)) + ansi(name, R.style_low) | |
style = R.style_selected_1 if changed else '' | |
value = ansi(value, style) + ' ' * (max_value - len(value)) | |
padding = ' ' * padding_column[i] | |
item = '{}{} {}'.format(padding, name, value) | |
out[j] += item | |
return out | |
def attributes(self): | |
return { | |
'column-major': { | |
'doc': 'Show registers in columns instead of rows.', | |
'default': False, | |
'name': 'column_major', | |
'type': bool | |
}, | |
'list': { | |
'doc': '''String of space-separated register names to display. | |
The empty list (default) causes to show all the available registers.''', | |
'default': '', | |
'name': 'register_list', | |
} | |
} | |
@staticmethod | |
def format_value(value): | |
try: | |
if value.type.code in [gdb.TYPE_CODE_INT, gdb.TYPE_CODE_PTR]: | |
int_value = to_unsigned(value, value.type.sizeof) | |
value_format = '0x{{:0{}x}}'.format(2 * value.type.sizeof) | |
return value_format.format(int_value) | |
except (gdb.error, ValueError): | |
# convert to unsigned but preserve code and flags information | |
pass | |
return str(value) | |
@staticmethod | |
def fetch_register_list(*match_groups): | |
names = [] | |
for line in run('maintenance print register-groups').split('\n'): | |
fields = line.split() | |
if len(fields) != 7: | |
continue | |
name, _, _, _, _, _, groups = fields | |
if not re.match('\w', name): | |
continue | |
for group in groups.split(','): | |
if group in (match_groups or ('general',)): | |
names.append(name) | |
break | |
return names | |
class Threads(Dashboard.Module): | |
'''List the currently available threads.''' | |
def label(self): | |
return 'Threads' | |
def lines(self, term_width, term_height, style_changed): | |
out = [] | |
selected_thread = gdb.selected_thread() | |
# do not restore the selected frame if the thread is not stopped | |
restore_frame = gdb.selected_thread().is_stopped() | |
if restore_frame: | |
selected_frame = gdb.selected_frame() | |
# fetch the thread list | |
threads = [] | |
for inferior in gdb.inferiors(): | |
if self.all_inferiors or inferior == gdb.selected_inferior(): | |
threads += gdb.Inferior.threads(inferior) | |
for thread in threads: | |
# skip running threads if requested | |
if self.skip_running and thread.is_running(): | |
continue | |
is_selected = (thread.ptid == selected_thread.ptid) | |
style = R.style_selected_1 if is_selected else R.style_selected_2 | |
if self.all_inferiors: | |
number = '{}.{}'.format(thread.inferior.num, thread.num) | |
else: | |
number = str(thread.num) | |
number = ansi(number, style) | |
tid = ansi(str(thread.ptid[1] or thread.ptid[2]), style) | |
info = '[{}] id {}'.format(number, tid) | |
if thread.name: | |
info += ' name {}'.format(ansi(thread.name, style)) | |
# switch thread to fetch info (unless is running in non-stop mode) | |
try: | |
thread.switch() | |
frame = gdb.newest_frame() | |
info += ' ' + Stack.get_pc_line(frame, style) | |
except gdb.error: | |
info += ' (running)' | |
out.append(info) | |
# restore thread and frame | |
selected_thread.switch() | |
if restore_frame: | |
selected_frame.select() | |
return out | |
def attributes(self): | |
return { | |
'skip-running': { | |
'doc': 'Skip running threads.', | |
'default': False, | |
'name': 'skip_running', | |
'type': bool | |
}, | |
'all-inferiors': { | |
'doc': 'Show threads from all inferiors.', | |
'default': False, | |
'name': 'all_inferiors', | |
'type': bool | |
}, | |
} | |
class Expressions(Dashboard.Module): | |
'''Watch user expressions.''' | |
def __init__(self): | |
self.table = set() | |
def label(self): | |
return 'Expressions' | |
def lines(self, term_width, term_height, style_changed): | |
out = [] | |
label_width = 0 | |
if self.align: | |
label_width = max(len(expression) for expression in self.table) if self.table else 0 | |
default_radix = Expressions.get_default_radix() | |
for expression in self.table: | |
label = expression | |
match = re.match('^/(\d+) +(.+)$', expression) | |
try: | |
if match: | |
radix, expression = match.groups() | |
run('set output-radix {}'.format(radix)) | |
value = format_value(gdb.parse_and_eval(expression)) | |
except gdb.error as e: | |
value = ansi(e, R.style_error) | |
finally: | |
if match: | |
run('set output-radix {}'.format(default_radix)) | |
label = ansi(expression, R.style_high) + ' ' * (label_width - len(expression)) | |
equal = ansi('=', R.style_low) | |
out.append('{} {} {}'.format(label, equal, value)) | |
return out | |
def commands(self): | |
return { | |
'watch': { | |
'action': self.watch, | |
'doc': 'Watch an expression using the format `[/<radix>] <expression>`.', | |
'complete': gdb.COMPLETE_EXPRESSION | |
}, | |
'unwatch': { | |
'action': self.unwatch, | |
'doc': 'Stop watching an expression.', | |
'complete': gdb.COMPLETE_EXPRESSION | |
}, | |
'clear': { | |
'action': self.clear, | |
'doc': 'Clear all the watched expressions.' | |
} | |
} | |
def attributes(self): | |
return { | |
'align': { | |
'doc': 'Align variables in column flag.', | |
'default': False, | |
'type': bool | |
} | |
} | |
def watch(self, arg): | |
if arg: | |
self.table.add(arg) | |
else: | |
raise Exception('Specify an expression') | |
def unwatch(self, arg): | |
if arg: | |
try: | |
self.table.remove(arg) | |
except: | |
raise Exception('Expression not watched') | |
else: | |
raise Exception('Specify an expression') | |
def clear(self, arg): | |
self.table.clear() | |
@staticmethod | |
def get_default_radix(): | |
try: | |
return gdb.parameter('output-radix') | |
except RuntimeError: | |
# XXX this is a fix for GDB <8.1.x see #161 | |
message = run('show output-radix') | |
match = re.match('^Default output radix for printing of values is (\d+)\.$', message) | |
return match.groups()[0] if match else 10 # fallback | |
class Breakpoints(Dashboard.Module): | |
'''Display the breakpoints list.''' | |
NAMES = { | |
gdb.BP_BREAKPOINT: 'break', | |
gdb.BP_WATCHPOINT: 'watch', | |
gdb.BP_HARDWARE_WATCHPOINT: 'write watch', | |
gdb.BP_READ_WATCHPOINT: 'read watch', | |
gdb.BP_ACCESS_WATCHPOINT: 'access watch' | |
} | |
def label(self): | |
return 'Breakpoints' | |
def lines(self, term_width, term_height, style_changed): | |
out = [] | |
breakpoints = fetch_breakpoints(watchpoints=True, pending=self.show_pending) | |
for breakpoint in breakpoints: | |
sub_lines = [] | |
# format common information | |
style = R.style_selected_1 if breakpoint['enabled'] else R.style_selected_2 | |
number = ansi(breakpoint['number'], style) | |
bp_type = ansi(Breakpoints.NAMES[breakpoint['type']], style) | |
if breakpoint['temporary']: | |
bp_type = bp_type + ' {}'.format(ansi('once', style)) | |
if not R.ansi and breakpoint['enabled']: | |
bp_type = 'disabled ' + bp_type | |
line = '[{}] {}'.format(number, bp_type) | |
if breakpoint['type'] == gdb.BP_BREAKPOINT: | |
for i, address in enumerate(breakpoint['addresses']): | |
addr = address['address'] | |
if i == 0 and addr: | |
# this is a regular breakpoint | |
line += ' at {}'.format(ansi(format_address(addr), style)) | |
# format source information | |
file_name = address.get('file_name') | |
file_line = address.get('file_line') | |
if file_name and file_line: | |
file_name = ansi(file_name, style) | |
file_line = ansi(file_line, style) | |
line += ' in {}:{}'.format(file_name, file_line) | |
elif i > 0: | |
# this is a sub breakpoint | |
sub_style = R.style_selected_1 if address['enabled'] else R.style_selected_2 | |
sub_number = ansi('{}.{}'.format(breakpoint['number'], i), sub_style) | |
sub_line = '[{}]'.format(sub_number) | |
sub_line += ' at {}'.format(ansi(format_address(addr), sub_style)) | |
# format source information | |
file_name = address.get('file_name') | |
file_line = address.get('file_line') | |
if file_name and file_line: | |
file_name = ansi(file_name, sub_style) | |
file_line = ansi(file_line, sub_style) | |
sub_line += ' in {}:{}'.format(file_name, file_line) | |
sub_lines += [sub_line] | |
# format user location | |
location = breakpoint['location'] | |
line += ' for {}'.format(ansi(location, style)) | |
else: | |
# format user expression | |
expression = breakpoint['expression'] | |
line += ' for {}'.format(ansi(expression, style)) | |
# format condition | |
condition = breakpoint['condition'] | |
if condition: | |
line += ' if {}'.format(ansi(condition, style)) | |
# format hit count | |
hit_count = breakpoint['hit_count'] | |
if hit_count: | |
word = 'time{}'.format('s' if hit_count > 1 else '') | |
line += ' hit {} {}'.format(ansi(breakpoint['hit_count'], style), word) | |
# append the main line and possibly sub breakpoints | |
out.append(line) | |
out.extend(sub_lines) | |
return out | |
def attributes(self): | |
return { | |
'pending': { | |
'doc': 'Also show pending breakpoints.', | |
'default': True, | |
'name': 'show_pending', | |
'type': bool | |
} | |
} | |
# XXX traceback line numbers in this Python block must be increased by 1 | |
end | |
# Dump List ---------------------------------------------------------- | |
#import uuid | |
#import gdb | |
#class ListNodePrinter(gdb.Command): | |
# """Prints the ListNode from our example in a nice format!""" | |
# def __init__(self): | |
# super(ListNodePrinter, self).__init__("walklist", gdb.COMMAND_USER) | |
# def invoke(self, args, from_tty): | |
# # You can pass args here so this routine could actually evaluate different | |
# # variables at runtime | |
# print "Args Passed: %s" % args | |
# # Let's walk through the list starting with the head | |
# # | |
# # We can access value info by looking at: | |
# # https://sourceware.org/gdb/onlinedocs/gdb/Values-From-Inferior.html#Values-From-Inferior | |
# node_ptr = gdb.parse_and_eval("dnode") | |
# count = 0 | |
# while node_ptr != 0: | |
# if node_ptr['prev'] == node_ptr: | |
# continue | |
# schema_ptr = node_ptr['schema'] | |
# print "%d: Addr: 0x%x%s, value: %s" % \ | |
# (count, node_ptr, schema_ptr['name'], node_ptr['']) | |
# count += 1 | |
# node_ptr = node_ptr['next'] | |
# print "Found %d nodes" % count | |
#ListNodePrinter() | |
# Better GDB defaults ---------------------------------------------------------- | |
set history save | |
set verbose off | |
set print pretty on | |
set print array on | |
set print array-indexes on | |
set print elements unlimited | |
set python print-stack full | |
set print array on | |
# https://github.com/cyrus-and/gdb-dashboard/wiki/Use-multiple-terminals | |
# tty -> pts | |
# dashboard source -output /dev/pts/2 | |
# Start ------------------------------------------------------------------------ | |
python Dashboard.start() | |
# File variables --------------------------------------------------------------- | |
set filename-display absolute | |
dashboard -layout breakpoints expressions stack history source variables | |
dashboard -style max_value_length 250 | |
dashboard source -style height 40 | |
dashboard variables -style compact False | |
dashboard variables -style align True | |
define hookpost-up | |
dashboard | |
end | |
define hookpost-down | |
dashboard | |
end | |
define hookpost-frame | |
dashboard | |
end | |
b ly_log_cb | |
b break_point | |
commands | |
# NOTE: `finish` should not be used in commands | |
# apparently it breaks the dashboard | |
up | |
end | |
# vim: filetype=python | |
# Local Variables: | |
# mode: python | |
# End: | |
File Metadata
File Metadata
- Mime Type
- text/x-python
- Storage Engine
- local-disk
- Storage Format
- Raw Data
- Storage Handle
- 26/df/9aa43b1ef38b98c924fbfbebf46a
- Default Alt Text
- .gdbinit (90 KB)