diff --git a/include/debugger.h b/include/debugger.h index 9c698ee..0018367 100644 --- a/include/debugger.h +++ b/include/debugger.h @@ -59,12 +59,15 @@ struct dbg_cmd { void (*run)(unsigned nr_args, char **args); }; +struct dbg_info; + extern bool dbg_dap; extern bool dbg_enabled; extern bool dbg_start_in_debugger; extern unsigned dbg_current_frame; +extern struct dbg_info *dbg_info; -void dbg_init(void); +void dbg_init(const char *debug_info_path); void dbg_fini(void); void dbg_repl(enum dbg_stop_type, const char *msg); @@ -87,6 +90,7 @@ void dbg_print_frame(unsigned no); void dbg_print_stack_trace(void); void dbg_print_stack(void); void dbg_print_vm_state(void); +void dbg_print_current_line(void); union vm_value dbg_eval_string(const char *str, struct ain_type *type_out); struct string *dbg_value_to_string(struct ain_type *type, union vm_value value, int recursive); @@ -96,13 +100,21 @@ void dbg_dap_repl(struct dbg_stop *stop); void dbg_dap_handle_messages(void); void dbg_dap_log(const char *log, const char *fmt, va_list ap); +struct dbg_info *dbg_info_load(const char *path); +const char *dbg_info_source_name(const struct dbg_info *info, int file); +char *dbg_info_source_path(const struct dbg_info *info, int file); +const char *dbg_info_source_line(const struct dbg_info *info, int file, int line); +int dbg_info_find_file(const struct dbg_info *info, const char *filename); +bool dbg_info_addr2line(const struct dbg_info *info, int addr, int *file, int *line); +int dbg_info_line2addr(const struct dbg_info *info, int file, int line); + #ifdef HAVE_SCHEME void dbg_scm_init(void); void dbg_scm_fini(void); void dbg_scm_repl(void); #endif /* HAVE_SCHEME */ #else /* DEBUGGER ENABLED */ -#define dbg_init() +#define dbg_init(debug_info_path) #define dbg_repl(type, msg) #define dbg_dap 0 #define dbg_dap_handle_messages() diff --git a/src/debug.c b/src/debug.c index d21286e..f1236ae 100644 --- a/src/debug.c +++ b/src/debug.c @@ -24,6 +24,7 @@ #include #include "system4/dasm.h" +#include "system4/file.h" #include "system4/hashtable.h" #include "system4/little_endian.h" #include "system4/string.h" @@ -39,6 +40,7 @@ bool dbg_dap = false; bool dbg_enabled = true; bool dbg_start_in_debugger = false; +struct dbg_info *dbg_info = NULL; unsigned dbg_current_frame = 0; static jmp_buf dbg_continuation; @@ -94,8 +96,19 @@ void dbg_repl(enum dbg_stop_type type, const char *msg) dbg_start(_dbg_repl, &stop); } -void dbg_init(void) +void dbg_init(const char *debug_info_path) { + if (debug_info_path) { + dbg_info = dbg_info_load(debug_info_path); + } else { + // Try to load from the default location + char *path = gamedir_path("src/debug_info.json"); + if (file_exists(path)) { + dbg_info = dbg_info_load(path); + } + free(path); + } + if (dbg_dap) dbg_dap_init(); else @@ -437,8 +450,14 @@ void dbg_print_frame(unsigned no) unsigned cs_no = call_stack_ptr - (1 + no); struct ain_function *f = &ain->functions[call_stack[cs_no].fno]; uint32_t addr = no ? call_stack[cs_no+1].call_address : instr_ptr; - sys_message("%c #%d 0x%08x in %s\n", no == dbg_current_frame ? '*' : ' ', + sys_message("%c #%d 0x%08x in %s", no == dbg_current_frame ? '*' : ' ', no, addr, display_sjis0(f->name)); + int file, line; + if (dbg_info && dbg_info_addr2line(dbg_info, addr, &file, &line)) { + sys_message(" at %s:%d\n", display_utf0(dbg_info_source_name(dbg_info, file)), line); + } else { + sys_message("\n"); + } } void dbg_print_stack_trace(void) @@ -1094,3 +1113,29 @@ void dbg_print_vm_state(void) putchar('\n'); } } + +void dbg_print_current_line(void) +{ + if (call_stack_ptr < 1) { + DBG_ERROR("VM not running"); + return; + } + if (!dbg_info) { + DBG_ERROR("No debug information available"); + return; + } + + int file, line; + if (!dbg_info_addr2line(dbg_info, instr_ptr, &file, &line)) { + DBG_ERROR("No line number information for address: 0x%08x", instr_ptr); + return; + } + + const char *src_line = dbg_info_source_line(dbg_info, file, line); + if (!src_line) { + DBG_ERROR("No source text for %s:%d", display_utf0(dbg_info_source_name(dbg_info, file)), line); + return; + } + + printf("%s:%d\t%s\n", display_utf0(dbg_info_source_name(dbg_info, file)), line, display_utf1(src_line)); +} diff --git a/src/debug_info.c b/src/debug_info.c new file mode 100644 index 0000000..73e54bf --- /dev/null +++ b/src/debug_info.c @@ -0,0 +1,213 @@ +/* Copyright (C) 2024 kichikuou + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + */ + +#include +#include +#include +#include +#include +#include "system4/file.h" + +#include "cJSON.h" +#include "debugger.h" + +struct mapping { + int addr; + int file; + int line; +}; + +struct source_content { + int nr_lines; + char *lines[]; +}; + +struct dbg_info { + char *src_root; + int nr_sources; + char **sources; + struct source_content **source_contents; + int nr_mappings; + struct mapping *mappings; +}; + +struct dbg_info *dbg_info_load(const char *path) +{ + char *json_text = file_read(path, NULL); + if (!json_text) { + DBG_ERROR("Cannot load debug information file %s: %s", path, strerror(errno)); + return NULL; + } + + cJSON *root = cJSON_Parse(json_text); + free(json_text); + + cJSON *version = cJSON_GetObjectItem(root, "version"); + if (!cJSON_IsString(version) || strcmp(version->valuestring, "alpha-1")) { + DBG_ERROR("%s: Unsupported debug information format", display_utf0(path)); + cJSON_Delete(root); + return NULL; + } + + struct dbg_info *info = xcalloc(1, sizeof(struct dbg_info)); + cJSON *sources = cJSON_GetObjectItem(root, "sources"); + info->nr_sources = cJSON_GetArraySize(sources); + info->sources = xcalloc(info->nr_sources, sizeof(char *)); + cJSON *source; + int i = 0; + cJSON_ArrayForEach(source, sources) { + info->sources[i++] = xstrdup(source->valuestring); + } + + cJSON *mappings = cJSON_GetObjectItem(root, "mappings"); + info->nr_mappings = cJSON_GetArraySize(mappings); + info->mappings = xcalloc(info->nr_mappings, sizeof(struct mapping)); + + struct mapping *m = info->mappings; + cJSON *item; + cJSON_ArrayForEach(item, mappings) { + m->addr = cJSON_GetArrayItem(item, 0)->valueint; + m->file = cJSON_GetArrayItem(item, 1)->valueint; + m->line = cJSON_GetArrayItem(item, 2)->valueint; + m++; + } + + cJSON_Delete(root); + + info->src_root = xstrdup(path_dirname(path)); + info->source_contents = xcalloc(info->nr_sources, sizeof(struct source_content *)); + return info; +} + +static struct source_content *load_source_content(const char *path) +{ + char *text = file_read(path, NULL); + if (!text) { + DBG_ERROR("Cannot load source file %s", path); + return NULL; + } + + int nr_lines = 1; // count the last line even if it doesn't end with '\n' + for (char *p = text; *p; p++) { + if (*p == '\n') + nr_lines++; + } + struct source_content *content = xcalloc(1, sizeof(struct source_content) + nr_lines * sizeof(char *)); + content->nr_lines = nr_lines; + for (int i = 0;; i++) { + content->lines[i] = text; + char *next = strchr(text, '\n'); + if (!next) + break; + *next = '\0'; + text = next + 1; + } + return content; +} + +const char *dbg_info_source_name(const struct dbg_info *info, int file) +{ + if (file < 0 || file >= info->nr_sources) + return NULL; + return info->sources[file]; +} + +char *dbg_info_source_path(const struct dbg_info *info, int file) +{ + if (file < 0 || file >= info->nr_sources) + return NULL; + return path_join(info->src_root, info->sources[file]); +} + +const char *dbg_info_source_line(const struct dbg_info *info, int file, int line) +{ + if (file < 0 || file >= info->nr_sources) + return NULL; + if (!info->source_contents[file]) { + char *path = dbg_info_source_path(info, file); + info->source_contents[file] = load_source_content(path); + free(path); + if (!info->source_contents[file]) + return NULL; + } + if (line <= 0 || line > info->source_contents[file]->nr_lines) + return NULL; + return info->source_contents[file]->lines[line - 1]; +} + +int dbg_info_find_file(const struct dbg_info *info, const char *filename) +{ + for (int i = 0; i < info->nr_sources; i++) { + if (!strcmp(info->sources[i], filename) || !strcmp(path_basename(info->sources[i]), filename)) + return i; + } + return -1; +} + +bool dbg_info_addr2line(const struct dbg_info *info, int addr, int *file, int *line) +{ + // Find the last mapping whose address is less than or equal to `addr`. + int left = 0, right = info->nr_mappings; + while (left < right) { + int mid = (left + right) / 2; + if (info->mappings[mid].addr <= addr) + left = mid + 1; + else + right = mid; + } + if (left == 0) { + assert(info->nr_mappings == 0 || addr < info->mappings[0].addr); + return false; + } + + assert(info->mappings[left - 1].addr <= addr); + assert(left == info->nr_mappings || addr < info->mappings[left].addr); + + if (file) + *file = info->mappings[left - 1].file; + if (line) + *line = info->mappings[left - 1].line; + return true; +} + +int dbg_info_line2addr(const struct dbg_info *info, int file, int line) +{ + if (file < 0 || file >= info->nr_sources || line <= 0) + return -1; + + // Find the first mapping whose file is `file` and line is greater than or equal to `line`. + int left = 0, right = info->nr_mappings; + while (left < right) { + int mid = (left + right) / 2; + if (info->mappings[mid].file < file || (info->mappings[mid].file == file && info->mappings[mid].line < line)) + left = mid + 1; + else + right = mid; + } + if (left == info->nr_mappings) { + assert(info->nr_mappings == 0 || info->mappings[info->nr_mappings - 1].file < file || + (info->mappings[info->nr_mappings - 1].file == file && info->mappings[info->nr_mappings - 1].line < line)); + return -1; + } + + assert(left == 0 || info->mappings[left - 1].file < file || + (info->mappings[left - 1].file == file && info->mappings[left - 1].line < line)); + if (file < info->mappings[left].file) + return -1; + assert(file == info->mappings[left].file && line <= info->mappings[left].line); + + return info->mappings[left].addr; +} diff --git a/src/debugger_cmd.c b/src/debugger_cmd.c index aac1085..1d244f4 100644 --- a/src/debugger_cmd.c +++ b/src/debugger_cmd.c @@ -40,7 +40,14 @@ #include "xsystem4.h" struct dbg_cmd_node; -static bool stepping = false; +static enum { + NOT_STEPPING = 0, + STEPPING_INTO, + STEPPING_OVER, + STEPPING_FINISH +} stepping = NOT_STEPPING; +static int stepping_file = 0; +static int stepping_line = 0; struct dbg_cmd_list { unsigned nr_commands; @@ -67,17 +74,33 @@ static void dbg_cmd_backtrace(unsigned nr_args, char **args) static void dbg_cmd_breakpoint(unsigned nr_args, char **args) { - int addr = strtol(args[0], NULL, 0); - if (addr > 0) { + if (nr_args == 1) { + int addr = strtol(args[0], NULL, 0); + if (addr > 0) { + dbg_set_address_breakpoint(addr, NULL, NULL); + } else { + dbg_set_function_breakpoint(args[0], NULL, NULL); + } + } else if (nr_args == 2 && dbg_info) { + int file = dbg_info_find_file(dbg_info, args[0]); + if (file < 0) { + DBG_ERROR("No such file: %s", args[0]); + return; + } + int line = atoi(args[1]); + int addr = dbg_info_line2addr(dbg_info, file, line); + if (addr < 0) { + DBG_ERROR("No such line: %s:%d", args[0], line); + return; + } dbg_set_address_breakpoint(addr, NULL, NULL); } else { - dbg_set_function_breakpoint(args[0], NULL, NULL); + DBG_ERROR("Invalid breakpoint command"); } } static void dbg_cmd_continue(unsigned nr_args, char **args) { - stepping = false; dbg_continue(); } @@ -184,21 +207,29 @@ static void dbg_cmd_scene(unsigned nr_args, char **args) static void dbg_cmd_next(unsigned nr_args, char **args) { + stepping_file = stepping_line = 0; + if (dbg_info) + dbg_info_addr2line(dbg_info, instr_ptr, &stepping_file, &stepping_line); + if (!dbg_set_step_over_breakpoint()) { DBG_ERROR("Can't step over this instruction"); return; } - stepping = true; + stepping = STEPPING_OVER; dbg_continue(); } static void dbg_cmd_step(unsigned nr_args, char **args) { + stepping_file = stepping_line = 0; + if (dbg_info) + dbg_info_addr2line(dbg_info, instr_ptr, &stepping_file, &stepping_line); + if (!dbg_set_step_into_breakpoint()) { DBG_ERROR("Can't step into this instruction"); return; } - stepping = true; + stepping = STEPPING_INTO; dbg_continue(); } @@ -208,7 +239,7 @@ static void dbg_cmd_finish(unsigned nr_args, char **args) DBG_ERROR("Can't set finish breakpoint from this location"); return; } - stepping = true; + stepping = STEPPING_FINISH; dbg_continue(); } @@ -226,7 +257,7 @@ static void dbg_cmd_vm_state(unsigned nr_args, char **args) static struct dbg_cmd dbg_default_commands[] = { { "backtrace", "bt", NULL, "Display stack trace", 0, 0, dbg_cmd_backtrace }, - { "breakpoint", "bp", "", "Set breakpoint at a function or address", 1, 1, dbg_cmd_breakpoint }, + { "breakpoint", "bp", " |
| ", "Set breakpoint", 1, 2, dbg_cmd_breakpoint }, { "continue", "c", NULL, "Resume execution", 0, 0, dbg_cmd_continue }, { "finish", "fin", NULL, "Execute until the current function returns", 0, 0, dbg_cmd_finish }, { "frame", "f", "", "Set the current frame", 1, 1, dbg_cmd_frame }, @@ -477,10 +508,27 @@ static void execute_line(char *line) void dbg_cmd_repl(void) { - if (stepping) - dbg_print_vm_state(); - else + if (stepping) { + if (stepping_line) { + int file, line; + dbg_info_addr2line(dbg_info, instr_ptr, &file, &line); + if (file == stepping_file && line == stepping_line) { + if (stepping == STEPPING_INTO) + dbg_set_step_into_breakpoint(); + else if (stepping == STEPPING_OVER) + dbg_set_step_over_breakpoint(); + dbg_continue(); + } + } + stepping = NOT_STEPPING; + stepping_file = stepping_line = 0; + if (dbg_info) + dbg_print_current_line(); + else + dbg_print_vm_state(); + } else { puts("Entering the debugger REPL. Type 'help' for a list of commands."); + } while (1) { char *line = cmd_gets(); if (line) diff --git a/src/meson.build b/src/meson.build index 907a117..6b457d7 100644 --- a/src/meson.build +++ b/src/meson.build @@ -176,6 +176,7 @@ endif if get_option('debugger').allowed() add_project_arguments('-DDEBUGGER_ENABLED', language : 'c') xsystem4 += 'debug.c' + xsystem4 += 'debug_info.c' xsystem4 += 'debugger_cmd.c' xsystem4 += 'debugger_dap.c' if chibi.found() diff --git a/src/system4.c b/src/system4.c index f48ed08..5526a20 100644 --- a/src/system4.c +++ b/src/system4.c @@ -371,6 +371,7 @@ static void usage(void) #ifdef DEBUGGER_ENABLED puts(" --nodebug Disable debugger"); puts(" --debug Start in debugger"); + puts(" --debug-info Specify the path to the debug information file"); #endif } @@ -402,6 +403,7 @@ enum { LOPT_NODEBUG, LOPT_DEBUG, LOPT_DEBUG_API, + LOPT_DEBUG_INFO, #endif }; @@ -424,6 +426,7 @@ int main(int argc, char *argv[]) char *font_fnl = NULL; char *joypad = NULL; char *savedir = NULL; + char *debug_info_path = NULL; while (1) { static struct option long_options[] = { @@ -443,6 +446,7 @@ int main(int argc, char *argv[]) { "nodebug", no_argument, 0, LOPT_NODEBUG }, { "debug", no_argument, 0, LOPT_DEBUG }, { "debug-api", no_argument, 0, LOPT_DEBUG_API }, + { "debug-info", required_argument, 0, LOPT_DEBUG_INFO }, #endif { 0 } }; @@ -520,6 +524,9 @@ int main(int argc, char *argv[]) dbg_dap = true; sys_silent = true; break; + case LOPT_DEBUG_INFO: + debug_info_path = optarg; + break; #endif } } @@ -584,6 +591,6 @@ int main(int argc, char *argv[]) if (config.msgskip_delay) set_msgskip_delay(ain, config.msgskip_delay); asset_manager_init(); - dbg_init(); + dbg_init(debug_info_path); sys_exit(vm_execute_ain(ain)); }