[quickjs] Debugger

This commit is contained in:
John Doty 2023-09-22 13:26:33 -05:00
parent c96a1a4979
commit 6fa3e64b8d
3 changed files with 265 additions and 11 deletions

View file

@ -263,6 +263,8 @@ DEF( math_mod, 1, 2, 1, none)
/* must be the last non short and non temporary opcode */
DEF( nop, 1, 0, 0, none)
DEF( breakpoint, 1, 0, 1, none)
/* temporary opcodes: never emitted in the final bytecode */
def( enter_scope, 3, 0, 0, u16) /* emitted in phase 1, removed in phase 2 */

View file

@ -109,6 +109,8 @@
/* test the GC by forcing it before each object allocation */
//#define FORCE_GC_AT_MALLOC
#define CONFIG_DEBUGGER
#ifdef CONFIG_ATOMICS
#include <pthread.h>
#include <stdatomic.h>
@ -289,6 +291,9 @@ struct JSRuntime {
JSModuleLoaderFunc *module_loader_func;
void *module_loader_opaque;
JSMapSourceFunc *map_source_func;
void *map_source_opaque;
BOOL can_block : 8; /* TRUE if Atomics.wait can block */
/* used to allocate, free and clone SharedArrayBuffers */
JSSharedArrayBufferFunctions sab_funcs;
@ -306,8 +311,14 @@ struct JSRuntime {
uint32_t operator_count;
#endif
void *user_opaque;
JSMapSourceFunc *map_source_func;
void *map_source_opaque;
#ifdef CONFIG_DEBUGGER
struct list_head breakpoint_list;
JSDebugCallbackFunc *debug_callback;
void *debug_opaque;
int stepping_pc;
uint8_t stepping_byte;
#endif
};
struct JSClass {
@ -317,7 +328,7 @@ struct JSClass {
JSClassGCMark *gc_mark;
JSClassCall *call;
/* pointers for exotic behavior, can be NULL if none are present */
const JSClassExoticMethods *exotic;
const JSClassExoticMethods *exotic;
};
#define JS_MODE_STRICT (1 << 0)
@ -997,6 +1008,20 @@ enum OPCodeEnum {
OP_TEMP_END,
};
typedef struct JSBreakpoint {
JSRuntime *rt;
JSAtom file;
int line;
struct list_head link;
int ref_count;
JSFunctionBytecode *function;
int code_offset;
uint8_t replaced_byte;
JS_BOOL enabled;
} JSBreakpoint;
static int JS_InitAtoms(JSRuntime *rt);
static JSAtom __JS_NewAtomInit(JSRuntime *rt, const char *str, int len,
int atom_type);
@ -1253,6 +1278,9 @@ static JSValue JS_InstantiateFunctionListItem2(JSContext *ctx, JSObject *p,
JSAtom atom, void *opaque);
void JS_SetUncatchableError(JSContext *ctx, JSValueConst val, BOOL flag);
static JSBreakpoint *find_existing_breakpoint(JSContext *ctx, JSFunctionBytecode *func, int pc);
static int js_handle_breakpoint(JSContext *ctx, JSFunctionBytecode *b, int code_offset);
static const JSClassExoticMethods js_arguments_exotic_methods;
static const JSClassExoticMethods js_string_exotic_methods;
static const JSClassExoticMethods js_proxy_exotic_methods;
@ -1636,6 +1664,10 @@ JSRuntime *JS_NewRuntime2(const JSMallocFunctions *mf, void *opaque)
#endif
init_list_head(&rt->job_list);
#ifdef CONFIG_DEBUGGER
init_list_head(&rt->breakpoint_list);
#endif
if (JS_InitAtoms(rt))
goto fail;
@ -16227,10 +16259,14 @@ static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj,
size_t alloca_size;
#if !DIRECT_DISPATCH
#define SWITCH(pc) switch (opcode = *pc++)
#define CASE(op) case op
#define DEFAULT default
#define BREAK break
#define SWITCH(pc) \
opcode = *pc++; \
resume_breakpoint: \
switch (opcode)
#define RESUME_BREAKPOINT(op) opcode = op; goto resume_breakpoint;
#define CASE(op) case op
#define DEFAULT default
#define BREAK break
#else
static const void * const dispatch_table[256] = {
#define DEF(id, size, n_pop, n_push, f) && case_OP_ ## id,
@ -16242,10 +16278,11 @@ static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj,
#include "quickjs-opcode.h"
[ OP_COUNT ... 255 ] = &&case_default
};
#define SWITCH(pc) goto *dispatch_table[opcode = *pc++];
#define CASE(op) case_ ## op
#define DEFAULT case_default
#define BREAK SWITCH(pc)
#define SWITCH(pc) goto *dispatch_table[opcode = *pc++];
#define RESUME_BREAKPOINT(op) opcode = op; goto *dispatch_table[opcode];
#define CASE(op) case_ ## op
#define DEFAULT case_default
#define BREAK SWITCH(pc)
#endif
if (js_poll_interrupts(caller_ctx))
@ -18667,6 +18704,13 @@ static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj,
JS_FreeValue(ctx, sp[-1]);
sp[-1] = JS_FALSE;
BREAK;
CASE(OP_breakpoint):
{
int code_offset = (int)(pc - b->byte_code_buf - 1);
int next_op = js_handle_breakpoint(ctx, b, code_offset);
RESUME_BREAKPOINT(next_op);
}
BREAK;
CASE(OP_invalid):
DEFAULT:
JS_ThrowInternalError(ctx, "invalid opcode: pc=%u opcode=0x%02x",
@ -31142,6 +31186,7 @@ static __exception int resolve_variables(JSContext *ctx, JSFunctionDef *s)
case OP_nop:
/* remove erased code */
break;
case OP_set_class_name:
/* only used during parsing */
break;
@ -54086,3 +54131,182 @@ void JS_SetSourceMapFunc(JSRuntime *rt, JSMapSourceFunc *map_func, void *opaque)
rt->map_source_func = map_func;
rt->map_source_opaque = opaque;
}
/* Debugger */
static BOOL find_pc_offset(JSFunctionBytecode *b, int target_line, int *break_pc, int *actual_line)
{
const uint8_t *p_end, *p;
int new_line_num, line_num, new_pc, pc, v, ret;
unsigned int op;
if (!b->has_debug || !b->debug.pc2line_buf) {
/* function was stripped */
return FALSE;
}
if (target_line < b->debug.line_num) {
/* this line is before this function even starts */
return FALSE;
}
p = b->debug.pc2line_buf;
p_end = p + b->debug.pc2line_len;
pc = 0;
line_num = b->debug.line_num;
while (p < p_end) {
op = *p++;
if (op == 0) {
uint32_t val;
ret = get_leb128(&val, p, p_end);
if (ret < 0)
goto fail;
new_pc = pc + val;
p += ret;
ret = get_sleb128(&v, p, p_end);
if (ret < 0) {
fail:
/* should never happen */
return FALSE;
}
p += ret;
new_line_num = line_num + v;
} else {
op -= PC2LINE_OP_FIRST;
new_pc = pc + (op / PC2LINE_RANGE);
new_line_num = line_num + (op % PC2LINE_RANGE) + PC2LINE_BASE;
}
if (new_line_num > target_line) {
*break_pc = pc;
*actual_line = line_num;
return TRUE;
}
line_num = new_line_num;
pc = new_pc;
}
return FALSE;
}
static JSBreakpoint *find_existing_breakpoint(JSContext *ctx, JSFunctionBytecode *func, int pc) {
struct list_head *el;
list_for_each(el, &ctx->rt->breakpoint_list) {
JSBreakpoint *bp = list_entry(el, JSBreakpoint, link);
if (bp->function == func && bp->code_offset == pc) {
return JS_DupBreakpoint(bp);
}
}
return NULL;
}
typedef struct JSDebugContext {
JSContext *ctx;
} JSDebugContext;
static int js_handle_breakpoint(JSContext *ctx, JSFunctionBytecode *b, int code_offset) {
int next_op;
JSResumeMode resume = JS_RESUME_MODE_CONTINUE;
JSRuntime *rt = ctx->rt;
JSBreakpoint *bp = find_existing_breakpoint(ctx, b, code_offset);
if (bp) {
if (bp->enabled) {
if (rt->debug_callback) {
JSDebugContext dctx;
dctx.ctx = ctx;
resume = rt->debug_callback(&dctx, JS_BREAK_BREAKPOINT, bp, rt->debug_opaque);
}
}
next_op = bp->replaced_byte;
} else {
assert(code_offset == rt->stepping_pc);
rt->stepping_pc = -1;
next_op = rt->stepping_byte;
b->byte_code_buf[code_offset] = rt->stepping_byte;
if (rt->debug_callback) {
JSDebugContext dctx;
dctx.ctx = ctx;
resume = rt->debug_callback(&dctx, JS_BREAK_STEP, NULL, rt->debug_opaque);
}
}
return next_op;
}
JSBreakpoint *JS_SetBreakpoint(JSContext *ctx, JSAtom file, int line)
{
struct list_head *el;
list_for_each(el, &ctx->rt->gc_obj_list) {
JSGCObjectHeader *gp = list_entry(el, JSGCObjectHeader, link);
if (gp->gc_obj_type == JS_GC_OBJ_TYPE_FUNCTION_BYTECODE) {
JSFunctionBytecode *func = (JSFunctionBytecode *)gp;
if (func->debug.filename == file) {
int break_pc, actual_line;
if (find_pc_offset(func, line, &break_pc, &actual_line)) {
uint8_t code = func->byte_code_buf[break_pc];
if (code == OP_breakpoint) {
return find_existing_breakpoint(ctx, func, break_pc);
}
JSBreakpoint *breakpoint = js_mallocz(ctx, sizeof(JSBreakpoint));
if (!breakpoint) {
return NULL;
}
breakpoint->ref_count = 1;
breakpoint->rt = ctx->rt;
breakpoint->file = JS_DupAtom(ctx, file);
breakpoint->line = actual_line;
list_add_tail(&breakpoint->link, &ctx->rt->breakpoint_list);
breakpoint->function = func;
func->header.ref_count++;
breakpoint->code_offset = break_pc;
breakpoint->enabled = TRUE;
breakpoint->replaced_byte = code;
func->byte_code_buf[break_pc] = OP_breakpoint;
return breakpoint;
}
}
}
}
return NULL;
}
JSBreakpoint *JS_DupBreakpoint(JSBreakpoint *breakpoint)
{
breakpoint->ref_count += 1;
return breakpoint;
}
void JS_DeleteBreakpoint(JSBreakpoint *breakpoint)
{
breakpoint->ref_count -= 1;
if (breakpoint->ref_count == 0) {
breakpoint->function->byte_code_buf[breakpoint->code_offset] = breakpoint->replaced_byte;
list_del(&breakpoint->link);
JS_FreeValueRT(breakpoint->rt, JS_MKPTR(JS_TAG_FUNCTION_BYTECODE, breakpoint->function));
JS_FreeAtomRT(breakpoint->rt, breakpoint->file);
}
}
JS_BOOL JS_GetBreakpointEnabled(JSBreakpoint *breakpoint)
{
return breakpoint->enabled;
}
void JS_SetBreakpointEnabled(JSBreakpoint *breakpoint, JS_BOOL enabled)
{
breakpoint->enabled = enabled;
}
void JS_GetBreakpointLocation(JSBreakpoint *breakpoint, JSAtom *file, int *line)
{
*file = JS_DupAtomRT(breakpoint->rt, breakpoint->file);
*line = breakpoint->line;
}

View file

@ -1050,6 +1050,34 @@ typedef void JSMapSourceFunc(JSContext *ctx, void *opaque, JSAtom file, int line
void JS_SetSourceMapFunc(JSRuntime *rt, JSMapSourceFunc *map_func, void *opaque);
/* Debugger support */
typedef struct JSDebugContext JSDebugContext;
JSContext *JS_DebugGetContext(JSDebugContext *ctx);
typedef enum JSResumeMode {
JS_RESUME_MODE_CONTINUE,
JS_RESUME_MODE_STEP_OVER,
JS_RESUME_MODE_STEP_INTO,
} JSResumeMode;
typedef enum JSBreakReason {
JS_BREAK_BREAKPOINT,
JS_BREAK_STEP,
} JSBreakReason;
typedef struct JSBreakpoint JSBreakpoint;
typedef JSResumeMode JSDebugCallbackFunc(JSDebugContext *ctx, JSBreakReason reason, JSBreakpoint *breakpoint, void *opaque);
void JS_SetDebugCallbackFunc(JSRuntime *rt, JSDebugCallbackFunc *bp_func, void *opqaue);
JSBreakpoint *JS_SetBreakpoint(JSContext *ctx, JSAtom file, int line);
JSBreakpoint *JS_DupBreakpoint(JSBreakpoint *breakpoint);
void JS_DeleteBreakpoint(JSBreakpoint *breakpoint);
JS_BOOL JS_GetBreakpointEnabled(JSBreakpoint *breakpoint);
void JS_SetBreakpointEnabled(JSBreakpoint *breakpoint, JS_BOOL enabled);
void JS_GetBreakpointLocation(JSBreakpoint *breakpoint, JSAtom *file, int *line);
#undef js_unlikely
#undef js_force_inline