Files
gb-emu/src/apps/gbdb.c
Max Regan c4ded6d077 cpu: get blarg CPU instruction test 6 passing
Its been too long since a checkin, but here's some of the
improvements:

- Support for diffing with other emulators
- Better disassmbed output
- New CPU instructions implemented
- Lots of CPU fixes
2018-08-26 22:57:35 -07:00

915 lines
18 KiB
C

/*
* A CLI-based tester and debugger for the Gameboy's LR35902 CPU
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <stdint.h>
#include <inttypes.h>
#include <stdbool.h>
#include <signal.h>
#include <stdarg.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#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 <file>: loads the given file as the gameboy cartridge\n"
"run: runs the CPU until a breakpoint or halt is hit\n"
"break <addr>: Adds a breakpoint for the given addess\n"
"step <cycles>: 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 <value> <operator> <value>\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 <addr> (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 <file>\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 <file>\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 <addr>\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 <addr>\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 <bkpt num>\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;
}