/* debugger.c: Glulxe debugger functions. Designed by Andrew Plotkin http://eblong.com/zarf/glulx/index.html */ /* This module is a low-level debugger for Inform 6 (and 7) games. This code is only compiled in if the "#define VM_DEBUGGER" line in glulxe.h is uncommented. You will then have to compile with a Glk library that supports the debug feature. (See gi_debug.h as distributed with CheapGlk.) You will also need to link the libxml2 library. When debugging is compiled in, glulxe looks for a "Dbug" chunk in the Blorb file. This should contain the "gameinfo.dbg" file generated by the I6 compiler. (Must be the new XML-based debug format.) It also offers a bunch of new command-line options: --gameinfo filename: Read the "gameinfo.dbg" from an external file. (If you're not using Blorb or have not pulled the debug info into it.) --cpu: Display time and VM CPU cycles used for each input. --starttrap: Enter debug mode as soon as the game starts up. (Before any code has executed.) --quittrap: Enter debug mode when the game quits. --crashtrap: Enter debug mode if any fatal error occurs (including Glk library fatal errors). You do not need "gameinfo.dbg" data to use the debugger. However, that file contains the I6-level names of all functions and variables. So without it, you'll see a lot of "???" in the debug output. The debug model assumes that you have a "debug console" available, which can accept debug commands and display responses. (The implementation of this depends on the Glk library.) You can enter commands whenever the game is awaiting input, or if it pauses because of the --starttrap flag, the --crashtrap flag, a breakpoint, or a @debugtrap opcode. The debugger is currently pretty minimal. You can display the stack (including local variables), display global variables, and set breakpoints (only at the beginning of a function). Type "help" into the debug console for the command list. (If you've compiled with CheapGlk, then the "debug console" is just standard output. Enter a line beginning with "/" to send a debug command -- "/help", etc.) (This "/" convention is *only* used in CheapGlk. Other libraries will have debug interfaces that don't suck.) */ #include "glk.h" #include "glulxe.h" #if VM_DEBUGGER #include #include #include "gi_debug.h" #include /* Data structures to store the debug info in memory. */ typedef enum grouptype_enum { grp_None = 0, grp_Constant = 1, grp_Attribute = 2, grp_Property = 3, grp_Routine = 4, grp_Global = 5, grp_Object = 6, grp_Array = 7, } grouptype; /* Used for constants, globals, locals, objects -- the meaning of the value field varies. */ typedef struct infoconstant_struct { const xmlChar *identifier; int32_t value; } infoconstant; typedef struct inforoutine_struct { const xmlChar *identifier; int32_t address; int32_t length; /* Address of the next higher function. May be beyond length if there are gaps. */ int32_t nextaddress; /* The locals is a block of infoconstants where the value is frame-offset. We adopt Inform's assumption that locals are always 4 bytes long. */ int32_t numlocals; infoconstant *locals; } inforoutine; typedef struct infoobject_struct { const xmlChar *identifier; int32_t address; /* Address of the next higher function. May be beyond length if there are gaps. */ int32_t nextaddress; } infoobject; typedef struct infoarray_struct { const xmlChar *identifier; int32_t address; int32_t bytelength; int32_t bytesize; /* per element */ int32_t count; int lengthfield; /* true if the zeroth element is the length */ /* Address of the next higher function. May be beyond length if there are gaps. */ int32_t nextaddress; } infoarray; typedef struct breakpoint_struct { inforoutine *func; int32_t address; struct breakpoint_struct *next; /* linked list of breakpoints */ } breakpoint; typedef struct debuginfofile_struct { strid_t str; int32_t strread; int32_t strreadmax; int failed; grouptype curgrouptype; int tempcounter; infoconstant *tempconstant; inforoutine *temproutine; infoarray *temparray; infoobject *tempobject; int tempnumlocals; int templocalssize; infoconstant *templocals; const xmlChar *storyfileprefix; xmlHashTablePtr constants; xmlHashTablePtr attributes; xmlHashTablePtr properties; xmlHashTablePtr globals; xmlHashTablePtr objects; int numobjects; infoobject **objectlist; /* array, ordered by address */ xmlHashTablePtr arrays; int numarrays; infoarray **arraylist; /* array, ordered by address */ xmlHashTablePtr routines; int numroutines; inforoutine **routinelist; /* array, ordered by address */ } debuginfofile; /* This global holds the loaded debug info, if we have any. */ static debuginfofile *debuginfo = NULL; /* Lists of breakpoints. */ static breakpoint *funcbreakpoints = NULL; /* linked list */ /* Internal functions used while loading the debug info. */ static int xmlreadstreamfunc(void *rock, char *buffer, int len); static int xmlreadchunkfunc(void *rock, char *buffer, int len); static int xmlclosefunc(void *rock); static void xmlhandlenode(xmlTextReaderPtr reader, debuginfofile *context); static int finalize_debuginfo(debuginfofile *context); static debuginfofile *create_debuginfofile(void) { debuginfofile *context = (debuginfofile *)malloc(sizeof(debuginfofile)); context->str = NULL; context->failed = 0; context->tempconstant = NULL; context->temparray = NULL; context->tempobject = NULL; context->temproutine = NULL; context->tempnumlocals = 0; context->templocals = NULL; context->templocalssize = 0; context->curgrouptype = grp_None; context->constants = xmlHashCreate(16); context->attributes = xmlHashCreate(16); context->properties = xmlHashCreate(16); context->globals = xmlHashCreate(16); context->objects = xmlHashCreate(16); context->numobjects = 0; context->objectlist = NULL; context->arrays = xmlHashCreate(16); context->numarrays = 0; context->arraylist = NULL; context->routines = xmlHashCreate(16); context->numroutines = 0; context->routinelist = NULL; context->storyfileprefix = NULL; return context; } static void free_debuginfofile(debuginfofile *context) { if (!context) return; context->str = NULL; context->tempconstant = NULL; context->temparray = NULL; context->tempobject = NULL; context->temproutine = NULL; /* We don't bother to free the member structures, because this only happens once at startup and then only on error conditions. */ if (context->constants) { xmlHashFree(context->constants, NULL); context->constants = NULL; } if (context->attributes) { xmlHashFree(context->attributes, NULL); context->attributes = NULL; } if (context->properties) { xmlHashFree(context->properties, NULL); context->properties = NULL; } if (context->globals) { xmlHashFree(context->globals, NULL); context->globals = NULL; } if (context->objects) { xmlHashFree(context->objects, NULL); context->objects = NULL; } if (context->objectlist) { free(context->objectlist); context->objectlist = NULL; } if (context->arrays) { xmlHashFree(context->arrays, NULL); context->arrays = NULL; } if (context->arraylist) { free(context->arraylist); context->arraylist = NULL; } if (context->routines) { xmlHashFree(context->routines, NULL); context->routines = NULL; } if (context->routinelist) { free(context->routinelist); context->routinelist = NULL; } free(context); } static infoconstant *create_infoconstant(void) { infoconstant *cons = (infoconstant *)malloc(sizeof(infoconstant)); cons->identifier = NULL; cons->value = 0; return cons; } static infoobject *create_infoobject(void) { infoobject *object = (infoobject *)malloc(sizeof(infoobject)); object->identifier = NULL; object->address = 0; object->nextaddress = 0; return object; } static infoarray *create_infoarray(void) { infoarray *arr = (infoarray *)malloc(sizeof(infoarray)); arr->identifier = NULL; arr->address = 0; arr->bytelength = 0; arr->bytesize = 1; arr->count = 0; arr->lengthfield = 0; arr->nextaddress = 0; return arr; } static inforoutine *create_inforoutine(void) { inforoutine *func = (inforoutine *)malloc(sizeof(inforoutine)); func->identifier = NULL; func->address = 0; func->length = 0; func->nextaddress = 0; func->numlocals = 0; func->locals = NULL; return func; } static breakpoint *create_breakpoint(glui32 addr) { breakpoint *bp = (breakpoint *)malloc(sizeof(breakpoint)); bp->func = NULL; /* useful? */ bp->address = addr; bp->next = NULL; return bp; } static void add_object_to_table(void *obj, void *rock, xmlChar *name) { debuginfofile *context = rock; infoobject *object = obj; if (context->tempcounter >= context->numobjects) { printf("### object overflow!\n"); /*###*/ return; } context->objectlist[context->tempcounter++] = object; } static int sort_objects_table(const void *obj1, const void *obj2) { infoobject **object1 = (infoobject **)obj1; infoobject **object2 = (infoobject **)obj2; return ((*object1)->address - (*object2)->address); } static int find_object_in_table(const void *keyptr, const void *obj) { /* Binary-search callback. We rely on address and nextaddress so that there are no gaps. */ glui32 addr = *(glui32 *)(keyptr); infoobject **object = (infoobject **)obj; if (addr < (*object)->address) return -1; if (addr >= (*object)->nextaddress) return 1; return 0; } static void add_array_to_table(void *obj, void *rock, xmlChar *name) { debuginfofile *context = rock; infoarray *array = obj; if (context->tempcounter >= context->numarrays) { printf("### array overflow!\n"); /*###*/ return; } context->arraylist[context->tempcounter++] = array; } static int sort_arrays_table(const void *obj1, const void *obj2) { infoarray **array1 = (infoarray **)obj1; infoarray **array2 = (infoarray **)obj2; return ((*array1)->address - (*array2)->address); } static int find_array_in_table(const void *keyptr, const void *obj) { /* Binary-search callback. We rely on address and nextaddress so that there are no gaps. */ glui32 addr = *(glui32 *)(keyptr); infoarray **array = (infoarray **)obj; if (addr < (*array)->address) return -1; if (addr >= (*array)->nextaddress) return 1; return 0; } static void add_routine_to_table(void *obj, void *rock, xmlChar *name) { debuginfofile *context = rock; inforoutine *routine = obj; if (context->tempcounter >= context->numroutines) { printf("### array overflow!\n"); /*###*/ return; } context->routinelist[context->tempcounter++] = routine; } static int sort_routines_table(const void *obj1, const void *obj2) { inforoutine **routine1 = (inforoutine **)obj1; inforoutine **routine2 = (inforoutine **)obj2; return ((*routine1)->address - (*routine2)->address); } static int find_routine_in_table(const void *keyptr, const void *obj) { /* Binary-search callback. We rely on address and nextaddress so that there are no gaps. */ glui32 addr = *(glui32 *)(keyptr); inforoutine **routine = (inforoutine **)obj; if (addr < (*routine)->address) return -1; if (addr >= (*routine)->nextaddress) return 1; return 0; } /* Top-level function for loading debug info from a Glk stream. The debug data must take up the entire file; this will read until EOF. If successful, fills out the debuginfo global and returns true. If not, reports an error and returns false. (The stream will not be closed.) */ int debugger_load_info_stream(strid_t stream) { xmlTextReaderPtr reader; debuginfofile *context = create_debuginfofile(); context->str = stream; context->strread = 0; /* not used */ context->strreadmax = 0; /* not used */ reader = xmlReaderForIO(xmlreadstreamfunc, xmlclosefunc, context, NULL, NULL, XML_PARSE_RECOVER|XML_PARSE_NOENT|XML_PARSE_NONET|XML_PARSE_NOCDATA|XML_PARSE_COMPACT); if (!reader) { printf("Error: Unable to create XML reader.\n"); /*###*/ free_debuginfofile(context); return 0; } while (1) { int res = xmlTextReaderRead(reader); if (res < 0) { context->failed = 1; break; /* error */ } if (res == 0) { break; /* EOF */ } xmlhandlenode(reader, context); } xmlFreeTextReader(reader); context->str = NULL; /* the reader didn't close it, but we're done with it. */ if (context->failed) { printf("Error: Unable to load debug info.\n"); /*###*/ free_debuginfofile(context); return 0; } /* Now that all the data is loaded in, we go through and create some indexes that will be handy. */ return finalize_debuginfo(context); } /* Top-level function for loading debug info from a segment of a Glk stream. This starts at position pos in the file and reads len bytes. If successful, fills out the debuginfo global and returns true. If not, reports an error and returns false. (The stream will not be closed.) */ int debugger_load_info_chunk(strid_t stream, glui32 pos, glui32 len) { xmlTextReaderPtr reader; debuginfofile *context = create_debuginfofile(); context->str = stream; context->strread = 0; context->strreadmax = len; glk_stream_set_position(stream, pos, seekmode_Start); reader = xmlReaderForIO(xmlreadchunkfunc, xmlclosefunc, context, NULL, NULL, XML_PARSE_RECOVER|XML_PARSE_NOENT|XML_PARSE_NONET|XML_PARSE_NOCDATA|XML_PARSE_COMPACT); if (!reader) { printf("Error: Unable to create XML reader.\n"); /*###*/ free_debuginfofile(context); return 0; } while (1) { int res = xmlTextReaderRead(reader); if (res < 0) { context->failed = 1; break; /* error */ } if (res == 0) { break; /* EOF */ } xmlhandlenode(reader, context); } xmlFreeTextReader(reader); context->str = NULL; /* the reader didn't close it, but we're done with it. */ if (context->failed) { printf("Error: Unable to load debug info.\n"); /*###*/ free_debuginfofile(context); return 0; } /* Now that all the data is loaded in, we go through and create some indexes that will be handy. */ return finalize_debuginfo(context); } /* xmlReader callback to read from a stream (until EOF). */ static int xmlreadstreamfunc(void *rock, char *buffer, int len) { debuginfofile *context = rock; int res = glk_get_buffer_stream(context->str, buffer, len); if (res < 0) return -1; return res; } /* xmlReader callback to read from a stream (until a given position). */ static int xmlreadchunkfunc(void *rock, char *buffer, int len) { int res; debuginfofile *context = rock; if (context->strread >= context->strreadmax) return 0; if (len > context->strreadmax - context->strread) len = context->strreadmax - context->strread; res = glk_get_buffer_stream(context->str, buffer, len); if (res < 0) return -1; context->strread += res; return res; } /* xmlReader callback to stop reading a stream. We don't actually close the stream here, just discard the reference. */ static int xmlclosefunc(void *rock) { debuginfofile *context = rock; context->str = NULL; return 0; } /* xmlReader callback: an XML node has arrived. (Might be the beginning of a tag, the end of a tag, or a block of text.) All the work of parsing the debug format happens here, which is why this function is big and ugly. */ static void xmlhandlenode(xmlTextReaderPtr reader, debuginfofile *context) { int depth = xmlTextReaderDepth(reader); int nodetype = xmlTextReaderNodeType(reader); if (depth == 0) { if (nodetype == XML_ELEMENT_NODE) { const xmlChar *name = xmlTextReaderConstName(reader); if (xmlStrcmp(name, BAD_CAST "inform-story-file")) { printf("Error: This is not an Inform debug info file.\n"); /*###*/ context->failed = 1; } } else if (nodetype == XML_ELEMENT_DECL) { /* End of document */ context->curgrouptype = grp_None; context->tempconstant = NULL; context->temparray = NULL; context->tempobject = NULL; context->temproutine = NULL; } } else if (depth == 1) { if (nodetype == XML_ELEMENT_NODE) { const xmlChar *name = xmlTextReaderConstName(reader); if (!xmlStrcmp(name, BAD_CAST "constant")) { context->curgrouptype = grp_Constant; context->tempconstant = create_infoconstant(); } else if (!xmlStrcmp(name, BAD_CAST "attribute")) { context->curgrouptype = grp_Attribute; context->tempconstant = create_infoconstant(); } else if (!xmlStrcmp(name, BAD_CAST "property")) { context->curgrouptype = grp_Property; context->tempconstant = create_infoconstant(); } else if (!xmlStrcmp(name, BAD_CAST "routine")) { context->curgrouptype = grp_Routine; context->temproutine = create_inforoutine(); context->tempnumlocals = 0; } else if (!xmlStrcmp(name, BAD_CAST "global-variable")) { context->curgrouptype = grp_Global; context->tempconstant = create_infoconstant(); } else if (!xmlStrcmp(name, BAD_CAST "object")) { context->curgrouptype = grp_Object; context->tempobject = create_infoobject(); } else if (!xmlStrcmp(name, BAD_CAST "array")) { context->curgrouptype = grp_Array; context->temparray = create_infoarray(); } else if (!xmlStrcmp(name, BAD_CAST "story-file-prefix")) { xmlNodePtr nod = xmlTextReaderExpand(reader); if (nod && nod->children && nod->children->type == XML_TEXT_NODE) { context->storyfileprefix = xmlStrdup(nod->children->content); } } else { context->curgrouptype = grp_None; } } else if (nodetype == XML_ELEMENT_DECL) { /* End of group */ switch (context->curgrouptype) { case grp_Constant: if (context->tempconstant) { infoconstant *dat = context->tempconstant; context->tempconstant = NULL; xmlHashAddEntry(context->constants, dat->identifier, dat); } break; case grp_Attribute: if (context->tempconstant) { infoconstant *dat = context->tempconstant; context->tempconstant = NULL; xmlHashAddEntry(context->attributes, dat->identifier, dat); } break; case grp_Property: if (context->tempconstant) { infoconstant *dat = context->tempconstant; context->tempconstant = NULL; xmlHashAddEntry(context->properties, dat->identifier, dat); } break; case grp_Global: if (context->tempconstant) { infoconstant *dat = context->tempconstant; context->tempconstant = NULL; xmlHashAddEntry(context->globals, dat->identifier, dat); } break; case grp_Object: if (context->tempobject) { infoobject *dat = context->tempobject; context->tempobject = NULL; xmlHashAddEntry(context->objects, dat->identifier, dat); } break; case grp_Array: if (context->temparray) { infoarray *dat = context->temparray; context->temparray = NULL; dat->count = dat->bytelength / dat->bytesize; xmlHashAddEntry(context->arrays, dat->identifier, dat); } break; case grp_Routine: if (context->temproutine) { inforoutine *dat = context->temproutine; context->temproutine = NULL; /* Copy the list of locals into the inforoutine structure. This loop totally assumes that locals are found in order in the debug file! */ if (context->tempnumlocals && context->templocals) { int ix; dat->numlocals = context->tempnumlocals; dat->locals = (infoconstant *)malloc(context->tempnumlocals * sizeof(infoconstant)); for (ix=0; ixtempnumlocals; ix++) { dat->locals[ix].identifier = context->templocals[ix].identifier; dat->locals[ix].value = context->templocals[ix].value; context->templocals[ix].identifier = NULL; context->templocals[ix].value = 0; } } context->tempnumlocals = 0; /* An address of 0 is impossible. Inform uses this to indicate a routine that was eliminated because it was never called. */ if (dat->address != 0) { xmlHashAddEntry(context->routines, dat->identifier, dat); } } break; default: break; } context->curgrouptype = grp_None; } } else { if (nodetype == XML_ELEMENT_NODE) { const xmlChar *name = xmlTextReaderConstName(reader); /* These fields are always simple text nodes. */ if (!xmlStrcmp(name, BAD_CAST "identifier")) { xmlNodePtr nod = xmlTextReaderExpand(reader); if (nod && nod->children && nod->children->type == XML_TEXT_NODE) { xmlChar *text = nod->children->content; if (context->curgrouptype == grp_Constant || context->curgrouptype == grp_Attribute || context->curgrouptype == grp_Property || context->curgrouptype == grp_Global) { if (context->tempconstant) { if (depth == 2) context->tempconstant->identifier = xmlStrdup(text); } } else if (context->curgrouptype == grp_Object) { if (context->tempobject) { if (depth == 2) context->tempobject->identifier = xmlStrdup(text); } } else if (context->curgrouptype == grp_Array) { if (context->temparray) { if (depth == 2) context->temparray->identifier = xmlStrdup(text); } } else if (context->curgrouptype == grp_Routine) { if (context->temproutine) { if (depth == 2) context->temproutine->identifier = xmlStrdup(text); } } } } else if (!xmlStrcmp(name, BAD_CAST "value")) { xmlNodePtr nod = xmlTextReaderExpand(reader); if (nod && nod->children && nod->children->type == XML_TEXT_NODE) { if (context->curgrouptype == grp_Constant || context->curgrouptype == grp_Attribute || context->curgrouptype == grp_Property) { if (context->tempconstant) { if (depth == 2) context->tempconstant->value = strtol((char *)nod->children->content, NULL, 10); } } else if (context->curgrouptype == grp_Object) { if (context->tempobject) { if (depth == 2) context->tempobject->address = strtol((char *)nod->children->content, NULL, 10); } } else if (context->curgrouptype == grp_Array) { if (context->temparray) { if (depth == 2) context->temparray->address = strtol((char *)nod->children->content, NULL, 10); } } else if (context->curgrouptype == grp_Routine) { if (context->temproutine) { if (depth == 2) context->temproutine->address = strtol((char *)nod->children->content, NULL, 10); } } } } else if (!xmlStrcmp(name, BAD_CAST "address")) { xmlNodePtr nod = xmlTextReaderExpand(reader); if (nod && nod->children && nod->children->type == XML_TEXT_NODE) { if (context->curgrouptype == grp_Global) { if (context->tempconstant) { if (depth == 2) context->tempconstant->value = strtol((char *)nod->children->content, NULL, 10); } } } } else if (!xmlStrcmp(name, BAD_CAST "byte-count")) { xmlNodePtr nod = xmlTextReaderExpand(reader); if (nod && nod->children && nod->children->type == XML_TEXT_NODE) { if (context->curgrouptype == grp_Routine) { if (context->temproutine) { if (depth == 2) context->temproutine->length = strtol((char *)nod->children->content, NULL, 10); } } else if (context->curgrouptype == grp_Array) { if (context->temparray) { if (depth == 2) context->temparray->bytelength = strtol((char *)nod->children->content, NULL, 10); } } } } else if (!xmlStrcmp(name, BAD_CAST "bytes-per-element")) { xmlNodePtr nod = xmlTextReaderExpand(reader); if (nod && nod->children && nod->children->type == XML_TEXT_NODE) { if (context->curgrouptype == grp_Array) { if (context->temparray) { if (depth == 2) context->temparray->bytesize = strtol((char *)nod->children->content, NULL, 10); } } } } else if (!xmlStrcmp(name, BAD_CAST "zeroth-element-holds-length")) { xmlNodePtr nod = xmlTextReaderExpand(reader); if (nod && nod->children && nod->children->type == XML_TEXT_NODE) { if (context->curgrouptype == grp_Array) { if (context->temparray) { if (depth == 2) if (!xmlStrcmp(nod->children->content, BAD_CAST "true")) context->temparray->lengthfield = 1; } } } } else if (!xmlStrcmp(name, BAD_CAST "local-variable")) { xmlNodePtr nod = xmlTextReaderExpand(reader); if (nod) { infoconstant *templocal; if (!context->templocals) { context->templocalssize = 8; context->templocals = (infoconstant *)malloc(context->templocalssize * sizeof(infoconstant)); } if (context->tempnumlocals >= context->templocalssize) { context->templocalssize = 2*context->tempnumlocals + 4; context->templocals = (infoconstant *)realloc(context->templocals, context->templocalssize * sizeof(infoconstant)); } templocal = &(context->templocals[context->tempnumlocals]); context->tempnumlocals += 1; for (nod = nod->children; nod; nod=nod->next) { if (nod->type == XML_ELEMENT_NODE) { if (!xmlStrcmp(nod->name, BAD_CAST "identifier") && nod->children && nod->children->type == XML_TEXT_NODE) { templocal->identifier = xmlStrdup(nod->children->content); } if (!xmlStrcmp(nod->name, BAD_CAST "frame-offset") && nod->children && nod->children->type == XML_TEXT_NODE) { templocal->value = strtol((char *)nod->children->content, NULL, 10); } } } } } } } } /* This is called after the XML data is parsed. We wrap up what we've found and store it in the debuginfo global. Returns 1 on success. */ static int finalize_debuginfo(debuginfofile *context) { int ix; context->numarrays = xmlHashSize(context->arrays); context->arraylist = (infoarray **)malloc(context->numarrays * sizeof(infoarray *)); context->tempcounter = 0; xmlHashScan(context->arrays, add_array_to_table, context); if (context->tempcounter != context->numarrays) printf("### array underflow!\n"); /*###*/ qsort(context->arraylist, context->numarrays, sizeof(infoarray *), sort_arrays_table); for (ix=0; ixnumarrays; ix++) { if (ix+1 < context->numarrays) { context->arraylist[ix]->nextaddress = context->arraylist[ix+1]->address; } else { context->arraylist[ix]->nextaddress = context->arraylist[ix]->address + context->arraylist[ix]->bytelength; } } context->numobjects = xmlHashSize(context->objects); context->objectlist = (infoobject **)malloc(context->numobjects * sizeof(infoobject *)); context->tempcounter = 0; xmlHashScan(context->objects, add_object_to_table, context); if (context->tempcounter != context->numobjects) printf("### object underflow!\n"); /*###*/ qsort(context->objectlist, context->numobjects, sizeof(infoobject *), sort_objects_table); for (ix=0; ixnumobjects; ix++) { if (ix+1 < context->numobjects) { context->objectlist[ix]->nextaddress = context->objectlist[ix+1]->address; } else { context->objectlist[ix]->nextaddress = context->objectlist[ix]->address + 1; } } context->numroutines = xmlHashSize(context->routines); context->routinelist = (inforoutine **)malloc(context->numroutines * sizeof(inforoutine *)); context->tempcounter = 0; xmlHashScan(context->routines, add_routine_to_table, context); if (context->tempcounter != context->numroutines) printf("### array underflow!\n"); /*###*/ qsort(context->routinelist, context->numroutines, sizeof(inforoutine *), sort_routines_table); for (ix=0; ixnumroutines; ix++) { if (ix+1 < context->numroutines) { context->routinelist[ix]->nextaddress = context->routinelist[ix+1]->address; } else { context->routinelist[ix]->nextaddress = context->routinelist[ix]->address + context->routinelist[ix]->length; } } if (context->templocals) { free(context->templocals); context->templocals = NULL; } context->templocalssize = 0; context->tempnumlocals = 0; /* Install into global. */ debuginfo = context; return 1; } /* Compare main memory to the story-file-prefix we found. If it doesn't match, display a warning. */ void debugger_check_story_file() { const unsigned char *cx; int pos = 0; int count = 0; uint32_t word = 0; int fail = FALSE; if (!debuginfo || !debuginfo->storyfileprefix) return; /* Check that this looks like an Inform 6 game file. See the Glulx Inform Technical Reference. */ if (Mem4(0x24) != 0x496E666F) { /* 'Info' */ gidebug_output("Warning: This game file does not look like it was generated by Inform."); } /* Decode the storyfileprefix, which is in base64. */ for (cx = debuginfo->storyfileprefix; *cx && *cx != '='; cx++) { unsigned int sixbit = 0; if (*cx >= 'A' && *cx <= 'Z') sixbit = (*cx) - 'A'; else if (*cx >= 'a' && *cx <= 'z') sixbit = ((*cx) - 'a') + 26; else if (*cx >= '0' && *cx <= '9') sixbit = ((*cx) - '0') + 52; else if (*cx == '+') sixbit = 62; else if (*cx == '/') sixbit = 63; else sixbit = 0; switch (count) { case 0: word = (sixbit << 18); break; case 1: word |= (sixbit << 12); break; case 2: word |= (sixbit << 6); break; case 3: word |= (sixbit); break; } count++; if (count == 4) { unsigned char byte; byte = (word >> 16) & 0xFF; if (byte != Mem1(pos)) fail = TRUE; byte = (word >> 8) & 0xFF; if (byte != Mem1(pos+1)) fail = TRUE; byte = (word) & 0xFF; if (byte != Mem1(pos+2)) fail = TRUE; pos += 3; count = 0; } } if (fail) gidebug_output("Warning: debug info does not match this game file."); } /* The rest of this file is the debugger itself. */ static char *linebuf = NULL; static int linebufsize = 0; /* Expand the output line buffer to a given length, if necessary. If you want to write an N-character string, call ensure_line_buf(N). Then use snprintf() just to be safe. */ static void ensure_line_buf(int len) { len += 1; /* for the closing null */ if (linebuf && len <= linebufsize) return; linebufsize = linebufsize*2+16; if (linebufsize < len) linebufsize = len+16; if (!linebuf) linebuf = malloc(linebufsize * sizeof(char)); else linebuf = realloc(linebuf, linebufsize * sizeof(char)); } static int track_cpu = FALSE; static int start_trap = FALSE; static int crash_trap = FALSE; static int quit_trap = FALSE; static int debugger_invoked = FALSE; /* true if cycle handler called */ unsigned long debugger_opcount = 0; /* incremented in exec.c */ static struct timeval debugger_timer; /* Set the track-CPU flag. (In fact we always track the VM CPU usage. This flag determines whether we report it to the debug console.) */ void debugger_track_cpu(int flag) { track_cpu = flag; } /* Set the block-on-startup flag. */ void debugger_set_start_trap(int flag) { start_trap = flag; } /* Set the block-on-quit flag. */ void debugger_set_quit_trap(int flag) { quit_trap = flag; } /* Set the block-on-crash flag. */ void debugger_set_crash_trap(int flag) { crash_trap = flag; } /* Has the library activated debugger features? If not, we assume that the debug console is entirely hidden from the user. */ int debugger_ever_invoked() { return debugger_invoked; } /* Returns 1 if this is a (hex or decimal) numeric constant; 0 if not; -1 if it looks like a constant but is malformed. On 1, the number itself is returned in res. */ static int parse_numeric_constant(char *arg, glui32 *res) { if (arg[0] == '$') { char *cx; glui32 val = 0; for (cx=arg+1; *cx; cx++) { if (*cx >= '0' && *cx <= '9') { val = 16 * val + (*cx - '0'); continue; } if (*cx >= 'A' && *cx <= 'F') { val = 16 * val + (*cx - 'A' + 10); continue; } if (*cx >= 'a' && *cx <= 'f') { val = 16 * val + (*cx - 'a' + 10); continue; } snprintf(linebuf, linebufsize, "Invalid hex number"); gidebug_output(linebuf); return -1; } *res = val; return 1; } if (arg[0] >= '0' && arg[0] <= '9') { char *cx; glui32 val = 0; for (cx=arg; *cx; cx++) { if (*cx >= '0' && *cx <= '9') { val = 10 * val + (*cx - '0'); continue; } snprintf(linebuf, linebufsize, "Invalid number"); gidebug_output(linebuf); return -1; } *res = val; return 1; } return 0; } static infoarray *find_array_for_address(glui32 addr) { infoarray **res; infoarray *arr; if (!debuginfo) return NULL; res = bsearch(&addr, debuginfo->arraylist, debuginfo->numarrays, sizeof(infoarray *), find_array_in_table); if (!res) return NULL; arr = *res; if (addr < arr->address || addr >= arr->address + arr->bytelength) return NULL; return arr; } static infoobject *find_object_for_address(glui32 addr) { infoobject **res; infoobject *arr; if (!debuginfo) return NULL; res = bsearch(&addr, debuginfo->objectlist, debuginfo->numobjects, sizeof(infoobject *), find_object_in_table); if (!res) return NULL; arr = *res; if (addr != arr->address) return NULL; return arr; } static inforoutine *find_routine_for_address(glui32 addr) { inforoutine **res; inforoutine *func; if (!debuginfo) return NULL; res = bsearch(&addr, debuginfo->routinelist, debuginfo->numroutines, sizeof(inforoutine *), find_routine_in_table); if (!res) return NULL; func = *res; if (addr < func->address || addr >= func->address + func->length) return NULL; return func; } static void render_value_linebuf(glui32 val) { int tmplen; inforoutine *func; infoarray *arr; infoobject *object; /* Special case for single-digit numbers: display the decimal digit and stop. */ if (val < 10) { tmplen = strlen(linebuf); ensure_line_buf(tmplen+4); snprintf(linebuf+tmplen, linebufsize-tmplen, "%d", val); return; } /* Always display the signed decimal and unsigned hex. */ tmplen = strlen(linebuf); ensure_line_buf(tmplen+40); if (val <= 0x7FFFFFFF) snprintf(linebuf+tmplen, linebufsize-tmplen, "%d ($%X)", val, val); else snprintf(linebuf+tmplen, linebufsize-tmplen, "%d ($%X)", -(int)(0x100000000-val), val); /* If the address of a function, display it. (But not addresses in the middle of a function.) */ func = find_routine_for_address(val); if (func) { if (val == func->address) { tmplen = strlen(linebuf); ensure_line_buf(tmplen+40); snprintf(linebuf+tmplen, linebufsize-tmplen, ", %s()", func->identifier); } } /* If the address of an array, display it. */ arr = find_array_for_address(val); if (arr) { if (val == arr->address) { tmplen = strlen(linebuf); ensure_line_buf(tmplen+40); snprintf(linebuf+tmplen, linebufsize-tmplen, ", %s[%d]", arr->identifier, arr->count); } } /* If the address of an object, display it. */ object = find_object_for_address(val); if (object) { if (val == object->address) { tmplen = strlen(linebuf); ensure_line_buf(tmplen+40); snprintf(linebuf+tmplen, linebufsize-tmplen, ", %s", object->identifier); } } } static void debugcmd_backtrace(int wholestack) { if (stack) { glui32 curpc = pc; glui32 curframeptr = frameptr; glui32 curstackptr = stackptr; glui32 curvalstackbase = valstackbase; glui32 curlocalsbase = localsbase; ensure_line_buf(256); while (1) { glui32 locptr; int locnum; glui32 newframeptr; glui32 newpc; inforoutine *routine = find_routine_for_address(curpc); if (!routine) snprintf(linebuf, linebufsize, "- %s() (pc=$%.2X)", "???", curpc); else snprintf(linebuf, linebufsize, "- %s() (pc=$%.2X)", routine->identifier, curpc); gidebug_output(linebuf); strcpy(linebuf, " "); /* Again, this loop assumes that all locals are 4 bytes. */ for (locptr = curlocalsbase, locnum = 0; locptr < curvalstackbase; locptr += 4, locnum++) { glui32 val; int tmplen = strlen(linebuf); ensure_line_buf(tmplen+32); if (!routine || !routine->locals || locnum >= routine->numlocals) snprintf(linebuf+tmplen, linebufsize-tmplen, "%sloc#%d=", (locnum?"; ":""), locnum); else snprintf(linebuf+tmplen, linebufsize-tmplen, "%s%s=", (locnum?"; ":""), routine->locals[locnum].identifier); val = Stk4(locptr); render_value_linebuf(val); } if (!locnum) { strcat(linebuf, "(no locals)"); } gidebug_output(linebuf); if (!wholestack) break; curstackptr = curframeptr; if (curstackptr < 16) break; curstackptr -= 16; newframeptr = Stk4(curstackptr+12); newpc = Stk4(curstackptr+8); curframeptr = newframeptr; curpc = newpc; curvalstackbase = curframeptr + Stk4(curframeptr); curlocalsbase = curframeptr + Stk4(curframeptr+4); } } } static void debugcmd_set_breakpoint(char *arg) { int found; glui32 addr; breakpoint *bp; while (*arg == ' ') arg++; if (*arg == '\0') { gidebug_output("What function do you want to set a breakpoint at?"); return; } found = FALSE; addr = 0; if (!found) { int res = parse_numeric_constant(arg, &addr); if (res < 0) return; if (res > 0) { found = TRUE; /* If possible, check whether this looks like a function address. But if it doesn't, just print a warning. */ if (debuginfo) { inforoutine *routine = find_routine_for_address(addr); if (!routine || routine->address != addr) { ensure_line_buf(128); strcpy(linebuf, "This does not look like a function address: "); render_value_linebuf(addr); gidebug_output(linebuf); } } } } if (!found) { inforoutine *routine; if (!debuginfo) { gidebug_output("No debug info; cannot look up functions by name"); return; } routine = xmlHashLookup(debuginfo->routines, BAD_CAST arg); if (!routine) { ensure_line_buf(128); snprintf(linebuf, linebufsize, "Not a function name: %s", arg); gidebug_output(linebuf); return; } found = TRUE; addr = routine->address; } if (!found) return; for (bp = funcbreakpoints; bp; bp=bp->next) { if (bp->address == addr) { ensure_line_buf(128); strcpy(linebuf, "Breakpoint is already set for function: "); render_value_linebuf(addr); gidebug_output(linebuf); return; } } bp = create_breakpoint(addr); bp->next = funcbreakpoints; funcbreakpoints = bp; ensure_line_buf(128); strcpy(linebuf, "Breakpoint set for function: "); render_value_linebuf(addr); gidebug_output(linebuf); } static void debugcmd_clear_breakpoint(char *arg) { int found; glui32 addr; breakpoint **bpp; while (*arg == ' ') arg++; if (*arg == '\0') { gidebug_output("What function do you want to clear a breakpoint at?"); return; } found = FALSE; addr = 0; if (!found) { int res = parse_numeric_constant(arg, &addr); if (res < 0) return; if (res > 0) { found = TRUE; /* If possible, check whether this looks like a function address. But if it doesn't, just print a warning. */ if (debuginfo) { inforoutine *routine = find_routine_for_address(addr); if (!routine || routine->address != addr) { ensure_line_buf(128); strcpy(linebuf, "This does not look like a function address: "); render_value_linebuf(addr); gidebug_output(linebuf); } } } } if (!found) { inforoutine *routine; if (!debuginfo) { gidebug_output("No debug info; cannot look up functions by name"); return; } routine = xmlHashLookup(debuginfo->routines, BAD_CAST arg); if (!routine) { ensure_line_buf(128); snprintf(linebuf, linebufsize, "Not a function name: %s", arg); gidebug_output(linebuf); return; } found = TRUE; addr = routine->address; } if (!found) return; for (bpp = &funcbreakpoints; *bpp; bpp=&((*bpp)->next)) { if ((*bpp)->address == addr) { breakpoint *bp = *bpp; *bpp = bp->next; free(bp); ensure_line_buf(128); strcpy(linebuf, "Cleared breakpoint for function: "); render_value_linebuf(addr); gidebug_output(linebuf); return; } } ensure_line_buf(128); strcpy(linebuf, "No breakpoint found for function: "); render_value_linebuf(addr); gidebug_output(linebuf); } static void debugcmd_print(char *arg) { ensure_line_buf(128); /* for a start */ while (*arg == ' ') arg++; if (*arg == '\0') { gidebug_output("What do you want to print?"); return; } /* For plain numbers, and $HEX numbers, we print the value directly. */ { glui32 val = 0; int res = parse_numeric_constant(arg, &val); if (res < 0) return; if (res > 0) { strcpy(linebuf, ""); render_value_linebuf(val); gidebug_output(linebuf); return; } } /* Symbol recognition should be case-insensitive */ /* Is it a local variable name? */ if (debuginfo) { glui32 curpc = pc; glui32 curlocalsbase = localsbase; /* Should have a way to trawl up and down the stack. */ inforoutine *routine = find_routine_for_address(curpc); if (routine && routine->locals) { int ix; for (ix=0; ixnumlocals; ix++) { if (!xmlStrcmp(routine->locals[ix].identifier, BAD_CAST arg)) { glui32 locptr = curlocalsbase + 4*ix; glui32 val = Stk4(locptr); snprintf(linebuf, linebufsize, "local %s = ", routine->locals[ix].identifier); render_value_linebuf(val); gidebug_output(linebuf); return; } } } } /* Is it a constant name? */ if (debuginfo) { infoconstant *cons = xmlHashLookup(debuginfo->constants, BAD_CAST arg); if (cons) { snprintf(linebuf, linebufsize, "%d ($%X): constant", cons->value, cons->value); gidebug_output(linebuf); return; } } /* Is it an attribute name? */ if (debuginfo) { infoconstant *cons = xmlHashLookup(debuginfo->attributes, BAD_CAST arg); if (cons) { snprintf(linebuf, linebufsize, "%d ($%X): attribute", cons->value, cons->value); gidebug_output(linebuf); return; } } /* Is it a property name? */ if (debuginfo) { infoconstant *cons = xmlHashLookup(debuginfo->properties, BAD_CAST arg); if (cons) { snprintf(linebuf, linebufsize, "%d ($%X): property", cons->value, cons->value); gidebug_output(linebuf); return; } } /* Is it an object name? */ if (debuginfo) { infoobject *cons = xmlHashLookup(debuginfo->objects, BAD_CAST arg); if (cons) { snprintf(linebuf, linebufsize, "%d ($%X): object", cons->address, cons->address); gidebug_output(linebuf); return; } } /* Is it an array name? */ if (debuginfo) { infoarray *arr = xmlHashLookup(debuginfo->arrays, BAD_CAST arg); if (arr) { snprintf(linebuf, linebufsize, "%d ($%X): array[%d] of %d-byte values", arr->address, arr->address, arr->count, arr->bytesize); gidebug_output(linebuf); return; } } /* Is it a global name? */ if (debuginfo) { infoconstant *cons = xmlHashLookup(debuginfo->globals, BAD_CAST arg); if (cons) { glui32 val = Mem4(cons->value); snprintf(linebuf, linebufsize, "global %s = ", cons->identifier); render_value_linebuf(val); gidebug_output(linebuf); return; } } /* Is it a routine name? */ if (debuginfo) { inforoutine *routine = xmlHashLookup(debuginfo->routines, BAD_CAST arg); if (routine) { snprintf(linebuf, linebufsize, "%d ($%X): routine", routine->address, routine->address); gidebug_output(linebuf); return; } } gidebug_output("Symbol not found"); } static void debugcmd_help(char *arg) { gidebug_output("Glulxe built-in debugger. Commands:"); gidebug_output("- print : Print a symbol or number."); gidebug_output("- bt: Display the current stack backtrace, with local variables."); gidebug_output("- break : Set a breakpoint. (Must be a function name or the address of a function. Breakpoints currently only work at the start of a function.)"); gidebug_output("- clear : Clear a breakpoint."); gidebug_output("- cont: Continue execution. (From a breakpoint or other trap.)"); gidebug_output("- help/?: This list."); } /* Debug console callback: This is invoked whenever the user enters a debug command. Returns 0 for most commands, 1 if the command ends a debugger pause. */ int debugger_cmd_handler(char *cmd) { int len; char *cx; /* Trim spaces from start */ while (*cmd == ' ') cmd++; /* Trim spaces from end */ len = strlen(cmd); while (len > 0 && cmd[len-1] == ' ') { cmd[len-1] = '\0'; len--; } if (*cmd == '\0') return 0; /* empty command */ for (cx=cmd; *cx && *cx != ' '; cx++) { } len = (cx - cmd); if (len == 2 && !strncmp(cmd, "bt", len)) { debugcmd_backtrace(1); return 0; } if (len == 5 && !strncmp(cmd, "print", len)) { debugcmd_print(cx); return 0; } if (len == 5 && !strncmp(cmd, "break", len)) { debugcmd_set_breakpoint(cx); return 0; } if (len == 5 && !strncmp(cmd, "clear", len)) { debugcmd_clear_breakpoint(cx); return 0; } if (len == 4 && !strncmp(cmd, "cont", len)) { gidebug_output("Continuing..."); return 1; } if (len == 4 && !strncmp(cmd, "help", len)) { debugcmd_help(cx); return 0; } if (len == 1 && !strncmp(cmd, "?", len)) { debugcmd_help(cx); return 0; } ensure_line_buf(strlen(cmd) + 64); snprintf(linebuf, linebufsize, "Unknown debug command: %s", cmd); gidebug_output(linebuf); return 0; } /* Debug console callback: This is invoked when the game starts, when it ends, and when each input cycle begins and ends. We take this opportunity to report CPU usage, if the track_cpu flag is set. */ void debugger_cycle_handler(int cycle) { struct timeval now; double diff; debugger_invoked = TRUE; if (track_cpu) { switch (cycle) { case gidebug_cycle_Start: debugger_opcount = 0; gettimeofday(&debugger_timer, NULL); break; case gidebug_cycle_InputWait: case gidebug_cycle_DebugPause: gettimeofday(&now, NULL); diff = (now.tv_sec - debugger_timer.tv_sec) * 1000.0 + (now.tv_usec - debugger_timer.tv_usec) / 1000.0; ensure_line_buf(64); snprintf(linebuf, linebufsize, "VM: %ld cycles in %.3f ms", debugger_opcount, diff); gidebug_output(linebuf); break; case gidebug_cycle_InputAccept: case gidebug_cycle_DebugUnpause: debugger_opcount = 0; gettimeofday(&debugger_timer, NULL); break; } } } /* Do any start-time setup. This is called after VM memory is loaded, but before execution begins. */ void debugger_setup_start_state(void) { if (start_trap) { /* Block and debug right now. This doesn't install a trap, it just calls gidebug_pause() directly. */ gidebug_output("Break at start:"); debugcmd_backtrace(0); gidebug_pause(); } } /* Check whether we've set a breakpoint at this function address. If so, block and debug. */ void debugger_check_func_breakpoint(glui32 addr) { int pause = FALSE; breakpoint *bp; for (bp = funcbreakpoints; bp; bp=bp->next) { if (bp->address == addr) { pause = TRUE; break; } } if (pause) { gidebug_output("Breakpoint:"); debugcmd_backtrace(0); gidebug_pause(); } } /* Invoke the library's debug-block mechanism. */ void debugger_block_and_debug(char *msg) { gidebug_output(msg); gidebug_pause(); } /* If the quit-trap preference is set, enter debug mode. */ void debugger_handle_quit() { if (quit_trap) { gidebug_output("Quit trap, pausing..."); debugcmd_backtrace(0); gidebug_pause(); } } /* Report a fatal error to the debug console, along with the current stack trace. Depending on preferences, we may then enter full-on debug mode. */ void debugger_handle_crash(char *msg) { char *prefix = "Glulxe fatal error: "; ensure_line_buf(strlen(prefix) + strlen(msg)); strcpy(linebuf, prefix); strcat(linebuf, msg); gidebug_output(linebuf); debugcmd_backtrace(1); if (crash_trap) { gidebug_pause(); } } #endif /* VM_DEBUGGER */