/* * A CLI-based tester and debugger for the Gameboy's LR35902 CPU */ #include #include #include #include #include #include #include #include #include #include #include #include #include "common/common.h" #include "gb_disas.h" #include "gbemu/cpu.h" #include "gbemu/video.h" #include "common/tri.h" #define INPUT_MAX_LEN 512 #define MAX_BREAKPTS 16 /* Should be plenty for anyone */ typedef void (gbdb_cmd)(struct tokens **tokens); static void breakpoint_addr_hit(uint16_t id); static void breakpoint_cb(struct lr35902_state *lr); static void reset_breakpoints(void); static int64_t parse_val(const char *str); static const char prompt[] = "gbdb >"; static const char usage[] = "Available commands:\n" "load : loads the given file as the gameboy cartridge\n" "run: runs the CPU until a breakpoint or halt is hit\n" "break : Adds a breakpoint for the given addess\n" "step : executes \"cycle\" instructions (default 1)\n" "regs: dumps the state of the registers\n" "disas: view the next instruction to run\n" "exit: quit the program\n"; static struct lr35902_ops cpu_ops; static struct lr35902_state cpu; static struct gb_video video; static void *memory = NULL; static int paused_breakpoint = 0; static volatile sig_atomic_t paused_signal = 0; static volatile sig_atomic_t paused = 0; static struct { bool quiet; int log_fd; int trace_fd; } config; void gb_log(const char *fmt, ...) { va_list args, args_copy; va_start(args, fmt); va_copy(args_copy, args); if (!config.quiet) { vprintf(fmt, args); } va_end(args); if (config.log_fd != -1) { vdprintf(config.log_fd, fmt, args_copy); } } void gb_error(const char *fmt, ...) { va_list args, args_copy; va_start(args, fmt); va_copy(args_copy, args); vprintf(fmt, args); va_end(args); if (config.log_fd != -1) { vdprintf(config.log_fd, fmt, args_copy); } } static void break_execution_handler(int signum) { paused = 1; } static void strip_newline(char *string) { char *pos; if (string == NULL) return; if ((pos=strchr(string, '\n')) != NULL) { *pos = '\0'; } } static void init(void) { cpu_ops.undef_d3 = breakpoint_cb; lr35902_init(&cpu, memory, &cpu_ops); gb_video_init(&video, memory); gb_mem_init(memory, &video); config.log_fd = -1; config.trace_fd = -1; } static void print_trace(void) { if (config.trace_fd > -1) { uint8_t instr = gb_mem_read(memory, cpu.pc); dprintf(config.trace_fd, "A:%02X F:%c%c%c%c BC:%04X DE:%04x HL:%04x SP:%04x PC:%04x 0x%02x", cpu.a, (cpu.zf) ? 'Z' : '-', (cpu.nf) ? 'N' : '-', (cpu.hf) ? 'H' : '-', (cpu.cf) ? 'C' : '-', cpu.bc, cpu.de, cpu.hl, cpu.sp, cpu.pc, instr); if (instr == 0xcb) { dprintf(config.trace_fd, " 0x%02x", gb_mem_read(memory, cpu.pc + 1)); } dprintf(config.trace_fd, "\n"); } } static void cycle() { int cycles; print_trace(); cycles = lr35902_cycle(&cpu); gb_video_cycle(&video, cycles); } static void show_prompt() { printf("%s ", prompt); fflush(stdout); } static void call_next_cmd(struct tokens** tokens, struct tri *commands, const char *cmdname) { char * remainder; gbdb_cmd *cmd; bool ambiguous = false; char *token = token_next(tokens); if (!token) { return; } cmd = tri_get_string_autocomplete(commands, token, &ambiguous); if (ambiguous) { gb_error("ambiguous %s command: '%s'\n", cmdname, token); return; } if (cmd == NULL) { gb_error("unrecognized %s command: '%s'\n", cmdname, token); return; } cmd(tokens); } static void set_logfile(struct tokens **tokens) { char *token = token_next(tokens); int fd; if (token == NULL || strcmp(token, "") == 0) { if (config.log_fd != -1) { close(config.log_fd); } config.log_fd = -1; return; } fd = open(token, O_WRONLY | O_TRUNC | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); if (fd < 0) { gb_error("Failed to open logfile %s, %s\n", token, strerror(errno)); return; } if (config.log_fd != -1) { close(config.log_fd); } config.log_fd = fd; } static void set_tracefile(struct tokens **tokens) { char *token = token_next(tokens); int fd; if (token == NULL || strcmp(token, "") == 0) { if (config.trace_fd >= -1) { close(config.trace_fd); } config.trace_fd = -1; return; } fd = open(token, O_WRONLY | O_TRUNC | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); if (fd < 0) { gb_error("Failed to open tracefile %s, %s\n", token, strerror(errno)); return; } if (config.trace_fd != -1) { close(config.trace_fd); } config.trace_fd = fd; gb_log("Trace fd=%d\n", config.trace_fd); } static void set_quiet_on(struct tokens **tokens) { config.quiet = true; } static void set_quiet_off(struct tokens **tokens) { config.quiet = false; } static void set_quiet(struct tokens **tokens) { static bool init = false; static struct tri commands; if (!init) { tri_init(&commands); tri_add_string(&commands, "on", set_quiet_on); tri_add_string(&commands, "true", set_quiet_on); tri_add_string(&commands, "off", set_quiet_off); tri_add_string(&commands, "false", set_quiet_off); init = true; } call_next_cmd(tokens, &commands, "set quiet"); } static void set_video_debug(struct tokens **tokens) { video.debug_logging = true; } static void set(struct tokens **tokens) { static bool init = false; static struct tri commands; if (!init) { tri_init(&commands); tri_add_string(&commands, "quiet", set_quiet); tri_add_string(&commands, "logfile", set_logfile); tri_add_string(&commands, "trace", set_tracefile); tri_add_string(&commands, "video_debug", set_video_debug); init = true; } call_next_cmd(tokens, &commands, "set"); } static void echo(struct tokens **tokens) { char *token; while ((token = token_next(tokens)) != NULL) { gb_log("%s ", token); } gb_log("\n"); } static void breakpoint_cb(struct lr35902_state *lr) { int i; paused_breakpoint = 1; paused = 1; breakpoint_addr_hit(cpu.pc); } static void step(struct tokens **tokens) { uint64_t steps, end_steps; char *token; token = token_next(tokens); if (token == NULL) { steps = 1; } else { steps = strtol(token, NULL, 0); } paused = 0; signal(SIGINT, break_execution_handler); end_steps = cpu.metrics.retired_instrs + steps; cycle(); reset_breakpoints(); while(end_steps > cpu.metrics.retired_instrs && !paused) { cycle(); } if (end_steps <= cpu.metrics.retired_instrs) { gb_log("CPU stopped after %" PRId64 " instructions\n", steps); } else if (paused_breakpoint) { gb_log("Breakpoint hit\n"); } else if (paused_signal){ gb_log("Interrupted\n"); } paused = 0; paused_signal = 0; paused_breakpoint = 0; } static void regs(struct tokens **tokens) { uint8_t f = (cpu.nf << CPU_F_BIT_POS_N) | (cpu.zf << CPU_F_BIT_POS_Z) | (cpu.cf << CPU_F_BIT_POS_C) | (cpu.hf << CPU_F_BIT_POS_H); gb_log("AF: 0x%04x A: 0x%02x F: 0x%02x\n", (uint16_t) (cpu.a << 8) | f, cpu.a, f); gb_log("BC: 0x%04x B: 0x%02x C: 0x%02x\n", cpu.bc, cpu.b, cpu.c); gb_log("DE: 0x%04x D: 0x%02x E: 0x%02x\n", cpu.de, cpu.d, cpu.e); gb_log("HL: 0x%04x H: 0x%02x L: 0x%02x\n", cpu.hl, cpu.h, cpu.l); gb_log("PC: 0x%04x\n", cpu.pc); gb_log("SP: 0x%04x\n", cpu.sp); } static uint8_t decode_fetch_mem(void *opaque, uint16_t addr) { return gb_mem_read(memory, addr); } static void disas(struct tokens **tokens) { uint16_t pc = lr35902_get_reg_16(&cpu, LR35902_REG_PC); uint16_t old_pc = pc; const char *disas_cnt_str; int64_t num_disas, i; if (tokens) { disas_cnt_str = token_next(tokens); if (disas_cnt_str == NULL) { num_disas = 1; } else { num_disas = parse_val(disas_cnt_str); } } else { num_disas = 1; } for (i = 0; i < num_disas; i++) { const char *opcode; old_pc = pc; opcode = gb_byte_to_opcode(&pc, decode_fetch_mem, &memory); gb_log("0x%04x:%s\n", old_pc, opcode); free((void *) opcode); } } static void vstep(struct tokens **tokens) { disas(NULL); step(tokens); regs(NULL); } static void stats(struct tokens **tokens) { gb_log("Cycles: %" PRId64 "\n", cpu.metrics.cycles); gb_log("Retired Insructions %" PRId64 "\n", cpu.metrics.retired_instrs); gb_log("Memory Reads: %" PRId64 " \n", cpu.metrics.mem_reads); gb_log("Memory Writes: %" PRId64 "\n", cpu.metrics.mem_writes); } static void comment(struct tokens **tokens) { /* Gobble up everything */ do {} while (token_next(tokens)); } static void help(struct tokens **tokens) { gb_log(usage); } static int64_t parse_reg_str(const char *str) { int i; uint8_t f = (cpu.nf << CPU_F_BIT_POS_N) | (cpu.zf << CPU_F_BIT_POS_Z) | (cpu.cf << CPU_F_BIT_POS_C) | (cpu.hf << CPU_F_BIT_POS_H); const struct { const char *str; int64_t value; } entries[] = { { "af", (cpu.a << 8) | f }, { "bc", cpu.bc }, { "de", cpu.de }, { "sp", cpu.sp }, { "pc", cpu.pc }, { "hl", cpu.hl }, { "a", cpu.a }, { "b", cpu.b }, { "c", cpu.c }, { "d", cpu.d }, { "e", cpu.e }, { "f", f }, }; for (i = 0; i < ARRAY_SIZE(entries); i++) { if (strncmp(str, entries[i].str, strlen(entries[i].str)) == 0) { return entries[i].value; } } return 0; } static int64_t parse_val(const char *str) { if (str[0] == '$') { return parse_reg_str(str + 1); } return strtoll(str, NULL, 0); } static void assert(struct tokens **tokens) { char *val0_str = NULL, *val1_str = NULL, *operator_str = NULL; const char *usage = "usage: assert \n"; int64_t val0, val1; val0_str = strdup(token_next(tokens)); if (val0_str == NULL) { gb_error("%s", usage); return; } val0 = parse_val(val0_str); operator_str = strdup(token_next(tokens)); if (operator_str == NULL) { gb_error("%s", usage); return; } val1_str = strdup(token_next(tokens)); if (val1_str == NULL) { gb_error("%s", usage); return; } val1 = parse_val(val1_str); if (strcmp(operator_str, "==") == 0) { if (!(val0 == val1)) { goto fail; } } else if (strcmp(operator_str, "!=") == 0) { if (!(val0 != val1)) { goto fail; } } else if (strcmp(operator_str, ">=") == 0) { if (!(val0 >= val1)) { goto fail; } } else if (strcmp(operator_str, "<=") == 0) { if (!(val0 <= val1)) { goto fail; } } else if (strcmp(operator_str, "<") == 0) { if (!(val0 < val1)) { goto fail; } } else if (strcmp(operator_str, ">") == 0) { if (!(val0 > val1)) { goto fail; } } else { gb_error("%s", usage); } return; fail: gb_error("ASSERT: %s %s %s\n", val0_str, operator_str, val1_str); gb_error("%s=0x%" PRIx64 ", %s=0x%" PRIx64 "\n", val0_str, val0, val1_str, val1); free(val0_str); free(val1_str); free(operator_str); regs(NULL); stats(NULL); exit(1); } static void mem(struct tokens **tokens) { uint16_t addr, bytes, addr_end; char *token = token_next(tokens); int i; if (token == NULL) { gb_error("usage: mem (num bytes) (format)\n"); return; } addr = parse_val(token); token = token_next(tokens); if (token == NULL) { bytes = 1; } else { bytes = parse_val(token); } for (i = 0; i < bytes; i++) { //TODO: Make sure this has no side effects gb_log("0x%04x: 0x%02x\n", addr + i, gb_mem_read(memory, addr + i)); } } static void load(struct tokens **tokens) { char *token = token_next(tokens); if (token == NULL) { gb_error("usage: load \n"); return; } gb_log("loading %s\n", token); gb_memory_load_file(memory, GB_MEMORY_CART, token); } static void load_bootrom(struct tokens **tokens) { char *token = token_next(tokens); if (token == NULL) { gb_error("usage: bootrom \n"); return; } gb_memory_load_file(memory, GB_MEMORY_BOOTROM, token); } static void quit(struct tokens **tokens) { exit(0); } static __attribute__((hot)) void do_run(void) { paused = 0; signal(SIGINT, break_execution_handler); cycle(); reset_breakpoints(); while(!paused) { cycle(); } if (paused_signal) { gb_log("Interrupted.\n"); } else if (paused_breakpoint) { gb_log("Breakpoint hit\n"); } paused = 0; paused_breakpoint = 0; paused_signal = 0; } static void run(struct tokens **tokens ) { do_run(); } static struct breakpoint { uint16_t addr; int id; bool temp; uint8_t op; } breakpoints[MAX_BREAKPTS]; static int num_breakpoints = 0; static struct breakpoint *get_breakpoint(int id) { int i; for (i = 0; i < num_breakpoints; i++) { if (breakpoints[i].id == id) { return &breakpoints[i]; } } return NULL; } static void reset_breakpoints(void) { int i; for (i = 0; i < num_breakpoints; i++) { struct breakpoint *bkpt = &breakpoints[i]; gb_mem_force_write(memory, bkpt->addr, 0xd3); } } static int do_set_breakpoint(uint16_t addr, bool temp) { static int id = 0; if (num_breakpoints < ARRAY_SIZE(breakpoints)) { breakpoints[num_breakpoints].addr = addr; breakpoints[num_breakpoints].op = gb_mem_read(memory, addr); breakpoints[num_breakpoints].id = id++; breakpoints[num_breakpoints].temp = temp; gb_mem_force_write(memory, addr, 0xd3); num_breakpoints++; } else { gb_error("maximum number of breakpoints reached\n"); } } static void set_breakpoint(struct tokens **tokens) { uint16_t addr, i; char *token = token_next(tokens); if (token == NULL) { gb_error("usage: breakpoint add \n"); return; } addr = parse_val(token); do_set_breakpoint(addr, false); } static void runto(struct tokens **tokens) { uint16_t addr, i, rc; char *token = token_next(tokens); if (token == NULL) { gb_error("usage: runto \n"); return; } addr = parse_val(token); rc = do_set_breakpoint(addr, true); if (rc < 0) { gb_error("failed to set breakpoint\n"); return; } do_run(); } static int do_delete_breakpoint(int id) { struct breakpoint *bkpt = get_breakpoint(id); int index; if (bkpt == NULL) { return -1; } gb_mem_force_write(memory, bkpt->addr, bkpt->op); index = bkpt - breakpoints; memmove(&breakpoints[index], &breakpoints[index + 1], num_breakpoints - index - 1); num_breakpoints--; return 0; } static void breakpoint_addr_hit(uint16_t addr) { struct breakpoint *bkpt = NULL; int i; for (i = 0; i < num_breakpoints; i++) { if (breakpoints[i].addr == addr) { bkpt = &breakpoints[i]; break; } } if (bkpt == NULL) { gb_error("No breakpoint found at addr=%d\n", addr); return; } if (bkpt->temp) { do_delete_breakpoint(bkpt->id); } } static void delete_breakpoint(struct tokens **tokens) { char *token = token_next(tokens); int rc, bkpt; if (token == NULL) { gb_error("usage: breakpoint rm \n"); return; } bkpt = parse_val(token); rc = do_delete_breakpoint(bkpt); if (rc < 0) { gb_error("%d is not a valid breakpoint number\n", bkpt); } } static void display_breakpoints(struct tokens **tokens) { int i; if (num_breakpoints) { for (i = 0; i < num_breakpoints; i++) { gb_log("#%d: 0x%04x\n", breakpoints[i].id, breakpoints[i].addr); } } else { gb_log("No breakpoints set\n"); } } static void breakpoint(struct tokens **tokens) { static bool init = false; static struct tri commands; if (!init) { memset(breakpoints, 0, sizeof(breakpoints)); tri_init(&commands); tri_add_string(&commands, "add", set_breakpoint); tri_add_string(&commands, "set", set_breakpoint); tri_add_string(&commands, "delete", delete_breakpoint); tri_add_string(&commands, "remove", delete_breakpoint); tri_add_string(&commands, "info", display_breakpoints); tri_add_string(&commands, "display", display_breakpoints); tri_add_string(&commands, "list", display_breakpoints); init = true; } call_next_cmd(tokens, &commands, "breakpoint"); } static bool breakpoint_is_at_addr(uint16_t addr) { int i; for (i = 0; i < num_breakpoints; i++) { if (breakpoints[i].addr == addr) { return true; } } return false; } static void process_cmd(const char *cmd_str, struct tri *commands) { struct tokens *statements; struct tokens *tokens; char *statement, *token; statements = tokenize(cmd_str, ";\n"); if (!statements) { return; } while ((statement = token_next(&statements)) != NULL) { tokens = tokenize(statement, " "); if (!tokens) { continue; } call_next_cmd(&tokens, commands, ""); /* Ensure there are no leftover tokens to leak */ do {} while (token_next(&tokens)); } } static int load_initfile(const char *initfile_path, struct tri *commands) { FILE* file; char buffer[INPUT_MAX_LEN]; file = fopen(initfile_path, "r"); if (file == NULL) { return -errno; } while(fgets(buffer, sizeof(buffer), file)) { process_cmd(buffer, commands); } fclose(file); return 0; } int main(int argc, char **argv) { struct tri commands; char line_buffer[INPUT_MAX_LEN]; /* Buffer for input command */ char old_buffer[INPUT_MAX_LEN]; /* Buffer for previous input */ line_buffer[0] = '\0'; old_buffer[0] = '\0'; tri_init(&commands); tri_add_string(&commands, "#", comment); tri_add_string(&commands, "step", step); tri_add_string(&commands, "vstep", vstep); tri_add_string(&commands, "run", run); tri_add_string(&commands, "regs", regs); tri_add_string(&commands, "stats", stats); tri_add_string(&commands, "exit", quit); tri_add_string(&commands, "quit", quit); tri_add_string(&commands, "load", load); tri_add_string(&commands, "bootrom", load_bootrom); tri_add_string(&commands, "mem", mem); /* FIXME */ /* tri_add_string(&commands, "dump", mem_dump); */ tri_add_string(&commands, "disassemble", disas); tri_add_string(&commands, "help", help); tri_add_string(&commands, "?", help); tri_add_string(&commands, "breakpoint", breakpoint); tri_add_string(&commands, "runto", runto); tri_add_string(&commands, "reset", init); tri_add_string(&commands, "assert", assert); tri_add_string(&commands, "set", set); tri_add_string(&commands, "echo", echo); init(); if (argc > 1) { if (load_initfile(argv[1], &commands)) { gb_error("Failed to load initfile\n"); } } while (1) { /* String to use for input (line_buffer or * old_buffer) */ char *cmd_str; show_prompt(); cmd_str = fgets(line_buffer, INPUT_MAX_LEN, stdin); if (cmd_str == NULL) { printf("\n"); return 0; } if (line_buffer[0] != '\n') { cmd_str = line_buffer; strncpy(old_buffer, line_buffer, INPUT_MAX_LEN); } else { cmd_str = old_buffer; } process_cmd(cmd_str, &commands); } if (config.log_fd != -1) { close(config.log_fd); } if (config.trace_fd != -1) { close(config.log_fd); } return 0; }