You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2389 lines
92 KiB
2389 lines
92 KiB
python |
|
|
|
# GDB dashboard - Modular visual interface for GDB in Python. |
|
# |
|
# https://github.com/cyrus-and/gdb-dashboard |
|
|
|
# License ---------------------------------------------------------------------- |
|
|
|
# Copyright (c) 2015-2025 Andrea Cardaci <cyrus.and@gmail.com> |
|
# |
|
# 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': r'\[\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': r'\[\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 if tab_size else None |
|
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 if requested |
|
if self.tab_spaces: |
|
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() |
|
catch_what_regex = re.compile(r'([^,]+".*")?[^,]*') |
|
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) >= 5 and fields[1] == 'catchpoint': |
|
# only take before comma, but ignore commas in quotes |
|
what = catch_what_regex.search(' '.join(fields[4:])).group(0).strip() |
|
parsed_breakpoints[number] = [], False, what |
|
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, what = 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 |
|
breakpoint['what'] = what |
|
# 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 of struct winsize |
|
winsize_format = 'hhhh' |
|
buffer = b'\x00' * struct.calcsize(winsize_format) |
|
buffer = fcntl.ioctl(fd, termios.TIOCGWINSZ, buffer) |
|
height, width, _, _ = struct.unpack(winsize_format, buffer) |
|
return int(width), int(height) |
|
except (ImportError, OSError, SystemError): |
|
# 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() |
|
# skipping dotfiles |
|
for init in sorted(file for file in files if not file.startswith('.')): |
|
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): |
|
if callable(complete): |
|
Class = type('', (gdb.Command,), { |
|
'__doc__': doc, |
|
'invoke': invoke, |
|
'complete': complete |
|
}) |
|
Class(name, gdb.COMMAND_USER, prefix=is_prefix) |
|
else: |
|
Class = type('', (gdb.Command,), { |
|
'__doc__': doc, |
|
'invoke': invoke |
|
}) |
|
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[2J' + ('\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): |
|
label = 'Source' |
|
if self.show_path and self.file_name: |
|
label += ': {}'.format(self.file_name) |
|
return label |
|
|
|
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: |
|
self.file_name = None |
|
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 |
|
}, |
|
'path': { |
|
'doc': 'Path visibility flag in the module label.', |
|
'default': False, |
|
'name': 'show_path', |
|
'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 |
|
|
|
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, tab_size=None) |
|
# 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 |
|
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 level (XXX Frame.level() is a recent addition) |
|
start_level = 0 |
|
frame = gdb.newest_frame() |
|
while frame: |
|
if frame == gdb.selected_frame(): |
|
break |
|
frame = frame.older() |
|
start_level += 1 |
|
# gather the frames |
|
more = False |
|
frames = [gdb.selected_frame()] |
|
going_down = True |
|
while True: |
|
# stack frames limit reached |
|
if len(frames) == self.limit: |
|
more = True |
|
break |
|
# zigzag the frames starting from the selected one |
|
if going_down: |
|
frame = frames[-1].older() |
|
if frame: |
|
frames.append(frame) |
|
else: |
|
frame = frames[0].newer() |
|
if frame: |
|
frames.insert(0, frame) |
|
start_level -= 1 |
|
else: |
|
break |
|
else: |
|
frame = frames[0].newer() |
|
if frame: |
|
frames.insert(0, frame) |
|
start_level -= 1 |
|
else: |
|
frame = frames[-1].older() |
|
if frame: |
|
frames.append(frame) |
|
else: |
|
break |
|
# switch direction |
|
going_down = not going_down |
|
# format the output |
|
lines = [] |
|
for number, frame in enumerate(frames, start=start_level): |
|
selected = frame == gdb.selected_frame() |
|
lines.extend(self.get_frame_lines(number, frame, selected)) |
|
# 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 |
|
} |
|
} |
|
|
|
def get_frame_lines(self, number, frame, selected=False): |
|
# 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) |
|
return frame_lines |
|
|
|
@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)) |
|
# handle the empty register list |
|
if not registers: |
|
msg = 'No registers to show (check the "dashboard registers -style list" attribute)' |
|
return [ansi(msg, R.style_error)] |
|
# 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. For |
|
architectures different from x86 setting this attribute might be mandatory.''', |
|
'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(r'\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 = [] |
|
|
|
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 number, expression in enumerate(self.table, start=1): |
|
label = expression |
|
match = re.match(r'^/(\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)) |
|
number = ansi(str(number), R.style_selected_2) |
|
label = ansi(expression, R.style_high) + ' ' * (label_width - len(expression)) |
|
equal = ansi('=', R.style_low) |
|
out.append('[{}] {} {} {}'.format(number, 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 by index.' |
|
}, |
|
'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: |
|
if arg not in self.table: |
|
self.table.append(arg) |
|
else: |
|
raise Exception('Expression already watched') |
|
else: |
|
raise Exception('Specify an expression') |
|
|
|
def unwatch(self, arg): |
|
if arg: |
|
try: |
|
number = int(arg) - 1 |
|
except: |
|
number = -1 |
|
if 0 <= number < len(self.table): |
|
self.table.pop(number) |
|
else: |
|
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(r'^Default output radix for printing of values is (\d+)\.$', message) |
|
return match.groups()[0] if match else 10 # fallback |
|
|
|
# XXX workaround to support BP_BREAKPOINT in older GDB versions |
|
setattr(gdb, 'BP_CATCHPOINT', getattr(gdb, 'BP_CATCHPOINT', 26)) |
|
|
|
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', |
|
gdb.BP_CATCHPOINT: 'catch' |
|
} |
|
|
|
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)) |
|
elif breakpoint['type'] == gdb.BP_CATCHPOINT: |
|
what = breakpoint['what'] |
|
line += ' {}'.format(ansi(what, 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 |
|
|
|
# Better GDB defaults ---------------------------------------------------------- |
|
|
|
set history save |
|
set verbose off |
|
set print pretty on |
|
set print array off |
|
set print array-indexes on |
|
set python print-stack full |
|
|
|
# Start ------------------------------------------------------------------------ |
|
|
|
python Dashboard.start() |
|
|
|
# Fixes ------------------------------------------------------------------------ |
|
|
|
# workaround for the GDB readline issue, see #325 |
|
python import sys; sys.modules['readline'] = None |
|
|
|
# File variables --------------------------------------------------------------- |
|
|
|
# vim: filetype=python |
|
# Local Variables: |
|
# mode: python |
|
# End:
|
|
|