mirror of
https://github.com/nunuhara/xsystem4.git
synced 2026-09-27 09:28:10 +03:00
Previously, the debugger displayed `ref int` variables like this:
ref_int: <unsupported-data-type>
<void>: <unsupported-data-type>
This commit introduces `dbg_variable_to_string()` to dereference these
variables and display their underlying value. Both the command-line and
DAP debugger interfaces have been updated to use this new functionality.
1195 lines
29 KiB
C
1195 lines
29 KiB
C
/* 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/dasm.h"
|
|
#include "system4/file.h"
|
|
#include "system4/hashtable.h"
|
|
#include "system4/little_endian.h"
|
|
#include "system4/string.h"
|
|
#include "system4/utfsjis.h"
|
|
|
|
#include "vm.h"
|
|
#include "vm/heap.h"
|
|
#include "vm/page.h"
|
|
|
|
#include "debugger.h"
|
|
#include "xsystem4.h"
|
|
|
|
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;
|
|
|
|
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)
|
|
{
|
|
dbg_current_frame = 0;
|
|
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 *stop)
|
|
{
|
|
if (dbg_dap)
|
|
dbg_dap_repl(stop);
|
|
else
|
|
dbg_cmd_repl();
|
|
}
|
|
|
|
void dbg_repl(enum dbg_stop_type type, const char *msg)
|
|
{
|
|
if (!dbg_enabled)
|
|
return;
|
|
|
|
struct dbg_stop stop = {
|
|
.type = type,
|
|
.message = msg,
|
|
};
|
|
|
|
dbg_start(_dbg_repl, &stop);
|
|
}
|
|
|
|
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
|
|
dbg_cmd_init();
|
|
#ifdef HAVE_SCHEME
|
|
dbg_scm_init();
|
|
#endif
|
|
if (!dbg_dap && dbg_start_in_debugger) {
|
|
dbg_repl(DBG_STOP_PAUSE, "");
|
|
}
|
|
}
|
|
|
|
void dbg_fini(void)
|
|
{
|
|
#ifdef HAVE_SCHEME
|
|
dbg_scm_fini();
|
|
#endif
|
|
}
|
|
|
|
static struct hash_table *bp_table = NULL;
|
|
|
|
static void add_breakpoint(uint32_t addr, struct breakpoint *bp)
|
|
{
|
|
if (!bp_table)
|
|
bp_table = ht_create(64);
|
|
|
|
struct ht_slot *slot = ht_put_int(bp_table, addr, NULL);
|
|
if (slot->value) {
|
|
WARNING("Overwriting breakpoint at %0x%08x", addr);
|
|
free(slot->value);
|
|
}
|
|
slot->value = bp;
|
|
}
|
|
|
|
static void delete_breakpoint(uint32_t addr, struct breakpoint *bp)
|
|
{
|
|
// restore opcode
|
|
LittleEndian_putW(ain->code, addr, bp->restore_op);
|
|
|
|
// remove from hash table
|
|
struct ht_slot *slot = ht_put_int(bp_table, addr, NULL);
|
|
assert(slot->value == bp);
|
|
slot->value = NULL;
|
|
|
|
free(bp->message);
|
|
free(bp);
|
|
}
|
|
|
|
static struct breakpoint *get_breakpoint(uint32_t addr)
|
|
{
|
|
if (!bp_table)
|
|
return NULL;
|
|
return ht_get_int(bp_table, addr, NULL);
|
|
}
|
|
|
|
bool dbg_clear_breakpoint(uint32_t addr, void(*free_data)(void*))
|
|
{
|
|
struct breakpoint *bp = get_breakpoint(addr);
|
|
if (!bp)
|
|
return false;
|
|
if (free_data)
|
|
free_data(bp->data);
|
|
delete_breakpoint(addr, bp);
|
|
return true;
|
|
}
|
|
|
|
struct dbg_foreach_data {
|
|
void (*fun)(int addr, struct breakpoint*, void *data);
|
|
void *data;
|
|
};
|
|
|
|
void dbg_foreach_breakpoint_cb(struct ht_slot *slot, void *d_)
|
|
{
|
|
if (!slot->value)
|
|
return;
|
|
struct dbg_foreach_data *d = d_;
|
|
d->fun(slot->ikey, slot->value, d->data);
|
|
}
|
|
|
|
void dbg_foreach_breakpoint(void (*fun)(int addr, struct breakpoint*, void *data), void *data)
|
|
{
|
|
if (!bp_table)
|
|
return;
|
|
struct dbg_foreach_data d = { fun, data };
|
|
ht_foreach(bp_table, dbg_foreach_breakpoint_cb, &d);
|
|
}
|
|
|
|
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'", display_sjis0(_name));
|
|
return false;
|
|
}
|
|
|
|
struct ain_function *f = &ain->functions[fno];
|
|
struct breakpoint *bp = xcalloc(1, sizeof(struct breakpoint));
|
|
bp->restore_op = LittleEndian_getW(ain->code, f->address);
|
|
bp->cb = cb;
|
|
bp->data = data;
|
|
bp->message = xmalloc(512);
|
|
snprintf(bp->message, 511, "Hit breakpoint at function '%s' (0x%08x)",
|
|
display_utf0(_name), f->address);
|
|
LittleEndian_putW(ain->code, f->address, BREAKPOINT | bp->restore_op);
|
|
add_breakpoint(f->address, bp);
|
|
|
|
log_message("debug", "Set breakpoint at function '%s' (0x%08x)\n", display_utf0(_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;
|
|
}
|
|
|
|
struct breakpoint *bp = xcalloc(1, sizeof(struct breakpoint));
|
|
bp->restore_op = op;
|
|
bp->cb = cb;
|
|
bp->data = data;
|
|
bp->message = xmalloc(512);
|
|
snprintf(bp->message, 511, "Hit breakpoint at 0x%08x", address);
|
|
LittleEndian_putW(ain->code, address, BREAKPOINT | bp->restore_op);
|
|
add_breakpoint(address, bp);
|
|
|
|
log_message("debug", "Set breakpoint at 0x%08x\n", address);
|
|
return true;
|
|
}
|
|
|
|
static void dbg_step_breakpoint_cb(struct breakpoint *bp)
|
|
{
|
|
// XXX: mutually recursive functions could trigger breakpoint early.
|
|
// use size of call stack to check for this case
|
|
if ((intptr_t)bp->data != call_stack_ptr)
|
|
return;
|
|
delete_breakpoint(instr_ptr, bp);
|
|
struct dbg_stop stop = { DBG_STOP_STEP, NULL };
|
|
_dbg_repl(&stop);
|
|
}
|
|
|
|
static void dbg_set_step_breakpoint(int32_t address, int call_index)
|
|
{
|
|
// if a breakpoint is already set on the next address, leave it
|
|
enum opcode op = LittleEndian_getW(ain->code, address);
|
|
if ((op & OPTYPE_MASK) == BREAKPOINT)
|
|
return;
|
|
|
|
struct breakpoint *bp = xcalloc(1, sizeof(struct breakpoint));
|
|
bp->restore_op = op & ~OPTYPE_MASK;
|
|
assert(bp->restore_op >= 0 && bp->restore_op < NR_OPCODES);
|
|
bp->cb = dbg_step_breakpoint_cb;
|
|
bp->data = (void*)(intptr_t)call_index;
|
|
bp->message = NULL;
|
|
LittleEndian_putW(ain->code, address, BREAKPOINT | bp->restore_op);
|
|
add_breakpoint(address, bp);
|
|
}
|
|
|
|
static int32_t get_function_address(int fno)
|
|
{
|
|
assert(fno > 0 && fno < ain->nr_functions);
|
|
return ain->functions[fno].address;
|
|
}
|
|
|
|
/* Determine the next address at the current instruction. */
|
|
static int32_t dbg_next_address(bool into, int *call_index)
|
|
{
|
|
*call_index = call_stack_ptr;
|
|
enum opcode current = LittleEndian_getW(ain->code, instr_ptr) & ~OPTYPE_MASK;
|
|
assert(current >= 0 && current < NR_OPCODES);
|
|
switch (current) {
|
|
case JUMP:
|
|
return get_argument(0);
|
|
case IFZ:
|
|
if (stack_peek(0).i == 0)
|
|
return get_argument(0);
|
|
break;
|
|
case IFNZ:
|
|
if (stack_peek(0).i)
|
|
return get_argument(0);
|
|
break;
|
|
case RETURN: {
|
|
*call_index = call_stack_ptr-1;
|
|
return call_stack[call_stack_ptr-1].return_address;
|
|
}
|
|
case _MSG:
|
|
if (!into)
|
|
break;
|
|
// TODO: step into message function
|
|
return -1;
|
|
case SWITCH:
|
|
return get_switch_address(get_argument(0), stack_peek(0).i);
|
|
case STRSWITCH:
|
|
return get_strswitch_address(get_argument(0), heap_get_string(stack_peek(0).i));
|
|
case SJUMP: {
|
|
if (!into)
|
|
break;
|
|
// XXX: can't determine RETURN address of scenario call (VM_RETURN)
|
|
return -1;
|
|
/*
|
|
int fno = heap[stack_peek(0).i].page->index;
|
|
return ain->functions[fno].address;
|
|
*/
|
|
}
|
|
case CALLFUNC:
|
|
case CALLMETHOD:
|
|
case THISCALLMETHOD_NOPARAM:
|
|
if (!into)
|
|
break;
|
|
*call_index = call_stack_ptr + 1;
|
|
return get_function_address(get_argument(0));
|
|
case CALLFUNC2:
|
|
if (!into)
|
|
break;
|
|
*call_index = call_stack_ptr + 1;
|
|
return get_function_address(stack_peek(1).i);
|
|
case SH_IF_LOC_LT_IMM:
|
|
if (local_get(get_argument(0)).i < get_argument(1))
|
|
return get_argument(2);
|
|
break;
|
|
case SH_IF_LOC_GE_IMM:
|
|
if (local_get(get_argument(0)).i >= get_argument(1))
|
|
return get_argument(2);
|
|
break;
|
|
case SH_IF_STRUCTREF_NE_LOCALREF:
|
|
if (member_get(get_argument(0)).i != local_get(get_argument(1)).i)
|
|
return get_argument(2);
|
|
break;
|
|
case SH_IF_STRUCTREF_GT_IMM:
|
|
if (member_get(get_argument(0)).i > get_argument(1))
|
|
return get_argument(2);
|
|
break;
|
|
case SH_STRUCTREF2_CALLMETHOD_NO_PARAM:
|
|
if (!into)
|
|
break;
|
|
*call_index = call_stack_ptr + 1;
|
|
return get_function_address(get_argument(2));
|
|
case SH_IF_STRUCTREF_Z:
|
|
if (!member_get(get_argument(0)).i)
|
|
return get_argument(1);
|
|
break;
|
|
case SH_IF_STRUCT_A_NOT_EMPTY: {
|
|
struct page *array = heap_get_page(member_get(get_argument(0)).i);
|
|
if (array && array->nr_vars)
|
|
return get_argument(1);
|
|
break;
|
|
}
|
|
case SH_IF_LOC_GT_IMM:
|
|
if (local_get(get_argument(0)).i > get_argument(1))
|
|
return get_argument(2);
|
|
break;
|
|
case SH_IF_STRUCTREF_NE_IMM:
|
|
if (member_get(get_argument(0)).i != get_argument(1))
|
|
return get_argument(2);
|
|
break;
|
|
case SH_IF_STRUCTREF_EQ_IMM:
|
|
if (member_get(get_argument(0)).i == get_argument(1))
|
|
return get_argument(2);
|
|
break;
|
|
case SH_IF_SREF_NE_STR0: {
|
|
struct string *a = heap_get_string(stack_peek_var()->i);
|
|
struct string *b = ain->strings[get_argument(0)];
|
|
if (strcmp(a->text, b->text))
|
|
return get_argument(1);
|
|
break;
|
|
}
|
|
case DG_CALL: {
|
|
if (!into)
|
|
return get_argument(1);
|
|
// XXX: can't determine RETURN address of delegate call (VM_RETURN)
|
|
return -1;
|
|
}
|
|
default:
|
|
// XXX: catch any unhandled control-flow instructions
|
|
if (instructions[current].ip_inc == 0)
|
|
return -1;
|
|
break;
|
|
}
|
|
return instr_ptr + instruction_width(current);
|
|
}
|
|
|
|
bool dbg_set_step_over_breakpoint(void)
|
|
{
|
|
if (instr_ptr <= 0)
|
|
return false;
|
|
|
|
int call_index;
|
|
int32_t address = dbg_next_address(false, &call_index);
|
|
if (address < 0)
|
|
return false;
|
|
dbg_set_step_breakpoint(address, call_index);
|
|
return true;
|
|
}
|
|
|
|
bool dbg_set_step_into_breakpoint(void)
|
|
{
|
|
if (instr_ptr <= 0)
|
|
return false;
|
|
|
|
int call_index;
|
|
int32_t address = dbg_next_address(true, &call_index);
|
|
if (address < 0)
|
|
return false;
|
|
dbg_set_step_breakpoint(address, call_index);
|
|
return true;
|
|
}
|
|
|
|
bool dbg_set_finish_breakpoint(void)
|
|
{
|
|
if (call_stack_ptr < 2)
|
|
return false;
|
|
|
|
int32_t address = call_stack[call_stack_ptr-1].return_address;
|
|
// XXX: VM_RETURN
|
|
if (address < 0)
|
|
return false;
|
|
dbg_set_step_breakpoint(address, call_stack_ptr - 1);
|
|
return true;
|
|
}
|
|
|
|
static void _dbg_handle_breakpoint(void *data)
|
|
{
|
|
struct breakpoint *bp = data;
|
|
if (bp->cb) {
|
|
bp->cb(bp);
|
|
} else {
|
|
log_message("debug", "%s\n", bp->message);
|
|
struct dbg_stop stop = { DBG_STOP_BREAKPOINT, NULL };
|
|
_dbg_repl(&stop);
|
|
}
|
|
}
|
|
|
|
void dbg_handle_breakpoint(void)
|
|
{
|
|
struct breakpoint *bp = get_breakpoint(instr_ptr);
|
|
if (!bp) {
|
|
WARNING("Unregistered breakpoint");
|
|
return;
|
|
}
|
|
dbg_start(_dbg_handle_breakpoint, bp);
|
|
}
|
|
|
|
void dbg_print_frame(unsigned no)
|
|
{
|
|
if (no >= call_stack_ptr) {
|
|
DBG_ERROR("Invalid frame number: %d", no);
|
|
return;
|
|
}
|
|
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", 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)
|
|
{
|
|
for (int i = 0; i < call_stack_ptr; i++) {
|
|
dbg_print_frame(i);
|
|
}
|
|
}
|
|
|
|
struct parse_object {
|
|
struct string *ident;
|
|
int index;
|
|
};
|
|
|
|
static void free_parse_objects(struct parse_object *objects, unsigned n)
|
|
{
|
|
for (unsigned i = 0; i < n; i++) {
|
|
free_string(objects[i].ident);
|
|
}
|
|
free(objects);
|
|
}
|
|
|
|
static struct parse_object *dbg_parse_expression(const char *expr, unsigned *nr_out)
|
|
{
|
|
struct parse_object *objects = NULL;
|
|
unsigned nr_objects = 0;
|
|
|
|
// split on '.' character
|
|
int start = 0;
|
|
for (int i = 0; ; i++) {
|
|
if (expr[i] && expr[i] != '.')
|
|
continue;
|
|
// FIXME: allow leading and trailing whitespace
|
|
struct string *name = make_string(expr+start, i - start);
|
|
objects = xrealloc_array(objects, nr_objects, nr_objects+1, sizeof(struct parse_object));
|
|
objects[nr_objects++] = (struct parse_object) {
|
|
.ident = name,
|
|
.index = -1
|
|
};
|
|
start = i + 1;
|
|
if (!expr[i])
|
|
break;
|
|
}
|
|
|
|
// parse array subscripts
|
|
for (unsigned i = 0; i < nr_objects; i++) {
|
|
struct string *s = objects[i].ident;
|
|
// catch ".."
|
|
if (!s->size) {
|
|
DBG_ERROR("Empty variable name");
|
|
goto error;
|
|
}
|
|
|
|
// get location of subscript
|
|
char *p = strchr(s->text, '[');
|
|
if (!p)
|
|
continue;
|
|
|
|
// catch text following subscript
|
|
if (s->text[s->size - 1] != ']') {
|
|
DBG_ERROR("Text after array subscript: '%s'", s->text);
|
|
goto error;
|
|
}
|
|
|
|
// get subscript
|
|
unsigned p_i = p - s->text;
|
|
struct string *subscript = make_string(p+1, s->size - (p_i + 2));
|
|
struct ain_type subscript_type;
|
|
union vm_value value = dbg_eval_string(subscript->text, &subscript_type);
|
|
free_string(subscript);
|
|
|
|
// check subscript
|
|
if (subscript_type.data != AIN_INT) {
|
|
DBG_ERROR("Array subscript is not an integer");
|
|
goto error;
|
|
}
|
|
if (value.i < 0) {
|
|
DBG_ERROR("Negative array subscript");
|
|
goto error;
|
|
}
|
|
|
|
objects[i].index = value.i;
|
|
|
|
s->text[p_i] = '\0';
|
|
s->size = p_i;
|
|
}
|
|
|
|
*nr_out = nr_objects;
|
|
return objects;
|
|
error:
|
|
free_parse_objects(objects, nr_objects);
|
|
return NULL;
|
|
}
|
|
|
|
static struct ain_variable *dbg_get_local(const char *name, union vm_value *val_out)
|
|
{
|
|
struct page *page = get_local_page(dbg_current_frame);
|
|
if (!page)
|
|
return NULL;
|
|
assert(page->type == LOCAL_PAGE);
|
|
assert(page->index >= 0 && page->index < ain->nr_functions);
|
|
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];
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static struct ain_variable *dbg_get_global(const char *name, union vm_value *val_out)
|
|
{
|
|
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 union vm_value dbg_eval_subscript(struct parse_object *object, union vm_value value,
|
|
struct ain_type *type)
|
|
{
|
|
if (object->index < 0)
|
|
return value;
|
|
switch (type->data) {
|
|
case AIN_ARRAY_TYPE:
|
|
break;
|
|
default:
|
|
DBG_ERROR("Array subscript given for non-array: '%s'", object->ident->text);
|
|
goto error;
|
|
break;
|
|
}
|
|
|
|
struct page *page = heap_get_page(value.i);
|
|
assert(page && page->type == ARRAY_PAGE);
|
|
if (object->index >= page->nr_vars) {
|
|
DBG_ERROR("Array subscript out of bounds: %d (size=%d)", object->index, page->nr_vars);
|
|
goto error;
|
|
}
|
|
|
|
if (page->array.rank > 1) {
|
|
type->rank--;
|
|
} else {
|
|
type->data = array_type(page->index);
|
|
type->struc = page->array.struct_type;
|
|
type->rank = 0;
|
|
}
|
|
return page->values[object->index];
|
|
|
|
error:
|
|
type->data = AIN_VOID;
|
|
return (union vm_value) { .i = 0 };
|
|
}
|
|
|
|
static union vm_value dbg_eval_object(struct parse_object *object, struct ain_type *type_out)
|
|
{
|
|
if (!strcmp(object->ident->text, "this")) {
|
|
int32_t page_i = call_stack[call_stack_ptr- (dbg_current_frame + 1)].struct_page;
|
|
if (page_i < 0) {
|
|
DBG_ERROR("'this' used outside of method");
|
|
goto error;
|
|
}
|
|
struct page *page = heap_get_page(page_i);
|
|
assert(page && page->type == STRUCT_PAGE);
|
|
assert(page->index >= 0 && page->index < ain->nr_structures);
|
|
type_out->data = AIN_STRUCT;
|
|
type_out->struc = page->index;
|
|
type_out->rank = 0;
|
|
return dbg_eval_subscript(object, (union vm_value){.i=page_i}, type_out);
|
|
}
|
|
|
|
char *endptr;
|
|
long i = strtol(object->ident->text, &endptr, 0);
|
|
if (*endptr == '\0') {
|
|
type_out->data = AIN_INT;
|
|
type_out->struc = -1;
|
|
type_out->rank = 0;
|
|
return (union vm_value) { .i = i };
|
|
}
|
|
|
|
union vm_value value;
|
|
struct ain_variable *var;
|
|
if ((var = dbg_get_local(object->ident->text, &value))) {
|
|
*type_out = var->type;
|
|
return dbg_eval_subscript(object, value, type_out);
|
|
}
|
|
if ((var = dbg_get_global(object->ident->text, &value))) {
|
|
*type_out = var->type;
|
|
return dbg_eval_subscript(object, value, type_out);
|
|
}
|
|
DBG_ERROR("Unbound variable: '%s'", object->ident->text);
|
|
error:
|
|
type_out->data = AIN_VOID;
|
|
return (union vm_value) { .i = 0 };
|
|
}
|
|
|
|
static union vm_value dbg_eval_member(struct parse_object *object, union vm_value value,
|
|
struct ain_type *type)
|
|
{
|
|
if (type->data != AIN_STRUCT) {
|
|
DBG_ERROR("Attempt to access member '%s' in non-struct", object->ident->text);
|
|
goto error;
|
|
}
|
|
|
|
// get struct page
|
|
struct page *page = heap_get_page(value.i);
|
|
assert(page && page->type == STRUCT_PAGE);
|
|
assert(page->index >= 0 && page->index < ain->nr_structures);
|
|
assert(page->index == type->struc);
|
|
struct ain_struct *struc = &ain->structures[page->index];
|
|
|
|
// get index of member
|
|
int index = -1;
|
|
for (int i = 0; i < struc->nr_members; i++) {
|
|
if (!strcmp(struc->members[i].name, object->ident->text)) {
|
|
index = i;
|
|
break;
|
|
}
|
|
}
|
|
if (index < 0) {
|
|
DBG_ERROR("Member '%s' does not exist in struct %s", object->ident->text,
|
|
struc->name);
|
|
goto error;
|
|
}
|
|
|
|
*type = struc->members[index].type;
|
|
return dbg_eval_subscript(object, page->values[index], type);
|
|
error:
|
|
type->data = AIN_VOID;
|
|
return (union vm_value) { .i = 0 };
|
|
}
|
|
|
|
static union vm_value dbg_eval_expression(struct parse_object *objects, unsigned nr_objects,
|
|
struct ain_type *type_out)
|
|
{
|
|
if (nr_objects < 1) {
|
|
DBG_ERROR("Empty expression");
|
|
goto error;
|
|
}
|
|
|
|
struct ain_type type;
|
|
union vm_value value = dbg_eval_object(&objects[0], &type);
|
|
if (type.data == AIN_VOID)
|
|
goto error;
|
|
|
|
for (unsigned i = 1; i < nr_objects; i++) {
|
|
value = dbg_eval_member(&objects[i], value, &type);
|
|
if (type.data == AIN_VOID)
|
|
goto error;
|
|
}
|
|
|
|
*type_out = type;
|
|
return value;
|
|
error:
|
|
type_out->data = AIN_VOID;
|
|
return (union vm_value) { .i = 0 };
|
|
}
|
|
|
|
union vm_value dbg_eval_string(const char *str, struct ain_type *type_out)
|
|
{
|
|
// parse string
|
|
unsigned nr_objects;
|
|
struct parse_object *objects = dbg_parse_expression(str, &nr_objects);
|
|
if (!objects) {
|
|
type_out->data = AIN_VOID;
|
|
return (union vm_value) { .i = 0 };
|
|
}
|
|
|
|
// evaluate parsed expression
|
|
union vm_value value = dbg_eval_expression(objects, nr_objects, type_out);
|
|
free_parse_objects(objects, nr_objects);
|
|
return value;
|
|
}
|
|
|
|
static void dbg_indent(struct string **s, int indent_level)
|
|
{
|
|
for (int i = 0; i < indent_level; i++) {
|
|
string_append_cstr(s, "\t", 1);
|
|
}
|
|
}
|
|
static struct string *_dbg_value_to_string(struct ain_type *type, union vm_value value,
|
|
int recursive, int indent);
|
|
static struct string *_dbg_variable_to_string(
|
|
struct ain_type *type, struct page *page, int slot, int recursive, int indent);
|
|
|
|
static struct string *dbg_string_to_string(struct string *str)
|
|
{
|
|
struct string *out = cstr_to_string("\"");
|
|
string_append(&out, str);
|
|
string_push_back(&out, '"');
|
|
return out;
|
|
}
|
|
|
|
static struct string *dbg_struct_to_string(struct ain_type *type, union vm_value value,
|
|
int recursive, int indent)
|
|
{
|
|
if (value.i < 0)
|
|
return cstr_to_string("NULL");
|
|
|
|
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("{\n");
|
|
for (int i = 0; i < page->nr_vars; i++) {
|
|
struct ain_variable *m = &ain->structures[type->struc].members[i];
|
|
if (m->type.data == AIN_VOID)
|
|
continue;
|
|
dbg_indent(&out, indent + 1);
|
|
string_append_cstr(&out, m->name, strlen(m->name));
|
|
string_append_cstr(&out, " = ", 3);
|
|
struct string *tmp = _dbg_variable_to_string(&m->type, page, i,
|
|
recursive - 1, indent + 1);
|
|
string_append(&out, tmp);
|
|
free_string(tmp);
|
|
string_append_cstr(&out, ";\n", 2);
|
|
}
|
|
dbg_indent(&out, indent);
|
|
string_push_back(&out, '}');
|
|
return out;
|
|
}
|
|
|
|
static struct string *dbg_array_to_string(struct ain_type *type, union vm_value value,
|
|
int recursive, int indent)
|
|
{
|
|
if (value.i < 0)
|
|
return cstr_to_string("[]");
|
|
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("[\n");
|
|
for (int i = 0; i < page->nr_vars; i++) {
|
|
dbg_indent(&out, indent + 1);
|
|
struct string *tmp = _dbg_value_to_string(&t, page->values[i],
|
|
recursive - 1, indent + 1);
|
|
string_append(&out, tmp);
|
|
free_string(tmp);
|
|
string_append_cstr(&out, ";\n", 2);
|
|
}
|
|
dbg_indent(&out, indent);
|
|
string_push_back(&out, ']');
|
|
return out;
|
|
}
|
|
|
|
static struct string *_dbg_value_to_string(struct ain_type *type, union vm_value value,
|
|
int recursive, int indent)
|
|
{
|
|
switch (type->data) {
|
|
case AIN_INT:
|
|
case AIN_LONG_INT:
|
|
return integer_to_string(value.i);
|
|
case AIN_FLOAT:
|
|
return float_to_string(value.f, 6);
|
|
case AIN_BOOL:
|
|
return cstr_to_string(value.i ? "true" : "false");
|
|
case AIN_STRING:
|
|
return dbg_string_to_string(heap_get_string(value.i));
|
|
case AIN_STRUCT:
|
|
case AIN_REF_STRUCT:
|
|
return dbg_struct_to_string(type, value, recursive, indent);
|
|
case AIN_ARRAY_TYPE:
|
|
case AIN_REF_ARRAY_TYPE:
|
|
return dbg_array_to_string(type, value, recursive, indent);
|
|
default:
|
|
return cstr_to_string("<unsupported-data-type>");
|
|
}
|
|
return string_ref(&EMPTY_STRING);
|
|
|
|
}
|
|
|
|
struct string *dbg_value_to_string(struct ain_type *type, union vm_value value, int recursive)
|
|
{
|
|
return _dbg_value_to_string(type, value, recursive, 0);
|
|
}
|
|
|
|
static struct string *_dbg_variable_to_string(
|
|
struct ain_type *type, struct page *page, int slot, int recursive, int indent)
|
|
{
|
|
enum ain_data_type value_type;
|
|
switch (type->data) {
|
|
case AIN_REF_INT: value_type = AIN_INT; break;
|
|
case AIN_REF_LONG_INT: value_type = AIN_LONG_INT; break;
|
|
case AIN_REF_BOOL: value_type = AIN_BOOL; break;
|
|
case AIN_REF_FLOAT: value_type = AIN_FLOAT; break;
|
|
default:
|
|
return _dbg_value_to_string(type, page->values[slot], recursive, indent);
|
|
}
|
|
|
|
// get referenced value
|
|
int pageno = page->values[slot].i;
|
|
int varno = page->values[slot + 1].i;
|
|
if (pageno < 0) {
|
|
return cstr_to_string("NULL");
|
|
}
|
|
struct ain_type t = {.data = value_type};
|
|
return _dbg_value_to_string(&t, heap[pageno].page->values[varno], recursive, indent);
|
|
}
|
|
|
|
struct string *dbg_variable_to_string(struct ain_type *type, struct page *page, int slot, int recursive)
|
|
{
|
|
return _dbg_variable_to_string(type, page, slot, recursive, 0);
|
|
}
|
|
|
|
// the maximum number of instructions preceeding the instruction pointer to be displayed
|
|
#define DASM_REWIND 16
|
|
// the number of instructions following the instruction pointer to be displayed
|
|
#define DASM_FWD 8
|
|
|
|
// Rewind to DASM_REWIND instructions before the current instruction pointer.
|
|
static bool dbg_init_dasm(struct dasm *dasm)
|
|
{
|
|
if (call_stack_ptr < 1)
|
|
goto error;
|
|
|
|
unsigned addr_i = 0;
|
|
size_t addr[DASM_REWIND] = {0};
|
|
int fno = call_stack[call_stack_ptr-1].fno;
|
|
|
|
dasm_init(dasm, ain);
|
|
dasm_jump(dasm, ain->functions[fno].address - 6);
|
|
|
|
for (; dasm_addr(dasm) < instr_ptr && !dasm_eof(dasm); dasm_next(dasm)) {
|
|
addr[addr_i++ % DASM_REWIND] = dasm_addr(dasm);
|
|
}
|
|
|
|
if (dasm_addr(dasm) != instr_ptr) {
|
|
goto error;
|
|
}
|
|
|
|
addr_i = (addr_i+1) % DASM_REWIND;
|
|
if (addr[addr_i]) {
|
|
dasm_jump(dasm, addr[addr_i]);
|
|
} else if (addr[0]) {
|
|
dasm_jump(dasm, addr[0]);
|
|
}
|
|
|
|
return true;
|
|
error:
|
|
DBG_ERROR("Couldn't locate instruction pointer from current function");
|
|
return false;
|
|
}
|
|
|
|
static void dbg_print_string(const char *str)
|
|
{
|
|
// TODO: escape
|
|
printf("\"%s\"", display_sjis0(str));
|
|
}
|
|
|
|
static void dbg_print_identifier(const char *str)
|
|
{
|
|
if (strchr(str, ' '))
|
|
dbg_print_string(str);
|
|
else
|
|
printf("%s", display_sjis0(str));
|
|
}
|
|
|
|
static void dbg_print_function_name(struct ain_function *fun)
|
|
{
|
|
int i = ain_get_function_index(ain, fun);
|
|
char *name = fun->name;
|
|
char buf[512];
|
|
if (i > 0) {
|
|
snprintf(buf, 512, "%s#%d", fun->name, i);
|
|
name = buf;
|
|
}
|
|
dbg_print_identifier(name);
|
|
}
|
|
|
|
static void dbg_print_local(struct dasm *dasm, int32_t n)
|
|
{
|
|
int fno = dasm_function(dasm);
|
|
if (fno < 0 || fno >= ain->nr_functions) {
|
|
printf("%d", n);
|
|
return;
|
|
}
|
|
|
|
struct ain_function *f = &ain->functions[fno];
|
|
if (n < 0 || n >= f->nr_vars) {
|
|
printf("<invalid local variable number: %d>", n);
|
|
return;
|
|
}
|
|
|
|
int dup_no = 0;
|
|
for (int i = 0; i < f->nr_vars; i++) {
|
|
if (i == n)
|
|
break;
|
|
if (!strcmp(f->vars[i].name, f->vars[n].name))
|
|
dup_no++;
|
|
}
|
|
|
|
char *name;
|
|
char buf[512];
|
|
if (dup_no) {
|
|
snprintf(buf, 512, "%s#%d", f->vars[n].name, dup_no);
|
|
name = buf;
|
|
} else {
|
|
name = f->vars[n].name;
|
|
}
|
|
|
|
dbg_print_identifier(name);
|
|
}
|
|
|
|
static void dbg_print_arg(struct dasm *dasm, int n)
|
|
{
|
|
static int hll = 0;
|
|
int32_t value = dasm_arg(dasm, n);
|
|
switch (dasm_arg_type(dasm, n)) {
|
|
case T_INT:
|
|
case T_SWITCH:
|
|
printf("%d", value);
|
|
break;
|
|
case T_FLOAT: {
|
|
union { int32_t i; float f; } cast = { .i = value };
|
|
printf("%f", cast.f);
|
|
break;
|
|
}
|
|
case T_ADDR:
|
|
printf("0x%08x", value);
|
|
break;
|
|
case T_FUNC:
|
|
if (value < 0 || value >= ain->nr_functions)
|
|
printf("<invalid function number: %d>", value);
|
|
else
|
|
dbg_print_function_name(&ain->functions[value]);
|
|
break;
|
|
case T_DLG:
|
|
if (value < 0 || value >= ain->nr_delegates)
|
|
printf("<invalid delegate number: %d>", value);
|
|
else
|
|
dbg_print_identifier(ain->delegates[value].name);
|
|
break;
|
|
case T_STRING:
|
|
if (value < 0 || value >= ain->nr_strings)
|
|
printf("<invalid string number: %d>", value);
|
|
else
|
|
dbg_print_string(ain->strings[value]->text);
|
|
break;
|
|
case T_MSG:
|
|
if (value < 0 || value >= ain->nr_messages)
|
|
printf("<invalid message number: %d>", value);
|
|
else
|
|
printf("%d ; %s", value, display_sjis0(ain->messages[value]->text));
|
|
break;
|
|
case T_LOCAL:
|
|
dbg_print_local(dasm, value);
|
|
break;
|
|
case T_GLOBAL:
|
|
if (value < 0 || value >= ain->nr_globals)
|
|
printf("<invalid global number: %d>", value);
|
|
else
|
|
dbg_print_identifier(ain->globals[value].name);
|
|
break;
|
|
case T_STRUCT:
|
|
if (value < 0 || value >= ain->nr_structures)
|
|
printf("<invalid struct number: %d>", value);
|
|
else
|
|
dbg_print_identifier(ain->structures[value].name);
|
|
break;
|
|
case T_SYSCALL:
|
|
if (value < 0 || value >= NR_SYSCALLS || !syscalls[value].name)
|
|
printf("<invalid syscall number: %d>", value);
|
|
else
|
|
printf("%s", syscalls[value].name);
|
|
break;
|
|
case T_HLL:
|
|
if (value < 0 || value >= ain->nr_libraries) {
|
|
printf("<invalid library number: %d>", value);
|
|
} else {
|
|
dbg_print_identifier(ain->libraries[value].name);
|
|
hll = value;
|
|
}
|
|
break;
|
|
case T_HLLFUNC:
|
|
if (hll < 0 || hll >= ain->nr_libraries)
|
|
printf("%d", value);
|
|
else if (value < 0 || value >= ain->libraries[hll].nr_functions)
|
|
printf("<invalid library function number: %d>", value);
|
|
else
|
|
dbg_print_identifier(ain->libraries[hll].functions[value].name);
|
|
break;
|
|
case T_FILE:
|
|
if (!ain->nr_filenames)
|
|
printf("%d", value);
|
|
else if (value < 0 || value >= ain->nr_filenames)
|
|
printf("<invalid file number: %d>", value);
|
|
else
|
|
dbg_print_identifier(ain->filenames[value]);
|
|
break;
|
|
default:
|
|
printf("<invalid arg type: %d>", dasm_arg_type(dasm, n));
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void dbg_print_instruction(struct dasm *dasm)
|
|
{
|
|
char c = dasm_addr(dasm) == instr_ptr ? '*' : ' ';
|
|
printf("%c 0x%08x: %s", c, (unsigned)dasm_addr(dasm), dasm_instruction(dasm)->name);
|
|
for (int i = 0; i < dasm_nr_args(dasm); i++) {
|
|
putchar(' ');
|
|
dbg_print_arg(dasm, i);
|
|
}
|
|
}
|
|
|
|
static bool stack_print_finished(int stack_i)
|
|
{
|
|
return stack_i >= stack_ptr;
|
|
}
|
|
|
|
static bool dasm_print_finished(struct dasm *dasm, int dasm_i)
|
|
{
|
|
if (dasm_eof(dasm))
|
|
return true;
|
|
if (dasm_addr(dasm) < instr_ptr)
|
|
return false;
|
|
if (dasm_i < DASM_FWD)
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
#define STACK_MAX (DASM_REWIND + DASM_FWD)
|
|
|
|
void dbg_print_vm_state(void)
|
|
{
|
|
if (call_stack_ptr < 1) {
|
|
DBG_ERROR("VM not running");
|
|
return;
|
|
}
|
|
|
|
puts("");
|
|
puts(" Stack Disassembly");
|
|
puts("----------------- -----------");
|
|
|
|
struct dasm dasm;
|
|
if (!dbg_init_dasm(&dasm))
|
|
return;
|
|
|
|
int stack_i = 0;
|
|
if (stack_ptr > STACK_MAX)
|
|
stack_i = stack_ptr - STACK_MAX;
|
|
|
|
int dasm_i = 0;
|
|
while (!stack_print_finished(stack_i) || !dasm_print_finished(&dasm, dasm_i)) {
|
|
// print stack line
|
|
if (stack_i < stack_ptr) {
|
|
printf(" [%02d]: 0x%08x ", stack_i, stack[stack_i].i);
|
|
stack_i++;
|
|
} else {
|
|
printf(" ");
|
|
}
|
|
|
|
// print disassembly line
|
|
if (!dasm_print_finished(&dasm, dasm_i)) {
|
|
if (dasm_addr(&dasm) >= instr_ptr)
|
|
dasm_i++;
|
|
dbg_print_instruction(&dasm);
|
|
// XXX: stop printing at ENDFUNC
|
|
if (dasm_instruction(&dasm)->opcode == ENDFUNC) {
|
|
dasm.addr = dasm.ain->code_size;
|
|
}
|
|
dasm_next(&dasm);
|
|
}
|
|
|
|
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));
|
|
}
|