From e11436d2e2b14dae3e1e5b0aef451a7007513610 Mon Sep 17 00:00:00 2001 From: nwlyoc Date: Mon, 18 Dec 2023 15:08:10 +0100 Subject: [PATCH 1/3] rewrite memory module to use GDB's "x" output This is a rewrite of the memory module to enable watching a memory region with the format options of GDB's "x" command. Instead of getting a raw memory dump like before, an "x" command is created, evaluated by GDB and its output is formatted according to the dashboard settings. The watch command of the previous memory module can be used and extended for backwards compatibility. Alternatively the watch command also accepts an "x" command as it would be entered into GDB directly. --- .gdbinit | 306 ++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 237 insertions(+), 69 deletions(-) diff --git a/.gdbinit b/.gdbinit index ca24cf9..a2f31bd 100644 --- a/.gdbinit +++ b/.gdbinit @@ -1819,14 +1819,22 @@ class History(Dashboard.Module): } class Memory(Dashboard.Module): - '''Allow to inspect memory regions.''' + '''Allow to inspect memory regions with GDB's "x" command.''' - DEFAULT_LENGTH = 16 + DEFAULT_COUNT = 16 + DEFAULT_FORMAT = 'x' + DEFAULT_SIZE = 'b' + DEFAULT_PER_LINE = 4 + SEPARATION_SPACE_RAW = 2 + SEPARATION_SPACE_TEXT = 1 class Region(): - def __init__(self, expression, length, module): - self.expression = expression - self.length = length + def __init__(self, xcmd, object_base, object_size, per_line, module): + self.xcmd = xcmd + self.object_base = object_base + self.object_size = object_size + self.address = 0 + self.per_line = per_line self.module = module self.original = None self.latest = None @@ -1835,53 +1843,121 @@ class Memory(Dashboard.Module): self.original = None self.latest = None - def format(self, per_line): + def fetch_memory(self): + memory_raw = gdb.execute(self.xcmd, to_string=True) + if memory_raw.startswith("warning"): + raise gdb.error('GDB threw warning, try command directly for more info') + # split output string into items and strip addresses + memory = [] + for l in memory_raw.split('\n'): + if l: + address_separator = re.search(r':\s+', l) + if not address_separator: + raise gdb.error('Unexpected output from GDB') + if self.object_base == 'i': + # one instruction per line + l_elements = [l[address_separator.end():]] + elif self.object_base == 'c': + # always in pairs, separated by tabs + l_elements = (l[address_separator.end():]).split('\t') + else: + l_elements = l[address_separator.end():].split() + # with string format GDB doesn't throw an error directly + if self.object_base == 's' and ' max_per_line: # use maximum horizontal space + per_line = max_per_line + elif choice > 0: + per_line = choice else: - return Memory.DEFAULT_LENGTH + per_line = 1 + return per_line + + def parse_arg(self, arg): + msg_invalid = 'Invalid command, see "help dashboard memory watch"' + # values for GDB's x command (every letter is unique) + x_vals_size = { 'b': 1, 'h': 2, 'w': 4, 'g': 8} # object size + x_vals_format = { + 'x': 16, 'z': 16, 'o': 8, 'd': 10, 'u': 10, 't': 2, # object base + 'i': 'i', 'c': 'c', 's': 's', 'f': 'other', 'a': 'other' # string objects + } + cmd_options = {'address': '', 'count': 0, + 'base': ('', 0), 'size': ('', 0), 'per_line': 0} + # argument can be either "x[/[COUNT][FORMAT][SIZE]] [PER_LINE]" or + # "ADDRESS [COUNT] [FORMAT] [SIZE] [PER_LINE]"; + # bring argument into common format + arg_parts = arg.split() + # regardless of format, per_line arg must be at least the 3rd argument + # and it must be numeric + if len(arg_parts) > 2 and arg_parts[-1].isdigit(): + cmd_options['per_line'] = self.parse_as_address(arg_parts[-1]) + del(arg_parts[-1]) + if arg.startswith('x'): # format of GDB's "x" command + if len(arg_parts) > 1: + cmd_options['address'] = arg_parts[1] + x_options = arg_parts[0][2:] # formatted as "x/FMT"; go past slash + else: # gdb-dashboard format + cmd_options['address'] = arg_parts[0] + x_options = ''.join(arg_parts[1:]) + # parse command + if len(x_options): + count_pos = re.search(r'^-?\d*', x_options) + if count_pos and count_pos.start() != count_pos.end(): + cmd_options['count'] = int(x_options[:count_pos.end()]) + x_options = x_options[count_pos.end():] + if len(x_options) > 2: + raise Exception(msg_invalid) + elif len(x_options): + for opt in x_options: + if opt in x_vals_format: + cmd_options['base'] = (opt, x_vals_format[opt]) + elif opt in x_vals_size: + cmd_options['size'] = (opt, x_vals_size[opt]) + else: + raise Exception(msg_invalid) + # set defaults where needed + if not cmd_options['count']: + cmd_options['count'] = self.DEFAULT_COUNT + if not cmd_options['base'][0]: + cmd_options['base'] = (self.DEFAULT_FORMAT, x_vals_format[self.DEFAULT_FORMAT]) + if not cmd_options['size'][0]: + cmd_options['size'] = (self.DEFAULT_SIZE, x_vals_size[self.DEFAULT_SIZE]) + if not cmd_options['per_line']: + cmd_options['per_line'] = self.DEFAULT_PER_LINE + if cmd_options['per_line'] > abs(cmd_options['count']): + # avoid unnecessary placeholders + cmd_options['per_line'] = abs(cmd_options['count']) + return cmd_options @staticmethod def parse_as_address(expression): From 173a454c1b61e184ae78b47a6562ec62302f33b6 Mon Sep 17 00:00:00 2001 From: nwlyoc Date: Mon, 18 Dec 2023 17:51:48 +0100 Subject: [PATCH 2/3] Fix address display for instructions and strings; minor cleanup Instructions and strings have variable sizes, so the addresses for consecutive objects can't be calculated. To fix this, display only one object per line when the format is "i" or "s" (like in GDB's x output) and keep the addresses from GDB's output. This also has the benefit of displaying address tags. --- .gdbinit | 58 ++++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 37 insertions(+), 21 deletions(-) diff --git a/.gdbinit b/.gdbinit index a2f31bd..11f3443 100644 --- a/.gdbinit +++ b/.gdbinit @@ -1847,6 +1847,13 @@ class Memory(Dashboard.Module): memory_raw = gdb.execute(self.xcmd, to_string=True) if memory_raw.startswith("warning"): raise gdb.error('GDB threw warning, try command directly for more info') + # get start address from GDB's output rather than input, so GDB can + # use default if no address input is provided and to account for + # negative range + self.address = memory_raw[:memory_raw.index(':')] + # remove current position indicator if present and parse + self.address = re.sub(r'^=> ', '', self.address) + self.address = Memory.parse_as_address(self.address.split()[0]) # split output string into items and strip addresses memory = [] for l in memory_raw.split('\n'): @@ -1854,12 +1861,13 @@ class Memory(Dashboard.Module): address_separator = re.search(r':\s+', l) if not address_separator: raise gdb.error('Unexpected output from GDB') - if self.object_base == 'i': - # one instruction per line - l_elements = [l[address_separator.end():]] + if self.object_base == 'i' or self.object_base == 's': + # one instruction/string per line with variable size, so + # keep address + l_elements = [l] elif self.object_base == 'c': # always in pairs, separated by tabs - l_elements = (l[address_separator.end():]).split('\t') + l_elements = l[address_separator.end():].split('\t') else: l_elements = l[address_separator.end():].split() # with string format GDB doesn't throw an error directly @@ -1870,10 +1878,6 @@ class Memory(Dashboard.Module): # set the original memory snapshot if needed if not self.original: self.original = memory - # get start address from GDB's output rather than input, so GDB can - # use default if no address input is provided and to account for - # negative range - self.address = Memory.parse_as_address(memory_raw[:memory_raw.index(':')].split()[0]) return memory def format_compute_changes(self, memory, start, count, text): @@ -1908,17 +1912,20 @@ class Memory(Dashboard.Module): else: separation = self.module.SEPARATION_SPACE_RAW elem_length = max_elem_len - placeholder = '{}'.format(self.module.placeholder[0] * elem_length) for elem_cont in elems: - # instructions and strings are left aligned + # instructions and strings are left aligned and on a single line if self.object_base == 'i' or self.object_base == 's': + # address is included in element, format differently + addr_sep = elem_cont[0].index(':') line += '{}{}{}'.format(' ' * separation, - elem_cont[0], - ' ' * (elem_length - elem_cont[1])) + ansi(elem_cont[0][:addr_sep+1], R.style_low), + elem_cont[0][addr_sep+1:]) + #line = '{}'.format(ansi(address_str, R.style_low)) else: line += '{}{}{}'.format(' ' * separation, ' ' * (elem_length - elem_cont[1]), elem_cont[0]) + placeholder = '{}'.format(self.module.placeholder[0] * elem_length) for _ in range(pad): line += '{}{}'.format(' ' * separation, ansi(placeholder, R.style_low)) return line @@ -1932,8 +1939,11 @@ class Memory(Dashboard.Module): msg = msg.format(self.xcmd, e) return [ansi(msg, R.style_error)] max_elem_len = len(max(memory, key=lambda x: len(str(x)))) - # text representation is only possible if object format is integer - if type(self.object_base) == type(1): + if self.object_base == 'i' or self.object_base == 's': + # instructions and strings are always in separate lines + per_line_current = 1 + elif type(self.object_base) == type(1): + # text representation is only possible if object format is integer per_line_current = self.module.get_per_line(term_width, self.per_line, max_elem_len, @@ -1948,9 +1958,14 @@ class Memory(Dashboard.Module): out = [] for i in range(0, len(memory), per_line_current): pad = per_line_current - len(memory[i:i + per_line_current]) - address_str = format_address(self.address + i*self.object_size) raw = self.format_compute_changes(memory, i, per_line_current, False) - line = '{}'.format(ansi(address_str, R.style_low)) + # TODO instruction and string format don't have fixed size, so + # address can't be calculated + if self.object_base == 'i' or self.object_base == 's': + line = '' + else: + address_str = format_address(self.address + i*self.object_size) + line = '{}'.format(ansi(address_str, R.style_low)) line = self.format_output_line(line, raw, pad, False, max_elem_len) # if no text representation is available, line is finished here; # otherwise repeat for text @@ -1988,11 +2003,12 @@ ADDRESS [COUNT] [FORMAT] [SIZE] [PER_LINE] x[/[COUNT][FORMAT][SIZE]] [PER_LINE] where: -ADDRESS: starting address, -COUNT: number of objects to display (default: 16), -FORMAT: object format (o, x, d, u, t, f, a, i, c or s, see "help x"; default: x), -SIZE: object size (b, h, w or g, see "help x"; default: b) and -PER_LINE: number of objects that should be printed per line (default: 4)''', +ADDRESS: starting address +COUNT: number of objects to display (default: 16) +FORMAT: object format (o, x, d, u, t, f, a, i, c or s, see "help x"; default: x) +SIZE: object size (b, h, w or g, see "help x"; default: b) +PER_LINE: number of objects that should be printed per line (default: 4; always +1 if FORMAT is i or s)''', 'complete': gdb.COMPLETE_EXPRESSION }, 'unwatch': { From 023329226345714b90f52b1e842aee3c7ade8498 Mon Sep 17 00:00:00 2001 From: nwlyoc Date: Mon, 18 Dec 2023 18:15:40 +0100 Subject: [PATCH 3/3] improve address formatting for instructions and strings --- .gdbinit | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/.gdbinit b/.gdbinit index 11f3443..a554da7 100644 --- a/.gdbinit +++ b/.gdbinit @@ -1891,8 +1891,14 @@ class Memory(Dashboard.Module): self.object_size) else: elem = str(memory[rel]) - # save strlen because ANSI escape sequences are added later - elem_cont = [elem, len(elem)] + if self.object_base == 'i' or self.object_base == 's': + # instructions and strings need no padding, but address + # should be separated for formatting + addr_sep = elem.index(':') + elem_cont = [elem[addr_sep+1:], ansi(elem[:addr_sep+1], R.style_low)] + else: + # save strlen to calculate padding because ANSI is added later + elem_cont = [elem, len(elem)] # differences against the latest have the highest priority if self.latest and memory[rel] != self.latest[rel]: elem_cont[0] = ansi(elem_cont[0], R.style_selected_1) @@ -1915,12 +1921,9 @@ class Memory(Dashboard.Module): for elem_cont in elems: # instructions and strings are left aligned and on a single line if self.object_base == 'i' or self.object_base == 's': - # address is included in element, format differently - addr_sep = elem_cont[0].index(':') line += '{}{}{}'.format(' ' * separation, - ansi(elem_cont[0][:addr_sep+1], R.style_low), - elem_cont[0][addr_sep+1:]) - #line = '{}'.format(ansi(address_str, R.style_low)) + elem_cont[1], + elem_cont[0]) else: line += '{}{}{}'.format(' ' * separation, ' ' * (elem_length - elem_cont[1]), @@ -1959,7 +1962,7 @@ class Memory(Dashboard.Module): for i in range(0, len(memory), per_line_current): pad = per_line_current - len(memory[i:i + per_line_current]) raw = self.format_compute_changes(memory, i, per_line_current, False) - # TODO instruction and string format don't have fixed size, so + # instruction and string format don't have fixed size, so # address can't be calculated if self.object_base == 'i' or self.object_base == 's': line = ''