From 1095b265702684acdfd81518792f3401fc60f838 Mon Sep 17 00:00:00 2001 From: Yuri D'Elia Date: Tue, 25 May 2021 00:26:32 +0200 Subject: [PATCH] Add several low-level debugging tools --- tools/README.md | 37 +++++++++++ tools/dump_eeprom | 17 +++++ tools/dump_sram | 17 +++++ tools/elf_mem_map | 153 ++++++++++++++++++++++++++++++++++++++++++++ tools/noreset | 12 ++++ tools/update_eeprom | 54 ++++++++++++++++ 6 files changed, 290 insertions(+) create mode 100644 tools/README.md create mode 100755 tools/dump_eeprom create mode 100755 tools/dump_sram create mode 100755 tools/elf_mem_map create mode 100755 tools/noreset create mode 100755 tools/update_eeprom diff --git a/tools/README.md b/tools/README.md new file mode 100644 index 00000000..2b4c7b82 --- /dev/null +++ b/tools/README.md @@ -0,0 +1,37 @@ +# Host debugging tools for Prusa MK3 firmware + +## Tools + +### ``dump_eeprom`` + +Dump the content of the entire EEPROM using the D3 command. +Requires ``printcore`` from [Pronterface]. + +### ``dump_sram`` + +Dump the content of the entire SRAM using the D2 command. +Requires ``printcore`` from [Pronterface]. + +### ``elf_mem_map`` + +Generate a symbol table map starting directly from an ELF firmware with DWARF2 debugging information (which is the default using the stock board definition). + +When used along with a memory dump obtained from the D2 g-code, show the value of each symbol which is within the address range. + +This assumes the running firmware generating the dump and the elf file are the same. +Requires Python3 and the [pyelftools](https://github.com/eliben/pyelftools) module. + +### ``update_eeprom`` + +Given one EEPROM dump, convert the dump to update instructions that can be sent to a printer. + +Given two EEPROM dumps, produces only the required instructions needed to update the contents from the first to the second. This is currently quite crude and assumes dumps are aligned (starting from the same address or same stride). + +Optionally writes the instructions to the specified port (requires ``printcore`` from [Pronterface]). + +### ``noreset`` + +Set the required TTY flags on the specified port to avoid reset-on-connect for *subsequent* requests (issuing this command might still cause the printer to reset). + + +[Pronterface]: https://github.com/kliment/Printrun diff --git a/tools/dump_eeprom b/tools/dump_eeprom new file mode 100755 index 00000000..5e546416 --- /dev/null +++ b/tools/dump_eeprom @@ -0,0 +1,17 @@ +#!/bin/sh +prg=$(basename "$0") +port="$1" +if [ -z "$port" -o "$port" = "-h" ] +then + echo "usage: $0 " >&2 + echo "Connect to and dump the content of the EEPROM using D3 to stdout" >&2 + exit 1 +fi + +set -e +tmp=$(mktemp) +trap "rm -f \"$tmp\"" EXIT + +echo D3 > "$tmp" +printcore -v "$port" "$tmp" 2>&1 | \ + sed -ne '/^RECV: D3 /,/RECV: ok$/s/^RECV: //p' diff --git a/tools/dump_sram b/tools/dump_sram new file mode 100755 index 00000000..93ef4e92 --- /dev/null +++ b/tools/dump_sram @@ -0,0 +1,17 @@ +#!/bin/sh +prg=$(basename "$0") +port="$1" +if [ -z "$port" -o "$port" = "-h" ] +then + echo "usage: $0 " >&2 + echo "Connect to and dump the content of the SRAM using D2 to stdout" >&2 + exit 1 +fi + +set -e +tmp=$(mktemp) +trap "rm -f \"$tmp\"" EXIT + +echo D2 > "$tmp" +printcore -v "$port" "$tmp" 2>&1 | \ + sed -ne '/^RECV: D2 /,/RECV: ok$/s/^RECV: //p' diff --git a/tools/elf_mem_map b/tools/elf_mem_map new file mode 100755 index 00000000..46f1e1c1 --- /dev/null +++ b/tools/elf_mem_map @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +import argparse +import elftools.elf.elffile +import elftools.dwarf.descriptions +from struct import unpack + +SRAM_OFFSET = 0x800000 +EEPROM_OFFSET = 0x810000 +FILL_BYTE = b'\0' + + +def get_elf_globals(path): + fd = open(path, "rb") + if fd is None: + return + elffile = elftools.elf.elffile.ELFFile(fd) + if elffile is None or not elffile.has_dwarf_info(): + return + + # probably not needed, since we're decoding expressions manually + elftools.dwarf.descriptions.set_global_machine_arch(elffile.get_machine_arch()) + dwarfinfo = elffile.get_dwarf_info() + + grefs = [] + for CU in dwarfinfo.iter_CUs(): + for DIE in CU.iter_DIEs(): + # handle only variable types + if DIE.tag != 'DW_TAG_variable': + continue + if 'DW_AT_name' not in DIE.attributes: + continue + if 'DW_AT_location' not in DIE.attributes: + continue + if 'DW_AT_type' not in DIE.attributes: + continue + + # handle locations encoded directly as DW_OP_addr (leaf globals) + at_loc = DIE.attributes['DW_AT_location'] + if at_loc.form != 'DW_FORM_block1' or at_loc.value[0] != 3: + continue + loc = (at_loc.value[1]) + (at_loc.value[2] << 8) \ + + (at_loc.value[3] << 16) + (at_loc.value[4] << 24) + if loc < SRAM_OFFSET or loc >= EEPROM_OFFSET: + continue + loc -= SRAM_OFFSET + + # variable name + name = DIE.attributes['DW_AT_name'].value.decode('ascii') + + # recurse on type to find the leaf definition + type_DIE = DIE + while 'DW_AT_type' in type_DIE.attributes: + type_DIE = type_DIE.get_DIE_from_attribute('DW_AT_type') + byte_size = type_DIE.attributes.get('DW_AT_byte_size') + if byte_size is None: + continue + size = byte_size.value + + grefs.append([name, loc, size]) + + return grefs + + +def decode_dump(path): + fd = open(path, 'r') + if fd is None: + return None + + buf_addr = None # starting address + buf_data = None # data + + for line in fd: + tokens = line.split(maxsplit=1) + if len(tokens) == 0 or tokens[0] == 'ok': + break + elif len(tokens) < 2 or tokens[0] == 'D2': + continue + + addr = int.from_bytes(bytes.fromhex(tokens[0]), 'big') + data = bytes.fromhex(tokens[1]) + + if buf_addr is None: + buf_addr = addr + buf_data = data + else: + # grow buffer as needed + if addr < buf_addr: + buf_data = FILL_BYTE * (buf_addr - addr) + buf_addr = addr + addr_end = addr + len(data) + buf_end = buf_addr + len(buf_data) + if addr_end > buf_end: + buf_data += FILL_BYTE * (addr_end - buf_end) + + # replace new part + rep_start = addr - buf_addr + rep_end = rep_start + len(data) + buf_data = buf_data[:rep_start] + data + buf_data[rep_end:] + + return (buf_addr, buf_data) + + +def annotate_refs(grefs, addr, data, width=45): + for name, loc, size in grefs: + if loc < addr: + continue + if loc + size > addr + len(data): + continue + + pos = loc-addr + buf = data[pos:pos+size] + + buf_repr = '' + if len(buf) in [1, 2, 4]: + # attempt to decode as integers + buf_repr += ' I:' + str(int.from_bytes(buf, 'big')).rjust(10) + if len(buf) in [4, 8]: + # attempt to decode as floats + buf_repr += ' F:' + '{:10.3f}'.format(unpack('f', buf)[0]) + + print('{:04x} {} {:4}{} R:{}'.format(loc, name.ljust(width), size, + buf_repr, buf.hex())) + + +def print_map(grefs): + print('OFFSET\tSIZE\tNAME') + for name, loc, size in grefs: + print('{:x}\t{}\t{}'.format(loc, size, name)) + + +def main(): + ap = argparse.ArgumentParser(description=""" + Generate a symbol table map starting directly from an ELF + firmware with DWARF2 debugging information. + When used along with a memory dump obtained from the D2 g-code, + show the value of each symbol which is within the address range. + """) + ap.add_argument('elf', help='ELF file containing DWARF2 debugging information') + g = ap.add_mutually_exclusive_group(required=True) + g.add_argument('dump', nargs='?', help='RAM dump obtained from D2 g-code') + g.add_argument('--map', action='store_true', help='dump global memory map') + args = ap.parse_args() + + grefs = get_elf_globals(args.elf) + grefs = list(sorted(grefs, key=lambda x: x[1])) + if args.dump is None: + print_map(grefs) + else: + addr, data = decode_dump(args.dump) + annotate_refs(grefs, addr, data) + +if __name__ == '__main__': + exit(main()) diff --git a/tools/noreset b/tools/noreset new file mode 100755 index 00000000..3fd32416 --- /dev/null +++ b/tools/noreset @@ -0,0 +1,12 @@ +#!/bin/sh +prg=$(basename "$0") +port="$1" +if [ -z "$port" -o "$port" = "-h" ] +then + echo "usage: $0 " >&2 + echo "Set TTY flags on to avoid reset-on-connect" >&2 + exit 1 +fi + +set -e +stty -F "$port" -hup diff --git a/tools/update_eeprom b/tools/update_eeprom new file mode 100755 index 00000000..a72f9d71 --- /dev/null +++ b/tools/update_eeprom @@ -0,0 +1,54 @@ +#!/bin/sh +prg=$(basename "$0") + +# parse arguments +while getopts f:h optname +do + case $optname in + f) port="$OPTARG" ;; + *) help=1 ;; + esac +done +shift `expr $OPTIND - 1` + +old="$1" +new="$2" + +if [ -z "$old" -o "$help" = "-h" -o "$#" -gt 2 ] +then + echo "usage: $0 [-f ] []" >&2 + echo "Convert to instructions to update instructions." >&2 + echo "With , generate instructions to update EEPROM changes only." >&2 + echo "Optionally write such changes directly if if given." >&2 + exit 1 +fi + +set -e +instr=$(mktemp) +trap "rm -f \"$instr\"" EXIT + +convert() +{ + sed -ne 's/^\([0-9a-f]\{4\}\) \([0-9a-f ]*\)$/D3 Ax\1 C16 X\2/p' "$@" +} + +if [ -z "$new" ]; then + # convert the instructions to updates + convert "$old" > "$instr" +else + tmp1=$(mktemp) + tmp2=$(mktemp) + trap "rm -f \"$tmp1\" \"$tmp2\"" EXIT + + convert "$old" > "$tmp1" + convert "$new" > "$tmp2" + + comm -13 "$tmp1" "$tmp2" > "$instr" +fi + +# write the instructions if requested +if [ -z "$port" ]; then + cat "$instr" +else + printcore -v "$port" "$instr" +fi