From 4ea8ea650a1d81cf6362e1485d2fdce2617d8d8e Mon Sep 17 00:00:00 2001 From: Matt Hunter Date: Wed, 13 Aug 2025 01:04:57 -0400 Subject: Add architecture-specific single step support ARM 32-bit is the first platform added to misplays which lacks underlying hardware support for single step traps - so the kernel does not implement PTRACE_SINGLESTEP in this case. We will work around this in a similar way as gdb does and how the kernel used to do it until 2011. arm_singlestep() implements logic which disassembles the program's current instruction and analyzes it to determine all possible next locations - eg: the next instruction in memory, or the jump target of a branch instruction, etc. This logic is dynamically dispatched by the debugger core if an ARM build is running in 32-bit mode. arm_singlestep() uses breakpoints to stop execution at it's computed next locations. However, misplays is currently very careful about controling the use of breakpoints in order to avoid issues with thread single steps - so a new flag (called "step") is added to breakpoints to enable the debugger to selectively install this subset of breakpoints for each thread's single step action, and more or less keep treating thread free-run as normal. install_breakpoints() is updated to take a "step" parameter to control which set of breakpoints is installed at any given time. resume_threads() is updated to perform this new single step dynamic dispatch, and manage the installation of step breakpoints. add_breakpoint() is also given a "step" parameter. This initializes the flag for the new breakpoint, but crucially is used to sort the new breakpoint into the process breakpoint list. Since step breakpoints will always be installed first, prioritize them in the list so that uninstall_breakpoints() doesn't corrupt memory when it runs the list backward to remove them. Signed-off-by: Matt Hunter --- debugger.c | 50 +++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 43 insertions(+), 7 deletions(-) (limited to 'debugger.c') diff --git a/debugger.c b/debugger.c index 2bbf866..06447c3 100644 --- a/debugger.c +++ b/debugger.c @@ -10,6 +10,7 @@ #include +#include "arch/arm-singlestep.h" #include "debugger.h" #include "helpers.h" @@ -67,14 +68,14 @@ static void free_breakpoints(struct process *proc) { } } -static void install_breakpoints(struct thread *th) { +static void install_breakpoints(struct thread *th, int step) { struct archinfo archinfo; struct iovec regs = { &th->state->regs, th->state->regsize }; architecture_info(&archinfo, ®s); struct list *breaks = &th->proc->breakpoints; for (struct breakpoint *b = breaks->head; b != breaks->end; b = b->next) { - if (!b->installed) { + if (!b->installed && (b->step == step)) { unsigned long word; word = ptrace(PTRACE_PEEKTEXT, th->id, b->address, NULL); b->text = word; @@ -272,13 +273,34 @@ static void continue_all_threads(struct process *proc) { } } +static void compute_step_breakpoints(struct thread *th) { +#ifdef ARCH_AARCH64 + if (th->state->regsize == sizeof(struct user_regs_32)) { + arm_singlestep(th); + } +#endif +} + +static void do_singlestep(struct thread *th) { +#ifdef ARCH_AARCH64 + if (th->state->regsize == sizeof(struct user_regs_32)) { + ptrace(PTRACE_CONT, th->id, NULL, th->signal); + return; + } +#endif + + ptrace(PTRACE_SINGLESTEP, th->id, NULL, th->signal); +} + static void resume_threads(struct process *proc) { struct list *threads = &proc->threads; int once = 0; for (struct thread *th = threads->head; th != threads->end; th = th->next) { if (th->stopped && th->doing == PTRACE_SINGLESTEP) { - ptrace(PTRACE_SINGLESTEP, th->id, NULL, th->signal); + compute_step_breakpoints(th); + install_breakpoints(th, 1); + do_singlestep(th); th->stopped = 0; th->signal = 0; strcpy(th->status, "RUNNING"); @@ -291,7 +313,7 @@ static void resume_threads(struct process *proc) { usleep(SCHEDULER_DELAY); once = 1; } - install_breakpoints(th); + install_breakpoints(th, 0); ptrace(th->doing, th->id, NULL, th->signal); th->stopped = 0; th->signal = 0; @@ -446,7 +468,7 @@ static int wait_thread(struct thread *th) { return -1; } -struct breakpoint *add_breakpoint(struct process *proc, unsigned long address) { +struct breakpoint *add_breakpoint(struct process *proc, unsigned long address, int step) { struct breakpoint *b = xmalloc(sizeof(*b)); b->address = address; b->text = 0; @@ -454,10 +476,24 @@ struct breakpoint *add_breakpoint(struct process *proc, unsigned long address) { b->previously_installed = 0; b->hits = 0; b->user = 1; + b->step = step; b->stack = 0; b->tid = 0; b->enabled = 1; - list_insert(proc->breakpoints.end, b); + + if (step) { + struct breakpoint *p; + struct list *breaks = &proc->breakpoints; + for (p = breaks->head; p != breaks->end; p = p->next) { + if (p->step == 0) { + break; + } + } + list_insert(p, b); + } else { + list_insert(proc->breakpoints.end, b); + } + return b; } @@ -656,7 +692,7 @@ int dbg_stepover(struct thread *th) { cs_disasm_iter(handle, &code, &size, &address, insn); if (insn->id == archinfo.cs_call) { - struct breakpoint *b = add_breakpoint(th->proc, address); + struct breakpoint *b = add_breakpoint(th->proc, address, 0); b->user = 0; b->stack = archinfo.stackptr; b->tid = th->id; -- cgit v1.2.3 From 4c31b8af05f5f388c1eea328a0ec69a6acb3a7f8 Mon Sep 17 00:00:00 2001 From: Matt Hunter Date: Sun, 24 Aug 2025 07:10:14 -0400 Subject: Update detect_breakpoint() to better handle single stepping A stopped thread should sometimes restart, even if a breakpoint interrupted a single step. detect_breakpoint() and its interaction with wait_thread() is updated such that any restart during a single step scenario properly "requeues" the thread's run intent, by preserving the doing/donext flags. Furthermore, detect_breakpoint() ditches its call to get_breakpoint() and considers any and all breakpoints impacting the current thread PC, since they may each suggest different restarting requirements. In effect, the thread will only remain stopped if at least one relevant breakpoint allows it to do so. This reverts commit 5589a9e3afd5 ("Ignore breakpoints during singlestep"). Signed-off-by: Matt Hunter --- debugger.c | 48 ++++++++++++++++++++++++++++++------------------ 1 file changed, 30 insertions(+), 18 deletions(-) (limited to 'debugger.c') diff --git a/debugger.c b/debugger.c index 06447c3..173acbd 100644 --- a/debugger.c +++ b/debugger.c @@ -109,8 +109,9 @@ static void uninstall_breakpoints(struct thread *th) { } static int detect_breakpoint(struct thread *th, int *restart) { - int ret = 0; - *restart = 0; + int is_bp = 0; /* at least 1 effective breakpoint at this PC */ + int is_user = 0; /* at least 1 user-defined bp at this PC */ + *restart = 0; /* at least 1 bp which must stop at this PC */ /* Hack: Need to manually fetch registers here, since capture_state() has * not yet run for this stop. It is not guaranteed that we even want to @@ -123,8 +124,27 @@ static int detect_breakpoint(struct thread *th, int *restart) { architecture_info(&archinfo, &ivregs); unsigned long breakpt_address = archinfo.progmctr - archinfo.bp_adjust; - struct breakpoint *b = get_breakpoint(th->proc, breakpt_address); - if (b && b->installed && th->doing != PTRACE_SINGLESTEP) { + + struct list *breaks = &th->proc->breakpoints; + for (struct breakpoint *b = breaks->head; b != breaks->end; b = b->next) { + if (b->address == breakpt_address) { + if (b->installed /* && th->doing != PTRACE_SINGLESTEP*/) { + /* todo - issues with singlestep? */ + /* todo - put conditional guard around `hits++` */ + is_bp = 1; + b->hits++; + is_user |= b->user; + + if ((b->stack == 0 || b->stack == archinfo.stackptr) + && (b->tid == 0 || b->tid == th->id) + && (b->enabled)) { + *restart = 1; + } + } + } + } + + if (is_bp) { /* restore actual program counter to breakpoint address */ if (ivregs.iov_len == sizeof(struct user_regs_32)) { regs.PROGMCTR_32 = breakpt_address; @@ -132,20 +152,10 @@ static int detect_breakpoint(struct thread *th, int *restart) { regs.PROGMCTR_64 = breakpt_address; } ptrace(PTRACE_SETREGSET, th->id, NT_PRSTATUS, &ivregs); - - b->hits++; /* todo: consider whether this is firing too much */ - ret = b->user; - - if (b->stack != 0 && b->stack != archinfo.stackptr) { - *restart = 1; - } else if (b->tid != 0 && b->tid != th->id) { - *restart = 1; - } else if (!b->enabled) { - *restart = 1; - } } - return ret; + *restart = (is_bp ? !*restart : 0); + return is_user; } static void free_states(struct thread *th) { @@ -441,8 +451,10 @@ static int wait_thread(struct thread *th) { strcpy(th->status, (bp ? "BREAKPOINT" : "STEP")); if (restart) { - th->donext = th->doing; - th->doing = (th->doing ? PTRACE_SINGLESTEP : 0); + if (th->doing != PTRACE_SINGLESTEP) { + th->donext = th->doing; + th->doing = (th->doing ? PTRACE_SINGLESTEP : 0); + } } else { th->doing = th->donext; th->donext = 0; -- cgit v1.2.3 From b2d6f5a4c75e8f68cb27edad38455375b500323b Mon Sep 17 00:00:00 2001 From: Matt Hunter Date: Mon, 25 Aug 2025 02:37:49 -0400 Subject: Always prune step breakpoints when uninstalling from memory On completion of an initial single step, we can and should discard these breakpoints, even though the thread may not be "stopping" and go on to continue in free-run. They have served their purpose at this point and we would like to avoid any other thread encountering them. Also, whenever uninstall_breakpoints() is called, it is because we are cycling a process's threads and are about to run resume_threads() which currently does the work of re-computing a thread's step breakpoints if a previous single step was interrupted, whether or not the step breakpoints for that instant have already been figured. So this also addresses a bug where one thread, with repeatedly interrupted single steps, would accumulate more and more redundant breakpoint entries until it was finally able to proceed and eventually stop. Signed-off-by: Matt Hunter --- debugger.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'debugger.c') diff --git a/debugger.c b/debugger.c index 173acbd..33bdbc0 100644 --- a/debugger.c +++ b/debugger.c @@ -96,7 +96,12 @@ static void uninstall_breakpoints(struct thread *th) { b->installed = 0; } - if (b->previously_installed && b->enabled < 0) { + if (b->step) { + struct breakpoint *del = b; + b = b->next; + list_remove(del); + free(del); + } else if (b->enabled < 0 && b->previously_installed) { struct thread *t = NULL; if (b->tid == 0 || ((t = thread_by_id(th->proc, b->tid)) && !t->doing)) { struct breakpoint *del = b; -- cgit v1.2.3