/* * vm_debug.c */ #include #include #include #include #include #include #include "apple2/apple2.h" #include "apple2/hires.h" #include "mos6502/mos6502.h" #include "mos6502/dis.h" #include "vm_debug.h" #include "vm_di.h" #include "vm_reflect.h" /* * The largest address size we can set a breakpoint for */ #define BREAKPOINTS_MAX 0x10000 /* * A table of breakpoints, arranged by address in a CPU. If * breakpoints[i] is false, then there is no breakpoint. If it is * true, then there is a breakpoint at address i. */ static bool breakpoints[BREAKPOINTS_MAX]; /* * A table of commands that we support in the debugger. This list is * printed out (in somewhat readable form) by the help/h command. */ vm_debug_cmd cmdtable[] = { { "break", "b", vm_debug_cmd_break, 1, "", "Add breakpoint at ", }, { "dblock", "db", vm_debug_cmd_dblock, 2, " ", "Disassemble a block of code", }, { "disasm", "d", vm_debug_cmd_disasm, 0, "", "Toggle disassembly", }, { "help", "h", vm_debug_cmd_help, 0, "", "Print out this list of commands", }, { "hdump", "hd", vm_debug_cmd_hdump, 2, " ", "Hex dump memory in a given region", }, { "hidump", "hid", vm_debug_cmd_hidump, 1, "", "Dump hires graphics memory to file", }, { "jump", "j", vm_debug_cmd_jump, 1, "", "Jump to for next execution", }, { "printaddr", "pa", vm_debug_cmd_printaddr, 1, "", "Print the value at memory address ", }, { "printstate", "ps", vm_debug_cmd_printstate, 0, "", "Print the machine and CPU state", }, { "quit", "q", vm_debug_cmd_quit, 0, "", "Quit the emulator", }, { "resume", "r", vm_debug_cmd_resume, 0, "", "Resume execution", }, { "step", "s", vm_debug_cmd_step, 0, "", "Execute the current opcode and break at the next", }, { "unbreak", "u", vm_debug_cmd_unbreak, 1, "", "Remove breakpoint at ", }, { "writeaddr", "wa", vm_debug_cmd_writeaddr, 2, " ", "Write at ", }, { "writestate", "ws", vm_debug_cmd_writestate, 2, " ", "Write into ", }, }; #define CMDTABLE_SIZE (sizeof(cmdtable) / sizeof(vm_debug_cmd)) /* * Return the next argument in a string passed in for input with the * debugger. All arguments are space-separated. */ char * vm_debug_next_arg(char **str) { char *tok; while ((tok = strsep(str, " "))) { if (*tok == '\0') { continue; } break; } return tok; } /* * Given a string (str), parse it into a hexadecimal value. This * function returns -1 if the string is invalid or was NULL to begin * with. */ int vm_debug_addr(const char *str) { int addr; if (str == NULL) { return -1; } // We do accept the $XX notation for hexadecimals; it's easy to do, // since we can just skip past that part of the string. if (*str == '$') { str++; } addr = strtol(str, NULL, 16); if (addr == 0 && errno == EINVAL) { return -1; } return addr; } /* * Add a breakpoint for addr */ void vm_debug_break(int addr) { if (addr < 0 || addr >= BREAKPOINTS_MAX) { return; } breakpoints[addr] = true; } /* * Remove a breakpoint for addr, if one is set */ void vm_debug_unbreak(int addr) { if (addr < 0 || addr >= BREAKPOINTS_MAX) { return; } breakpoints[addr] = false; } /* * Return true if there is a breakpoint set for addr */ bool vm_debug_broke(int addr) { if (addr < 0 || addr >= BREAKPOINTS_MAX) { return false; } return breakpoints[addr]; } /* * Remove all breakpoints that have been set for any address. This * function doesn't have a ton of functional value--it's main use is in * testing, when we destroy all breakpoints in the singleton breakpoint * array when tearing down for a given test. */ void vm_debug_unbreak_all() { memset(breakpoints, false, BREAKPOINTS_MAX); } /* * Print a debugger prompt and return a newly-allocated string * containing the contents of the input given from the user. (You * _must_ free the string that is returned when you are done with it! * Unless you're ok with memory leaks!) */ char * vm_debug_prompt() { char buf[256]; FILE *stream = (FILE *)vm_di_get(VM_OUTPUT); if (feof(stdin)) { return NULL; } fputs("erc> ", stream); if (fgets(buf, 256, stdin) == NULL) { return NULL; } // Cut off the newline character, if there is one buf[strlen(buf)-1] = '\0'; return strdup(buf); } /* * Given a string (str), execute the command with any arguments given to * it (assuming they are space-separated). */ void vm_debug_execute(const char *str) { char *tok, *ebuf, *orig; vm_debug_cmd *cmd; vm_debug_args args; orig = ebuf = strdup(str); cmd = NULL; tok = vm_debug_next_arg(&ebuf); // No input if (tok == NULL) { free(orig); return; } cmd = vm_debug_find_cmd(tok); // No command found if (cmd == NULL) { free(orig); return; } args.addr1 = 0; args.addr2 = 0; args.target = NULL; switch (cmd->nargs) { case 2: args.target = vm_debug_next_arg(&ebuf); // This _may_ be -1 if we have a string target for argument // 1, as in the writestate command args.addr1 = vm_debug_addr(args.target); args.addr2 = vm_debug_addr(vm_debug_next_arg(&ebuf)); // But if this is -1, then something went wrong if (args.addr2 == -1) { free(orig); return; } break; case 1: args.target = vm_debug_next_arg(&ebuf); args.addr1 = vm_debug_addr(args.target); break; case 0: default: break; } cmd->handler(&args); free(orig); } /* * Compare a string key (k) with a vm_debug_cmd (elem) name or abbrev * field. This is the function we use for bsearch() in * vm_debug_find_cmd(). */ static int cmd_compar(const void *k, const void *elem) { const char *key = (const char *)k; const vm_debug_cmd *cmd = (const vm_debug_cmd *)elem; if (strlen(key) < 3) { return strcmp(key, cmd->abbrev); } return strcmp(key, cmd->name); } /* * Return the cmd struct for a command that matches str, which can * either be an abbreviation (if 1 or 2 characters) or a full name (if * otherwise). If no matching cmd can be found, return NULL. */ vm_debug_cmd * vm_debug_find_cmd(const char *str) { return (vm_debug_cmd *)bsearch(str, &cmdtable, CMDTABLE_SIZE, sizeof(vm_debug_cmd), cmd_compar); } /* * Add a breakpoint at the address given in args->addr1 */ DEBUG_CMD(break) { vm_debug_break(args->addr1); } /* * Print a list of the commands we support (including this one!) */ DEBUG_CMD(help) { FILE *stream = (FILE *)vm_di_get(VM_OUTPUT); vm_debug_cmd *cmd; for (int i = 0; i < CMDTABLE_SIZE; i++) { cmd = &cmdtable[i]; fprintf(stream, "%-15s%-5s%-15s%s\n", cmd->name, cmd->abbrev, cmd->argdesc, cmd->desc); } } /* * Remove any breakpoint at the current PC and unpause execution */ DEBUG_CMD(resume) { mos6502 *cpu = (mos6502 *)vm_di_get(VM_CPU); // If we paused because of a breakpoint, then we need to clear it // before we can really keep moving. vm_debug_unbreak(cpu->PC); vm_reflect_pause(NULL); } /* * Print the state contents of the CPU and machine */ DEBUG_CMD(printstate) { apple2 *mach = (apple2 *)vm_di_get(VM_MACHINE); mos6502 *cpu = mach->cpu; FILE *out = (FILE *)vm_di_get(VM_OUTPUT); fprintf(out, "CPU: A:%02x X:%02x Y:%02x P:%02x S:%02x PC:%04x\n", cpu->A, cpu->X, cpu->Y, cpu->P, cpu->S, cpu->PC); fprintf(out, "MACH: BS:%02x CM:%02x DM:%02x MM:%02x STROBE:%02x\n", mach->bank_switch, mach->color_mode, mach->display_mode, mach->memory_mode, mach->strobe); } /* * Print the value at the address given in args->addr1 */ DEBUG_CMD(printaddr) { // FIXME: This is... too machine-specific; we need to abstract this logic mos6502 *cpu = (mos6502 *)vm_di_get(VM_CPU); FILE *stream = (FILE *)vm_di_get(VM_OUTPUT); fprintf(stream, "$%02X\n", mos6502_get(cpu, args->addr1)); } /* * Jump from the current PC to one given in args->addr1 */ DEBUG_CMD(jump) { // FIXME: same issue as for printaddr -- overall we need to refactor // vm_reflect quite a bit mos6502 *cpu = (mos6502 *)vm_di_get(VM_CPU); cpu->PC = args->addr1; } /* * Write the byte value given in args->addr2 at the address given in * args->addr1. */ DEBUG_CMD(writeaddr) { mos6502 *cpu = (mos6502 *)vm_di_get(VM_CPU); mos6502_set(cpu, args->addr1, args->addr2); } /* * Change the machine state given in args->target to the value in * args->addr2. Right now, you can only change CPU registers, but * ultimately it'd be nice to change other things like strobe, display * mode, etc. */ DEBUG_CMD(writestate) { mos6502 *cpu = (mos6502 *)vm_di_get(VM_CPU); switch (tolower(*args->target)) { case 'a': cpu->A = args->addr2; break; case 'p': cpu->P = args->addr2; break; case 's': cpu->S = args->addr2; break; case 'x': cpu->X = args->addr2; break; case 'y': cpu->Y = args->addr2; break; } } /* * Quit the program entirely */ DEBUG_CMD(quit) { exit(0); } /* * Remove the breakpoint at the given address */ DEBUG_CMD(unbreak) { vm_debug_unbreak(args->addr1); } /* * Execute the current opcode at PC in the CPU, then break at the * next opcode. */ DEBUG_CMD(step) { mos6502 *cpu = (mos6502 *)vm_di_get(VM_CPU); vm_debug_unbreak(cpu->PC); mos6502_execute(cpu); vm_debug_break(cpu->PC); } /* * Toggle disassembly, and print a notice of the change and its new * status. */ DEBUG_CMD(disasm) { apple2 *mach = (apple2 *)vm_di_get(VM_MACHINE); FILE *stream = (FILE *)vm_di_get(VM_OUTPUT); vm_reflect_disasm(NULL); fprintf(stream, "disassembly %s\n", mach->disasm ? "ON" : "OFF"); } /* * Disassemble a block of memory from one given address to another. * Useful when you want to see what memory looks like in a given region. * Note that the disassembler is a bit dumb, and can't tell what data * are opcodes is meant to be standalone binary data (the sort you would * get if you used "DFB" notation in an assembler). */ DEBUG_CMD(dblock) { if (args->addr1 > args->addr2) { return; } mos6502 *cpu = (mos6502 *)vm_di_get(VM_CPU); FILE *stream = log_stream(); mos6502_dis_scan(cpu, stream, args->addr1, args->addr2); } /* * Print a hex dump of a region of memory */ DEBUG_CMD(hdump) { if (args->addr1 > args->addr2) { return; } mos6502 *cpu = (mos6502 *)vm_di_get(VM_CPU); FILE *stream = log_stream(); vm_segment_hexdump(cpu->rmem, stream, args->addr1, args->addr2); } DEBUG_CMD(hidump) { FILE *stream; stream = fopen(args->target, "w"); if (stream == NULL) { return; } apple2 *mach = (apple2 *)vm_di_get(VM_MACHINE); apple2_hires_dump(mach, stream); fclose(stream); }