mirror of
https://github.com/nunuhara/xsystem4.git
synced 2026-10-03 12:28:01 +03:00
Implement simple command-based debugger interface
Add a new debugger interface using a simple command language. This is
less powerful than the scheme interface (which still exists) but more
ergonomic for interactive debugging.
This debugger has no external dependencies (it will however use readline
if available) and is included in builds by default.
A sample interaction might look like:
xsystem4 --debug /path/to/game
dbg(cmd)> breakpoint bar
Set breakpoint at function 'bar' (0x00001234)
dbg(cmd)> continue
Hit breakpoint at function 'bar' (0x00001234)
dbg(cmd)> backtrace
#0 0x00001234 in bar
#1 0x00002222 in foo
#2 0x00004444 in main
dbg(cmd)> locals
[0] i: 2
[1] s: "baz"
dbg(cmd)> locals 1
[0] j: 3
[1] obj: { m_i = 0; m_s = "" }
dbg(cmd)> quit
This commit is contained in:
@@ -20,6 +20,17 @@
|
||||
|
||||
#include <stdbool.h>
|
||||
#include "system4/instructions.h"
|
||||
#include "xsystem4.h"
|
||||
|
||||
#define DBG_ERROR(fmt, ...) sys_warning("ERROR: " fmt "\n", ##__VA_ARGS__)
|
||||
|
||||
struct breakpoint {
|
||||
enum opcode restore_op;
|
||||
void *data;
|
||||
void (*cb)(struct breakpoint*);
|
||||
char *message;
|
||||
int count;
|
||||
};
|
||||
|
||||
extern bool dbg_enabled;
|
||||
|
||||
@@ -27,7 +38,19 @@ void dbg_init(void);
|
||||
void dbg_fini(void);
|
||||
void dbg_repl(void);
|
||||
|
||||
void dbg_continue(void);
|
||||
void dbg_quit(void);
|
||||
void dbg_start(void(*fun)(void*), void *data);
|
||||
void dbg_cmd_repl(void);
|
||||
enum opcode dbg_handle_breakpoint(unsigned bp_no);
|
||||
bool dbg_set_function_breakpoint(const char *_name, void(*cb)(struct breakpoint*), void *data);
|
||||
bool dbg_set_address_breakpoint(uint32_t address, void(*cb)(struct breakpoint*), void *data);
|
||||
void dbg_print_stack_trace(void);
|
||||
|
||||
#ifdef HAVE_SCHEME
|
||||
void dbg_scm_init(void);
|
||||
void dbg_scm_fini(void);
|
||||
void dbg_scm_repl(void);
|
||||
#endif /* HAVE_SCHEME */
|
||||
#endif /* DEBUGGER_ENABLED */
|
||||
#endif /* SYSTEM4_DEBUGGER_H */
|
||||
|
||||
@@ -43,6 +43,13 @@ struct config {
|
||||
|
||||
extern struct config config;
|
||||
|
||||
// Convenience functions to avoid cumbersome memory management when passing
|
||||
// text to printf-like functions
|
||||
const char *display_sjis0(const char *sjis);
|
||||
const char *display_sjis1(const char *sjis);
|
||||
const char *display_utf0(const char *utf);
|
||||
const char *display_utf1(const char *utf);
|
||||
|
||||
char *unix_path(const char *path);
|
||||
char *gamedir_path(const char *path);
|
||||
char *savedir_path(const char *path);
|
||||
|
||||
+2
-1
@@ -24,7 +24,8 @@ else
|
||||
gl_deps = [gl, glew]
|
||||
endif
|
||||
|
||||
chibi = dependency('chibi-scheme', required : get_option('debugger'))
|
||||
chibi = dependency('chibi-scheme', required : false)
|
||||
readline = dependency('readline', required : false)
|
||||
|
||||
flex = find_program('flex')
|
||||
bison = find_program('bison')
|
||||
|
||||
+1
-1
@@ -72,7 +72,7 @@ sexp sexp_get_page_stub (sexp ctx, sexp self, sexp_sint_t n, sexp arg0) {
|
||||
|
||||
sexp sexp_backtrace_stub (sexp ctx, sexp self, sexp_sint_t n) {
|
||||
sexp res;
|
||||
res = ((print_stack_trace()), SEXP_VOID);
|
||||
res = ((dbg_print_stack_trace()), SEXP_VOID);
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -82,7 +82,7 @@
|
||||
(int varno variable-varno)
|
||||
(vm_value value variable-vm-value))
|
||||
|
||||
(define-c void (backtrace print_stack_trace) ())
|
||||
(define-c void (backtrace dbg_print_stack_trace) ())
|
||||
|
||||
(define-c (maybe-null page) (get-page get_page) (int))
|
||||
(define-c (maybe-null page) (frame-page frame_page) (int))
|
||||
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
/* Copyright (C) 2019 Nunuhara Cabbage <nunuhara@haniwa.technology>
|
||||
*
|
||||
* 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 <http://gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#define VM_PRIVATE
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <setjmp.h>
|
||||
#include <ctype.h>
|
||||
#include <assert.h>
|
||||
|
||||
#include "system4/string.h"
|
||||
#include "system4/utfsjis.h"
|
||||
|
||||
#include "vm.h"
|
||||
#include "vm/heap.h"
|
||||
#include "vm/page.h"
|
||||
|
||||
#include "debugger.h"
|
||||
#include "little_endian.h"
|
||||
#include "xsystem4.h"
|
||||
|
||||
static jmp_buf dbg_continuation;
|
||||
|
||||
enum {
|
||||
DBG_CONTINUE = 1,
|
||||
DBG_QUIT = 2,
|
||||
};
|
||||
|
||||
void dbg_continue(void)
|
||||
{
|
||||
longjmp(dbg_continuation, DBG_CONTINUE);
|
||||
}
|
||||
|
||||
void dbg_quit(void)
|
||||
{
|
||||
longjmp(dbg_continuation, DBG_QUIT);
|
||||
}
|
||||
|
||||
void dbg_start(void(*fun)(void*), void *data)
|
||||
{
|
||||
switch (setjmp(dbg_continuation)) {
|
||||
case 0:
|
||||
break;
|
||||
case DBG_CONTINUE:
|
||||
return;
|
||||
case DBG_QUIT:
|
||||
vm_exit(0);
|
||||
default:
|
||||
ERROR("setjmp returned unexpected value");
|
||||
}
|
||||
fun(data);
|
||||
}
|
||||
|
||||
static void _dbg_repl(void *_)
|
||||
{
|
||||
dbg_cmd_repl();
|
||||
}
|
||||
|
||||
void dbg_repl(void)
|
||||
{
|
||||
if (!dbg_enabled)
|
||||
return;
|
||||
dbg_start(_dbg_repl, NULL);
|
||||
}
|
||||
|
||||
void dbg_init(void)
|
||||
{
|
||||
#ifdef HAVE_SCHEME
|
||||
dbg_scm_init();
|
||||
#endif
|
||||
}
|
||||
|
||||
void dbg_fini(void)
|
||||
{
|
||||
#ifdef HAVE_SCHEME
|
||||
dbg_scm_fini();
|
||||
#endif
|
||||
}
|
||||
|
||||
static struct breakpoint *breakpoints = NULL;
|
||||
static unsigned nr_breakpoints = 0;
|
||||
|
||||
bool dbg_set_function_breakpoint(const char *_name, void(*cb)(struct breakpoint*), void *data)
|
||||
{
|
||||
char *name = utf2sjis(_name, 0);
|
||||
int fno = ain_get_function(ain, name);
|
||||
free(name);
|
||||
|
||||
if (fno < 0) {
|
||||
DBG_ERROR("No function with name '%s'", _name);
|
||||
return false;
|
||||
}
|
||||
|
||||
struct ain_function *f = &ain->functions[fno];
|
||||
breakpoints = xrealloc_array(breakpoints, nr_breakpoints, nr_breakpoints+1, sizeof(struct breakpoint));
|
||||
breakpoints[nr_breakpoints].restore_op = LittleEndian_getW(ain->code, f->address);
|
||||
breakpoints[nr_breakpoints].cb = cb;
|
||||
breakpoints[nr_breakpoints].data = data;
|
||||
breakpoints[nr_breakpoints].message = xmalloc(512);
|
||||
snprintf(breakpoints[nr_breakpoints].message, 511, "Hit breakpoint at function '%s' (0x%08x)", _name, f->address);
|
||||
LittleEndian_putW(ain->code, f->address, BREAKPOINT + nr_breakpoints);
|
||||
nr_breakpoints++;
|
||||
|
||||
NOTICE("Set breakpoint at function '%s' (0x%08x)", _name, f->address);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool dbg_set_address_breakpoint(uint32_t address, void(*cb)(struct breakpoint*), void *data)
|
||||
{
|
||||
if (address & 1 || address >= ain->code_size - 2) {
|
||||
DBG_ERROR("Invalid address: 0x%08x", address);
|
||||
return false;
|
||||
}
|
||||
|
||||
// XXX: this is not very robust; some illegal addresses will slip through
|
||||
// if the data looks like a valid opcode
|
||||
enum opcode op = LittleEndian_getW(ain->code, address);
|
||||
if (op < 0 || op >= NR_OPCODES) {
|
||||
DBG_ERROR("Invalid address: 0x%08x", address);
|
||||
return false;
|
||||
}
|
||||
|
||||
breakpoints = xrealloc_array(breakpoints, nr_breakpoints, nr_breakpoints+1, sizeof(struct breakpoint));
|
||||
breakpoints[nr_breakpoints].restore_op = op;
|
||||
breakpoints[nr_breakpoints].cb = cb;
|
||||
breakpoints[nr_breakpoints].data = data;
|
||||
breakpoints[nr_breakpoints].message = xmalloc(512);
|
||||
snprintf(breakpoints[nr_breakpoints].message, 511, "Hit breakpoint at 0x%08x", address);
|
||||
LittleEndian_putW(ain->code, address, BREAKPOINT + nr_breakpoints);
|
||||
nr_breakpoints++;
|
||||
|
||||
NOTICE("Set breakpoint at 0x%08x", address);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void _dbg_handle_breakpoint(void *data)
|
||||
{
|
||||
unsigned bp_no = *((unsigned*)data);
|
||||
if (breakpoints[bp_no].cb) {
|
||||
breakpoints[bp_no].cb(&breakpoints[bp_no]);
|
||||
} else {
|
||||
NOTICE("%s", breakpoints[bp_no].message);
|
||||
dbg_cmd_repl();
|
||||
}
|
||||
}
|
||||
|
||||
enum opcode dbg_handle_breakpoint(unsigned bp_no)
|
||||
{
|
||||
assert(bp_no < nr_breakpoints);
|
||||
dbg_start(_dbg_handle_breakpoint, &bp_no);
|
||||
return breakpoints[bp_no].restore_op;
|
||||
}
|
||||
|
||||
void dbg_print_stack_trace(void)
|
||||
{
|
||||
for (int i = call_stack_ptr - 1, j = 0; i >= 0; i--, j++) {
|
||||
struct ain_function *f = &ain->functions[call_stack[i].fno];
|
||||
char *u = sjis2utf(f->name, strlen(f->name));
|
||||
uint32_t addr = (i == call_stack_ptr - 1) ? instr_ptr : call_stack[i+1].call_address;
|
||||
NOTICE("#%d 0x%08x in %s", j, addr, u);
|
||||
free(u);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
/* Copyright (C) 2019 Nunuhara Cabbage <nunuhara@haniwa.technology>
|
||||
*
|
||||
* 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 <http://gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#define VM_PRIVATE
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <setjmp.h>
|
||||
#include <ctype.h>
|
||||
#include <assert.h>
|
||||
|
||||
#include "system4/string.h"
|
||||
#include "system4/utfsjis.h"
|
||||
|
||||
#include "vm.h"
|
||||
#include "vm/heap.h"
|
||||
#include "vm/page.h"
|
||||
|
||||
#include "debugger.h"
|
||||
#include "little_endian.h"
|
||||
#include "xsystem4.h"
|
||||
|
||||
struct dbg_cmd {
|
||||
const char *fullname;
|
||||
const char *shortname;
|
||||
const char *description;
|
||||
unsigned min_args;
|
||||
unsigned max_args;
|
||||
void (*run)(unsigned nr_args, char **args);
|
||||
};
|
||||
|
||||
static void dbg_cmd_help(unsigned nr_args, char **args);
|
||||
|
||||
static void dbg_cmd_backtrace(unsigned nr_args, char **args)
|
||||
{
|
||||
dbg_print_stack_trace();
|
||||
}
|
||||
|
||||
static void dbg_cmd_breakpoint(unsigned nr_args, char **args)
|
||||
{
|
||||
int addr = atoi(args[0]);
|
||||
if (addr > 0) {
|
||||
dbg_set_address_breakpoint(addr, NULL, NULL);
|
||||
} else {
|
||||
dbg_set_function_breakpoint(args[0], NULL, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
static void dbg_cmd_continue(unsigned nr_args, char **args)
|
||||
{
|
||||
dbg_continue();
|
||||
}
|
||||
|
||||
static struct string *value_to_string(struct ain_type *type, union vm_value value, int recursive)
|
||||
{
|
||||
switch (type->data) {
|
||||
case AIN_INT:
|
||||
return integer_to_string(value.i);
|
||||
case AIN_FLOAT:
|
||||
return float_to_string(value.f, 6);
|
||||
break;
|
||||
case AIN_STRING: {
|
||||
struct string *out = cstr_to_string("\"");
|
||||
string_append(&out, heap_get_string(value.i));
|
||||
string_push_back(&out, '"');
|
||||
return out;
|
||||
}
|
||||
case AIN_STRUCT:
|
||||
case AIN_REF_STRUCT: {
|
||||
struct page *page = heap_get_page(value.i);
|
||||
if (page->nr_vars == 0) {
|
||||
return cstr_to_string("{}");
|
||||
}
|
||||
if (!recursive) {
|
||||
return cstr_to_string("{ <...> }");
|
||||
}
|
||||
struct string *out = cstr_to_string("{ ");
|
||||
for (int i = 0; i < page->nr_vars; i++) {
|
||||
struct ain_variable *m = &ain->structures[type->struc].members[i];
|
||||
if (i) {
|
||||
string_append_cstr(&out, "; ", 2);
|
||||
}
|
||||
string_append_cstr(&out, m->name, strlen(m->name));
|
||||
string_append_cstr(&out, " = ", 3);
|
||||
struct string *tmp = value_to_string(&m->type, page->values[i], recursive-1);
|
||||
string_append(&out, tmp);
|
||||
free_string(tmp);
|
||||
}
|
||||
string_append_cstr(&out, " }", 2);
|
||||
return out;
|
||||
}
|
||||
case AIN_ARRAY_TYPE:
|
||||
case AIN_REF_ARRAY_TYPE: {
|
||||
struct page *page = heap_get_page(value.i);
|
||||
if (!page || page->nr_vars == 0) {
|
||||
return cstr_to_string("[]");
|
||||
}
|
||||
// get member type
|
||||
struct ain_type t;
|
||||
t.data = variable_type(page, 0, &t.struc, &t.rank);
|
||||
t.array_type = NULL;
|
||||
if (!recursive) {
|
||||
switch (t.data) {
|
||||
case AIN_STRUCT:
|
||||
case AIN_REF_STRUCT:
|
||||
case AIN_ARRAY_TYPE:
|
||||
case AIN_REF_ARRAY_TYPE:
|
||||
return cstr_to_string("[ <...> ]");
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
struct string *out = cstr_to_string("[ ");
|
||||
for (int i = 0; i < page->nr_vars; i++) {
|
||||
if (i) {
|
||||
string_append_cstr(&out, "; ", 2);
|
||||
}
|
||||
struct string *tmp = value_to_string(&t, page->values[i], recursive-1);
|
||||
string_append(&out, tmp);
|
||||
free_string(tmp);
|
||||
}
|
||||
string_append_cstr(&out, " ]", 2);
|
||||
return out;
|
||||
}
|
||||
default:
|
||||
return cstr_to_string("<unsupported-data-type>");
|
||||
}
|
||||
return string_ref(&EMPTY_STRING);
|
||||
}
|
||||
|
||||
static void dbg_cmd_locals(unsigned nr_args, char **args)
|
||||
{
|
||||
unsigned frame_no = nr_args > 0 ? atoi(args[0]) : 0;
|
||||
if (frame_no < 0 || frame_no >= call_stack_ptr) {
|
||||
DBG_ERROR("Invalid frame number: %u", frame_no);
|
||||
return;
|
||||
}
|
||||
|
||||
struct page *page = heap_get_page(call_stack[call_stack_ptr - (frame_no+1)].page_slot);
|
||||
struct ain_function *f = &ain->functions[page->index];
|
||||
for (int i = 0; i < f->nr_vars; i++) {
|
||||
struct string *value = value_to_string(&f->vars[i].type, page->values[i], 1);
|
||||
printf("[%d] %s: %s\n", i, display_sjis0(f->vars[i].name), display_sjis1(value->text));
|
||||
free_string(value);
|
||||
}
|
||||
}
|
||||
|
||||
static void log_handler(struct breakpoint *bp)
|
||||
{
|
||||
struct ain_function *f = bp->data;
|
||||
struct page *locals = local_page();
|
||||
printf("%s(", display_sjis0(f->name));
|
||||
for (int i = 0; i < f->nr_args; i++) {
|
||||
struct string *value = value_to_string(&f->vars[i].type, locals->values[i], 1);
|
||||
printf(i > 0 ? ", %s" : "%s", display_sjis0(value->text));
|
||||
free_string(value);
|
||||
}
|
||||
printf(")\n");
|
||||
}
|
||||
|
||||
static void dbg_cmd_log(unsigned nr_args, char **args)
|
||||
{
|
||||
int fno = ain_get_function(ain, args[0]);
|
||||
if (fno < 0) {
|
||||
DBG_ERROR("No function with name '%s'", args[0]);
|
||||
return;
|
||||
}
|
||||
dbg_set_function_breakpoint(args[0], log_handler, &ain->functions[fno]);
|
||||
}
|
||||
|
||||
struct ain_variable *dbg_get_variable(const char *name, union vm_value *val_out)
|
||||
{
|
||||
struct page *page = local_page();
|
||||
struct ain_function *f = &ain->functions[page->index];
|
||||
for (int i = 0; i < f->nr_vars; i++) {
|
||||
if (!strcmp(f->vars[i].name, name)) {
|
||||
*val_out = page->values[i];
|
||||
return &f->vars[i];
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < ain->nr_globals; i++) {
|
||||
if (!strcmp(ain->globals[i].name, name)) {
|
||||
*val_out = global_get(i);
|
||||
return &ain->globals[i];
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void dbg_cmd_print(unsigned nr_args, char **args)
|
||||
{
|
||||
union vm_value value;
|
||||
struct ain_variable *var = dbg_get_variable(args[0], &value);
|
||||
if (var) {
|
||||
struct string *v = value_to_string(&var->type, value, 1);
|
||||
printf("%s\n", display_sjis0(v->text));
|
||||
free_string(v);
|
||||
return;
|
||||
}
|
||||
// TODO: print functions, etc.
|
||||
DBG_ERROR("Undefined variable: %s", args[0]);
|
||||
}
|
||||
|
||||
static void dbg_cmd_quit(unsigned nr_args, char **args)
|
||||
{
|
||||
dbg_quit();
|
||||
}
|
||||
|
||||
#ifdef HAVE_SCHEME
|
||||
static void dbg_cmd_scheme(unsigned nr_args, char **args)
|
||||
{
|
||||
dbg_scm_repl();
|
||||
}
|
||||
#endif
|
||||
|
||||
static struct dbg_cmd dbg_commands[] = {
|
||||
{ "help", "h", "[command-name] - Display this message", 0, 1, dbg_cmd_help },
|
||||
{ "backtrace", "bt", "- Display stack trace", 0, 0, dbg_cmd_backtrace },
|
||||
{ "breakpoint", "bp", "<function-or-address> - Set breakpoint", 1, 1, dbg_cmd_breakpoint },
|
||||
{ "continue", "c", "- Resume execution", 0, 0, dbg_cmd_continue },
|
||||
{ "locals", "l", "[frame-number] - Print local variables", 0, 1, dbg_cmd_locals },
|
||||
{ "log", NULL, "<function-name> - Log function calls", 1, 1, dbg_cmd_log },
|
||||
{ "print", "p", "<variable-name> - Print a variable", 1, 1, dbg_cmd_print },
|
||||
{ "quit", "q", "- Quit xsystem4", 0, 0, dbg_cmd_quit },
|
||||
#ifdef HAVE_SCHEME
|
||||
{ "scheme", "scm", "- Drop into Scheme REPL", 0, 0, dbg_cmd_scheme },
|
||||
#endif
|
||||
};
|
||||
|
||||
static void dbg_cmd_help(unsigned nr_args, char **args)
|
||||
{
|
||||
NOTICE("Available Commands");
|
||||
NOTICE("------------------");
|
||||
for (unsigned i = 0; i < sizeof(dbg_commands)/sizeof(*dbg_commands); i++) {
|
||||
struct dbg_cmd *cmd = &dbg_commands[i];
|
||||
if (!nr_args || !strcmp(args[0], cmd->fullname) || !strcmp(args[0], cmd->shortname)) {
|
||||
if (cmd->shortname)
|
||||
NOTICE("%s (%s) %s", cmd->fullname, cmd->shortname, cmd->description);
|
||||
else
|
||||
NOTICE("%s %s", cmd->fullname, cmd->description);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Get a command by name.
|
||||
*/
|
||||
static struct dbg_cmd *dbg_get_command(const char *name)
|
||||
{
|
||||
for (unsigned i = 0; i < sizeof(dbg_commands)/sizeof(*dbg_commands); i++) {
|
||||
struct dbg_cmd *cmd = &dbg_commands[i];
|
||||
if (!strcmp(name, cmd->fullname) || (cmd->shortname && !strcmp(name, cmd->shortname)))
|
||||
return cmd;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
#ifdef HAVE_READLINE
|
||||
#include <readline/readline.h>
|
||||
#include <readline/history.h>
|
||||
/*
|
||||
* Get a string from the user. The result can be modified but shouldn't be free'd.
|
||||
*/
|
||||
static char *cmd_gets(void)
|
||||
{
|
||||
static char *line_read = NULL;
|
||||
free(line_read);
|
||||
line_read = readline("dbg(cmd)> ");
|
||||
if (line_read && *line_read)
|
||||
add_history(line_read);
|
||||
return line_read;
|
||||
}
|
||||
#else
|
||||
/*
|
||||
* Get a string from the user. The result can be modified but shouldn't be free'd.
|
||||
*/
|
||||
static char *cmd_gets(void)
|
||||
{
|
||||
static char line[1024];
|
||||
|
||||
printf("dbg(cmd)> ");
|
||||
fflush(stdout);
|
||||
fgets(line, 1024, stdin);
|
||||
return line;
|
||||
}
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Parse a line into an array of words. Modified the input string.
|
||||
*/
|
||||
static char **cmd_parse(char *line, unsigned *nr_words)
|
||||
{
|
||||
static char *words[32];
|
||||
|
||||
*nr_words = 0;
|
||||
while (*line) {
|
||||
// skip whitespace
|
||||
while (*line && isspace(*line)) line++;
|
||||
if (!*line) break;
|
||||
// save word ptr
|
||||
words[(*nr_words)++] = line;
|
||||
// skip word
|
||||
while (*line && !isspace(*line)) line++;
|
||||
if (!*line) break;
|
||||
*line = '\0';
|
||||
line++;
|
||||
}
|
||||
return words;
|
||||
}
|
||||
|
||||
void dbg_cmd_repl(void)
|
||||
{
|
||||
NOTICE("Entering the debugger REPL. Type 'help' for a list of commands.");
|
||||
while (1) {
|
||||
char *line = cmd_gets();
|
||||
if (!line)
|
||||
continue;
|
||||
|
||||
unsigned nr_words;
|
||||
char **words = cmd_parse(line, &nr_words);
|
||||
if (!nr_words)
|
||||
continue;
|
||||
|
||||
struct dbg_cmd *cmd = dbg_get_command(words[0]);
|
||||
if (!cmd) {
|
||||
NOTICE("Invalid command: %s (type 'help' for a list of commands)", words[0]);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((nr_words-1) < cmd->min_args || (nr_words-1) > cmd->max_args) {
|
||||
NOTICE("Wrong number of arguments to '%s' command", cmd->fullname);
|
||||
continue;
|
||||
}
|
||||
|
||||
cmd->run(nr_words-1, words+1);
|
||||
}
|
||||
}
|
||||
@@ -31,8 +31,6 @@
|
||||
#include "debugger.h"
|
||||
#include "little_endian.h"
|
||||
|
||||
#define SCM_ERROR(fmt, ...) sys_warning("ERROR: " fmt "\n", ##__VA_ARGS__)
|
||||
|
||||
bool dbg_enabled = true;
|
||||
|
||||
struct variable {
|
||||
@@ -131,85 +129,53 @@ static struct variable *page_ref(struct page *page, int i)
|
||||
return v;
|
||||
}
|
||||
|
||||
struct breakpoint {
|
||||
enum opcode restore_op;
|
||||
sexp handler;
|
||||
char *message;
|
||||
int count;
|
||||
};
|
||||
|
||||
static struct breakpoint *breakpoints = NULL;
|
||||
static unsigned nr_breakpoints = 0;
|
||||
|
||||
static sexp dbg_ctx;
|
||||
|
||||
static void set_function_breakpoint(const char *_name, sexp handler)
|
||||
static void set_parameter(const char *param, sexp value)
|
||||
{
|
||||
if (!sexp_procedurep(handler) && handler != SEXP_FALSE) {
|
||||
SCM_ERROR("Breakpoint handler must be a procedure or #f");
|
||||
sexp_eval(dbg_ctx, sexp_list2(dbg_ctx, sexp_intern(dbg_ctx, param, -1), value), NULL);
|
||||
}
|
||||
|
||||
static void dbg_scm_breakpoint_handler(struct breakpoint *bp)
|
||||
{
|
||||
set_parameter("breakpoint-count", sexp_make_integer(dbg_ctx, bp->count++));
|
||||
// TODO: breakpoint-function parameter with function name, argument count, etc.
|
||||
|
||||
if ((sexp)bp->data == SEXP_FALSE) {
|
||||
NOTICE("%s", bp->message);
|
||||
dbg_scm_repl();
|
||||
} else {
|
||||
sexp_apply(dbg_ctx, (sexp)bp->data, SEXP_NULL);
|
||||
}
|
||||
}
|
||||
|
||||
static void set_function_breakpoint(const char *name, sexp handler)
|
||||
{
|
||||
if (handler == SEXP_FALSE) {
|
||||
dbg_set_function_breakpoint(name, NULL, NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
char *name = utf2sjis(_name, 0);
|
||||
int fno = ain_get_function(ain, name);
|
||||
free(name);
|
||||
|
||||
if (fno < 0) {
|
||||
SCM_ERROR("No function with name '%s'", _name);
|
||||
if (!sexp_procedurep(handler)) {
|
||||
DBG_ERROR("Breakpoint handler must be a procedure or #f");
|
||||
return;
|
||||
}
|
||||
|
||||
struct ain_function *f = &ain->functions[fno];
|
||||
breakpoints = xrealloc_array(breakpoints, nr_breakpoints, nr_breakpoints+1, sizeof(struct breakpoint));
|
||||
breakpoints[nr_breakpoints].restore_op = LittleEndian_getW(ain->code, f->address);
|
||||
breakpoints[nr_breakpoints].handler = handler;
|
||||
if (handler != SEXP_FALSE)
|
||||
if (dbg_set_function_breakpoint(name, dbg_scm_breakpoint_handler, handler)) {
|
||||
sexp_preserve_object(dbg_ctx, handler);
|
||||
breakpoints[nr_breakpoints].message = xmalloc(512);
|
||||
snprintf(breakpoints[nr_breakpoints].message, 511, "Hit breakpoint at function '%s' (0x%08x)", _name, f->address);
|
||||
LittleEndian_putW(ain->code, f->address, BREAKPOINT + nr_breakpoints);
|
||||
nr_breakpoints++;
|
||||
}
|
||||
}
|
||||
|
||||
static void set_address_breakpoint(uint32_t address, sexp handler)
|
||||
{
|
||||
if (!sexp_procedurep(handler) && handler != SEXP_FALSE) {
|
||||
SCM_ERROR("Breakpoint handler must be a procedure or #f");
|
||||
if (handler == SEXP_FALSE) {
|
||||
dbg_set_address_breakpoint(address, NULL, NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
if (address & 1 || address >= ain->code_size - 2) {
|
||||
SCM_ERROR("Invalid instruction address: 0x%08x", address);
|
||||
if (!sexp_procedurep(handler)) {
|
||||
DBG_ERROR("Breakpoint handler must be a procedure or #f");
|
||||
return;
|
||||
}
|
||||
|
||||
// XXX: this is not very robust; some illegal addresses will slip through
|
||||
// if the data looks like a valid opcode
|
||||
enum opcode op = LittleEndian_getW(ain->code, address);
|
||||
if (op < 0 || op >= NR_OPCODES) {
|
||||
SCM_ERROR("Invalid instruction address: 0x%08x", address);
|
||||
return;
|
||||
}
|
||||
|
||||
breakpoints = xrealloc_array(breakpoints, nr_breakpoints, nr_breakpoints+1, sizeof(struct breakpoint));
|
||||
breakpoints[nr_breakpoints].restore_op = op;
|
||||
breakpoints[nr_breakpoints].handler = handler;
|
||||
if (handler != SEXP_FALSE)
|
||||
if (dbg_set_address_breakpoint(address, dbg_scm_breakpoint_handler, handler)) {
|
||||
sexp_preserve_object(dbg_ctx, handler);
|
||||
breakpoints[nr_breakpoints].message = xmalloc(512);
|
||||
snprintf(breakpoints[nr_breakpoints].message, 511, "Hit breakpoint at 0x%08x", address);
|
||||
LittleEndian_putW(ain->code, address, BREAKPOINT + nr_breakpoints);
|
||||
nr_breakpoints++;
|
||||
}
|
||||
|
||||
static void print_stack_trace(void)
|
||||
{
|
||||
for (int i = call_stack_ptr - 1, j = 0; i >= 0; i--, j++) {
|
||||
struct ain_function *f = &ain->functions[call_stack[i].fno];
|
||||
char *u = sjis2utf(f->name, strlen(f->name));
|
||||
uint32_t addr = (i == call_stack_ptr - 1) ? instr_ptr : call_stack[i+1].call_address;
|
||||
NOTICE("#%d 0x%08x in %s", j, addr, u);
|
||||
free(u);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,7 +193,7 @@ static struct page *frame_page(int i)
|
||||
// autogenerated with `chibi-ffi src/debug-ffi.stub`
|
||||
#include "debug-ffi.c"
|
||||
|
||||
void dbg_init(void)
|
||||
void dbg_scm_init(void)
|
||||
{
|
||||
if (!dbg_enabled)
|
||||
return;
|
||||
@@ -237,7 +203,7 @@ void dbg_init(void)
|
||||
sexp_init_library(dbg_ctx, NULL, 0, sexp_context_env(dbg_ctx), sexp_version, SEXP_ABI_IDENTIFIER);
|
||||
sexp_eval_string(dbg_ctx, "(import (scheme base))", -1, NULL);
|
||||
sexp_eval_string(dbg_ctx, "(import (chibi repl))", -1, NULL);
|
||||
sexp_eval_string(dbg_ctx, "(define *prompt* (make-parameter \"dbg> \"))", -1, NULL);
|
||||
sexp_eval_string(dbg_ctx, "(define *prompt* (make-parameter \"dbg(scm)> \"))", -1, NULL);
|
||||
sexp_eval_string(dbg_ctx, "(define breakpoint-count (make-parameter 0))", -1, NULL);
|
||||
sexp_eval_string(dbg_ctx, "(define (repl-make-prompt m) (*prompt*))", -1, NULL);
|
||||
|
||||
@@ -247,38 +213,19 @@ void dbg_init(void)
|
||||
sexp_eval_string(dbg_ctx, "(load \"" XSYS4_DATA_DIR "/debugger.scm\")", -1, NULL);
|
||||
}
|
||||
|
||||
void dbg_fini(void)
|
||||
void dbg_scm_fini(void)
|
||||
{
|
||||
if (!dbg_enabled)
|
||||
return;
|
||||
sexp_destroy_context(dbg_ctx);
|
||||
}
|
||||
|
||||
void dbg_repl(void)
|
||||
void dbg_scm_repl(void)
|
||||
{
|
||||
if (!dbg_enabled)
|
||||
return;
|
||||
NOTICE("Entering the Scheme debugger REPL. Type '\\exit' to exit.");
|
||||
sexp_eval_string(dbg_ctx, "(repl 'escape: #\\\\ 'make-prompt: repl-make-prompt)", -1, NULL);
|
||||
}
|
||||
|
||||
static void set_parameter(const char *param, sexp value)
|
||||
{
|
||||
sexp_eval(dbg_ctx, sexp_list2(dbg_ctx, sexp_intern(dbg_ctx, param, -1), value), NULL);
|
||||
}
|
||||
|
||||
enum opcode dbg_handle_breakpoint(unsigned bp_no)
|
||||
{
|
||||
assert(bp_no < nr_breakpoints);
|
||||
|
||||
set_parameter("breakpoint-count", sexp_make_integer(dbg_ctx, breakpoints[bp_no].count++));
|
||||
// TODO: breakpoint-function parameter with function name, argument count, etc.
|
||||
|
||||
if (breakpoints[bp_no].handler == SEXP_FALSE) {
|
||||
NOTICE("%s", breakpoints[bp_no].message);
|
||||
dbg_repl();
|
||||
} else {
|
||||
sexp_apply(dbg_ctx, breakpoints[bp_no].handler, SEXP_NULL);
|
||||
}
|
||||
|
||||
return breakpoints[bp_no].restore_op;
|
||||
}
|
||||
+13
-3
@@ -25,6 +25,7 @@ xsystem4 = [version_h,
|
||||
'sprite.c',
|
||||
'system4.c',
|
||||
'text.c',
|
||||
'util.c',
|
||||
'video.c',
|
||||
'vm.c',
|
||||
|
||||
@@ -101,10 +102,19 @@ xsystem4 = [version_h,
|
||||
|
||||
xsystem4_deps = [libm, zlib, sdl2, sdl2ttf, ffi, cglm, sndfile, libsys4_dep] + gl_deps
|
||||
|
||||
if chibi.found()
|
||||
xsystem4_deps += chibi
|
||||
xsystem4 += 'debugger.c'
|
||||
if get_option('debugger').allowed()
|
||||
add_project_arguments('-DDEBUGGER_ENABLED', language : 'c')
|
||||
xsystem4 += 'debug.c'
|
||||
xsystem4 += 'debugger_cmd.c'
|
||||
if chibi.found()
|
||||
xsystem4_deps += chibi
|
||||
xsystem4 += 'debugger_scm.c'
|
||||
add_project_arguments('-DHAVE_SCHEME', language : 'c')
|
||||
endif
|
||||
if readline.found()
|
||||
xsystem4_deps += readline
|
||||
add_project_arguments('-DHAVE_READLINE', language : 'c')
|
||||
endif
|
||||
endif
|
||||
|
||||
executable('xsystem4', xsystem4,
|
||||
|
||||
+4
-76
@@ -22,7 +22,6 @@
|
||||
#include <dirent.h>
|
||||
#include <ctype.h>
|
||||
#include <getopt.h>
|
||||
#include <limits.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "system4.h"
|
||||
@@ -61,75 +60,6 @@ struct config config = {
|
||||
.ex_path = NULL,
|
||||
};
|
||||
|
||||
char *unix_path(const char *path)
|
||||
{
|
||||
char *utf = sjis2utf(path, strlen(path));
|
||||
for (int i = 0; utf[i]; i++) {
|
||||
if (utf[i] == '\\')
|
||||
utf[i] = '/';
|
||||
}
|
||||
return utf;
|
||||
}
|
||||
|
||||
static bool is_absolute_path(const char *path)
|
||||
{
|
||||
#if (defined(_WIN32) || defined(__WIN32__))
|
||||
int i = (isalpha(path[0]) && path[1] == ':') ? 2 : 0;
|
||||
return path[i] == '/' || path[i] == '\\';
|
||||
#else
|
||||
return path[0] == '/';
|
||||
#endif
|
||||
}
|
||||
|
||||
static char *resolve_path(const char *dir, const char *path)
|
||||
{
|
||||
char *utf = unix_path(path);
|
||||
if (is_absolute_path(utf))
|
||||
return utf;
|
||||
|
||||
char *resolved = xmalloc(strlen(dir) + strlen(utf) + 2);
|
||||
strcpy(resolved, dir);
|
||||
strcat(resolved, "/");
|
||||
strcat(resolved, utf);
|
||||
|
||||
free(utf);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
char *gamedir_path(const char *path)
|
||||
{
|
||||
return resolve_path(config.game_dir, path);
|
||||
}
|
||||
|
||||
char *savedir_path(const char *path)
|
||||
{
|
||||
return resolve_path(config.save_dir, path);
|
||||
}
|
||||
|
||||
void get_date(int *year, int *month, int *mday, int *wday)
|
||||
{
|
||||
time_t t = time(NULL);
|
||||
struct tm *tm = localtime(&t);
|
||||
|
||||
*year = tm->tm_year;
|
||||
*month = tm->tm_mon;
|
||||
*mday = tm->tm_mday;
|
||||
*wday = tm->tm_wday;
|
||||
}
|
||||
|
||||
void get_time(int *hour, int *min, int *sec, int *ms)
|
||||
{
|
||||
time_t t = time(NULL);
|
||||
struct tm *tm = localtime(&t);
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_REALTIME, &ts);
|
||||
|
||||
*hour = tm->tm_hour;
|
||||
*min = tm->tm_min;
|
||||
*sec = ts.tv_sec;
|
||||
*ms = ts.tv_nsec / 1000000;
|
||||
}
|
||||
|
||||
static struct string *ini_string(struct ini_entry *entry)
|
||||
{
|
||||
if (entry->value.type != INI_STRING)
|
||||
@@ -381,8 +311,8 @@ static void usage(void)
|
||||
puts(" --font-mincho Specify the path to the mincho font to use");
|
||||
puts(" --font-gothic Specify the path to the gothic font to use");
|
||||
puts(" -j, --joypad Enable joypad");
|
||||
puts(" --nodebug Disable debugger");
|
||||
#ifdef DEBUGGER_ENABLED
|
||||
puts(" --nodebug Disable debugger");
|
||||
puts(" --debug Start in debugger");
|
||||
#endif
|
||||
}
|
||||
@@ -395,8 +325,8 @@ enum {
|
||||
LOPT_FONT_MINCHO,
|
||||
LOPT_FONT_GOTHIC,
|
||||
LOPT_JOYPAD,
|
||||
LOPT_NODEBUG,
|
||||
#ifdef DEBUGGER_ENABLED
|
||||
LOPT_NODEBUG,
|
||||
LOPT_DEBUG,
|
||||
#endif
|
||||
};
|
||||
@@ -423,10 +353,11 @@ int main(int argc, char *argv[])
|
||||
{ "font-mincho", required_argument, 0, LOPT_FONT_MINCHO },
|
||||
{ "font-gothic", required_argument, 0, LOPT_FONT_GOTHIC },
|
||||
{ "joypad", no_argument, 0, LOPT_JOYPAD },
|
||||
{ "nodebug", no_argument, 0, LOPT_NODEBUG },
|
||||
#ifdef DEBUGGER_ENABLED
|
||||
{ "nodebug", no_argument, 0, LOPT_NODEBUG },
|
||||
{ "debug", no_argument, 0, LOPT_DEBUG },
|
||||
#endif
|
||||
{ 0 }
|
||||
};
|
||||
int option_index = 0;
|
||||
int c = getopt_long(argc, argv, "haejv", long_options, &option_index);
|
||||
@@ -467,9 +398,6 @@ int main(int argc, char *argv[])
|
||||
case LOPT_DEBUG:
|
||||
start_in_debugger = true;
|
||||
break;
|
||||
#else
|
||||
case LOPT_NODEBUG:
|
||||
break;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
/* Copyright (C) 2019 Nunuhara Cabbage <nunuhara@haniwa.technology>
|
||||
*
|
||||
* 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 <http://gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "system4.h"
|
||||
#include "system4/utfsjis.h"
|
||||
|
||||
#include "xsystem4.h"
|
||||
|
||||
// In a Windows environment, console output should be SJIS-encoded
|
||||
// (assuming the user is using Japanese non-unicode locale).
|
||||
#if (defined(_WIN32) || defined(__WIN32))
|
||||
#define NATIVE_SJIS
|
||||
#endif
|
||||
|
||||
const char *display_sjis0(const char *sjis)
|
||||
{
|
||||
#ifdef NATIVE_SJIS
|
||||
return sjis;
|
||||
#else
|
||||
static char *utf = NULL;
|
||||
free(utf);
|
||||
utf = sjis2utf(sjis, 0);
|
||||
return utf;
|
||||
#endif
|
||||
}
|
||||
|
||||
const char *display_sjis1(const char *sjis)
|
||||
{
|
||||
#ifdef NATIVE_SJIS
|
||||
return sjis;
|
||||
#else
|
||||
static char *utf = NULL;
|
||||
free(utf);
|
||||
utf = sjis2utf(sjis, 0);
|
||||
return utf;
|
||||
#endif
|
||||
}
|
||||
|
||||
const char *display_utf0(const char *utf)
|
||||
{
|
||||
#ifdef NATIVE_SJIS
|
||||
static char *sjis = NULL;
|
||||
free(sjis);
|
||||
sjis = utf2sjis(utf, 0);
|
||||
return sjis;
|
||||
#else
|
||||
return utf;
|
||||
#endif
|
||||
}
|
||||
|
||||
const char *display_utf1(const char *utf)
|
||||
{
|
||||
#ifdef NATIVE_SJIS
|
||||
static char *sjis = NULL;
|
||||
free(sjis);
|
||||
sjis = utf2sjis(utf, 0);
|
||||
return sjis;
|
||||
#else
|
||||
return utf;
|
||||
#endif
|
||||
}
|
||||
|
||||
char *unix_path(const char *path)
|
||||
{
|
||||
char *utf = sjis2utf(path, strlen(path));
|
||||
for (int i = 0; utf[i]; i++) {
|
||||
if (utf[i] == '\\')
|
||||
utf[i] = '/';
|
||||
}
|
||||
return utf;
|
||||
}
|
||||
|
||||
static bool is_absolute_path(const char *path)
|
||||
{
|
||||
#if (defined(_WIN32) || defined(__WIN32__))
|
||||
int i = (isalpha(path[0]) && path[1] == ':') ? 2 : 0;
|
||||
return path[i] == '/' || path[i] == '\\';
|
||||
#else
|
||||
return path[0] == '/';
|
||||
#endif
|
||||
}
|
||||
|
||||
static char *resolve_path(const char *dir, const char *path)
|
||||
{
|
||||
char *utf = unix_path(path);
|
||||
if (is_absolute_path(utf))
|
||||
return utf;
|
||||
|
||||
char *resolved = xmalloc(strlen(dir) + strlen(utf) + 2);
|
||||
strcpy(resolved, dir);
|
||||
strcat(resolved, "/");
|
||||
strcat(resolved, utf);
|
||||
|
||||
free(utf);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
char *gamedir_path(const char *path)
|
||||
{
|
||||
return resolve_path(config.game_dir, path);
|
||||
}
|
||||
|
||||
char *savedir_path(const char *path)
|
||||
{
|
||||
return resolve_path(config.save_dir, path);
|
||||
}
|
||||
|
||||
void get_date(int *year, int *month, int *mday, int *wday)
|
||||
{
|
||||
time_t t = time(NULL);
|
||||
struct tm *tm = localtime(&t);
|
||||
|
||||
*year = tm->tm_year;
|
||||
*month = tm->tm_mon;
|
||||
*mday = tm->tm_mday;
|
||||
*wday = tm->tm_wday;
|
||||
}
|
||||
|
||||
void get_time(int *hour, int *min, int *sec, int *ms)
|
||||
{
|
||||
time_t t = time(NULL);
|
||||
struct tm *tm = localtime(&t);
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_REALTIME, &ts);
|
||||
|
||||
*hour = tm->tm_hour;
|
||||
*min = tm->tm_min;
|
||||
*sec = ts.tv_sec;
|
||||
*ms = ts.tv_nsec / 1000000;
|
||||
}
|
||||
Reference in New Issue
Block a user