├── .gdbinit ├── LICENSE └── README.md /.gdbinit: -------------------------------------------------------------------------------- 1 | python 2 | 3 | # GDB dashboard - Modular visual interface for GDB in Python. 4 | # 5 | # https://github.com/cyrus-and/gdb-dashboard 6 | 7 | # License ---------------------------------------------------------------------- 8 | 9 | # Copyright (c) 2015-2025 Andrea Cardaci 10 | # 11 | # Permission is hereby granted, free of charge, to any person obtaining a copy 12 | # of this software and associated documentation files (the "Software"), to deal 13 | # in the Software without restriction, including without limitation the rights 14 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | # copies of the Software, and to permit persons to whom the Software is 16 | # furnished to do so, subject to the following conditions: 17 | # 18 | # The above copyright notice and this permission notice shall be included in all 19 | # copies or substantial portions of the Software. 20 | # 21 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 27 | # SOFTWARE. 28 | 29 | # Imports ---------------------------------------------------------------------- 30 | 31 | import ast 32 | import io 33 | import itertools 34 | import math 35 | import os 36 | import re 37 | import struct 38 | import traceback 39 | 40 | # Common attributes ------------------------------------------------------------ 41 | 42 | class R(): 43 | 44 | @staticmethod 45 | def attributes(): 46 | return { 47 | # miscellaneous 48 | 'ansi': { 49 | 'doc': 'Control the ANSI output of the dashboard.', 50 | 'default': True, 51 | 'type': bool 52 | }, 53 | 'syntax_highlighting': { 54 | 'doc': '''Pygments style to use for syntax highlighting. 55 | 56 | Using an empty string (or a name not in the list) disables this feature. The 57 | list of all the available styles can be obtained with (from GDB itself): 58 | 59 | python from pygments.styles import * 60 | python for style in get_all_styles(): print(style)''', 61 | 'default': 'monokai' 62 | }, 63 | 'discard_scrollback': { 64 | 'doc': '''Discard the scrollback buffer at each redraw. 65 | 66 | This makes scrolling less confusing by discarding the previously printed 67 | dashboards but only works with certain terminals.''', 68 | 'default': True, 69 | 'type': bool 70 | }, 71 | # values formatting 72 | 'compact_values': { 73 | 'doc': 'Display complex objects in a single line.', 74 | 'default': True, 75 | 'type': bool 76 | }, 77 | 'max_value_length': { 78 | 'doc': 'Maximum length of displayed values before truncation.', 79 | 'default': 100, 80 | 'type': int 81 | }, 82 | 'value_truncation_string': { 83 | 'doc': 'String to use to mark value truncation.', 84 | 'default': '…', 85 | }, 86 | 'dereference': { 87 | 'doc': 'Annotate pointers with the pointed value.', 88 | 'default': True, 89 | 'type': bool 90 | }, 91 | # prompt 92 | 'prompt': { 93 | 'doc': '''GDB prompt. 94 | 95 | This value is used as a Python format string where `{status}` is expanded with 96 | the substitution of either `prompt_running` or `prompt_not_running` attributes, 97 | according to the target program status. The resulting string must be a valid GDB 98 | prompt, see the command `python print(gdb.prompt.prompt_help())`''', 99 | 'default': '{status}' 100 | }, 101 | 'prompt_running': { 102 | 'doc': '''Define the value of `{status}` when the target program is running. 103 | 104 | See the `prompt` attribute. This value is used as a Python format string where 105 | `{pid}` is expanded with the process identifier of the target program.''', 106 | 'default': r'\[\e[1;35m\]>>>\[\e[0m\]' 107 | }, 108 | 'prompt_not_running': { 109 | 'doc': '''Define the value of `{status}` when the target program is running. 110 | 111 | See the `prompt` attribute. This value is used as a Python format string.''', 112 | 'default': r'\[\e[90m\]>>>\[\e[0m\]' 113 | }, 114 | # divider 115 | 'omit_divider': { 116 | 'doc': 'Omit the divider in external outputs when only one module is displayed.', 117 | 'default': False, 118 | 'type': bool 119 | }, 120 | 'divider_fill_char_primary': { 121 | 'doc': 'Filler around the label for primary dividers', 122 | 'default': '─' 123 | }, 124 | 'divider_fill_char_secondary': { 125 | 'doc': 'Filler around the label for secondary dividers', 126 | 'default': '─' 127 | }, 128 | 'divider_fill_style_primary': { 129 | 'doc': 'Style for `divider_fill_char_primary`', 130 | 'default': '36' 131 | }, 132 | 'divider_fill_style_secondary': { 133 | 'doc': 'Style for `divider_fill_char_secondary`', 134 | 'default': '90' 135 | }, 136 | 'divider_label_style_on_primary': { 137 | 'doc': 'Label style for non-empty primary dividers', 138 | 'default': '1;33' 139 | }, 140 | 'divider_label_style_on_secondary': { 141 | 'doc': 'Label style for non-empty secondary dividers', 142 | 'default': '1;37' 143 | }, 144 | 'divider_label_style_off_primary': { 145 | 'doc': 'Label style for empty primary dividers', 146 | 'default': '33' 147 | }, 148 | 'divider_label_style_off_secondary': { 149 | 'doc': 'Label style for empty secondary dividers', 150 | 'default': '90' 151 | }, 152 | 'divider_label_skip': { 153 | 'doc': 'Gap between the aligning border and the label.', 154 | 'default': 3, 155 | 'type': int, 156 | 'check': check_ge_zero 157 | }, 158 | 'divider_label_margin': { 159 | 'doc': 'Number of spaces around the label.', 160 | 'default': 1, 161 | 'type': int, 162 | 'check': check_ge_zero 163 | }, 164 | 'divider_label_align_right': { 165 | 'doc': 'Label alignment flag.', 166 | 'default': False, 167 | 'type': bool 168 | }, 169 | # common styles 170 | 'style_selected_1': { 171 | 'default': '1;32' 172 | }, 173 | 'style_selected_2': { 174 | 'default': '32' 175 | }, 176 | 'style_low': { 177 | 'default': '90' 178 | }, 179 | 'style_high': { 180 | 'default': '1;37' 181 | }, 182 | 'style_error': { 183 | 'default': '31' 184 | }, 185 | 'style_critical': { 186 | 'default': '0;41' 187 | } 188 | } 189 | 190 | # Common ----------------------------------------------------------------------- 191 | 192 | class Beautifier(): 193 | 194 | def __init__(self, hint, tab_size=4): 195 | self.tab_spaces = ' ' * tab_size if tab_size else None 196 | self.active = False 197 | if not R.ansi or not R.syntax_highlighting: 198 | return 199 | # attempt to set up Pygments 200 | try: 201 | import pygments 202 | from pygments.lexers import GasLexer, NasmLexer 203 | from pygments.formatters import Terminal256Formatter 204 | if hint == 'att': 205 | self.lexer = GasLexer() 206 | elif hint == 'intel': 207 | self.lexer = NasmLexer() 208 | else: 209 | from pygments.lexers import get_lexer_for_filename 210 | self.lexer = get_lexer_for_filename(hint, stripnl=False) 211 | self.formatter = Terminal256Formatter(style=R.syntax_highlighting) 212 | self.active = True 213 | except ImportError: 214 | # Pygments not available 215 | pass 216 | except pygments.util.ClassNotFound: 217 | # no lexer for this file or invalid style 218 | pass 219 | 220 | def process(self, source): 221 | # convert tabs if requested 222 | if self.tab_spaces: 223 | source = source.replace('\t', self.tab_spaces) 224 | if self.active: 225 | import pygments 226 | source = pygments.highlight(source, self.lexer, self.formatter) 227 | return source.rstrip('\n') 228 | 229 | def run(command): 230 | return gdb.execute(command, to_string=True) 231 | 232 | def ansi(string, style): 233 | if R.ansi: 234 | return '\x1b[{}m{}\x1b[0m'.format(style, string) 235 | else: 236 | return string 237 | 238 | def divider(width, label='', primary=False, active=True): 239 | if primary: 240 | divider_fill_style = R.divider_fill_style_primary 241 | divider_fill_char = R.divider_fill_char_primary 242 | divider_label_style_on = R.divider_label_style_on_primary 243 | divider_label_style_off = R.divider_label_style_off_primary 244 | else: 245 | divider_fill_style = R.divider_fill_style_secondary 246 | divider_fill_char = R.divider_fill_char_secondary 247 | divider_label_style_on = R.divider_label_style_on_secondary 248 | divider_label_style_off = R.divider_label_style_off_secondary 249 | if label: 250 | if active: 251 | divider_label_style = divider_label_style_on 252 | else: 253 | divider_label_style = divider_label_style_off 254 | skip = R.divider_label_skip 255 | margin = R.divider_label_margin 256 | before = ansi(divider_fill_char * skip, divider_fill_style) 257 | middle = ansi(label, divider_label_style) 258 | after_length = width - len(label) - skip - 2 * margin 259 | after = ansi(divider_fill_char * after_length, divider_fill_style) 260 | if R.divider_label_align_right: 261 | before, after = after, before 262 | return ''.join([before, ' ' * margin, middle, ' ' * margin, after]) 263 | else: 264 | return ansi(divider_fill_char * width, divider_fill_style) 265 | 266 | def check_gt_zero(x): 267 | return x > 0 268 | 269 | def check_ge_zero(x): 270 | return x >= 0 271 | 272 | def to_unsigned(value, size=8): 273 | # values from GDB can be used transparently but are not suitable for 274 | # being printed as unsigned integers, so a conversion is needed 275 | mask = (2 ** (size * 8)) - 1 276 | return int(value.cast(gdb.Value(mask).type)) & mask 277 | 278 | def to_string(value): 279 | # attempt to convert an inferior value to string; OK when (Python 3 || 280 | # simple ASCII); otherwise (Python 2.7 && not ASCII) encode the string as 281 | # utf8 282 | try: 283 | value_string = str(value) 284 | except UnicodeEncodeError: 285 | value_string = unicode(value).encode('utf8') 286 | except gdb.error as e: 287 | value_string = ansi(e, R.style_error) 288 | return value_string 289 | 290 | def format_address(address): 291 | pointer_size = gdb.parse_and_eval('$pc').type.sizeof 292 | return ('0x{{:0{}x}}').format(pointer_size * 2).format(address) 293 | 294 | def format_value(value, compact=None): 295 | # format references as referenced values 296 | # (TYPE_CODE_RVALUE_REF is not supported by old GDB) 297 | if value.type.code in (getattr(gdb, 'TYPE_CODE_REF', None), 298 | getattr(gdb, 'TYPE_CODE_RVALUE_REF', None)): 299 | try: 300 | value = value.referenced_value() 301 | except gdb.error as e: 302 | return ansi(e, R.style_error) 303 | # format the value 304 | out = to_string(value) 305 | # dereference up to the actual value if requested 306 | if R.dereference and value.type.code == gdb.TYPE_CODE_PTR: 307 | while value.type.code == gdb.TYPE_CODE_PTR: 308 | try: 309 | value = value.dereference() 310 | except gdb.error as e: 311 | break 312 | else: 313 | formatted = to_string(value) 314 | out += '{} {}'.format(ansi(':', R.style_low), formatted) 315 | # compact the value 316 | if compact is not None and compact or R.compact_values: 317 | out = re.sub(r'$\s*', '', out, flags=re.MULTILINE) 318 | # truncate the value 319 | if R.max_value_length > 0 and len(out) > R.max_value_length: 320 | out = out[0:R.max_value_length] + ansi(R.value_truncation_string, R.style_critical) 321 | return out 322 | 323 | # XXX parsing the output of `info breakpoints` is apparently the best option 324 | # right now, see: https://sourceware.org/bugzilla/show_bug.cgi?id=18385 325 | # XXX GDB version 7.11 (quire recent) does not have the pending field, so 326 | # fall back to the parsed information 327 | def fetch_breakpoints(watchpoints=False, pending=False): 328 | # fetch breakpoints addresses 329 | parsed_breakpoints = dict() 330 | catch_what_regex = re.compile(r'([^,]+".*")?[^,]*') 331 | for line in run('info breakpoints').split('\n'): 332 | # just keep numbered lines 333 | if not line or not line[0].isdigit(): 334 | continue 335 | # extract breakpoint number, address and pending status 336 | fields = line.split() 337 | number = int(fields[0].split('.')[0]) 338 | try: 339 | if len(fields) >= 5 and fields[1] == 'breakpoint': 340 | # multiple breakpoints have no address yet 341 | is_pending = fields[4] == '' 342 | is_multiple = fields[4] == '' 343 | address = None if is_multiple or is_pending else int(fields[4], 16) 344 | is_enabled = fields[3] == 'y' 345 | address_info = address, is_enabled 346 | parsed_breakpoints[number] = [address_info], is_pending, '' 347 | elif len(fields) >= 5 and fields[1] == 'catchpoint': 348 | # only take before comma, but ignore commas in quotes 349 | what = catch_what_regex.search(' '.join(fields[4:])).group(0).strip() 350 | parsed_breakpoints[number] = [], False, what 351 | elif len(fields) >= 3 and number in parsed_breakpoints: 352 | # add this address to the list of multiple locations 353 | address = int(fields[2], 16) 354 | is_enabled = fields[1] == 'y' 355 | address_info = address, is_enabled 356 | parsed_breakpoints[number][0].append(address_info) 357 | else: 358 | # watchpoints 359 | parsed_breakpoints[number] = [], False, '' 360 | except ValueError: 361 | pass 362 | # fetch breakpoints from the API and complement with address and source 363 | # information 364 | breakpoints = [] 365 | # XXX in older versions gdb.breakpoints() returns None 366 | for gdb_breakpoint in gdb.breakpoints() or []: 367 | # skip internal breakpoints 368 | if gdb_breakpoint.number < 0: 369 | continue 370 | addresses, is_pending, what = parsed_breakpoints[gdb_breakpoint.number] 371 | is_pending = getattr(gdb_breakpoint, 'pending', is_pending) 372 | if not pending and is_pending: 373 | continue 374 | if not watchpoints and gdb_breakpoint.type != gdb.BP_BREAKPOINT: 375 | continue 376 | # add useful fields to the object 377 | breakpoint = dict() 378 | breakpoint['number'] = gdb_breakpoint.number 379 | breakpoint['type'] = gdb_breakpoint.type 380 | breakpoint['enabled'] = gdb_breakpoint.enabled 381 | breakpoint['location'] = gdb_breakpoint.location 382 | breakpoint['expression'] = gdb_breakpoint.expression 383 | breakpoint['condition'] = gdb_breakpoint.condition 384 | breakpoint['temporary'] = gdb_breakpoint.temporary 385 | breakpoint['hit_count'] = gdb_breakpoint.hit_count 386 | breakpoint['pending'] = is_pending 387 | breakpoint['what'] = what 388 | # add addresses and source information 389 | breakpoint['addresses'] = [] 390 | for address, is_enabled in addresses: 391 | if address: 392 | sal = gdb.find_pc_line(address) 393 | breakpoint['addresses'].append({ 394 | 'address': address, 395 | 'enabled': is_enabled, 396 | 'file_name': sal.symtab.filename if address and sal.symtab else None, 397 | 'file_line': sal.line if address else None 398 | }) 399 | breakpoints.append(breakpoint) 400 | return breakpoints 401 | 402 | # Dashboard -------------------------------------------------------------------- 403 | 404 | class Dashboard(gdb.Command): 405 | '''Redisplay the dashboard.''' 406 | 407 | def __init__(self): 408 | gdb.Command.__init__(self, 'dashboard', gdb.COMMAND_USER, gdb.COMPLETE_NONE, True) 409 | # setup subcommands 410 | Dashboard.ConfigurationCommand(self) 411 | Dashboard.OutputCommand(self) 412 | Dashboard.EnabledCommand(self) 413 | Dashboard.LayoutCommand(self) 414 | # setup style commands 415 | Dashboard.StyleCommand(self, 'dashboard', R, R.attributes()) 416 | # main terminal 417 | self.output = None 418 | # used to inhibit redisplays during init parsing 419 | self.inhibited = None 420 | # enabled by default 421 | self.enabled = None 422 | self.enable() 423 | 424 | def on_continue(self, _): 425 | # try to contain the GDB messages in a specified area unless the 426 | # dashboard is printed to a separate file (dashboard -output ...) 427 | # or there are no modules to display in the main terminal 428 | enabled_modules = list(filter(lambda m: not m.output and m.enabled, self.modules)) 429 | if self.is_running() and not self.output and len(enabled_modules) > 0: 430 | width, _ = Dashboard.get_term_size() 431 | gdb.write(Dashboard.clear_screen()) 432 | gdb.write(divider(width, 'Output/messages', True)) 433 | gdb.write('\n') 434 | gdb.flush() 435 | 436 | def on_stop(self, _): 437 | if self.is_running(): 438 | self.render(clear_screen=False) 439 | 440 | def on_exit(self, _): 441 | if not self.is_running(): 442 | return 443 | # collect all the outputs 444 | outputs = set() 445 | outputs.add(self.output) 446 | outputs.update(module.output for module in self.modules) 447 | outputs.remove(None) 448 | # reset the terminal status 449 | for output in outputs: 450 | try: 451 | with open(output, 'w') as fs: 452 | fs.write(Dashboard.reset_terminal()) 453 | except: 454 | # skip cleanup for invalid outputs 455 | pass 456 | 457 | def enable(self): 458 | if self.enabled: 459 | return 460 | self.enabled = True 461 | # setup events 462 | gdb.events.cont.connect(self.on_continue) 463 | gdb.events.stop.connect(self.on_stop) 464 | gdb.events.exited.connect(self.on_exit) 465 | 466 | def disable(self): 467 | if not self.enabled: 468 | return 469 | self.enabled = False 470 | # setup events 471 | gdb.events.cont.disconnect(self.on_continue) 472 | gdb.events.stop.disconnect(self.on_stop) 473 | gdb.events.exited.disconnect(self.on_exit) 474 | 475 | def load_modules(self, modules): 476 | self.modules = [] 477 | for module in modules: 478 | info = Dashboard.ModuleInfo(self, module) 479 | self.modules.append(info) 480 | 481 | def redisplay(self, style_changed=False): 482 | # manually redisplay the dashboard 483 | if self.is_running() and not self.inhibited: 484 | self.render(True, style_changed) 485 | 486 | def inferior_pid(self): 487 | return gdb.selected_inferior().pid 488 | 489 | def is_running(self): 490 | return self.inferior_pid() != 0 491 | 492 | def render(self, clear_screen, style_changed=False): 493 | # fetch module content and info 494 | all_disabled = True 495 | display_map = dict() 496 | for module in self.modules: 497 | # fall back to the global value 498 | output = module.output or self.output 499 | # add the instance or None if disabled 500 | if module.enabled: 501 | all_disabled = False 502 | instance = module.instance 503 | else: 504 | instance = None 505 | display_map.setdefault(output, []).append(instance) 506 | # process each display info 507 | for output, instances in display_map.items(): 508 | try: 509 | buf = '' 510 | # use GDB stream by default 511 | fs = None 512 | if output: 513 | fs = open(output, 'w') 514 | fd = fs.fileno() 515 | fs.write(Dashboard.setup_terminal()) 516 | else: 517 | fs = gdb 518 | fd = 1 # stdout 519 | # get the terminal size (default main terminal if either the 520 | # output is not a file) 521 | try: 522 | width, height = Dashboard.get_term_size(fd) 523 | except: 524 | width, height = Dashboard.get_term_size() 525 | # clear the "screen" if requested for the main terminal, 526 | # auxiliary terminals are always cleared 527 | if fs is not gdb or clear_screen: 528 | buf += Dashboard.clear_screen() 529 | # show message if all the modules in this output are disabled 530 | if not any(instances): 531 | # skip the main terminal 532 | if fs is gdb: 533 | continue 534 | # write the error message 535 | buf += divider(width, 'Warning', True) 536 | buf += '\n' 537 | if self.modules: 538 | buf += 'No module to display (see `dashboard -layout`)' 539 | else: 540 | buf += 'No module loaded' 541 | buf += '\n' 542 | fs.write(buf) 543 | continue 544 | # process all the modules for that output 545 | for n, instance in enumerate(instances, 1): 546 | # skip disabled modules 547 | if not instance: 548 | continue 549 | try: 550 | # ask the module to generate the content 551 | lines = instance.lines(width, height, style_changed) 552 | except Exception as e: 553 | # allow to continue on exceptions in modules 554 | stacktrace = traceback.format_exc().strip() 555 | lines = [ansi(stacktrace, R.style_error)] 556 | # create the divider if needed 557 | div = [] 558 | if not R.omit_divider or len(instances) > 1 or fs is gdb: 559 | div = [divider(width, instance.label(), True, lines)] 560 | # write the data 561 | buf += '\n'.join(div + lines) 562 | # write the newline for all but last unless main terminal 563 | if n != len(instances) or fs is gdb: 564 | buf += '\n' 565 | # write the final newline and the terminator only if it is the 566 | # main terminal to allow the prompt to display correctly (unless 567 | # there are no modules to display) 568 | if fs is gdb and not all_disabled: 569 | buf += divider(width, primary=True) 570 | buf += '\n' 571 | fs.write(buf) 572 | except Exception as e: 573 | cause = traceback.format_exc().strip() 574 | Dashboard.err('Cannot write the dashboard\n{}'.format(cause)) 575 | finally: 576 | # don't close gdb stream 577 | if fs and fs is not gdb: 578 | fs.close() 579 | 580 | # Utility methods -------------------------------------------------------------- 581 | 582 | @staticmethod 583 | def start(): 584 | # save the instance for customization convenience 585 | global dashboard 586 | # initialize the dashboard 587 | dashboard = Dashboard() 588 | Dashboard.set_custom_prompt(dashboard) 589 | # parse Python inits, load modules then parse GDB inits 590 | dashboard.inhibited = True 591 | Dashboard.parse_inits(True) 592 | modules = Dashboard.get_modules() 593 | dashboard.load_modules(modules) 594 | Dashboard.parse_inits(False) 595 | dashboard.inhibited = False 596 | # GDB overrides 597 | run('set pagination off') 598 | # display if possible (program running and not explicitly disabled by 599 | # some configuration file) 600 | if dashboard.enabled: 601 | dashboard.redisplay() 602 | 603 | @staticmethod 604 | def get_term_size(fd=1): # defaults to the main terminal 605 | try: 606 | if sys.platform == 'win32': 607 | import curses 608 | # XXX always neglects the fd parameter 609 | height, width = curses.initscr().getmaxyx() 610 | curses.endwin() 611 | return int(width), int(height) 612 | else: 613 | import termios 614 | import fcntl 615 | # first 2 shorts (4 byte) of struct winsize 616 | raw = fcntl.ioctl(fd, termios.TIOCGWINSZ, ' ' * 4) 617 | height, width = struct.unpack('hh', raw) 618 | return int(width), int(height) 619 | except (ImportError, OSError): 620 | # this happens when no curses library is found on windows or when 621 | # the terminal is not properly configured 622 | return 80, 24 # hardcoded fallback value 623 | 624 | @staticmethod 625 | def set_custom_prompt(dashboard): 626 | def custom_prompt(_): 627 | # render thread status indicator 628 | if dashboard.is_running(): 629 | pid = dashboard.inferior_pid() 630 | status = R.prompt_running.format(pid=pid) 631 | else: 632 | status = R.prompt_not_running 633 | # build prompt 634 | prompt = R.prompt.format(status=status) 635 | prompt = gdb.prompt.substitute_prompt(prompt) 636 | return prompt + ' ' # force trailing space 637 | gdb.prompt_hook = custom_prompt 638 | 639 | @staticmethod 640 | def parse_inits(python): 641 | # paths where the .gdbinit.d directory might be 642 | search_paths = [ 643 | '/etc/gdb-dashboard', 644 | '{}/gdb-dashboard'.format(os.getenv('XDG_CONFIG_HOME', '~/.config')), 645 | '~/Library/Preferences/gdb-dashboard', 646 | '~/.gdbinit.d' 647 | ] 648 | # expand the tilde and walk the paths 649 | inits_dirs = (os.walk(os.path.expanduser(path)) for path in search_paths) 650 | # process all the init files in order 651 | for root, dirs, files in itertools.chain.from_iterable(inits_dirs): 652 | dirs.sort() 653 | # skipping dotfiles 654 | for init in sorted(file for file in files if not file.startswith('.')): 655 | path = os.path.join(root, init) 656 | _, ext = os.path.splitext(path) 657 | # either load Python files or GDB 658 | if python == (ext == '.py'): 659 | gdb.execute('source ' + path) 660 | 661 | @staticmethod 662 | def get_modules(): 663 | # scan the scope for modules 664 | modules = [] 665 | for name in globals(): 666 | obj = globals()[name] 667 | try: 668 | if issubclass(obj, Dashboard.Module): 669 | modules.append(obj) 670 | except TypeError: 671 | continue 672 | # sort modules alphabetically 673 | modules.sort(key=lambda x: x.__name__) 674 | return modules 675 | 676 | @staticmethod 677 | def create_command(name, invoke, doc, is_prefix, complete=None): 678 | if callable(complete): 679 | Class = type('', (gdb.Command,), { 680 | '__doc__': doc, 681 | 'invoke': invoke, 682 | 'complete': complete 683 | }) 684 | Class(name, gdb.COMMAND_USER, prefix=is_prefix) 685 | else: 686 | Class = type('', (gdb.Command,), { 687 | '__doc__': doc, 688 | 'invoke': invoke 689 | }) 690 | Class(name, gdb.COMMAND_USER, complete or gdb.COMPLETE_NONE, is_prefix) 691 | 692 | @staticmethod 693 | def err(string): 694 | print(ansi(string, R.style_error)) 695 | 696 | @staticmethod 697 | def complete(word, candidates): 698 | return filter(lambda candidate: candidate.startswith(word), candidates) 699 | 700 | @staticmethod 701 | def parse_arg(arg): 702 | # encode unicode GDB command arguments as utf8 in Python 2.7 703 | if type(arg) is not str: 704 | arg = arg.encode('utf8') 705 | return arg 706 | 707 | @staticmethod 708 | def clear_screen(): 709 | # ANSI: move the cursor to top-left corner and clear the screen 710 | # (optionally also clear the scrollback buffer if supported by the 711 | # terminal) 712 | return '\x1b[H\x1b[2J' + ('\x1b[3J' if R.discard_scrollback else '') 713 | 714 | @staticmethod 715 | def setup_terminal(): 716 | # ANSI: enable alternative screen buffer and hide cursor 717 | return '\x1b[?1049h\x1b[?25l' 718 | 719 | @staticmethod 720 | def reset_terminal(): 721 | # ANSI: disable alternative screen buffer and show cursor 722 | return '\x1b[?1049l\x1b[?25h' 723 | 724 | # Module descriptor ------------------------------------------------------------ 725 | 726 | class ModuleInfo: 727 | 728 | def __init__(self, dashboard, module): 729 | self.name = module.__name__.lower() # from class to module name 730 | self.enabled = True 731 | self.output = None # value from the dashboard by default 732 | self.instance = module() 733 | self.doc = self.instance.__doc__ or '(no documentation)' 734 | self.prefix = 'dashboard {}'.format(self.name) 735 | # add GDB commands 736 | self.add_main_command(dashboard) 737 | self.add_output_command(dashboard) 738 | self.add_style_command(dashboard) 739 | self.add_subcommands(dashboard) 740 | 741 | def add_main_command(self, dashboard): 742 | module = self 743 | def invoke(self, arg, from_tty, info=self): 744 | arg = Dashboard.parse_arg(arg) 745 | if arg == '': 746 | info.enabled ^= True 747 | if dashboard.is_running(): 748 | dashboard.redisplay() 749 | else: 750 | status = 'enabled' if info.enabled else 'disabled' 751 | print('{} module {}'.format(module.name, status)) 752 | else: 753 | Dashboard.err('Wrong argument "{}"'.format(arg)) 754 | doc_brief = 'Configure the {} module, with no arguments toggles its visibility.'.format(self.name) 755 | doc = '{}\n\n{}'.format(doc_brief, self.doc) 756 | Dashboard.create_command(self.prefix, invoke, doc, True) 757 | 758 | def add_output_command(self, dashboard): 759 | Dashboard.OutputCommand(dashboard, self.prefix, self) 760 | 761 | def add_style_command(self, dashboard): 762 | Dashboard.StyleCommand(dashboard, self.prefix, self.instance, self.instance.attributes()) 763 | 764 | def add_subcommands(self, dashboard): 765 | for name, command in self.instance.commands().items(): 766 | self.add_subcommand(dashboard, name, command) 767 | 768 | def add_subcommand(self, dashboard, name, command): 769 | action = command['action'] 770 | doc = command['doc'] 771 | complete = command.get('complete') 772 | def invoke(self, arg, from_tty, info=self): 773 | arg = Dashboard.parse_arg(arg) 774 | if info.enabled: 775 | try: 776 | action(arg) 777 | except Exception as e: 778 | Dashboard.err(e) 779 | return 780 | # don't catch redisplay errors 781 | dashboard.redisplay() 782 | else: 783 | Dashboard.err('Module disabled') 784 | prefix = '{} {}'.format(self.prefix, name) 785 | Dashboard.create_command(prefix, invoke, doc, False, complete) 786 | 787 | # GDB commands ----------------------------------------------------------------- 788 | 789 | # handler for the `dashboard` command itself 790 | def invoke(self, arg, from_tty): 791 | arg = Dashboard.parse_arg(arg) 792 | # show messages for checks in redisplay 793 | if arg != '': 794 | Dashboard.err('Wrong argument "{}"'.format(arg)) 795 | elif not self.is_running(): 796 | Dashboard.err('Is the target program running?') 797 | else: 798 | self.redisplay() 799 | 800 | class ConfigurationCommand(gdb.Command): 801 | '''Dump or save the dashboard configuration. 802 | 803 | With an optional argument the configuration will be written to the specified 804 | file. 805 | 806 | This command allows to configure the dashboard live then make the changes 807 | permanent, for example: 808 | 809 | dashboard -configuration ~/.gdbinit.d/init 810 | 811 | At startup the `~/.gdbinit.d/` directory tree is walked and files are evaluated 812 | in alphabetical order but giving priority to Python files. This is where user 813 | configuration files must be placed.''' 814 | 815 | def __init__(self, dashboard): 816 | gdb.Command.__init__(self, 'dashboard -configuration', 817 | gdb.COMMAND_USER, gdb.COMPLETE_FILENAME) 818 | self.dashboard = dashboard 819 | 820 | def invoke(self, arg, from_tty): 821 | arg = Dashboard.parse_arg(arg) 822 | if arg: 823 | with open(os.path.expanduser(arg), 'w') as fs: 824 | fs.write('# auto generated by GDB dashboard\n\n') 825 | self.dump(fs) 826 | self.dump(gdb) 827 | 828 | def dump(self, fs): 829 | # dump layout 830 | self.dump_layout(fs) 831 | # dump styles 832 | self.dump_style(fs, R) 833 | for module in self.dashboard.modules: 834 | self.dump_style(fs, module.instance, module.prefix) 835 | # dump outputs 836 | self.dump_output(fs, self.dashboard) 837 | for module in self.dashboard.modules: 838 | self.dump_output(fs, module, module.prefix) 839 | 840 | def dump_layout(self, fs): 841 | layout = ['dashboard -layout'] 842 | for module in self.dashboard.modules: 843 | mark = '' if module.enabled else '!' 844 | layout.append('{}{}'.format(mark, module.name)) 845 | fs.write(' '.join(layout)) 846 | fs.write('\n') 847 | 848 | def dump_style(self, fs, obj, prefix='dashboard'): 849 | attributes = getattr(obj, 'attributes', lambda: dict())() 850 | for name, attribute in attributes.items(): 851 | real_name = attribute.get('name', name) 852 | default = attribute.get('default') 853 | value = getattr(obj, real_name) 854 | if value != default: 855 | fs.write('{} -style {} {!r}\n'.format(prefix, name, value)) 856 | 857 | def dump_output(self, fs, obj, prefix='dashboard'): 858 | output = getattr(obj, 'output') 859 | if output: 860 | fs.write('{} -output {}\n'.format(prefix, output)) 861 | 862 | class OutputCommand(gdb.Command): 863 | '''Set the output file/TTY for the whole dashboard or single modules. 864 | 865 | The dashboard/module will be written to the specified file, which will be 866 | created if it does not exist. If the specified file identifies a terminal then 867 | its geometry will be used, otherwise it falls back to the geometry of the main 868 | GDB terminal. 869 | 870 | When invoked without argument on the dashboard, the output/messages and modules 871 | which do not specify an output themselves will be printed on standard output 872 | (default). 873 | 874 | When invoked without argument on a module, it will be printed where the 875 | dashboard will be printed. 876 | 877 | An overview of all the outputs can be obtained with the `dashboard -layout` 878 | command.''' 879 | 880 | def __init__(self, dashboard, prefix=None, obj=None): 881 | if not prefix: 882 | prefix = 'dashboard' 883 | if not obj: 884 | obj = dashboard 885 | prefix = prefix + ' -output' 886 | gdb.Command.__init__(self, prefix, gdb.COMMAND_USER, gdb.COMPLETE_FILENAME) 887 | self.dashboard = dashboard 888 | self.obj = obj # None means the dashboard itself 889 | 890 | def invoke(self, arg, from_tty): 891 | arg = Dashboard.parse_arg(arg) 892 | # reset the terminal status 893 | if self.obj.output: 894 | try: 895 | with open(self.obj.output, 'w') as fs: 896 | fs.write(Dashboard.reset_terminal()) 897 | except: 898 | # just do nothing if the file is not writable 899 | pass 900 | # set or open the output file 901 | if arg == '': 902 | self.obj.output = None 903 | else: 904 | self.obj.output = arg 905 | # redisplay the dashboard in the new output 906 | self.dashboard.redisplay() 907 | 908 | class EnabledCommand(gdb.Command): 909 | '''Enable or disable the dashboard. 910 | 911 | The current status is printed if no argument is present.''' 912 | 913 | def __init__(self, dashboard): 914 | gdb.Command.__init__(self, 'dashboard -enabled', gdb.COMMAND_USER) 915 | self.dashboard = dashboard 916 | 917 | def invoke(self, arg, from_tty): 918 | arg = Dashboard.parse_arg(arg) 919 | if arg == '': 920 | status = 'enabled' if self.dashboard.enabled else 'disabled' 921 | print('The dashboard is {}'.format(status)) 922 | elif arg == 'on': 923 | self.dashboard.enable() 924 | self.dashboard.redisplay() 925 | elif arg == 'off': 926 | self.dashboard.disable() 927 | else: 928 | msg = 'Wrong argument "{}"; expecting "on" or "off"' 929 | Dashboard.err(msg.format(arg)) 930 | 931 | def complete(self, text, word): 932 | return Dashboard.complete(word, ['on', 'off']) 933 | 934 | class LayoutCommand(gdb.Command): 935 | '''Set or show the dashboard layout. 936 | 937 | Accepts a space-separated list of directive. Each directive is in the form 938 | "[!]". Modules in the list are placed in the dashboard in the same order 939 | as they appear and those prefixed by "!" are disabled by default. Omitted 940 | modules are hidden and placed at the bottom in alphabetical order. 941 | 942 | Without arguments the current layout is shown where the first line uses the same 943 | form expected by the input while the remaining depict the current status of 944 | output files. 945 | 946 | Passing `!` as a single argument resets the dashboard original layout.''' 947 | 948 | def __init__(self, dashboard): 949 | gdb.Command.__init__(self, 'dashboard -layout', gdb.COMMAND_USER) 950 | self.dashboard = dashboard 951 | 952 | def invoke(self, arg, from_tty): 953 | arg = Dashboard.parse_arg(arg) 954 | directives = str(arg).split() 955 | if directives: 956 | # apply the layout 957 | if directives == ['!']: 958 | self.reset() 959 | else: 960 | if not self.layout(directives): 961 | return # in case of errors 962 | # redisplay or otherwise notify 963 | if from_tty: 964 | if self.dashboard.is_running(): 965 | self.dashboard.redisplay() 966 | else: 967 | self.show() 968 | else: 969 | self.show() 970 | 971 | def reset(self): 972 | modules = self.dashboard.modules 973 | modules.sort(key=lambda module: module.name) 974 | for module in modules: 975 | module.enabled = True 976 | 977 | def show(self): 978 | global_str = 'Dashboard' 979 | default = '(default TTY)' 980 | max_name_len = max(len(module.name) for module in self.dashboard.modules) 981 | max_name_len = max(max_name_len, len(global_str)) 982 | fmt = '{{}}{{:{}s}}{{}}'.format(max_name_len + 2) 983 | print((fmt + '\n').format(' ', global_str, self.dashboard.output or default)) 984 | for module in self.dashboard.modules: 985 | mark = ' ' if module.enabled else '!' 986 | style = R.style_high if module.enabled else R.style_low 987 | line = fmt.format(mark, module.name, module.output or default) 988 | print(ansi(line, style)) 989 | 990 | def layout(self, directives): 991 | modules = self.dashboard.modules 992 | # parse and check directives 993 | parsed_directives = [] 994 | selected_modules = set() 995 | for directive in directives: 996 | enabled = (directive[0] != '!') 997 | name = directive[not enabled:] 998 | if name in selected_modules: 999 | Dashboard.err('Module "{}" already set'.format(name)) 1000 | return False 1001 | if next((False for module in modules if module.name == name), True): 1002 | Dashboard.err('Cannot find module "{}"'.format(name)) 1003 | return False 1004 | parsed_directives.append((name, enabled)) 1005 | selected_modules.add(name) 1006 | # reset visibility 1007 | for module in modules: 1008 | module.enabled = False 1009 | # move and enable the selected modules on top 1010 | last = 0 1011 | for name, enabled in parsed_directives: 1012 | todo = enumerate(modules[last:], start=last) 1013 | index = next(index for index, module in todo if name == module.name) 1014 | modules[index].enabled = enabled 1015 | modules.insert(last, modules.pop(index)) 1016 | last += 1 1017 | return True 1018 | 1019 | def complete(self, text, word): 1020 | all_modules = (m.name for m in self.dashboard.modules) 1021 | return Dashboard.complete(word, all_modules) 1022 | 1023 | class StyleCommand(gdb.Command): 1024 | '''Access the stylable attributes. 1025 | 1026 | Without arguments print all the stylable attributes. 1027 | 1028 | When only the name is specified show the current value. 1029 | 1030 | With name and value set the stylable attribute. Values are parsed as Python 1031 | literals and converted to the proper type. ''' 1032 | 1033 | def __init__(self, dashboard, prefix, obj, attributes): 1034 | self.prefix = prefix + ' -style' 1035 | gdb.Command.__init__(self, self.prefix, gdb.COMMAND_USER, gdb.COMPLETE_NONE, True) 1036 | self.dashboard = dashboard 1037 | self.obj = obj 1038 | self.attributes = attributes 1039 | self.add_styles() 1040 | 1041 | def add_styles(self): 1042 | this = self 1043 | for name, attribute in self.attributes.items(): 1044 | # fetch fields 1045 | attr_name = attribute.get('name', name) 1046 | attr_type = attribute.get('type', str) 1047 | attr_check = attribute.get('check', lambda _: True) 1048 | attr_default = attribute['default'] 1049 | # set the default value (coerced to the type) 1050 | value = attr_type(attr_default) 1051 | setattr(self.obj, attr_name, value) 1052 | # create the command 1053 | def invoke(self, arg, from_tty, 1054 | name=name, 1055 | attr_name=attr_name, 1056 | attr_type=attr_type, 1057 | attr_check=attr_check): 1058 | new_value = Dashboard.parse_arg(arg) 1059 | if new_value == '': 1060 | # print the current value 1061 | value = getattr(this.obj, attr_name) 1062 | print('{} = {!r}'.format(name, value)) 1063 | else: 1064 | try: 1065 | # convert and check the new value 1066 | parsed = ast.literal_eval(new_value) 1067 | value = attr_type(parsed) 1068 | if not attr_check(value): 1069 | msg = 'Invalid value "{}" for "{}"' 1070 | raise Exception(msg.format(new_value, name)) 1071 | except Exception as e: 1072 | Dashboard.err(e) 1073 | else: 1074 | # set and redisplay 1075 | setattr(this.obj, attr_name, value) 1076 | this.dashboard.redisplay(True) 1077 | prefix = self.prefix + ' ' + name 1078 | doc = attribute.get('doc', 'This style is self-documenting') 1079 | Dashboard.create_command(prefix, invoke, doc, False) 1080 | 1081 | def invoke(self, arg, from_tty): 1082 | # an argument here means that the provided attribute is invalid 1083 | if arg: 1084 | Dashboard.err('Invalid argument "{}"'.format(arg)) 1085 | return 1086 | # print all the pairs 1087 | for name, attribute in self.attributes.items(): 1088 | attr_name = attribute.get('name', name) 1089 | value = getattr(self.obj, attr_name) 1090 | print('{} = {!r}'.format(name, value)) 1091 | 1092 | # Base module ------------------------------------------------------------------ 1093 | 1094 | # just a tag 1095 | class Module(): 1096 | '''Base class for GDB dashboard modules. 1097 | 1098 | Modules are instantiated once at initialization time and kept during the 1099 | whole the GDB session. 1100 | 1101 | The name of a module is automatically obtained by the class name. 1102 | 1103 | Optionally, a module may include a description which will appear in the 1104 | GDB help system by specifying a Python docstring for the class. By 1105 | convention the first line should contain a brief description.''' 1106 | 1107 | def label(self): 1108 | '''Return the module label which will appear in the divider.''' 1109 | pass 1110 | 1111 | def lines(self, term_width, term_height, style_changed): 1112 | '''Return a list of strings which will form the module content. 1113 | 1114 | When a module is temporarily unable to produce its content, it 1115 | should return an empty list; its divider will then use the styles 1116 | with the "off" qualifier. 1117 | 1118 | term_width and term_height are the dimension of the terminal where 1119 | this module will be displayed. If `style_changed` is `True` then 1120 | some attributes have changed since the last time so the 1121 | implementation may want to update its status.''' 1122 | pass 1123 | 1124 | def attributes(self): 1125 | '''Return the dictionary of available attributes. 1126 | 1127 | The key is the attribute name and the value is another dictionary 1128 | with items: 1129 | 1130 | - `default` is the initial value for this attribute; 1131 | 1132 | - `doc` is the optional documentation of this attribute which will 1133 | appear in the GDB help system; 1134 | 1135 | - `name` is the name of the attribute of the Python object (defaults 1136 | to the key value); 1137 | 1138 | - `type` is the Python type of this attribute defaulting to the 1139 | `str` type, it is used to coerce the value passed as an argument 1140 | to the proper type, or raise an exception; 1141 | 1142 | - `check` is an optional control callback which accept the coerced 1143 | value and returns `True` if the value satisfies the constraint and 1144 | `False` otherwise. 1145 | 1146 | Those attributes can be accessed from the implementation using 1147 | instance variables named `name`.''' 1148 | return {} 1149 | 1150 | def commands(self): 1151 | '''Return the dictionary of available commands. 1152 | 1153 | The key is the attribute name and the value is another dictionary 1154 | with items: 1155 | 1156 | - `action` is the callback to be executed which accepts the raw 1157 | input string from the GDB prompt, exceptions in these functions 1158 | will be shown automatically to the user; 1159 | 1160 | - `doc` is the documentation of this command which will appear in 1161 | the GDB help system; 1162 | 1163 | - `completion` is the optional completion policy, one of the 1164 | `gdb.COMPLETE_*` constants defined in the GDB reference manual 1165 | (https://sourceware.org/gdb/onlinedocs/gdb/Commands-In-Python.html).''' 1166 | return {} 1167 | 1168 | # Default modules -------------------------------------------------------------- 1169 | 1170 | class Source(Dashboard.Module): 1171 | '''Show the program source code, if available.''' 1172 | 1173 | def __init__(self): 1174 | self.file_name = None 1175 | self.source_lines = [] 1176 | self.ts = None 1177 | self.highlighted = False 1178 | self.offset = 0 1179 | 1180 | def label(self): 1181 | label = 'Source' 1182 | if self.show_path and self.file_name: 1183 | label += ': {}'.format(self.file_name) 1184 | return label 1185 | 1186 | def lines(self, term_width, term_height, style_changed): 1187 | # skip if the current thread is not stopped 1188 | if not gdb.selected_thread().is_stopped(): 1189 | return [] 1190 | # try to fetch the current line (skip if no line information) 1191 | sal = gdb.selected_frame().find_sal() 1192 | current_line = sal.line 1193 | if current_line == 0: 1194 | self.file_name = None 1195 | return [] 1196 | # try to lookup the source file 1197 | candidates = [ 1198 | sal.symtab.fullname(), 1199 | sal.symtab.filename, 1200 | # XXX GDB also uses absolute filename but it is harder to implement 1201 | # properly and IMHO useless 1202 | os.path.basename(sal.symtab.filename)] 1203 | for candidate in candidates: 1204 | file_name = candidate 1205 | ts = None 1206 | try: 1207 | ts = os.path.getmtime(file_name) 1208 | break 1209 | except: 1210 | # try another or delay error check to open() 1211 | continue 1212 | # style changed, different file name or file modified in the meanwhile 1213 | if style_changed or file_name != self.file_name or ts and ts > self.ts: 1214 | try: 1215 | # reload the source file if changed 1216 | with io.open(file_name, errors='replace') as source_file: 1217 | highlighter = Beautifier(file_name, self.tab_size) 1218 | self.highlighted = highlighter.active 1219 | source = highlighter.process(source_file.read()) 1220 | self.source_lines = source.split('\n') 1221 | # store file name and timestamp only if success to have 1222 | # persistent errors 1223 | self.file_name = file_name 1224 | self.ts = ts 1225 | except IOError as e: 1226 | msg = 'Cannot display "{}"'.format(file_name) 1227 | return [ansi(msg, R.style_error)] 1228 | # compute the line range 1229 | height = self.height or (term_height - 1) 1230 | start = current_line - 1 - int(height / 2) + self.offset 1231 | end = start + height 1232 | # extra at start 1233 | extra_start = 0 1234 | if start < 0: 1235 | extra_start = min(-start, height) 1236 | start = 0 1237 | # extra at end 1238 | extra_end = 0 1239 | if end > len(self.source_lines): 1240 | extra_end = min(end - len(self.source_lines), height) 1241 | end = len(self.source_lines) 1242 | else: 1243 | end = max(end, 0) 1244 | # return the source code listing 1245 | breakpoints = fetch_breakpoints() 1246 | out = [] 1247 | number_format = '{{:>{}}}'.format(len(str(end))) 1248 | for number, line in enumerate(self.source_lines[start:end], start + 1): 1249 | # properly handle UTF-8 source files 1250 | line = to_string(line) 1251 | if int(number) == current_line: 1252 | # the current line has a different style without ANSI 1253 | if R.ansi: 1254 | if self.highlighted and not self.highlight_line: 1255 | line_format = '{}' + ansi(number_format, R.style_selected_1) + ' {}' 1256 | else: 1257 | line_format = '{}' + ansi(number_format + ' {}', R.style_selected_1) 1258 | else: 1259 | # just show a plain text indicator 1260 | line_format = '{}' + number_format + '> {}' 1261 | else: 1262 | line_format = '{}' + ansi(number_format, R.style_low) + ' {}' 1263 | # check for breakpoint presence 1264 | enabled = None 1265 | for breakpoint in breakpoints: 1266 | addresses = breakpoint['addresses'] 1267 | is_root_enabled = addresses[0]['enabled'] 1268 | for address in addresses: 1269 | # note, despite the lookup path always use the relative 1270 | # (sal.symtab.filename) file name to match source files with 1271 | # breakpoints 1272 | if address['file_line'] == number and address['file_name'] == sal.symtab.filename: 1273 | enabled = enabled or (address['enabled'] and is_root_enabled) 1274 | if enabled is None: 1275 | breakpoint = ' ' 1276 | else: 1277 | breakpoint = ansi('!', R.style_critical) if enabled else ansi('-', R.style_low) 1278 | out.append(line_format.format(breakpoint, number, line.rstrip('\n'))) 1279 | # return the output along with scroll indicators 1280 | if len(out) <= height: 1281 | extra = [ansi('~', R.style_low)] 1282 | return extra_start * extra + out + extra_end * extra 1283 | else: 1284 | return out 1285 | 1286 | def commands(self): 1287 | return { 1288 | 'scroll': { 1289 | 'action': self.scroll, 1290 | 'doc': 'Scroll by relative steps or reset if invoked without argument.' 1291 | } 1292 | } 1293 | 1294 | def attributes(self): 1295 | return { 1296 | 'height': { 1297 | 'doc': '''Height of the module. 1298 | 1299 | A value of 0 uses the whole height.''', 1300 | 'default': 10, 1301 | 'type': int, 1302 | 'check': check_ge_zero 1303 | }, 1304 | 'tab-size': { 1305 | 'doc': 'Number of spaces used to display the tab character.', 1306 | 'default': 4, 1307 | 'name': 'tab_size', 1308 | 'type': int, 1309 | 'check': check_gt_zero 1310 | }, 1311 | 'path': { 1312 | 'doc': 'Path visibility flag in the module label.', 1313 | 'default': False, 1314 | 'name': 'show_path', 1315 | 'type': bool 1316 | }, 1317 | 'highlight-line': { 1318 | 'doc': 'Decide whether the whole current line should be highlighted.', 1319 | 'default': False, 1320 | 'name': 'highlight_line', 1321 | 'type': bool 1322 | } 1323 | } 1324 | 1325 | def scroll(self, arg): 1326 | if arg: 1327 | self.offset += int(arg) 1328 | else: 1329 | self.offset = 0 1330 | 1331 | class Assembly(Dashboard.Module): 1332 | '''Show the disassembled code surrounding the program counter. 1333 | 1334 | The instructions constituting the current statement are marked, if available.''' 1335 | 1336 | def __init__(self): 1337 | self.offset = 0 1338 | self.cache_key = None 1339 | self.cache_asm = None 1340 | 1341 | def label(self): 1342 | return 'Assembly' 1343 | 1344 | def lines(self, term_width, term_height, style_changed): 1345 | # skip if the current thread is not stopped 1346 | if not gdb.selected_thread().is_stopped(): 1347 | return [] 1348 | # flush the cache if the style is changed 1349 | if style_changed: 1350 | self.cache_key = None 1351 | # prepare the highlighter 1352 | try: 1353 | flavor = gdb.parameter('disassembly-flavor') 1354 | except: 1355 | flavor = 'att' # not always defined (see #36) 1356 | highlighter = Beautifier(flavor, tab_size=None) 1357 | # fetch the assembly code 1358 | line_info = None 1359 | frame = gdb.selected_frame() # PC is here 1360 | height = self.height or (term_height - 1) 1361 | try: 1362 | # disassemble the current block 1363 | asm_start, asm_end = self.fetch_function_boundaries() 1364 | asm = self.fetch_asm(asm_start, asm_end, False, highlighter) 1365 | # find the location of the PC 1366 | pc_index = next(index for index, instr in enumerate(asm) 1367 | if instr['addr'] == frame.pc()) 1368 | # compute the instruction range 1369 | start = pc_index - int(height / 2) + self.offset 1370 | end = start + height 1371 | # extra at start 1372 | extra_start = 0 1373 | if start < 0: 1374 | extra_start = min(-start, height) 1375 | start = 0 1376 | # extra at end 1377 | extra_end = 0 1378 | if end > len(asm): 1379 | extra_end = min(end - len(asm), height) 1380 | end = len(asm) 1381 | else: 1382 | end = max(end, 0) 1383 | # fetch actual interval 1384 | asm = asm[start:end] 1385 | # if there are line information then use it, it may be that 1386 | # line_info is not None but line_info.last is None 1387 | line_info = gdb.find_pc_line(frame.pc()) 1388 | line_info = line_info if line_info.last else None 1389 | except (gdb.error, RuntimeError, StopIteration): 1390 | # if it is not possible (stripped binary or the PC is not present in 1391 | # the output of `disassemble` as per issue #31) start from PC 1392 | try: 1393 | extra_start = 0 1394 | extra_end = 0 1395 | # allow to scroll down nevertheless 1396 | clamped_offset = min(self.offset, 0) 1397 | asm = self.fetch_asm(frame.pc(), height - clamped_offset, True, highlighter) 1398 | asm = asm[-clamped_offset:] 1399 | except gdb.error as e: 1400 | msg = '{}'.format(e) 1401 | return [ansi(msg, R.style_error)] 1402 | # fetch function start if available (e.g., not with @plt) 1403 | func_start = None 1404 | if self.show_function and frame.function(): 1405 | func_start = to_unsigned(frame.function().value()) 1406 | # compute the maximum offset size 1407 | if asm and func_start: 1408 | max_offset = max(len(str(abs(asm[0]['addr'] - func_start))), 1409 | len(str(abs(asm[-1]['addr'] - func_start)))) 1410 | # return the machine code 1411 | breakpoints = fetch_breakpoints() 1412 | max_length = max(instr['length'] for instr in asm) if asm else 0 1413 | inferior = gdb.selected_inferior() 1414 | out = [] 1415 | for index, instr in enumerate(asm): 1416 | addr = instr['addr'] 1417 | length = instr['length'] 1418 | text = instr['asm'] 1419 | addr_str = format_address(addr) 1420 | if self.show_opcodes: 1421 | # fetch and format opcode 1422 | region = inferior.read_memory(addr, length) 1423 | opcodes = (' '.join('{:02x}'.format(ord(byte)) for byte in region)) 1424 | opcodes += (max_length - len(region)) * 3 * ' ' + ' ' 1425 | else: 1426 | opcodes = '' 1427 | # compute the offset if available 1428 | if self.show_function: 1429 | if func_start: 1430 | offset = '{:+d}'.format(addr - func_start) 1431 | offset = offset.ljust(max_offset + 1) # sign 1432 | func_info = '{}{}'.format(frame.function(), offset) 1433 | else: 1434 | func_info = '?' 1435 | else: 1436 | func_info = '' 1437 | format_string = '{}{}{}{}{}{}' 1438 | indicator = ' ' 1439 | text = ' ' + text 1440 | if addr == frame.pc(): 1441 | if not R.ansi: 1442 | indicator = '> ' 1443 | addr_str = ansi(addr_str, R.style_selected_1) 1444 | indicator = ansi(indicator, R.style_selected_1) 1445 | opcodes = ansi(opcodes, R.style_selected_1) 1446 | func_info = ansi(func_info, R.style_selected_1) 1447 | if not highlighter.active or self.highlight_line: 1448 | text = ansi(text, R.style_selected_1) 1449 | elif line_info and line_info.pc <= addr < line_info.last: 1450 | if not R.ansi: 1451 | indicator = ': ' 1452 | addr_str = ansi(addr_str, R.style_selected_2) 1453 | indicator = ansi(indicator, R.style_selected_2) 1454 | opcodes = ansi(opcodes, R.style_selected_2) 1455 | func_info = ansi(func_info, R.style_selected_2) 1456 | if not highlighter.active or self.highlight_line: 1457 | text = ansi(text, R.style_selected_2) 1458 | else: 1459 | addr_str = ansi(addr_str, R.style_low) 1460 | func_info = ansi(func_info, R.style_low) 1461 | # check for breakpoint presence 1462 | enabled = None 1463 | for breakpoint in breakpoints: 1464 | addresses = breakpoint['addresses'] 1465 | is_root_enabled = addresses[0]['enabled'] 1466 | for address in addresses: 1467 | if address['address'] == addr: 1468 | enabled = enabled or (address['enabled'] and is_root_enabled) 1469 | if enabled is None: 1470 | breakpoint = ' ' 1471 | else: 1472 | breakpoint = ansi('!', R.style_critical) if enabled else ansi('-', R.style_low) 1473 | out.append(format_string.format(breakpoint, addr_str, indicator, opcodes, func_info, text)) 1474 | # return the output along with scroll indicators 1475 | if len(out) <= height: 1476 | extra = [ansi('~', R.style_low)] 1477 | return extra_start * extra + out + extra_end * extra 1478 | else: 1479 | return out 1480 | 1481 | def commands(self): 1482 | return { 1483 | 'scroll': { 1484 | 'action': self.scroll, 1485 | 'doc': 'Scroll by relative steps or reset if invoked without argument.' 1486 | } 1487 | } 1488 | 1489 | def attributes(self): 1490 | return { 1491 | 'height': { 1492 | 'doc': '''Height of the module. 1493 | 1494 | A value of 0 uses the whole height.''', 1495 | 'default': 10, 1496 | 'type': int, 1497 | 'check': check_ge_zero 1498 | }, 1499 | 'opcodes': { 1500 | 'doc': 'Opcodes visibility flag.', 1501 | 'default': False, 1502 | 'name': 'show_opcodes', 1503 | 'type': bool 1504 | }, 1505 | 'function': { 1506 | 'doc': 'Function information visibility flag.', 1507 | 'default': True, 1508 | 'name': 'show_function', 1509 | 'type': bool 1510 | }, 1511 | 'highlight-line': { 1512 | 'doc': 'Decide whether the whole current line should be highlighted.', 1513 | 'default': False, 1514 | 'name': 'highlight_line', 1515 | 'type': bool 1516 | } 1517 | } 1518 | 1519 | def scroll(self, arg): 1520 | if arg: 1521 | self.offset += int(arg) 1522 | else: 1523 | self.offset = 0 1524 | 1525 | def fetch_function_boundaries(self): 1526 | frame = gdb.selected_frame() 1527 | # parse the output of the disassemble GDB command to find the function 1528 | # boundaries, this should handle cases in which a function spans 1529 | # multiple discontinuous blocks 1530 | disassemble = run('disassemble') 1531 | for block_start, block_end in re.findall(r'Address range 0x([0-9a-f]+) to 0x([0-9a-f]+):', disassemble): 1532 | block_start = int(block_start, 16) 1533 | block_end = int(block_end, 16) 1534 | if block_start <= frame.pc() < block_end: 1535 | return block_start, block_end - 1 # need to be inclusive 1536 | # if function information is available then try to obtain the 1537 | # boundaries by looking at the superblocks 1538 | block = frame.block() 1539 | if frame.function(): 1540 | while block and (not block.function or block.function.name != frame.function().name): 1541 | block = block.superblock 1542 | block = block or frame.block() 1543 | return block.start, block.end - 1 1544 | 1545 | def fetch_asm(self, start, end_or_count, relative, highlighter): 1546 | # fetch asm from cache or disassemble 1547 | if self.cache_key == (start, end_or_count): 1548 | asm = self.cache_asm 1549 | else: 1550 | kwargs = { 1551 | 'start_pc': start, 1552 | 'count' if relative else 'end_pc': end_or_count 1553 | } 1554 | asm = gdb.selected_frame().architecture().disassemble(**kwargs) 1555 | self.cache_key = (start, end_or_count) 1556 | self.cache_asm = asm 1557 | # syntax highlight the cached entry 1558 | for instr in asm: 1559 | instr['asm'] = highlighter.process(instr['asm']) 1560 | return asm 1561 | 1562 | class Variables(Dashboard.Module): 1563 | '''Show arguments and locals of the selected frame.''' 1564 | 1565 | def label(self): 1566 | return 'Variables' 1567 | 1568 | def lines(self, term_width, term_height, style_changed): 1569 | return Variables.format_frame( 1570 | gdb.selected_frame(), self.show_arguments, self.show_locals, self.compact, self.align, self.sort) 1571 | 1572 | def attributes(self): 1573 | return { 1574 | 'arguments': { 1575 | 'doc': 'Frame arguments visibility flag.', 1576 | 'default': True, 1577 | 'name': 'show_arguments', 1578 | 'type': bool 1579 | }, 1580 | 'locals': { 1581 | 'doc': 'Frame locals visibility flag.', 1582 | 'default': True, 1583 | 'name': 'show_locals', 1584 | 'type': bool 1585 | }, 1586 | 'compact': { 1587 | 'doc': 'Single-line display flag.', 1588 | 'default': True, 1589 | 'type': bool 1590 | }, 1591 | 'align': { 1592 | 'doc': 'Align variables in column flag (only if not compact).', 1593 | 'default': False, 1594 | 'type': bool 1595 | }, 1596 | 'sort': { 1597 | 'doc': 'Sort variables by name.', 1598 | 'default': False, 1599 | 'type': bool 1600 | } 1601 | } 1602 | 1603 | @staticmethod 1604 | def format_frame(frame, show_arguments, show_locals, compact, align, sort): 1605 | out = [] 1606 | # fetch frame arguments and locals 1607 | decorator = gdb.FrameDecorator.FrameDecorator(frame) 1608 | separator = ansi(', ', R.style_low) 1609 | if show_arguments: 1610 | def prefix(line): 1611 | return Stack.format_line('arg', line) 1612 | frame_args = decorator.frame_args() 1613 | args_lines = Variables.fetch(frame, frame_args, compact, align, sort) 1614 | if args_lines: 1615 | if compact: 1616 | args_line = separator.join(args_lines) 1617 | single_line = prefix(args_line) 1618 | out.append(single_line) 1619 | else: 1620 | out.extend(map(prefix, args_lines)) 1621 | if show_locals: 1622 | def prefix(line): 1623 | return Stack.format_line('loc', line) 1624 | frame_locals = decorator.frame_locals() 1625 | locals_lines = Variables.fetch(frame, frame_locals, compact, align, sort) 1626 | if locals_lines: 1627 | if compact: 1628 | locals_line = separator.join(locals_lines) 1629 | single_line = prefix(locals_line) 1630 | out.append(single_line) 1631 | else: 1632 | out.extend(map(prefix, locals_lines)) 1633 | return out 1634 | 1635 | @staticmethod 1636 | def fetch(frame, data, compact, align, sort): 1637 | lines = [] 1638 | name_width = 0 1639 | if align and not compact: 1640 | name_width = max(len(str(elem.sym)) for elem in data) if data else 0 1641 | for elem in data or []: 1642 | name = ansi(elem.sym, R.style_high) + ' ' * (name_width - len(str(elem.sym))) 1643 | equal = ansi('=', R.style_low) 1644 | value = format_value(elem.sym.value(frame), compact) 1645 | lines.append('{} {} {}'.format(name, equal, value)) 1646 | if sort: 1647 | lines.sort() 1648 | return lines 1649 | 1650 | class Stack(Dashboard.Module): 1651 | '''Show the current stack trace including the function name and the file location, if available. 1652 | 1653 | Optionally list the frame arguments and locals too.''' 1654 | 1655 | def label(self): 1656 | return 'Stack' 1657 | 1658 | def lines(self, term_width, term_height, style_changed): 1659 | # skip if the current thread is not stopped 1660 | if not gdb.selected_thread().is_stopped(): 1661 | return [] 1662 | # find the selected frame level (XXX Frame.level() is a recent addition) 1663 | start_level = 0 1664 | frame = gdb.newest_frame() 1665 | while frame: 1666 | if frame == gdb.selected_frame(): 1667 | break 1668 | frame = frame.older() 1669 | start_level += 1 1670 | # gather the frames 1671 | more = False 1672 | frames = [gdb.selected_frame()] 1673 | going_down = True 1674 | while True: 1675 | # stack frames limit reached 1676 | if len(frames) == self.limit: 1677 | more = True 1678 | break 1679 | # zigzag the frames starting from the selected one 1680 | if going_down: 1681 | frame = frames[-1].older() 1682 | if frame: 1683 | frames.append(frame) 1684 | else: 1685 | frame = frames[0].newer() 1686 | if frame: 1687 | frames.insert(0, frame) 1688 | start_level -= 1 1689 | else: 1690 | break 1691 | else: 1692 | frame = frames[0].newer() 1693 | if frame: 1694 | frames.insert(0, frame) 1695 | start_level -= 1 1696 | else: 1697 | frame = frames[-1].older() 1698 | if frame: 1699 | frames.append(frame) 1700 | else: 1701 | break 1702 | # switch direction 1703 | going_down = not going_down 1704 | # format the output 1705 | lines = [] 1706 | for number, frame in enumerate(frames, start=start_level): 1707 | selected = frame == gdb.selected_frame() 1708 | lines.extend(self.get_frame_lines(number, frame, selected)) 1709 | # add the placeholder 1710 | if more: 1711 | lines.append('[{}]'.format(ansi('+', R.style_selected_2))) 1712 | return lines 1713 | 1714 | def attributes(self): 1715 | return { 1716 | 'limit': { 1717 | 'doc': 'Maximum number of displayed frames (0 means no limit).', 1718 | 'default': 10, 1719 | 'type': int, 1720 | 'check': check_ge_zero 1721 | }, 1722 | 'arguments': { 1723 | 'doc': 'Frame arguments visibility flag.', 1724 | 'default': False, 1725 | 'name': 'show_arguments', 1726 | 'type': bool 1727 | }, 1728 | 'locals': { 1729 | 'doc': 'Frame locals visibility flag.', 1730 | 'default': False, 1731 | 'name': 'show_locals', 1732 | 'type': bool 1733 | }, 1734 | 'compact': { 1735 | 'doc': 'Single-line display flag.', 1736 | 'default': False, 1737 | 'type': bool 1738 | }, 1739 | 'align': { 1740 | 'doc': 'Align variables in column flag (only if not compact).', 1741 | 'default': False, 1742 | 'type': bool 1743 | }, 1744 | 'sort': { 1745 | 'doc': 'Sort variables by name.', 1746 | 'default': False, 1747 | 'type': bool 1748 | } 1749 | } 1750 | 1751 | def get_frame_lines(self, number, frame, selected=False): 1752 | # fetch frame info 1753 | style = R.style_selected_1 if selected else R.style_selected_2 1754 | frame_id = ansi(str(number), style) 1755 | info = Stack.get_pc_line(frame, style) 1756 | frame_lines = [] 1757 | frame_lines.append('[{}] {}'.format(frame_id, info)) 1758 | # add frame arguments and locals 1759 | variables = Variables.format_frame( 1760 | frame, self.show_arguments, self.show_locals, self.compact, self.align, self.sort) 1761 | frame_lines.extend(variables) 1762 | return frame_lines 1763 | 1764 | @staticmethod 1765 | def format_line(prefix, line): 1766 | prefix = ansi(prefix, R.style_low) 1767 | return '{} {}'.format(prefix, line) 1768 | 1769 | @staticmethod 1770 | def get_pc_line(frame, style): 1771 | frame_pc = ansi(format_address(frame.pc()), style) 1772 | info = 'from {}'.format(frame_pc) 1773 | # if a frame function symbol is available then use it to fetch the 1774 | # current function name and address, otherwise fall back relying on the 1775 | # frame name 1776 | if frame.function(): 1777 | name = ansi(frame.function(), style) 1778 | func_start = to_unsigned(frame.function().value()) 1779 | offset = ansi(str(frame.pc() - func_start), style) 1780 | info += ' in {}+{}'.format(name, offset) 1781 | elif frame.name(): 1782 | name = ansi(frame.name(), style) 1783 | info += ' in {}'.format(name) 1784 | sal = frame.find_sal() 1785 | if sal and sal.symtab: 1786 | file_name = ansi(sal.symtab.filename, style) 1787 | file_line = ansi(str(sal.line), style) 1788 | info += ' at {}:{}'.format(file_name, file_line) 1789 | return info 1790 | 1791 | class History(Dashboard.Module): 1792 | '''List the last entries of the value history.''' 1793 | 1794 | def label(self): 1795 | return 'History' 1796 | 1797 | def lines(self, term_width, term_height, style_changed): 1798 | out = [] 1799 | # fetch last entries 1800 | for i in range(-self.limit + 1, 1): 1801 | try: 1802 | value = format_value(gdb.history(i)) 1803 | value_id = ansi('$${}', R.style_high).format(abs(i)) 1804 | equal = ansi('=', R.style_low) 1805 | line = '{} {} {}'.format(value_id, equal, value) 1806 | out.append(line) 1807 | except gdb.error: 1808 | continue 1809 | return out 1810 | 1811 | def attributes(self): 1812 | return { 1813 | 'limit': { 1814 | 'doc': 'Maximum number of values to show.', 1815 | 'default': 3, 1816 | 'type': int, 1817 | 'check': check_gt_zero 1818 | } 1819 | } 1820 | 1821 | class Memory(Dashboard.Module): 1822 | '''Allow to inspect memory regions.''' 1823 | 1824 | DEFAULT_LENGTH = 16 1825 | 1826 | class Region(): 1827 | def __init__(self, expression, length, module): 1828 | self.expression = expression 1829 | self.length = length 1830 | self.module = module 1831 | self.original = None 1832 | self.latest = None 1833 | 1834 | def reset(self): 1835 | self.original = None 1836 | self.latest = None 1837 | 1838 | def format(self, per_line): 1839 | # fetch the memory content 1840 | try: 1841 | address = Memory.parse_as_address(self.expression) 1842 | inferior = gdb.selected_inferior() 1843 | memory = inferior.read_memory(address, self.length) 1844 | # set the original memory snapshot if needed 1845 | if not self.original: 1846 | self.original = memory 1847 | except gdb.error as e: 1848 | msg = 'Cannot access {} bytes starting at {}: {}' 1849 | msg = msg.format(self.length, self.expression, e) 1850 | return [ansi(msg, R.style_error)] 1851 | # format the memory content 1852 | out = [] 1853 | for i in range(0, len(memory), per_line): 1854 | region = memory[i:i + per_line] 1855 | pad = per_line - len(region) 1856 | address_str = format_address(address + i) 1857 | # compute changes 1858 | hexa = [] 1859 | text = [] 1860 | for j in range(len(region)): 1861 | rel = i + j 1862 | byte = memory[rel] 1863 | hexa_byte = '{:02x}'.format(ord(byte)) 1864 | text_byte = self.module.format_byte(byte) 1865 | # differences against the latest have the highest priority 1866 | if self.latest and memory[rel] != self.latest[rel]: 1867 | hexa_byte = ansi(hexa_byte, R.style_selected_1) 1868 | text_byte = ansi(text_byte, R.style_selected_1) 1869 | # cumulative changes if enabled 1870 | elif self.module.cumulative and memory[rel] != self.original[rel]: 1871 | hexa_byte = ansi(hexa_byte, R.style_selected_2) 1872 | text_byte = ansi(text_byte, R.style_selected_2) 1873 | # format the text differently for clarity 1874 | else: 1875 | text_byte = ansi(text_byte, R.style_high) 1876 | hexa.append(hexa_byte) 1877 | text.append(text_byte) 1878 | # output the formatted line 1879 | hexa_placeholder = ' {}'.format(self.module.placeholder[0] * 2) 1880 | text_placeholder = self.module.placeholder[0] 1881 | out.append('{} {}{} {}{}'.format( 1882 | ansi(address_str, R.style_low), 1883 | ' '.join(hexa), ansi(pad * hexa_placeholder, R.style_low), 1884 | ''.join(text), ansi(pad * text_placeholder, R.style_low))) 1885 | # update the latest memory snapshot 1886 | self.latest = memory 1887 | return out 1888 | 1889 | def __init__(self): 1890 | self.table = {} 1891 | 1892 | def label(self): 1893 | return 'Memory' 1894 | 1895 | def lines(self, term_width, term_height, style_changed): 1896 | out = [] 1897 | for expression, region in self.table.items(): 1898 | out.append(divider(term_width, expression)) 1899 | out.extend(region.format(self.get_per_line(term_width))) 1900 | return out 1901 | 1902 | def commands(self): 1903 | return { 1904 | 'watch': { 1905 | 'action': self.watch, 1906 | 'doc': '''Watch a memory region by expression and length. 1907 | 1908 | The length defaults to 16 bytes.''', 1909 | 'complete': gdb.COMPLETE_EXPRESSION 1910 | }, 1911 | 'unwatch': { 1912 | 'action': self.unwatch, 1913 | 'doc': 'Stop watching a memory region by expression.', 1914 | 'complete': gdb.COMPLETE_EXPRESSION 1915 | }, 1916 | 'clear': { 1917 | 'action': self.clear, 1918 | 'doc': 'Clear all the watched regions.' 1919 | } 1920 | } 1921 | 1922 | def attributes(self): 1923 | return { 1924 | 'cumulative': { 1925 | 'doc': 'Highlight changes cumulatively, watch again to reset.', 1926 | 'default': False, 1927 | 'type': bool 1928 | }, 1929 | 'full': { 1930 | 'doc': 'Take the whole horizontal space.', 1931 | 'default': False, 1932 | 'type': bool 1933 | }, 1934 | 'placeholder': { 1935 | 'doc': 'Placeholder used for missing items and unprintable characters.', 1936 | 'default': '·' 1937 | } 1938 | } 1939 | 1940 | def watch(self, arg): 1941 | if arg: 1942 | expression, _, length_str = arg.partition(' ') 1943 | length = Memory.parse_as_address(length_str) if length_str else Memory.DEFAULT_LENGTH 1944 | # keep the length when the memory is watched to reset the changes 1945 | region = self.table.get(expression) 1946 | if region and not length_str: 1947 | region.reset() 1948 | else: 1949 | self.table[expression] = Memory.Region(expression, length, self) 1950 | else: 1951 | raise Exception('Specify a memory location') 1952 | 1953 | def unwatch(self, arg): 1954 | if arg: 1955 | try: 1956 | del self.table[arg] 1957 | except KeyError: 1958 | raise Exception('Memory expression not watched') 1959 | else: 1960 | raise Exception('Specify a matched memory expression') 1961 | 1962 | def clear(self, arg): 1963 | self.table.clear() 1964 | 1965 | def format_byte(self, byte): 1966 | # `type(byte) is bytes` in Python 3 1967 | if 0x20 < ord(byte) < 0x7f: 1968 | return chr(ord(byte)) 1969 | else: 1970 | return self.placeholder[0] 1971 | 1972 | def get_per_line(self, term_width): 1973 | if self.full: 1974 | padding = 3 # two double spaces separator (one is part of below) 1975 | elem_size = 4 # HH + 1 space + T 1976 | address_length = gdb.parse_and_eval('$pc').type.sizeof * 2 + 2 # 0x 1977 | return max(int((term_width - address_length - padding) / elem_size), 1) 1978 | else: 1979 | return Memory.DEFAULT_LENGTH 1980 | 1981 | @staticmethod 1982 | def parse_as_address(expression): 1983 | value = gdb.parse_and_eval(expression) 1984 | return to_unsigned(value) 1985 | 1986 | class Registers(Dashboard.Module): 1987 | '''Show the CPU registers and their values.''' 1988 | 1989 | def __init__(self): 1990 | self.table = {} 1991 | 1992 | def label(self): 1993 | return 'Registers' 1994 | 1995 | def lines(self, term_width, term_height, style_changed): 1996 | # skip if the current thread is not stopped 1997 | if not gdb.selected_thread().is_stopped(): 1998 | return [] 1999 | # obtain the registers to display 2000 | if style_changed: 2001 | self.table = {} 2002 | if self.register_list: 2003 | register_list = self.register_list.split() 2004 | else: 2005 | register_list = Registers.fetch_register_list() 2006 | # fetch registers status 2007 | registers = [] 2008 | for name in register_list: 2009 | # exclude registers with a dot '.' or parse_and_eval() will fail 2010 | if '.' in name: 2011 | continue 2012 | value = gdb.parse_and_eval('${}'.format(name)) 2013 | string_value = Registers.format_value(value) 2014 | # exclude unavailable registers (see #255) 2015 | if string_value == '': 2016 | continue 2017 | changed = self.table and (self.table.get(name, '') != string_value) 2018 | self.table[name] = string_value 2019 | registers.append((name, string_value, changed)) 2020 | # handle the empty register list 2021 | if not registers: 2022 | msg = 'No registers to show (check the "dashboard registers -style list" attribute)' 2023 | return [ansi(msg, R.style_error)] 2024 | # compute lengths considering an extra space between and around the 2025 | # entries (hence the +2 and term_width - 1) 2026 | max_name = max(len(name) for name, _, _ in registers) 2027 | max_value = max(len(value) for _, value, _ in registers) 2028 | max_width = max_name + max_value + 2 2029 | columns = min(int((term_width - 1) / max_width) or 1, len(registers)) 2030 | rows = int(math.ceil(float(len(registers)) / columns)) 2031 | # build the registers matrix 2032 | if self.column_major: 2033 | matrix = list(registers[i:i + rows] for i in range(0, len(registers), rows)) 2034 | else: 2035 | matrix = list(registers[i::columns] for i in range(columns)) 2036 | # compute the lengths column wise 2037 | max_names_column = list(max(len(name) for name, _, _ in column) for column in matrix) 2038 | max_values_column = list(max(len(value) for _, value, _ in column) for column in matrix) 2039 | line_length = sum(max_names_column) + columns + sum(max_values_column) 2040 | extra = term_width - line_length 2041 | # compute padding as if there were one more column 2042 | base_padding = int(extra / (columns + 1)) 2043 | padding_column = [base_padding] * columns 2044 | # distribute the remainder among columns giving the precedence to 2045 | # internal padding 2046 | rest = extra % (columns + 1) 2047 | while rest: 2048 | padding_column[rest % columns] += 1 2049 | rest -= 1 2050 | # format the registers 2051 | out = [''] * rows 2052 | for i, column in enumerate(matrix): 2053 | max_name = max_names_column[i] 2054 | max_value = max_values_column[i] 2055 | for j, (name, value, changed) in enumerate(column): 2056 | name = ' ' * (max_name - len(name)) + ansi(name, R.style_low) 2057 | style = R.style_selected_1 if changed else '' 2058 | value = ansi(value, style) + ' ' * (max_value - len(value)) 2059 | padding = ' ' * padding_column[i] 2060 | item = '{}{} {}'.format(padding, name, value) 2061 | out[j] += item 2062 | return out 2063 | 2064 | def attributes(self): 2065 | return { 2066 | 'column-major': { 2067 | 'doc': 'Show registers in columns instead of rows.', 2068 | 'default': False, 2069 | 'name': 'column_major', 2070 | 'type': bool 2071 | }, 2072 | 'list': { 2073 | 'doc': '''String of space-separated register names to display. 2074 | 2075 | The empty list (default) causes to show all the available registers. For 2076 | architectures different from x86 setting this attribute might be mandatory.''', 2077 | 'default': '', 2078 | 'name': 'register_list', 2079 | } 2080 | } 2081 | 2082 | @staticmethod 2083 | def format_value(value): 2084 | try: 2085 | if value.type.code in [gdb.TYPE_CODE_INT, gdb.TYPE_CODE_PTR]: 2086 | int_value = to_unsigned(value, value.type.sizeof) 2087 | value_format = '0x{{:0{}x}}'.format(2 * value.type.sizeof) 2088 | return value_format.format(int_value) 2089 | except (gdb.error, ValueError): 2090 | # convert to unsigned but preserve code and flags information 2091 | pass 2092 | return str(value) 2093 | 2094 | @staticmethod 2095 | def fetch_register_list(*match_groups): 2096 | names = [] 2097 | for line in run('maintenance print register-groups').split('\n'): 2098 | fields = line.split() 2099 | if len(fields) != 7: 2100 | continue 2101 | name, _, _, _, _, _, groups = fields 2102 | if not re.match(r'\w', name): 2103 | continue 2104 | for group in groups.split(','): 2105 | if group in (match_groups or ('general',)): 2106 | names.append(name) 2107 | break 2108 | return names 2109 | 2110 | class Threads(Dashboard.Module): 2111 | '''List the currently available threads.''' 2112 | 2113 | def label(self): 2114 | return 'Threads' 2115 | 2116 | def lines(self, term_width, term_height, style_changed): 2117 | out = [] 2118 | selected_thread = gdb.selected_thread() 2119 | # do not restore the selected frame if the thread is not stopped 2120 | restore_frame = gdb.selected_thread().is_stopped() 2121 | if restore_frame: 2122 | selected_frame = gdb.selected_frame() 2123 | # fetch the thread list 2124 | threads = [] 2125 | for inferior in gdb.inferiors(): 2126 | if self.all_inferiors or inferior == gdb.selected_inferior(): 2127 | threads += gdb.Inferior.threads(inferior) 2128 | for thread in threads: 2129 | # skip running threads if requested 2130 | if self.skip_running and thread.is_running(): 2131 | continue 2132 | is_selected = (thread.ptid == selected_thread.ptid) 2133 | style = R.style_selected_1 if is_selected else R.style_selected_2 2134 | if self.all_inferiors: 2135 | number = '{}.{}'.format(thread.inferior.num, thread.num) 2136 | else: 2137 | number = str(thread.num) 2138 | number = ansi(number, style) 2139 | tid = ansi(str(thread.ptid[1] or thread.ptid[2]), style) 2140 | info = '[{}] id {}'.format(number, tid) 2141 | if thread.name: 2142 | info += ' name {}'.format(ansi(thread.name, style)) 2143 | # switch thread to fetch info (unless is running in non-stop mode) 2144 | try: 2145 | thread.switch() 2146 | frame = gdb.newest_frame() 2147 | info += ' ' + Stack.get_pc_line(frame, style) 2148 | except gdb.error: 2149 | info += ' (running)' 2150 | out.append(info) 2151 | # restore thread and frame 2152 | selected_thread.switch() 2153 | if restore_frame: 2154 | selected_frame.select() 2155 | return out 2156 | 2157 | def attributes(self): 2158 | return { 2159 | 'skip-running': { 2160 | 'doc': 'Skip running threads.', 2161 | 'default': False, 2162 | 'name': 'skip_running', 2163 | 'type': bool 2164 | }, 2165 | 'all-inferiors': { 2166 | 'doc': 'Show threads from all inferiors.', 2167 | 'default': False, 2168 | 'name': 'all_inferiors', 2169 | 'type': bool 2170 | }, 2171 | } 2172 | 2173 | class Expressions(Dashboard.Module): 2174 | '''Watch user expressions.''' 2175 | 2176 | def __init__(self): 2177 | self.table = [] 2178 | 2179 | def label(self): 2180 | return 'Expressions' 2181 | 2182 | def lines(self, term_width, term_height, style_changed): 2183 | out = [] 2184 | label_width = 0 2185 | if self.align: 2186 | label_width = max(len(expression) for expression in self.table) if self.table else 0 2187 | default_radix = Expressions.get_default_radix() 2188 | for number, expression in enumerate(self.table, start=1): 2189 | label = expression 2190 | match = re.match(r'^/(\d+) +(.+)$', expression) 2191 | try: 2192 | if match: 2193 | radix, expression = match.groups() 2194 | run('set output-radix {}'.format(radix)) 2195 | value = format_value(gdb.parse_and_eval(expression)) 2196 | except gdb.error as e: 2197 | value = ansi(e, R.style_error) 2198 | finally: 2199 | if match: 2200 | run('set output-radix {}'.format(default_radix)) 2201 | number = ansi(str(number), R.style_selected_2) 2202 | label = ansi(expression, R.style_high) + ' ' * (label_width - len(expression)) 2203 | equal = ansi('=', R.style_low) 2204 | out.append('[{}] {} {} {}'.format(number, label, equal, value)) 2205 | return out 2206 | 2207 | def commands(self): 2208 | return { 2209 | 'watch': { 2210 | 'action': self.watch, 2211 | 'doc': 'Watch an expression using the format `[/] `.', 2212 | 'complete': gdb.COMPLETE_EXPRESSION 2213 | }, 2214 | 'unwatch': { 2215 | 'action': self.unwatch, 2216 | 'doc': 'Stop watching an expression by index.' 2217 | }, 2218 | 'clear': { 2219 | 'action': self.clear, 2220 | 'doc': 'Clear all the watched expressions.' 2221 | } 2222 | } 2223 | 2224 | def attributes(self): 2225 | return { 2226 | 'align': { 2227 | 'doc': 'Align variables in column flag.', 2228 | 'default': False, 2229 | 'type': bool 2230 | } 2231 | } 2232 | 2233 | def watch(self, arg): 2234 | if arg: 2235 | if arg not in self.table: 2236 | self.table.append(arg) 2237 | else: 2238 | raise Exception('Expression already watched') 2239 | else: 2240 | raise Exception('Specify an expression') 2241 | 2242 | def unwatch(self, arg): 2243 | if arg: 2244 | try: 2245 | number = int(arg) - 1 2246 | except: 2247 | number = -1 2248 | if 0 <= number < len(self.table): 2249 | self.table.pop(number) 2250 | else: 2251 | raise Exception('Expression not watched') 2252 | else: 2253 | raise Exception('Specify an expression') 2254 | 2255 | def clear(self, arg): 2256 | self.table.clear() 2257 | 2258 | @staticmethod 2259 | def get_default_radix(): 2260 | try: 2261 | return gdb.parameter('output-radix') 2262 | except RuntimeError: 2263 | # XXX this is a fix for GDB <8.1.x see #161 2264 | message = run('show output-radix') 2265 | match = re.match(r'^Default output radix for printing of values is (\d+)\.$', message) 2266 | return match.groups()[0] if match else 10 # fallback 2267 | 2268 | # XXX workaround to support BP_BREAKPOINT in older GDB versions 2269 | setattr(gdb, 'BP_CATCHPOINT', getattr(gdb, 'BP_CATCHPOINT', 26)) 2270 | 2271 | class Breakpoints(Dashboard.Module): 2272 | '''Display the breakpoints list.''' 2273 | 2274 | NAMES = { 2275 | gdb.BP_BREAKPOINT: 'break', 2276 | gdb.BP_WATCHPOINT: 'watch', 2277 | gdb.BP_HARDWARE_WATCHPOINT: 'write watch', 2278 | gdb.BP_READ_WATCHPOINT: 'read watch', 2279 | gdb.BP_ACCESS_WATCHPOINT: 'access watch', 2280 | gdb.BP_CATCHPOINT: 'catch' 2281 | } 2282 | 2283 | def label(self): 2284 | return 'Breakpoints' 2285 | 2286 | def lines(self, term_width, term_height, style_changed): 2287 | out = [] 2288 | breakpoints = fetch_breakpoints(watchpoints=True, pending=self.show_pending) 2289 | for breakpoint in breakpoints: 2290 | sub_lines = [] 2291 | # format common information 2292 | style = R.style_selected_1 if breakpoint['enabled'] else R.style_selected_2 2293 | number = ansi(breakpoint['number'], style) 2294 | bp_type = ansi(Breakpoints.NAMES[breakpoint['type']], style) 2295 | if breakpoint['temporary']: 2296 | bp_type = bp_type + ' {}'.format(ansi('once', style)) 2297 | if not R.ansi and breakpoint['enabled']: 2298 | bp_type = 'disabled ' + bp_type 2299 | line = '[{}] {}'.format(number, bp_type) 2300 | if breakpoint['type'] == gdb.BP_BREAKPOINT: 2301 | for i, address in enumerate(breakpoint['addresses']): 2302 | addr = address['address'] 2303 | if i == 0 and addr: 2304 | # this is a regular breakpoint 2305 | line += ' at {}'.format(ansi(format_address(addr), style)) 2306 | # format source information 2307 | file_name = address.get('file_name') 2308 | file_line = address.get('file_line') 2309 | if file_name and file_line: 2310 | file_name = ansi(file_name, style) 2311 | file_line = ansi(file_line, style) 2312 | line += ' in {}:{}'.format(file_name, file_line) 2313 | elif i > 0: 2314 | # this is a sub breakpoint 2315 | sub_style = R.style_selected_1 if address['enabled'] else R.style_selected_2 2316 | sub_number = ansi('{}.{}'.format(breakpoint['number'], i), sub_style) 2317 | sub_line = '[{}]'.format(sub_number) 2318 | sub_line += ' at {}'.format(ansi(format_address(addr), sub_style)) 2319 | # format source information 2320 | file_name = address.get('file_name') 2321 | file_line = address.get('file_line') 2322 | if file_name and file_line: 2323 | file_name = ansi(file_name, sub_style) 2324 | file_line = ansi(file_line, sub_style) 2325 | sub_line += ' in {}:{}'.format(file_name, file_line) 2326 | sub_lines += [sub_line] 2327 | # format user location 2328 | location = breakpoint['location'] 2329 | line += ' for {}'.format(ansi(location, style)) 2330 | elif breakpoint['type'] == gdb.BP_CATCHPOINT: 2331 | what = breakpoint['what'] 2332 | line += ' {}'.format(ansi(what, style)) 2333 | else: 2334 | # format user expression 2335 | expression = breakpoint['expression'] 2336 | line += ' for {}'.format(ansi(expression, style)) 2337 | # format condition 2338 | condition = breakpoint['condition'] 2339 | if condition: 2340 | line += ' if {}'.format(ansi(condition, style)) 2341 | # format hit count 2342 | hit_count = breakpoint['hit_count'] 2343 | if hit_count: 2344 | word = 'time{}'.format('s' if hit_count > 1 else '') 2345 | line += ' hit {} {}'.format(ansi(breakpoint['hit_count'], style), word) 2346 | # append the main line and possibly sub breakpoints 2347 | out.append(line) 2348 | out.extend(sub_lines) 2349 | return out 2350 | 2351 | def attributes(self): 2352 | return { 2353 | 'pending': { 2354 | 'doc': 'Also show pending breakpoints.', 2355 | 'default': True, 2356 | 'name': 'show_pending', 2357 | 'type': bool 2358 | } 2359 | } 2360 | 2361 | # XXX traceback line numbers in this Python block must be increased by 1 2362 | end 2363 | 2364 | # Better GDB defaults ---------------------------------------------------------- 2365 | 2366 | set history save 2367 | set verbose off 2368 | set print pretty on 2369 | set print array off 2370 | set print array-indexes on 2371 | set python print-stack full 2372 | 2373 | # Start ------------------------------------------------------------------------ 2374 | 2375 | python Dashboard.start() 2376 | 2377 | # Fixes ------------------------------------------------------------------------ 2378 | 2379 | # workaround for the GDB readline issue, see #325 2380 | python import sys; sys.modules['readline'] = None 2381 | 2382 | # File variables --------------------------------------------------------------- 2383 | 2384 | # vim: filetype=python 2385 | # Local Variables: 2386 | # mode: python 2387 | # End: 2388 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2024 Andrea Cardaci 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GDB dashboard 2 | 3 | GDB dashboard is a standalone `.gdbinit` file written using the [Python API][] that enables a modular interface showing relevant information about the program being debugged. Its main goal is to reduce the number of GDB commands needed to inspect the status of current program thus allowing the developer to primarily focus on the control flow. 4 | 5 | ![Screenshot](https://raw.githubusercontent.com/wiki/cyrus-and/gdb-dashboard/Screenshot.png) 6 | 7 | [Python API]: https://sourceware.org/gdb/onlinedocs/gdb/Python-API.html 8 | 9 | ## Quickstart 10 | 11 | Just place [`.gdbinit`][] in your home directory, for example with: 12 | 13 | ``` 14 | wget -P ~ https://github.com/cyrus-and/gdb-dashboard/raw/master/.gdbinit 15 | ``` 16 | 17 | Optionally install [Pygments][] to enable syntax highlighting: 18 | 19 | ``` 20 | pip install pygments 21 | ``` 22 | 23 | Then debug as usual, the dashboard will appear automatically every time the inferior program stops. 24 | 25 | Keep in mind that no GDB command has been redefined, instead all the features are available via the main `dashboard` command (see `help dashboard`). 26 | 27 | Head to the [wiki][] to learn how to perform the most important tasks. 28 | 29 | [`.gdbinit`]: https://raw.githubusercontent.com/cyrus-and/gdb-dashboard/master/.gdbinit 30 | [Pygments]: http://pygments.org/ 31 | [wiki]: https://github.com/cyrus-and/gdb-dashboard/wiki 32 | --------------------------------------------------------------------------------