/* Copyright (C) 2019 Nunuhara Cabbage * * 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 . */ #define VM_PRIVATE #include #include #include #include #include #include #include "system4/dasm.h" #include "system4/hashtable.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" bool dbg_enabled = true; bool dbg_start_in_debugger = false; 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 *_) { dbg_cmd_repl(); } void dbg_repl(void) { if (!dbg_enabled) return; dbg_start(_dbg_repl, NULL); } void dbg_init(void) { dbg_cmd_init(); #ifdef HAVE_SCHEME dbg_scm_init(); #endif } 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_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); printf("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); printf("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); dbg_cmd_repl(); } 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) { 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) { 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; } static void _dbg_handle_breakpoint(void *data) { struct breakpoint *bp = data; if (bp->cb) { bp->cb(bp); } else { printf("%s\n", bp->message); dbg_cmd_repl(); } } 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; printf("%c #%d 0x%08x in %s\n", no == dbg_current_frame ? '*' : ' ', no, addr, display_sjis0(f->name)); } void dbg_print_stack_trace(void) { for (int i = 0; i < call_stack_ptr; i++) { dbg_print_frame(i); } } struct ain_variable *dbg_get_member(const char *name, union vm_value *val_out) { struct page *page = get_struct_page(dbg_current_frame); if (!page) return NULL; assert(page->type == STRUCT_PAGE); assert(page->index >= 0 && page->index < ain->nr_structures); struct ain_struct *s = &ain->structures[page->index]; assert(page->nr_vars == s->nr_members); for (int i = 0; i < s->nr_members; i++) { if (!strcmp(s->members[i].name, name)) { *val_out = page->values[i]; return &s->members[i]; } } return NULL; } 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; } 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; } struct ain_variable *dbg_get_variable(const char *name, union vm_value *val_out) { struct ain_variable *var; if (!strncmp(name, "this.", 5) && (var = dbg_get_member(name+5, val_out))) return var; if ((var = dbg_get_local(name, val_out))) return var; if ((var = dbg_get_global(name, val_out))) return var; return NULL; } struct string *dbg_value_to_string(struct ain_type *type, union vm_value value, int recursive) { 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: { 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: { 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("{ "); 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 = dbg_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: { 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("[ "); for (int i = 0; i < page->nr_vars; i++) { if (i) { string_append_cstr(&out, "; ", 2); } struct string *tmp = dbg_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(""); } return string_ref(&EMPTY_STRING); } // 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) { 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("", 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("", value); else dbg_print_function_name(&ain->functions[value]); break; case T_DLG: if (value < 0 || value >= ain->nr_delegates) printf("", value); else dbg_print_identifier(ain->delegates[value].name); break; case T_STRING: if (value < 0 || value >= ain->nr_strings) printf("", value); else dbg_print_string(ain->strings[value]->text); break; case T_MSG: if (value < 0 || value >= ain->nr_messages) printf("", 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("", value); else dbg_print_identifier(ain->globals[value].name); break; case T_STRUCT: if (value < 0 || value >= ain->nr_structures) printf("", value); else dbg_print_identifier(ain->structures[value].name); break; case T_SYSCALL: if (value < 0 || value >= NR_SYSCALLS || !syscalls[value].name) printf("", value); else printf("%s", syscalls[value].name); break; case T_HLL: if (value < 0 || value >= ain->nr_libraries) { printf("", 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("", 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("", value); else dbg_print_identifier(ain->filenames[value]); break; default: printf("", 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); } putchar('\n'); } void dbg_print_dasm(void) { struct dasm dasm; if (!dbg_init_dasm(&dasm)) return; for (; dasm_addr(&dasm) < instr_ptr && !dasm_eof(&dasm); dasm_next(&dasm)) { dbg_print_instruction(&dasm); } for (int i = 0; i < DASM_FWD && !dasm_eof(&dasm); dasm_next(&dasm), i++) { dbg_print_instruction(&dasm); } } #define STACK_MAX 16 void dbg_print_stack(void) { int i = 0; if (stack_ptr > STACK_MAX) i = stack_ptr - STACK_MAX; for (; i < stack_ptr; i++) { printf(" [%d]: 0x%08x\n", i, stack[i].i); } } void dbg_print_vm_state(void) { puts(" Disassembly"); puts(" -----------"); dbg_print_dasm(); puts(""); puts(" Stack"); puts(" -----"); dbg_print_stack(); }