diff options
Diffstat (limited to 'source/notes/pic-mcu/pickit2/pk2cmd-stuff/pydare.py')
-rw-r--r-- | source/notes/pic-mcu/pickit2/pk2cmd-stuff/pydare.py | 610 |
1 files changed, 610 insertions, 0 deletions
diff --git a/source/notes/pic-mcu/pickit2/pk2cmd-stuff/pydare.py b/source/notes/pic-mcu/pickit2/pk2cmd-stuff/pydare.py new file mode 100644 index 0000000..f24f69f --- /dev/null +++ b/source/notes/pic-mcu/pickit2/pk2cmd-stuff/pydare.py @@ -0,0 +1,610 @@ +import json +import importlib +import traceback +import sys +import os + +import pic16 +import pic18 +import pic24 + +running = True +DEBUG = False + +ANSI_BLUE = "\x1b[34m" +ANSI_YELLOW = "\x1b[33m" +ANSI_DARK_GREEN = "\x1b[32m" +ANSI_GREEN = "\x1b[1m\x1b[32m" +ANSI_RED = "\x1b[31m" +ANSI_RESET = "\x1b[0m" + +COLOR_REGISTER = ANSI_YELLOW +COLOR_FUNCTION = ANSI_GREEN +COLOR_LABEL = ANSI_DARK_GREEN +COLOR_ADDRESS = ANSI_DARK_GREEN + +state = { + 'running': True, + 'dirty': False +} + +def parse_parts(msg): + parts = [] + i = 0 + wordstart = 0 + word = "" + stripped = msg.strip() + + in_quotes = False + escaping = False + + while i < len(stripped): + if escaping: + # escaping means even spaces aren't the end of the word, so + word = word + stripped[i] + escaping = False + else: + if stripped[i] == ' ': + if in_quotes: + # this isn't actually the end of a word + word = word + stripped[i] + else: + parts.append(word) + word = "" + elif stripped[i] == '\\': + escaping = True + elif stripped[i] == '"': + in_quotes = not in_quotes + else: + word = word + stripped[i] + + i = i + 1 + + parts.append(word) + word = "" + + # todo: signal unmatched quotes + + return parts + +def readnum(s): + if s.startswith("0x"): + return int(s[2:], 16) + else: + return int(s) + +def parse_cmd(cmd): + cmd_parts = parse_parts(cmd) + smol_to_big = { + "o": "open", + "c": "define-comment", + "c+": "define-comment", + "q": "quit", + "d": "disassemble", + "h": "help" + } + + result = {} + + if len(cmd_parts) > 0: + result['type'] = cmd_parts[0] + if result['type'] in smol_to_big: + result['type'] = smol_to_big[result['type']] + + result['params'] = cmd_parts[1:] + result['raw_text'] = cmd + + if len(cmd_parts) > 1 and cmd_parts[-1][0] == '@': + result['where'] = resolve_addr(cmd_parts[-1][1:], state) + elif len(cmd_parts) > 2 and cmd_parts[-2] == '@': + result['where'] = resolve_addr(cmd_parts[-1], state) + + if 'where' in result and result['where'] is None: + result['invalid'] = True + + return result + +def resolve_addr(where, state): + # where may be the name of a label or function + if where in state['notes']['functions-reverse']: + return state['notes']['functions-reverse'][where] + elif where in state['notes']['labels-reverse']: + return state['notes']['labels-reverse'][where] + else: + # if it's not there, it better be a number... + try: + return readnum(where) + except ValueError: + print("{} is not a known function or label.".format(where)) + +def do_help(cmd, state): + print("haha, help") + +def do_open(cmd, state): + f = open(cmd['params'][0]) + buf = f.read(6) + f.close() + if buf == ":02000": + return do_openhex(cmd, state) + else: + return do_openbin(cmd, state) + +def do_openbin(cmd, state): + return inneropen(cmd, state, lambda x: { 0: x.read() }) + +def do_openhex(cmd, state): + return inneropen(cmd, state, lambda x: read_hex(x)) + +def read_hex(f): + lines = f.readlines() + regions = { 0: [] } + curr_region = regions[0] + + for line in lines: + if not line.startswith(':'): + continue +# raise Exception("invalid hex line, needs to start with a ':', but was: " + line) + bytecount = int(line[1:3], 16) + addr = int(line[3:7], 16) + rec_type = int(line[7:9], 16) + data_end = bytecount * 2 + 9 + data = line[9:data_end] + # ignoring checksum because lazy + + if rec_type == 4: + ext_linear_addr = int(data, 16) + if ext_linear_addr not in regions: + regions[ext_linear_addr] = [] + + curr_region = regions[ext_linear_addr] + elif rec_type == 0: + for i in range(bytecount): + if len(curr_region) <= addr + i: + curr_region.extend([0] * (1 + addr + i - len(curr_region))) + curr_region[addr + i] = int(data[i*2:i*2 + 2], 16) + elif rec_type == 1: + if DEBUG: + print("Read HEX file ({} regions)".format(len(regions))) + return regions + else: + raise Exception("Unsupported record type: " + str(rec_type)) + +def do_goto(cmd, state): + try: + dest = int(cmd['params'][0]) + except ValueError: + # parse error, might be a function or label! + # labels first... + name = cmd['params'][0] + if name in state['notes']['labels-reverse']: + dest = state['notes']['labels-reverse'][name] + if name in state['notes']['functions-reverse']: + dest = state['notes']['functions-reverse'][name] + state['cursor'] = int(cmd['params'][0]) + +def do_goto_bank(cmd, state): + param = cmd['params'][0] + if not param in state['all-regions']: + state['all-regions'][param] = [] + + state['data'] = state['all-regions'][param] + +def inneropen(cmd, state, readfn): + newfile = cmd['params'][0] + if os.path.isfile(newfile): + newdata = None + try: + f = open(newfile) + newdata = readfn(f) + f.close() + except Exception as e: + print("Failed to open new file: {}".format(newfile)) + print("Got: {}".format(e)) + print(traceback.format_exc()) + return + state['notes'] = { + 'comments': {}, + 'functions': {}, + 'functions-reverse': {}, + 'labels': {}, + 'labels-reverse': {} + } + state['cursor'] = 0 + state['file'] = newfile + state['all-regions'] = newdata + state['selected-region'] = newdata.keys()[0] + state['data'] = newdata[state['selected-region']] + elif os.path.isdir(newfile): + print("Cannot open {}, it is a directory".format(newfile)) + else: + print("File {} does not exist".format(newfile)) + + if 'arch-name' not in state or state['arch-name'] is None: + # default arch to the best cpu, pic16 + do_setarch({ 'params': ['pic16'] }, state) + + if DEBUG: + print("Opened {} as {}".format(newfile, state['arch-name'])) + +def do_comment(cmd, state): + state['notes']['comments'][cmd['where']] = cmd['params'][0] + state['dirty'] = True + +def new_function(name): + return { + "name": name, + "params": None, + "returns": None + } + +def do_undefine_function(cmd, state): + name = cmd['params'][0] + if cmd['params'][0] in state['notes']['functions-reverse']: + fnaddr = state['notes']['functions-reverse'][cmd['params'][0]] + del state['notes']['functions'][fnaddr] + del state['notes']['functions-reverse'][name] + return + else: + print("Function {} is not defined".format(name)) + +def do_define_function(cmd, state): + if cmd['params'][0] in state['notes']['functions-reverse']: + fnaddr = state['notes']['functions-reverse'][cmd['params'][0]] + if fnaddr != cmd['where']: + print("Function {} is already defined at {}".format(cmd['params'][0], fnaddr)) + return + else: + newname = cmd['params'][0] + fn = state['notes']['functions'][cmd['where']] + del state['notes']['functions-reverse'][fn['name']] + state['notes']['functions-reverse'][newname] = fn + fn['name'] = newname + else: + state['notes']['functions'][cmd['where']] = new_function(cmd['params'][0]) + state['notes']['functions-reverse'][cmd['params'][0]] = cmd['where'] + state['dirty'] = True + +def do_define_label(cmd, state): + state['notes']['labels'][cmd['where']] = cmd['params'][0] + state['notes']['labels-reverse'][cmd['params'][0]] = cmd['where'] + state['dirty'] = True + +def colorize(string, color): + return "{}{}{}".format( + color, + string, + ANSI_RESET + ) + +def do_disassemble(cmd, state): + if 'data' not in state: + print("No file currently open") + return + if 'cursor' not in state: + print("No cursor into the file (this is a bug - a file must be open?") + return + + if 'where' in cmd: + where = cmd['where'] + else: + where = state['cursor'] + + if len(cmd['params']) > 0: + count = int(cmd['params'][0]) + else: + count = 1 + + arch = state['arch'] + + try: + disassembled = 0 + while disassembled < count: + prewhere = where + (where, instr) = arch.disassemble(state['data'], where) + if 'ops' in instr: + newops = instr['ops'] + for i, op in enumerate(instr['ops']): + if isinstance(op, dict) and 'type' in op: + note = None + if op['type'] == "absolutedest": + if op['value'] in state['notes']['functions']: + note = state['notes']['functions'][op['value']]['name'] + note = colorize(note, COLOR_FUNCTION) + elif op['value'] in state['notes']['labels']: + note = state['notes']['labels'][op['value']] + note = colorize(note, COLOR_LABEL) + else: + note = colorize(hex(op['value']), COLOR_ADDRESS) + elif op['type'] == "relpostdest": + # if there's a thing there replace with it + newops[i] = "relpostdest" + ea = prewhere + instr['length'] * 2 + op['value'] + if ea in state['notes']['functions']: + note = state['notes']['functions'][ea]['name'] + note = colorize(note, COLOR_FUNCTION) + elif ea in state['notes']['labels']: + note = state['notes']['labels'][ea] + note = colorize(note, COLOR_LABEL) + else: + note = "0x{:x} (ip+2{}{})".format( + ea, + "+" if op['value'] >= 0 else "", + hex(op['value']) + ) + note = colorize(note, COLOR_ADDRESS) + elif op['type'] == "relpredest": + pass + elif op['type'] == "register" or op['type'] == "banked-register": + nicename = arch.reg_name(op['value']) + if nicename is None: + if isinstance(op['value'], str): + note = op['value'] + else: + note = hex(op['value']) + else: + note = nicename + + note = colorize(note, COLOR_REGISTER) + + if not note is None: + newops[i] = str(note) + + instr['ops'] = newops + + prefix = None + if prewhere in state['notes']['functions']: + fn = state['notes']['functions'][prewhere] + prefix = colorize( + "{:08x}> start of {}\n".format(prewhere, fn['name']), + COLOR_FUNCTION + ) + elif prewhere in state['notes']['labels']: + label = state['notes']['labels'][prewhere] + prefix = colorize( + "{:08x}: {}\n".format(prewhere, label), + COLOR_LABEL + ) + instrstring = arch.render(instr) + to_show = "{:08x}: {}".format(prewhere, instrstring) + if prewhere in state['notes']['comments']: + to_show = "{: <35} {}; {}{}".format( + to_show, + ANSI_BLUE, + state['notes']['comments'][prewhere], + ANSI_RESET + ) + if not prefix is None: + to_show = prefix + to_show + print(to_show) + disassembled = disassembled + 1 + except Exception as e: + print("Exception while disassembling: {}".format(e)) + print(traceback.format_exc()) + +def do_setarch(cmd, state): + try: + loaded_arch = importlib.import_module(cmd['params'][0]) + except ImportError: + print("Cannot find module for arch '{}'".format(cmd['params'][0])) + except Exception as e: + print("General error loading '{}': {}".format(cmd['params'][0], e)) + + state['arch-name'] = cmd['params'][0] + state['arch'] = loaded_arch + +def dict_from_file(path): + f = open(path) + result = json.loads(f.read()) + f.close() + return result + +def dict_to_file(path, data): + f = open(path, 'w') + f.write(json.dumps(data, sort_keys=True, indent=2)) + f.close() + +# so json.dumps turns numeric keys into strings. +# this breaks some dictionaries. +def fix_keys(obj): + redo = [] + for k in obj: + try: + knum = int(k) + redo.append(k) + except ValueError: + pass + + for k in redo: + knum = int(k) + obj[knum] = obj[k] + del obj[k] + +def do_loaddb(cmd, state): + if len(cmd['params']) == 0: + if 'file' in state: + dbpath = state['file'] + '.nrt' + else: + print("No db path provided nor file loaded - I don't know what to load!") + return + else: + dbpath = cmd['params'][0] + if os.path.isdir(dbpath) and os.path.isdir(dbpath + '/.git'): + if DEBUG: + print("Loading {} ...".format(dbpath)) + try: + dbroot = dict_from_file(dbpath + '/root.json') + do_setarch({ "params": [dbroot['arch-name']] }, state) + state['notes']['comments'] = dict_from_file(dbpath + '/comments.json') + fix_keys(state['notes']['comments']) + state['notes']['functions'] = dict_from_file(dbpath + '/functions.json') + state['notes']['functions-reverse'] = dict_from_file(dbpath + '/functions-reverse.json') + fix_keys(state['notes']['functions']) + state['notes']['labels'] = dict_from_file(dbpath + '/labels.json') + state['notes']['labels-reverse'] = dict_from_file(dbpath + '/labels-reverse.json') + fix_keys(state['notes']['labels']) + state['default-dbpath'] = dbpath +# check db file path matches current file path? + if DEBUG: + print("wow you're really using this, huh?") + except Exception as e: + print("Error loading db: {}".format(e)) + elif os.path.isdir(dbpath): + print("Cannot load {}, there is no repository there".format(dbpath)) + elif os.path.isfile(dbpath): + print("Cannot load {}, it is a file".format(dbpath)) + else: + print("Cannot load {}, it does not exist".format(dbpath)) + +def do_savedb(cmd, state): + if len(cmd['params']) > 0: + dbpath = cmd['params'][0] + elif 'default-dbpath' in state: + dbpath = state['default-dbpath'] + else: + dbpath = state['file'] + '.nrt' + + if os.path.isfile(dbpath): + print("File is present, but should be a directory. Cannot save.") + return + elif os.path.isdir(dbpath): + if not os.path.isdir(dbpath + '/.git'): + print("dbpath is a directory, but there is no git repo there. Opting to not save.") + return + else: + # dbpath exists, and there's a git repo there, we can proceed + pass + else: + # none of the directories exist, so we can start fresh + os.mkdir(dbpath) + # TODO: pray the input filename doesn't have a ' in it i guess + os.system('cd \'{}\' && git init'.format(dbpath)) + + to_save = {} + to_save['arch-name'] = state['arch-name'] + dict_to_file(dbpath + '/root.json', to_save) + dict_to_file(dbpath + '/comments.json', state['notes']['comments']) + dict_to_file(dbpath + '/functions.json', state['notes']['functions']) + dict_to_file(dbpath + '/functions-reverse.json', state['notes']['functions-reverse']) + dict_to_file(dbpath + '/labels.json', state['notes']['labels']) + dict_to_file(dbpath + '/labels-reverse.json', state['notes']['labels-reverse']) + os.system('cd \'{}\' && git add . && git commit -m "automatic save"'.format(dbpath)) + state['dirty'] = False + +def do_quit(cmd, state): + # prompt before saving + if not 'dirty' in state or state['dirty']: + do_savedb({ "params": [] }, state) + state['running'] = False + +def do_sh(cmd, state): + if len(cmd['params']) == 1: + os.system(cmd['params'][0]) + else: + print("sh expects exactly one argument (may be a string)") + +def do_info(cmd, state): + if 'file' in state: + print("File: {}".format(state['file'])) + print("Regions:") + keys = state['all-regions'].keys() + keys.sort() + for key in keys: + print(" {}{}: {} bytes".format( + '*' if key == state['selected-region'] else ' ', + key, + hex(len(state['all-regions'][key])) + )) + +def readnum(string): + if string.startswith('0x'): + return int(string, 16) + else: + return int(string) + +def do_list_comments(cmd, state): + print("Comments:") + for l in state['notes']['comments']: + print("{}: 0x{:x}".format(state['notes']['comments'][l], l)) + +def do_list_functions(cmd, state): + print("Functions:") + for l in state['notes']['functions']: + print("{}: 0x{:x}".format(state['notes']['functions'][l]['name'], l)) + +def do_list_labels(cmd, state): + print("Labels:") + for l in state['notes']['labels']: + print("{}: 0x{:x}".format(state['notes']['labels'][l], l)) + +def do_hexprint(cmd, state): + count = readnum(cmd['params'][0]) + start = cmd['where'] + idx = start + + while count > idx - start: + sys.stdout.write("{:08x}: ".format(idx)) + for i in range(16): + if count <= idx - start: + break + sys.stdout.write("{:02x} ".format(state['data'][idx]),) + idx = idx + 1 + sys.stdout.write('\n') + +cmdmap = { + "px": do_hexprint, + "define-comment": do_comment, + "define-function": do_define_function, + "undefine-function": do_undefine_function, + "define-label": do_define_label, + "list-comments": do_list_comments, + "list-functions": do_list_functions, + "list-labels": do_list_labels, + "open": do_open, + "openhex": do_openhex, + "arch": do_setarch, + "disassemble": do_disassemble, + "loaddb": do_loaddb, + "savedb": do_savedb, + "quit": do_quit, + "help": do_help, + "info": do_info, + "goto": do_goto, + "goto-bank": do_goto_bank, + "sh": do_sh +} + +def do_cmd_thing(cmd, state): + if 'invalid' in cmd: + pass # can't do anything with it. + elif cmd['type'] in cmdmap: + cmdmap[cmd['type']](cmd, state) + else: + print("I don't recognize the command `{}`".format(cmd['raw_text'])) + +for i, arg in enumerate(sys.argv): + # just the path of this script + # i bet that only applies because i'm running as + # `python pydare.py ...` + if i == 0: + continue + do_cmd_thing(parse_cmd(arg), state) + +# if len(sys.argv) > 1: +# do_open({"params": [sys.argv[1]]}, state) +# if len(sys.argv) > 2: +# do_setarch({"params": [sys.argv[2]]}, state) + +import readline +readline.parse_and_bind("") +if os.path.isfile(".pydare_history"): + readline.read_history_file(".pydare_history") + +while state['running']: + try: + do_cmd_thing(parse_cmd(raw_input("> ")), state) + except Exception as e: + print("Unhandled exception: {}".format(e)) + print(traceback.format_exc()) + +if not os.path.isdir(".pydare_history"): + readline.write_history_file(".pydare_history") |