diff --git a/CMakeLists.txt b/CMakeLists.txt index 4bf6dbd..ec0769e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -67,6 +67,10 @@ elseif (EMSCRIPTEN) ) else() # NOT (ANDROID OR EMSCRIPTEN) + if (NOT NINTENDO_SWITCH) + option(ENABLE_DEBUGGER "Enable built-in debugger" ON) + endif() + add_executable(system3) target_link_libraries(system3 PRIVATE ymfm) @@ -176,6 +180,14 @@ target_sources(system3 PRIVATE src/sys/nact_sys3.cpp src/sys/scenario.cpp ) +if (ENABLE_DEBUGGER) + target_compile_definitions(system3 PRIVATE ENABLE_DEBUGGER) + target_sources(system3 PRIVATE + src/debugger/debug_info.cpp + src/debugger/debugger.cpp + src/debugger/cli_frontend.cpp + ) +endif() if(NINTENDO_SWITCH) set(RESOURCE_PATH romfs:/) diff --git a/src/config.cpp b/src/config.cpp index 574e193..9c66c71 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -77,6 +77,23 @@ std::pair parse_keyval(char *line) return { trim(line), trim(eq + 1) }; } +#ifdef ENABLE_DEBUGGER +DebuggerMode parse_debugger_mode(const char *s, int lineno = -1) +{ + if (!strcasecmp(s, "none")) + return DebuggerMode::DISABLED; + if (!strcasecmp(s, "cli")) + return DebuggerMode::CLI; + if (!strcasecmp(s, "dap")) + return DebuggerMode::DAP; + if (lineno == -1) + ERROR("Command line: Invalid debugger mode '%s'", s); + else + ERROR(INIFILENAME ":%d Invalid debugger mode '%s'", lineno, s); + return DebuggerMode::DISABLED; +} +#endif + TexthookMode parse_texthook_mode(const char *s, int lineno = -1) { if (!strcasecmp(s, "none")) @@ -135,6 +152,10 @@ Config::Config(int argc, char *argv[]) title = argv[++i]; else if (strcmp(argv[i], "-scanline") == 0) scanline = true; +#ifdef ENABLE_DEBUGGER + else if (strcmp(argv[i], "-debugger") == 0) + debugger_mode = parse_debugger_mode(argv[++i]); +#endif else if (strcmp(argv[i], "-texthook") == 0) texthook_mode = parse_texthook_mode(argv[++i]); else if (strcmp(argv[i], "-texthook_suppress") == 0) @@ -197,6 +218,10 @@ void Config::load_ini() title = val; else if (!strcasecmp(key, "scanline")) scanline = to_bool(val, lineno); +#ifdef ENABLE_DEBUGGER + else if (!strcasecmp(key, "debugger")) + debugger_mode = parse_debugger_mode(val, lineno); +#endif else if (!strcasecmp(key, "texthook")) texthook_mode = parse_texthook_mode(val, lineno); else if (!strcasecmp(key, "texthook_suppress")) diff --git a/src/config.h b/src/config.h index a89518a..7a0c617 100644 --- a/src/config.h +++ b/src/config.h @@ -11,6 +11,12 @@ enum class TexthookMode { COPY, }; +enum class DebuggerMode { + DISABLED, + CLI, + DAP, +}; + struct Strings { std::string back; std::string next_page; @@ -39,6 +45,7 @@ struct Config { bool no_antialias = false; bool scanline = false; TexthookMode texthook_mode = TexthookMode::NONE; + DebuggerMode debugger_mode = DebuggerMode::DISABLED; std::string texthook_suppressions; private: diff --git a/src/debugger/cli_frontend.cpp b/src/debugger/cli_frontend.cpp new file mode 100644 index 0000000..4ee799d --- /dev/null +++ b/src/debugger/cli_frontend.cpp @@ -0,0 +1,408 @@ +#include "debugger/frontend.h" +#include +#include "debugger/debug_info.h" +#include "debugger/debugger.h" +#include "encoding.h" +#include "nact.h" +#ifndef _WIN32 +#include +#endif + +namespace debugger { + +namespace { + +const char whitespaces[] = " \t\r\n"; +const size_t CMD_BUFFER_SIZE = 256; + +#ifndef _WIN32 +void handle_sigint(int) { + g_debugger->set_state(State::STOPPED_INTERRUPT); +} + +void set_sigint_handler() { + struct sigaction sa; + sa.sa_handler = handle_sigint; + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; + sigaction(SIGINT, &sa, nullptr); +} +#endif + +} // namespace + +class CliFrontend : public Frontend { +public: + CliFrontend(Debugger* backend, const DebugInfo& symbols) : Frontend(backend, symbols) { + backend->set_state(State::STOPPED_ENTRY); +#ifndef _WIN32 + set_sigint_handler(); +#endif + } + + ~CliFrontend() override = default; + + void repl(int bp_no) override { + if (backend->get_state() == State::STOPPED_BREAKPOINT && bp_no) + printf("Breakpoint %d\n", bp_no); + printf("Stopped at %s\n", format_address(g_nact->sco.page(), g_nact->sco.cmd_addr()).c_str()); + backend->set_state(State::RUNNING); + + char buf[CMD_BUFFER_SIZE]; + while (printf("dbg> "), fflush(stdout), fgets(buf, CMD_BUFFER_SIZE, stdin)) { + // If input line is empty, repeat the last command. + if (buf[0] == '\n') + strcpy(buf, default_command); + else + strcpy(default_command, buf); + + char *token = strtok(buf, whitespaces); + if (!token) + continue; + + const Command *cmd = find_command(token); + if (cmd) { + if ((this->*(cmd->func))() == EXIT_REPL) + return; + } + } + } + void on_sleep() override { + if (backend->get_state() == State::STOPPED_INTERRUPT) + backend->repl(0); + } + + void on_palette_change() override { + } + + void console_output(int lv, const char *output) override { + } + +private: + enum CommandResult { CONTINUE_REPL, EXIT_REPL }; + + char default_command[CMD_BUFFER_SIZE]; + + CommandResult cmd_backtrace(); + CommandResult cmd_break(); + CommandResult cmd_continue(); + CommandResult cmd_delete(); + CommandResult cmd_help(); + CommandResult cmd_list(); + CommandResult cmd_step(); + CommandResult cmd_finish(); + CommandResult cmd_next(); + CommandResult cmd_print(); + CommandResult cmd_quit(); + CommandResult cmd_string(); + + struct Command { + const char *name; + const char *alias; + const char *description; + const char *help; + CommandResult (CliFrontend::* func)(); + }; + + static const Command commands[]; + + const Command *find_command(const char *str); + + void cmd_print_help(const Command& cmd) { + if (cmd.alias) + printf("%s, %s -- %s\n", cmd.name, cmd.alias, cmd.description); + else + printf("%s -- %s\n", cmd.name, cmd.description); + } + + std::string format_address(int page, int addr) { + char buf[200]; + const char *src = symbols.page2src(page); + int line = symbols.addr2line(page, addr); + if (src && line > 0) + snprintf(buf, sizeof(buf), "%s:%d", src, line); + else + snprintf(buf, sizeof(buf), "%d:0x%x", page, addr); + return buf; + } + + + bool parse_address(const char *str, int *page, int *addr) { + // :
+ if (sscanf(str, "%i:%i", page, addr) == 2) + return true; + + // : + char filename[100]; + int line_no; + if (sscanf(str, "%[^:]:%i", filename, &line_no) == 2) { + *page = symbols.src2page(filename); + if (*page < 0) { + printf("No source file named %s.\n", filename); + return false; + } + *addr = symbols.line2addr(*page, line_no); + if (*addr < 0) { + printf("No line %d in file %s.\n", line_no, filename); + return false; + } + return true; + } + + *page = g_nact->sco.page(); + + // + char *endptr; + line_no = strtol(str, &endptr, 0); + if (!*endptr) { + *addr = symbols.line2addr(g_nact->sco.page(), line_no); + if (*addr < 0) { + printf("No line %d in current file.\n", line_no); + return false; + } + return true; + } + + return false; + } +}; + +static const char desc_backtrace[] = "Print backtrace of stack frames."; +static const char * const help_backtrace = nullptr; +CliFrontend::CommandResult CliFrontend::cmd_backtrace() { + for (const StackFrame &frame : backend->stack_trace()) { + if (frame.src && frame.line > 0) { + printf("\t%s:%d\n", frame.src, frame.line); + } else { + printf("\t%d:%04x\n", frame.page, frame.addr); + } + } + return CONTINUE_REPL; +} + +static const char desc_break[] = "Set breakpoint at specified location."; +static const char help_break[] = + "Syntax: break : [if ]\n" + " break [if ]\n" + " break :
[if ]"; + +CliFrontend::CommandResult CliFrontend::cmd_break() { + char *token = strtok(nullptr, whitespaces); + int page, addr; + if (!token || !parse_address(token, &page, &addr)) { + puts(help_break); + return CONTINUE_REPL; + } + int bp_no = backend->set_breakpoint(page, addr, false); + if (bp_no < 0) { + printf("Failed to set breakpoint at %d:%04x: invalid address\n", page, addr); + return CONTINUE_REPL; + } + + printf("Breakpoint %d at %s\n", bp_no, format_address(page, addr).c_str()); + return CONTINUE_REPL; +} + +static const char desc_continue[] = "Continue program execution."; +static const char * const help_continue = nullptr; +CliFrontend::CommandResult CliFrontend::cmd_continue() { + return EXIT_REPL; +} + +static const char desc_delete[] = "Delete breakpoints."; +static const char help_delete[] = + "Syntax: delete ..."; +CliFrontend::CommandResult CliFrontend::cmd_delete() { + char *arg = strtok(nullptr, whitespaces); + if (!arg) { + puts(help_delete); + return CONTINUE_REPL; + } + + while (arg) { + char *endptr; + int bp_no = strtol(arg, &endptr, 0); + if (*endptr) + printf("Bad breakpoint number %s\n", arg); + else { + if (!backend->delete_breakpoint(bp_no)) + printf("No breakpoint number %d.\n", bp_no); + } + arg = strtok(nullptr, whitespaces); + } + return CONTINUE_REPL; +} + +static const char desc_list[] = "List specified line."; +static const char help_list[] = + "Syntax: list\n" + " list \n" + " list :\n" + "\n" + "With no argument, lists ten lines around current location."; + +CliFrontend::CommandResult CliFrontend::cmd_list() { + char *arg = strtok(NULL, whitespaces); + int page, addr; + if (arg) { + if (!parse_address(arg, &page, &addr)) { + puts(help_list); + return CONTINUE_REPL; + } + } else { + page = g_nact->sco.page(); + addr = g_nact->sco.cmd_addr(); + } + + int line_no = symbols.addr2line(page, addr); + if (line_no <= 0) { + printf("Cannot determine source location for %x:%x\n", page, addr); + return CONTINUE_REPL; + } + + if ((line_no -= 5) <= 0) + line_no = 1; + + for (int i = 0; i < 10; i++) { + const char *line = symbols.source_line(page, line_no + i); + if (!line) { + if (i == 0) + printf("No source text for %s:%d\n", symbols.page2src(page), line_no); + break; + } + printf("%d\t%s\n", line_no + i, line); + } + + // Empty REPL input will print the next 10 lines. + snprintf(default_command, CMD_BUFFER_SIZE, "l %s:%d", symbols.page2src(page), line_no + 15); + + return CONTINUE_REPL; +} + +static const char desc_step[] = "Step program until it reaches a different source line."; +static const char * const help_step = nullptr; +CliFrontend::CommandResult CliFrontend::cmd_step() { + backend->stepin(); + return EXIT_REPL; +} + +static const char desc_finish[] = "Execute until current function returns."; +static const char * const help_finish = nullptr; +CliFrontend::CommandResult CliFrontend::cmd_finish() { + backend->stepout(); + return EXIT_REPL; +} + +static const char desc_next[] = "Step program, proceeding through subroutine calls."; +static const char * const help_next = nullptr; +CliFrontend::CommandResult CliFrontend::cmd_next() { + backend->next(); + return EXIT_REPL; +} + +static const char desc_print[] = "Print value of variable."; +static const char help_print[] = + "Syntax: print "; + +CliFrontend::CommandResult CliFrontend::cmd_print() { + char *arg = strtok(NULL, whitespaces); + if (!arg) { + puts(help_print); + return CONTINUE_REPL; + } + int var = symbols.lookup_variable(arg); + if (var < 0) { + printf("Unrecognized variable name \"%s\".\n", arg); + return CONTINUE_REPL; + } + printf("%s = %d\n", arg, g_nact->get_var(var)); + return CONTINUE_REPL; +} + +static const char desc_quit[] = "Exit system3-sdl2."; +static const char * const help_quit = nullptr; +CliFrontend::CommandResult CliFrontend::cmd_quit() { + g_nact->quit(0); + return EXIT_REPL; +} + +static const char desc_string[] = "Print value of string variable."; +static const char help_string[] = + "Syntax: string "; + +CliFrontend::CommandResult CliFrontend::cmd_string() { + char *arg = strtok(NULL, whitespaces); + if (!arg) { + puts(help_string); + return CONTINUE_REPL; + } + + while (arg) { + int no = atoi(arg); + if (no < 1 || no > MAX_STRVAR) { + printf("Bad string index %s\n", arg); + } else { + char *utf = g_nact->encoding->toUtf8(g_nact->get_string(no - 1)); + printf("string[%d] = \"%s\"\n", no, utf); // TODO: escaping + free(utf); + } + arg = strtok(NULL, whitespaces); + } + return CONTINUE_REPL; +} + +static const char desc_help[] = "Print list of commands."; +static const char * const help_help = nullptr; + +const CliFrontend::Command CliFrontend::commands[] = { + {"backtrace", "bt", desc_backtrace, help_backtrace, &CliFrontend::cmd_backtrace}, + {"break", "b", desc_break, help_break, &CliFrontend::cmd_break}, + {"continue", "c", desc_continue, help_continue, &CliFrontend::cmd_continue}, + {"delete", "d", desc_delete, help_delete, &CliFrontend::cmd_delete}, + {"help", "h", desc_help, help_help, &CliFrontend::cmd_help}, + {"list", "l", desc_list, help_list, &CliFrontend::cmd_list}, + {"step", "s", desc_step, help_step, &CliFrontend::cmd_step}, + {"finish", nullptr, desc_finish, help_finish, &CliFrontend::cmd_finish}, + {"next", "n", desc_next, help_next, &CliFrontend::cmd_next}, + {"print", "p", desc_print, help_print, &CliFrontend::cmd_print}, + {"quit", "q", desc_quit, help_quit, &CliFrontend::cmd_quit}, + {"string", "str", desc_string, help_string, &CliFrontend::cmd_string}, +}; + +const CliFrontend::Command *CliFrontend::find_command(const char *str) { + for (const Command &cmd : commands) { + if (!strcmp(str, cmd.name) || (cmd.alias && !strcmp(str, cmd.alias))) + return &cmd; + } + printf("Unknown command \"%s\". Try \"help\".\n", str); + return nullptr; +} + +CliFrontend::CommandResult CliFrontend::cmd_help() { + char *token = strtok(nullptr, whitespaces); + if (!token) { + puts("List of commands:"); + puts(""); + for (auto& cmd : commands) + cmd_print_help(cmd); + puts(""); + puts("Type \"help\" followed by command name for full documentation."); + return CONTINUE_REPL; + } + const Command *cmd = find_command(token); + if (cmd) { + cmd_print_help(*cmd); + if (cmd->help) { + puts(""); + puts(cmd->help); + } + } + return CONTINUE_REPL; +} + +// static +Frontend* Frontend::create_cli(Debugger* backend, const DebugInfo& symbols) { + return new CliFrontend(backend, symbols); +} + +} // namespace debugger diff --git a/src/debugger/debug_info.cpp b/src/debugger/debug_info.cpp new file mode 100644 index 0000000..c969996 --- /dev/null +++ b/src/debugger/debug_info.cpp @@ -0,0 +1,220 @@ +#include "debugger/debug_info.h" +#include +#include +#include +#include "fileio.h" + +namespace debugger { + +namespace { + +inline uint32_t LittleEndian_getDW(uint8_t* buf, int ofs) { + return buf[ofs] | (buf[ofs + 1] << 8) | (buf[ofs + 2] << 16) | (buf[ofs + 3] << 24); +} + +std::string fget4cc(FILEIO& fio) { + char buf[5] = {0}; + if (!fio.read(buf, 4)) + buf[0] = '\0'; + return buf; +} + +bool ensure_srcs(std::vector& srcs, uint32_t nr_srcs) { + if (srcs.empty()) { + srcs.resize(nr_srcs); + } else if (nr_srcs != srcs.size()) { + WARNING("malformed debug information"); + return false; + } + return true; +} + +bool load_srcs(std::vector& srcs, std::vector& buf) { + uint32_t nr_srcs = LittleEndian_getDW(buf.data(), 0); + if (!ensure_srcs(srcs, nr_srcs)) + return false; + + char *p = reinterpret_cast(buf.data()) + 4; + for (uint32_t i = 0; i < nr_srcs; i++) { + srcs[i].filename = p; + p += strlen(p) + 1; + } + return true; +} + +void store_src_lines(SrcInfo& info, char *src) { + while (src) { + char *eol = strchr(src, '\n'); + if (eol) { + if (src < eol && eol[-1] == '\r') + eol[-1] = '\0'; + else + eol[0] = '\0'; + eol++; + } + info.lines.push_back(src); + src = eol; + } +} + +bool load_scnt(std::vector& srcs, std::vector& buf) { + uint32_t nr_srcs = LittleEndian_getDW(buf.data(), 0); + if (!ensure_srcs(srcs, nr_srcs)) + return false; + + char *p = reinterpret_cast(buf.data()) + 4; + for (uint32_t i = 0; i < nr_srcs; i++) { + size_t len = strlen(p); + store_src_lines(srcs[i], p); + p += len + 1; + } + return true; +} + +bool load_line(std::vector& srcs, std::vector& buf) { + uint32_t nr_srcs = LittleEndian_getDW(buf.data(), 0); + if (!ensure_srcs(srcs, nr_srcs)) + return false; + + int ofs = 4; + for (uint32_t i = 0; i < nr_srcs; i++) { + SrcInfo& info = srcs[i]; + uint32_t nr_mappings = LittleEndian_getDW(buf.data(), ofs); + ofs += 4; + info.mappings.resize(nr_mappings); + for (uint32_t i = 0; i < nr_mappings; i++) { + info.mappings[i].line = LittleEndian_getDW(buf.data(), ofs); + info.mappings[i].addr = LittleEndian_getDW(buf.data(), ofs + 4); + ofs += 8; + } + } + return true; +} + +bool load_vari(std::vector& variables, std::vector& buf) { + uint32_t nr_vars = LittleEndian_getDW(buf.data(), 0); + char *p = reinterpret_cast(buf.data()) + 4; + for (uint32_t i = 0; i < nr_vars; i++) { + variables.push_back(p); + p += strlen(p) + 1; + } + return true; +} + +} // namespace + +int SrcInfo::line2addr(int line) const { + // Find the first mapping with line >= `line`. + auto it = std::lower_bound(mappings.begin(), mappings.end(), line, + [](const Mapping& mapping, int line) { + return mapping.line < line; + }); + return it != mappings.end() ? it->addr : -1; +} + +int SrcInfo::addr2line(int addr) const { + // Find the last mapping with addr <= `addr`. + auto it = std::upper_bound(mappings.begin(), mappings.end(), addr, + [](int addr, const Mapping& mapping) { + return addr < mapping.addr; + }); + return it != mappings.begin() ? (it - 1)->line : -1; +} + +void DebugInfo::load(const char *path) { + auto fio = FILEIO::open(path, FILEIO_READ_BINARY); + if (!fio) { + WARNING("Cannot open %s: %s", path, strerror(errno)); + return; + } + if (fget4cc(*fio) != "DSYM") { + WARNING("%s: wrong signature", path); + return; + } + int version = fio->getdw(); + if (version != 0) { + WARNING("%s: unsupported debug info version", path); + return; + } + + uint32_t nr_sections = fio->getdw(); + for (uint32_t i = 0; i < nr_sections; i++) { + std::string tag = fget4cc(*fio); + uint32_t section_size = fio->getdw(); + std::vector section_content(section_size - 8); + if (!fio->read(section_content.data(), section_content.size())) { + WARNING("%s: %s", path, strerror(errno)); + return; + } + + if (tag == "SRCS") { + if (!load_srcs(srcs, section_content)) return; + } else if (tag == "SCNT") { + if (!load_scnt(srcs, section_content)) return; + } else if (tag == "LINE") { + if (!load_line(srcs, section_content)) return; + } else if (tag == "VARI") { + if (!load_vari(variables, section_content)) return; + } else { + WARNING("%s: unrecognized section %s", path, tag.c_str()); + } + } + if (fio->getc() != EOF) { + WARNING("%s: broken debug information structure", path); + return; + } + loaded_ = !srcs.empty() && !variables.empty(); +} + +int DebugInfo::src2page(const char *fname) const { + for (size_t i = 0; i < srcs.size(); i++) { + if (!strcasecmp(srcs[i].filename.c_str(), fname)) + return static_cast(i); + } + return -1; +} + +const char *DebugInfo::page2src(int page) const { + if (static_cast(page) >= srcs.size()) + return NULL; + return srcs[page].filename.c_str(); +} + +int DebugInfo::line2addr(int page, int line) const { + if (static_cast(page) >= srcs.size()) + return -1; + return srcs[page].line2addr(line); +} + +int DebugInfo::addr2line(int page, int addr) const { + if (static_cast(page) >= srcs.size()) + return -1; + return srcs[page].addr2line(addr); +} + +const char *DebugInfo::source_line(int page, int line) const { + if (static_cast(page) >= srcs.size()) + return NULL; + const SrcInfo& info = srcs[page]; + + line--; // 1-based to 0-based index + if (static_cast(line) >= info.lines.size()) + return NULL; + return info.lines[line].c_str(); +} + +const char *DebugInfo::variable_name(int i) const { + if (static_cast(i) >= variables.size()) + return NULL; + return variables[i].c_str(); +} + +int DebugInfo::lookup_variable(const char *name) const { + for (size_t i = 0; i < variables.size(); i++) { + if (variables[i] == name) + return static_cast(i); + } + return -1; +} + +} // namespace debugger diff --git a/src/debugger/debug_info.h b/src/debugger/debug_info.h new file mode 100644 index 0000000..5848c78 --- /dev/null +++ b/src/debugger/debug_info.h @@ -0,0 +1,43 @@ +#ifndef _DEBUG_INFO_H_ +#define _DEBUG_INFO_H_ + +#include +#include + +namespace debugger { + +struct SrcInfo { + struct Mapping { + int line; + int addr; + }; + std::string filename; + std::vector lines; + std::vector mappings; + + int line2addr(int line) const; + int addr2line(int addr) const; +}; + +class DebugInfo { +public: + void load(const char *filename); + bool loaded() const { return loaded_; } + int src2page(const char *fname) const; + const char *page2src(int page) const; + int line2addr(int page, int line) const; + int addr2line(int page, int addr) const; + const char *source_line(int page, int line) const; + size_t num_variables() const { return variables.size(); } + const char *variable_name(int i) const; + int lookup_variable(const char *name) const; + +private: + bool loaded_ = false; + std::vector srcs; + std::vector variables; +}; + +} // namespace debugger + +#endif // _DEBUG_INFO_H_ diff --git a/src/debugger/debugger.cpp b/src/debugger/debugger.cpp new file mode 100644 index 0000000..121cdfd --- /dev/null +++ b/src/debugger/debugger.cpp @@ -0,0 +1,219 @@ +#include "debugger/debugger.h" +#include +#include +#include "common.h" +#include "nact.h" +#include "debugger/frontend.h" + +namespace debugger { + +namespace { + +const int INTERNAL_BREAKPOINT_NO = -1; + +struct Breakpoint { + int page; + int addr; + int no; + uint8_t restore_op; +}; + +std::map, Breakpoint> breakpoints; +int next_breakpoint_no = 1; + +// Used in State::STOPPED_STEP and State::STOPPED_NEXT +struct { + int page; + int line; +} step_exec_state; + +bool should_continue_step(const DebugInfo& symbols) { + if (step_exec_state.line < 0) + return false; // line info was not available + return step_exec_state.page == g_nact->sco.page() + && step_exec_state.line == symbols.addr2line(step_exec_state.page, g_nact->sco.cmd_addr()); +} + +int get_retaddr_if_funcall() { + int orig_addr = g_nact->sco.current_addr(); + int retaddr = -1; + + g_nact->sco.jump_to(g_nact->sco.cmd_addr()); + int c0 = g_nact->sco.getd(); + if (c0 == BREAKPOINT_INSTRUCTION) { + auto bp = breakpoints.find({g_nact->sco.page(), g_nact->sco.cmd_addr()}); + if (bp == breakpoints.end()) + sys_error("Illegal BREAKPOINT instruction"); + c0 = bp->second.restore_op; + } + switch (c0) { + case '%': + if (g_nact->cali() != 0) + retaddr = g_nact->sco.current_addr(); + break; + case '\\': + if (g_nact->sco.getw() != 0) + retaddr = g_nact->sco.current_addr(); + break; + } + g_nact->sco.jump_to(orig_addr); + return retaddr; +} + +} // namespace + +Debugger::Debugger(const char *symbols_path, DebuggerMode mode) { + symbols.load(symbols_path); + if (mode == DebuggerMode::DAP) { + // frontend = std::make_unique(this); + } else { + frontend = std::unique_ptr(Frontend::create_cli(this, symbols)); + } +} + +Debugger::~Debugger() = default; + +void Debugger::repl(int bp_no) { + delete_breakpoint(INTERNAL_BREAKPOINT_NO); + + switch (state) { + case State::STOPPED_STEP: + if (should_continue_step(symbols)) + return; + break; + case State::STOPPED_NEXT: + if (should_continue_step(symbols)) { + do_next(); + return; + } + break; + default: + break; + } + frontend->repl(bp_no); +} + +void Debugger::on_sleep() { + frontend->on_sleep(); +} + +void Debugger::on_palette_change() { + frontend->on_palette_change(); +} + +uint8_t Debugger::handle_breakpoint(int page, int addr) { + auto bp = breakpoints.find({page, addr}); + if (bp == breakpoints.end()) + sys_error("Illegal BREAKPOINT instruction"); + + uint8_t restore_op = bp->second.restore_op; + + state = bp->second.no == INTERNAL_BREAKPOINT_NO ? State::STOPPED_NEXT : State::STOPPED_BREAKPOINT; + + repl(bp->second.no); // this may destroy bp + return restore_op; +} + +bool Debugger::console_vprintf(int lv, const char *format, va_list ap) { + return false; +} +void Debugger::post_command(void *data) { +} + +int Debugger::set_breakpoint(int page, int addr, bool is_internal) +{ + int restore_op = g_nact->sco.write_instruction(page, addr, BREAKPOINT_INSTRUCTION); + if (restore_op == -1) + return -1; + if (restore_op == BREAKPOINT_INSTRUCTION) { + auto bp = breakpoints.find({page, addr}); + if (bp != breakpoints.end()) { + sys_error("Illegal BREAKPOINT instruction"); + } else { + return bp->second.no; + } + } + int no = is_internal ? INTERNAL_BREAKPOINT_NO : next_breakpoint_no++; + breakpoints[{page, addr}] = {page, addr, no, static_cast(restore_op)}; + return no; +} + +bool Debugger::delete_breakpoint(int no) +{ + for (auto it = breakpoints.begin(); it != breakpoints.end(); ++it) { + if (it->second.no == no) { + g_nact->sco.write_instruction(it->second.page, it->second.addr, it->second.restore_op); + breakpoints.erase(it); + return true; + } + } + return false; +} + +void Debugger::delete_breakpoints_in_page(int page) +{ + auto lower = breakpoints.lower_bound({page, 0}); + auto upper = breakpoints.upper_bound({page, 0xffff}); + for (auto it = lower; it != upper; ++it) { + g_nact->sco.write_instruction(it->second.page, it->second.addr, it->second.restore_op); + } + breakpoints.erase(lower, upper); +} + +void Debugger::stepin() +{ + step_exec_state.page = g_nact->sco.page(); + step_exec_state.line = symbols.addr2line(step_exec_state.page, g_nact->sco.cmd_addr()); + state = State::STOPPED_STEP; +} + +void Debugger::stepout() +{ + // Set an internal breakpoint at the return address of current frame. + auto& call_stack = g_nact->sco.get_call_stack(); + if (call_stack.empty()) { + return; + } + const auto& frame = call_stack.back(); + set_breakpoint(frame.page, frame.addr, true); +} + +void Debugger::next() +{ + step_exec_state.page = g_nact->sco.page(); + step_exec_state.line = symbols.addr2line(step_exec_state.page, g_nact->sco.cmd_addr()); + state = State::STOPPED_NEXT; + do_next(); +} + +void Debugger::do_next() +{ + assert(state == State::STOPPED_NEXT); + + int retaddr = get_retaddr_if_funcall(); + if (retaddr >= 0) { + set_breakpoint(step_exec_state.page, retaddr, true); + state = State::RUNNING; + } +} + +std::vector Debugger::stack_trace() +{ + std::vector frames; + const char *src = symbols.page2src(g_nact->sco.page()); + int line = symbols.addr2line(g_nact->sco.page(), g_nact->sco.cmd_addr()); + frames.emplace_back(g_nact->sco.page(), g_nact->sco.cmd_addr(), src, line); + + auto& call_stack = g_nact->sco.get_call_stack(); + std::for_each(call_stack.rbegin(), call_stack.rend(), [&](const auto& frame) { + const char* src = symbols.page2src(frame.page); + // -1 because the return address is the address of the next command + int line = symbols.addr2line(frame.page, frame.addr - 1); + frames.emplace_back(frame.page, frame.addr, src, line); + }); + return frames; +} + +} // namespace debugger + +std::unique_ptr g_debugger; diff --git a/src/debugger/debugger.h b/src/debugger/debugger.h new file mode 100644 index 0000000..b2749f6 --- /dev/null +++ b/src/debugger/debugger.h @@ -0,0 +1,76 @@ +#ifndef _DEBUGGER_H_ +#define _DEBUGGER_H_ + +#ifdef ENABLE_DEBUGGER + +#include +#include +#include "config.h" +#include "debugger/debug_info.h" + +namespace debugger { + +const uint8_t BREAKPOINT_INSTRUCTION = 0x0f; + +class Frontend; + +enum class State { + RUNNING, + STOPPED_ENTRY, + STOPPED_STEP, + STOPPED_NEXT, + STOPPED_BREAKPOINT, + STOPPED_INTERRUPT, + STOPPED_EXCEPTION, +}; + +struct StackFrame { + int page; + int addr; + const char *src; + int line; + + StackFrame(int page, int addr, const char *src, int line) + : page(page), addr(addr), src(src), line(line) {} +}; + +class Debugger { +public: + Debugger(const char *symbols_path, DebuggerMode mode); + ~Debugger(); + void repl(int bp_no); + + // API for VM + bool trapped() const { return state != State::RUNNING; } + void on_sleep(); + void on_palette_change(); + uint8_t handle_breakpoint(int page, int addr); + bool console_vprintf(int lv, const char *format, va_list ap); + void post_command(void *data); + + // API for frontend + State get_state() const { return state; } + void set_state(State s) { state = s; } + int set_breakpoint(int page, int addr, bool is_internal); + bool delete_breakpoint(int no); + void delete_breakpoints_in_page(int page); + void stepin(); + void stepout(); + void next(); + std::vector stack_trace(); + +private: + State state = State::RUNNING; + DebugInfo symbols; + std::unique_ptr frontend; + + void do_next(); +}; + +} // namespace debugger + +extern std::unique_ptr g_debugger; + +#endif // ENABLE_DEBUGGER + +#endif /* _DEBUGGER_H_ */ diff --git a/src/debugger/frontend.h b/src/debugger/frontend.h new file mode 100644 index 0000000..f441349 --- /dev/null +++ b/src/debugger/frontend.h @@ -0,0 +1,26 @@ +#ifndef _DEBUGGER_FRONTEND_H_ +#define _DEBUGGER_FRONTEND_H_ + +namespace debugger { + +class Debugger; +class DebugInfo; + +class Frontend { +public: + static Frontend* create_cli(Debugger* backend, const DebugInfo& symbols); + + Frontend(Debugger* backend, const DebugInfo& symbols) : backend(backend), symbols(symbols) {} + virtual ~Frontend() = default; + virtual void repl(int bp_no) = 0; + virtual void on_sleep() = 0; + virtual void on_palette_change() = 0; + virtual void console_output(int lv, const char *output) = 0; +protected: + Debugger* backend; + const DebugInfo& symbols; +}; + +} // namespace debugger + +#endif /* _DEBUGGER_FRONTEND_H_ */ diff --git a/src/fileio.h b/src/fileio.h index 6b4e90f..248f388 100644 --- a/src/fileio.h +++ b/src/fileio.h @@ -44,6 +44,13 @@ public: int val = getc(); return val | (getc() << 8); } + uint32_t getdw() { + uint32_t val = getc(); + val |= getc() << 8; + val |= getc() << 16; + val |= getc() << 24; + return val; + } void putc(int c) { fputc(c, fp); } void putw(int w) { putc(w & 0xff); diff --git a/src/sdlmain.cpp b/src/sdlmain.cpp index f29884f..0e0a14b 100644 --- a/src/sdlmain.cpp +++ b/src/sdlmain.cpp @@ -4,6 +4,7 @@ #include "fileio.h" #include "texthook.h" #include "sys/nact.h" +#include "debugger/debugger.h" #ifdef _WIN32 #include #else @@ -108,6 +109,12 @@ int main(int argc, char *argv[]) texthook_set_mode(config.texthook_mode); texthook_set_suppression_list(config.texthook_suppressions.c_str()); +#ifdef ENABLE_DEBUGGER + if (config.debugger_mode != DebuggerMode::DISABLED) { + g_debugger = std::make_unique("ADISK.DAT.symbols", config.debugger_mode); + } +#endif + int exit_code = 0; while (g_nact) { exit_code = g_nact->mainloop(); diff --git a/src/sys/msgskip.cpp b/src/sys/msgskip.cpp index c3c0a6a..61301b0 100644 --- a/src/sys/msgskip.cpp +++ b/src/sys/msgskip.cpp @@ -39,8 +39,7 @@ void MsgSkip::load_from_file() { auto fio = FILEIO::open(MSGSKIP_FILENAME, FILEIO_READ_BINARY | FILEIO_SAVEDATA); if (fio) { - uint32 size = fio->getw(); - size |= fio->getw() << 16; + uint32 size = fio->getdw(); if (size == BLOOM_FILTER_SIZE) fio->read(bloom, BLOOM_FILTER_SIZE / 8); } diff --git a/src/sys/nact.cpp b/src/sys/nact.cpp index 133560e..5542d09 100644 --- a/src/sys/nact.cpp +++ b/src/sys/nact.cpp @@ -16,6 +16,7 @@ #include "../config.h" #include "../fileio.h" #include "game_id.h" +#include "debugger/debugger.h" extern SDL_Window* g_window; @@ -86,6 +87,12 @@ int NACT::mainloop() int sleep_cnt = 0; while(!terminate) { +#ifdef ENABLE_DEBUGGER + if (g_debugger && g_debugger->trapped()) { + sco.update_cmd_addr(); + g_debugger->repl(0); + } +#endif execute(); // 512コマンド実行毎にSleep(10) if(!(sleep_cnt = (sleep_cnt + 1) & 0x1ff)) { @@ -132,6 +139,14 @@ void NACT::execute() return; } +#ifdef ENABLE_DEBUGGER + if (cmd == debugger::BREAKPOINT_INSTRUCTION) { + if (!g_debugger) + sys_error("Illegal BREAKPOINT instruction"); + cmd = g_debugger->handle_breakpoint(sco.page(), sco.cmd_addr()); + } +#endif + switch(cmd) { case '!': cmd_calc(); @@ -817,6 +832,10 @@ void NACT::wait_after_open_menu() void NACT::sys_sleep(int ms) { pump_events(); ags->update_screen(); +#ifdef ENABLE_DEBUGGER + if (g_debugger) + g_debugger->on_sleep(); +#endif #ifdef __EMSCRIPTEN__ emscripten_sleep(ms); #else diff --git a/src/sys/nact.h b/src/sys/nact.h index 73bcfe1..d04c67a 100644 --- a/src/sys/nact.h +++ b/src/sys/nact.h @@ -80,6 +80,7 @@ #define MAX_VERB 128 #define MAX_OBJ 256 #define MAX_PCM 256 +#define MAX_STRVAR 10 class AGS; class MAKO; @@ -97,6 +98,8 @@ public: NACT(const Config& config, const GameId& game_id); virtual ~NACT(); + Scenario sco; + protected: AGS* ags; MAKO* mako; @@ -105,13 +108,11 @@ protected: // コマンドパーサ void execute(); - Scenario sco; - // 変数 uint16 var[512] = {}; uint16 var_stack[30][20] = {}; - char tvar[10][33] = {}; - char tvar_stack[30][10][22] = {}; + char tvar[MAX_STRVAR][33] = {}; + char tvar_stack[30][MAX_STRVAR][22] = {}; int tvar_index = 0; int tvar_maxlen; @@ -194,7 +195,6 @@ protected: int pcm[MAX_PCM] = {}; // 下位関数 - virtual uint16 cali() = 0; bool is_message(uint8_t c) { return c == ' ' || c & 0x80; } uint16 random(uint16 range); @@ -237,6 +237,8 @@ public: void text_wait(); void set_skip_menu_state(bool enabled, bool checked); + virtual uint16 cali() = 0; + bool mouse_move_enabled = true; const Config& config; const GameId& game_id; @@ -247,6 +249,8 @@ public: void output_console(const char *format, ...); int get_scenario_page() const { return sco.page(); } + uint16 get_var(int index) const { return var[index]; } + const char* get_string(int index) const { return tvar[index]; } private: void pump_events(); diff --git a/src/sys/scenario.cpp b/src/sys/scenario.cpp index c78d1da..027ab43 100644 --- a/src/sys/scenario.cpp +++ b/src/sys/scenario.cpp @@ -6,17 +6,44 @@ uint8_t Scenario::fetch_command() { - if (addr_ < 2 || static_cast(addr_) >= data_.size()) + if (addr_ < 2 || static_cast(addr_) >= data_size_) sys_error("Scenario error: invalid address %d:%04x", page_, addr_); // Skip SysEng's "new style" marker if (page_ == 0 && addr_ == 2 && data_[2] == 'R' && data_[3] == 'E' && data_[4] == 'V') addr_ = 5; - cmd_addr_ = addr_; + update_cmd_addr(); return getd(); } +std::vector* Scenario::load_page(int page) +{ + if (static_cast(page) < pages_.size() && !pages_[page].empty()) + return &pages_[page]; + std::vector data = adisk.load(page + 1); + if (data.empty()) + return nullptr; + if (static_cast(page) >= pages_.size()) + pages_.resize(page + 1); + pages_[page] = std::move(data); + return &pages_[page]; +} + +void Scenario::page_jump(int page, int addr) +{ + std::vector* page_data = load_page(page); + if (page_data) { + data_ = page_data->data(); + data_size_ = page_data->size(); + } else { + data_ = nullptr; + data_size_ = 0; + } + page_ = page; + cmd_addr_ = addr_ = addr; +} + void Scenario::skip_syseng_string(Encoding *enc, uint8_t terminator) { for (uint8_t c = getd(); c != terminator; c = getd()) { @@ -136,4 +163,16 @@ void Scenario::page_stack_clear() } else { sys_error("Unknown Command: %02x at %d:%04x", cmd, page_, cmd_addr_); } -} \ No newline at end of file +} + +int Scenario::write_instruction(int page, int addr, uint8_t op) +{ + std::vector* page_data = load_page(page); + if (!page_data) + return -1; + if (addr < 0 || addr >= static_cast(page_data->size())) + return -1; + uint8_t old_op = (*page_data)[addr]; + (*page_data)[addr] = op; + return old_op; +} diff --git a/src/sys/scenario.h b/src/sys/scenario.h index f73e5ce..8900fe3 100644 --- a/src/sys/scenario.h +++ b/src/sys/scenario.h @@ -11,11 +11,7 @@ class Encoding; class Scenario { public: void open(const char* file_name) { adisk.open(file_name); } - void page_jump(int page, int addr) { - data_ = adisk.load(page + 1); - page_ = page; - cmd_addr_ = addr_ = addr; - } + void page_jump(int page, int addr); int default_addr() { return data_[0] | data_[1] << 8; } int page() const { return page_; } @@ -28,6 +24,7 @@ public: void skip(int n) { addr_ += n; } uint8_t fetch_command(); + void update_cmd_addr() { cmd_addr_ = addr_; } uint8_t getd() { return data_[addr_++]; } uint16_t getw() { @@ -63,8 +60,12 @@ public: int write_instruction(int page, int addr, uint8_t op); private: + std::vector* load_page(int page); + Dri adisk; - std::vector data_; + std::vector> pages_; + const uint8_t* data_; + size_t data_size_; int page_; int addr_; int cmd_addr_;