diff options
author | Pedro Alves <pedro@palves.net> | 2021-11-12 20:50:29 +0000 |
---|---|---|
committer | Pedro Alves <pedro@palves.net> | 2023-11-13 14:16:09 +0000 |
commit | 0d36baa9af0d9929c96b89a184a469c432c68b0d (patch) | |
tree | 5f01799868d0082aed9b3639501d3a9366b50b40 /gdb/infrun.c | |
parent | 6a534f85cbdb065a307aa96fbbb670948ee135c8 (diff) | |
download | gdb-0d36baa9af0d9929c96b89a184a469c432c68b0d.zip gdb-0d36baa9af0d9929c96b89a184a469c432c68b0d.tar.gz gdb-0d36baa9af0d9929c96b89a184a469c432c68b0d.tar.bz2 |
Step over clone syscall w/ breakpoint, TARGET_WAITKIND_THREAD_CLONED
(A good chunk of the problem statement in the commit log below is
Andrew's, adjusted for a different solution, and for covering
displaced stepping too. The testcase is mostly Andrew's too.)
This commit addresses bugs gdb/19675 and gdb/27830, which are about
stepping over a breakpoint set at a clone syscall instruction, one is
about displaced stepping, and the other about in-line stepping.
Currently, when a new thread is created through a clone syscall, GDB
sets the new thread running. With 'continue' this makes sense
(assuming no schedlock):
- all-stop mode, user issues 'continue', all threads are set running,
a newly created thread should also be set running.
- non-stop mode, user issues 'continue', other pre-existing threads
are not affected, but as the new thread is (sort-of) a child of the
thread the user asked to run, it makes sense that the new threads
should be created in the running state.
Similarly, if we are stopped at the clone syscall, and there's no
software breakpoint at this address, then the current behaviour is
fine:
- all-stop mode, user issues 'stepi', stepping will be done in place
(as there's no breakpoint to step over). While stepping the thread
of interest all the other threads will be allowed to continue. A
newly created thread will be set running, and then stopped once the
thread of interest has completed its step.
- non-stop mode, user issues 'stepi', stepping will be done in place
(as there's no breakpoint to step over). Other threads might be
running or stopped, but as with the continue case above, the new
thread will be created running. The only possible issue here is
that the new thread will be left running after the initial thread
has completed its stepi. The user would need to manually select
the thread and interrupt it, this might not be what the user
expects. However, this is not something this commit tries to
change.
The problem then is what happens when we try to step over a clone
syscall if there is a breakpoint at the syscall address.
- For both all-stop and non-stop modes, with in-line stepping:
+ user issues 'stepi',
+ [non-stop mode only] GDB stops all threads. In all-stop mode all
threads are already stopped.
+ GDB removes s/w breakpoint at syscall address,
+ GDB single steps just the thread of interest, all other threads
are left stopped,
+ New thread is created running,
+ Initial thread completes its step,
+ [non-stop mode only] GDB resumes all threads that it previously
stopped.
There are two problems in the in-line stepping scenario above:
1. The new thread might pass through the same code that the initial
thread is in (i.e. the clone syscall code), in which case it will
fail to hit the breakpoint in clone as this was removed so the
first thread can single step,
2. The new thread might trigger some other stop event before the
initial thread reports its step completion. If this happens we
end up triggering an assertion as GDB assumes that only the
thread being stepped should stop. The assert looks like this:
infrun.c:5899: internal-error: int finish_step_over(execution_control_state*): Assertion `ecs->event_thread->control.trap_expected' failed.
- For both all-stop and non-stop modes, with displaced stepping:
+ user issues 'stepi',
+ GDB starts the displaced step, moves thread's PC to the
out-of-line scratch pad, maybe adjusts registers,
+ GDB single steps the thread of interest, [non-stop mode only] all
other threads are left as they were, either running or stopped.
In all-stop, all other threads are left stopped.
+ New thread is created running,
+ Initial thread completes its step, GDB re-adjusts its PC,
restores/releases scratchpad,
+ [non-stop mode only] GDB resumes the thread, now past its
breakpoint.
+ [all-stop mode only] GDB resumes all threads.
There is one problem with the displaced stepping scenario above:
3. When the parent thread completed its step, GDB adjusted its PC,
but did not adjust the child's PC, thus that new child thread
will continue execution in the scratch pad, invoking undefined
behavior. If you're lucky, you see a crash. If unlucky, the
inferior gets silently corrupted.
What is needed is for GDB to have more control over whether the new
thread is created running or not. Issue #1 above requires that the
new thread not be allowed to run until the breakpoint has been
reinserted. The only way to guarantee this is if the new thread is
held in a stopped state until the single step has completed. Issue #3
above requires that GDB is informed of when a thread clones itself,
and of what is the child's ptid, so that GDB can fixup both the parent
and the child.
When looking for solutions to this problem I considered how GDB
handles fork/vfork as these have some of the same issues. The main
difference between fork/vfork and clone is that the clone events are
not reported back to core GDB. Instead, the clone event is handled
automatically in the target code and the child thread is immediately
set running.
Note we have support for requesting thread creation events out of the
target (TARGET_WAITKIND_THREAD_CREATED). However, those are reported
for the new/child thread. That would be sufficient to address in-line
stepping (issue #1), but not for displaced-stepping (issue #3). To
handle displaced-stepping, we need an event that is reported to the
_parent_ of the clone, as the information about the displaced step is
associated with the clone parent. TARGET_WAITKIND_THREAD_CREATED
includes no indication of which thread is the parent that spawned the
new child. In fact, for some targets, like e.g., Windows, it would be
impossible to know which thread that was, as thread creation there
doesn't work by "cloning".
The solution implemented here is to model clone on fork/vfork, and
introduce a new TARGET_WAITKIND_THREAD_CLONED event. This event is
similar to TARGET_WAITKIND_FORKED and TARGET_WAITKIND_VFORKED, except
that we end up with a new thread in the same process, instead of a new
thread of a new process. Like FORKED and VFORKED, THREAD_CLONED
waitstatuses have a child_ptid property, and the child is held stopped
until GDB explicitly resumes it. This addresses the in-line stepping
case (issues #1 and #2).
The infrun code that handles displaced stepping fixup for the child
after a fork/vfork event is thus reused for THREAD_CLONE, with some
minimal conditions added, addressing the displaced stepping case
(issue #3).
The native Linux backend is adjusted to unconditionally report
TARGET_WAITKIND_THREAD_CLONED events to the core.
Following the follow_fork model in core GDB, we introduce a
target_follow_clone target method, which is responsible for making the
new clone child visible to the rest of GDB.
Subsequent patches will add clone events support to the remote
protocol and gdbserver.
displaced_step_in_progress_thread becomes unused with this patch, but
a new use will reappear later in the series. To avoid deleting it and
readding it back, this patch marks it with attribute unused, and the
latter patch removes the attribute again. We need to do this because
the function is static, and with no callers, the compiler would warn,
(error with -Werror), breaking the build.
This adds a new gdb.threads/stepi-over-clone.exp testcase, which
exercises stepping over a clone syscall, with displaced stepping vs
inline stepping, and all-stop vs non-stop. We already test stepping
over clone syscalls with gdb.base/step-over-syscall.exp, but this test
uses pthreads, while the other test uses raw clone, and this one is
more thorough. The testcase passes on native GNU/Linux, but fails
against GDBserver. GDBserver will be fixed by a later patch in the
series.
Co-authored-by: Andrew Burgess <aburgess@redhat.com>
Bug: https://sourceware.org/bugzilla/show_bug.cgi?id=19675
Bug: https://sourceware.org/bugzilla/show_bug.cgi?id=27830
Change-Id: I95c06024736384ae8542a67ed9fdf6534c325c8e
Reviewed-By: Andrew Burgess <aburgess@redhat.com>
Diffstat (limited to 'gdb/infrun.c')
-rw-r--r-- | gdb/infrun.c | 158 |
1 files changed, 88 insertions, 70 deletions
diff --git a/gdb/infrun.c b/gdb/infrun.c index c60cfc0..e3157f8 100644 --- a/gdb/infrun.c +++ b/gdb/infrun.c @@ -1606,6 +1606,7 @@ step_over_info_valid_p (void) /* Return true if THREAD is doing a displaced step. */ static bool +ATTRIBUTE_UNUSED displaced_step_in_progress_thread (thread_info *thread) { gdb_assert (thread != nullptr); @@ -1967,6 +1968,31 @@ static displaced_step_finish_status displaced_step_finish (thread_info *event_thread, const target_waitstatus &event_status) { + /* Check whether the parent is displaced stepping. */ + struct regcache *regcache = get_thread_regcache (event_thread); + struct gdbarch *gdbarch = regcache->arch (); + inferior *parent_inf = event_thread->inf; + + /* If this was a fork/vfork/clone, this event indicates that the + displaced stepping of the syscall instruction has been done, so + we perform cleanup for parent here. Also note that this + operation also cleans up the child for vfork, because their pages + are shared. */ + + /* If this is a fork (child gets its own address space copy) and + some displaced step buffers were in use at the time of the fork, + restore the displaced step buffer bytes in the child process. + + Architectures which support displaced stepping and fork events + must supply an implementation of + gdbarch_displaced_step_restore_all_in_ptid. This is not enforced + during gdbarch validation to support architectures which support + displaced stepping but not forks. */ + if (event_status.kind () == TARGET_WAITKIND_FORKED + && gdbarch_supports_displaced_stepping (gdbarch)) + gdbarch_displaced_step_restore_all_in_ptid + (gdbarch, parent_inf, event_status.child_ptid ()); + displaced_step_thread_state *displaced = &event_thread->displaced_step_state; /* Was this thread performing a displaced step? */ @@ -1986,8 +2012,39 @@ displaced_step_finish (thread_info *event_thread, /* Do the fixup, and release the resources acquired to do the displaced step. */ - return gdbarch_displaced_step_finish (displaced->get_original_gdbarch (), - event_thread, event_status); + displaced_step_finish_status status + = gdbarch_displaced_step_finish (displaced->get_original_gdbarch (), + event_thread, event_status); + + if (event_status.kind () == TARGET_WAITKIND_FORKED + || event_status.kind () == TARGET_WAITKIND_VFORKED + || event_status.kind () == TARGET_WAITKIND_THREAD_CLONED) + { + /* Since the vfork/fork/clone syscall instruction was executed + in the scratchpad, the child's PC is also within the + scratchpad. Set the child's PC to the parent's PC value, + which has already been fixed up. Note: we use the parent's + aspace here, although we're touching the child, because the + child hasn't been added to the inferior list yet at this + point. */ + + struct regcache *child_regcache + = get_thread_arch_aspace_regcache (parent_inf, + event_status.child_ptid (), + gdbarch, + parent_inf->aspace); + /* Read PC value of parent. */ + CORE_ADDR parent_pc = regcache_read_pc (regcache); + + displaced_debug_printf ("write child pc from %s to %s", + paddress (gdbarch, + regcache_read_pc (child_regcache)), + paddress (gdbarch, parent_pc)); + + regcache_write_pc (child_regcache, parent_pc); + } + + return status; } /* Data to be passed around while handling an event. This data is @@ -5854,67 +5911,13 @@ handle_inferior_event (struct execution_control_state *ecs) case TARGET_WAITKIND_FORKED: case TARGET_WAITKIND_VFORKED: - /* Check whether the inferior is displaced stepping. */ - { - struct regcache *regcache = get_thread_regcache (ecs->event_thread); - struct gdbarch *gdbarch = regcache->arch (); - inferior *parent_inf = find_inferior_ptid (ecs->target, ecs->ptid); - - /* If this is a fork (child gets its own address space copy) - and some displaced step buffers were in use at the time of - the fork, restore the displaced step buffer bytes in the - child process. - - Architectures which support displaced stepping and fork - events must supply an implementation of - gdbarch_displaced_step_restore_all_in_ptid. This is not - enforced during gdbarch validation to support architectures - which support displaced stepping but not forks. */ - if (ecs->ws.kind () == TARGET_WAITKIND_FORKED - && gdbarch_supports_displaced_stepping (gdbarch)) - gdbarch_displaced_step_restore_all_in_ptid - (gdbarch, parent_inf, ecs->ws.child_ptid ()); - - /* If displaced stepping is supported, and thread ecs->ptid is - displaced stepping. */ - if (displaced_step_in_progress_thread (ecs->event_thread)) - { - struct regcache *child_regcache; - CORE_ADDR parent_pc; - - /* GDB has got TARGET_WAITKIND_FORKED or TARGET_WAITKIND_VFORKED, - indicating that the displaced stepping of syscall instruction - has been done. Perform cleanup for parent process here. Note - that this operation also cleans up the child process for vfork, - because their pages are shared. */ - displaced_step_finish (ecs->event_thread, ecs->ws); - /* Start a new step-over in another thread if there's one - that needs it. */ - start_step_over (); - - /* Since the vfork/fork syscall instruction was executed in the scratchpad, - the child's PC is also within the scratchpad. Set the child's PC - to the parent's PC value, which has already been fixed up. - FIXME: we use the parent's aspace here, although we're touching - the child, because the child hasn't been added to the inferior - list yet at this point. */ - - child_regcache - = get_thread_arch_aspace_regcache (parent_inf, - ecs->ws.child_ptid (), - gdbarch, - parent_inf->aspace); - /* Read PC value of parent process. */ - parent_pc = regcache_read_pc (regcache); - - displaced_debug_printf ("write child pc from %s to %s", - paddress (gdbarch, - regcache_read_pc (child_regcache)), - paddress (gdbarch, parent_pc)); - - regcache_write_pc (child_regcache, parent_pc); - } - } + case TARGET_WAITKIND_THREAD_CLONED: + + displaced_step_finish (ecs->event_thread, ecs->ws); + + /* Start a new step-over in another thread if there's one that + needs it. */ + start_step_over (); context_switch (ecs); @@ -5930,7 +5933,7 @@ handle_inferior_event (struct execution_control_state *ecs) need to unpatch at follow/detach time instead to be certain that new breakpoints added between catchpoint hit time and vfork follow are detached. */ - if (ecs->ws.kind () != TARGET_WAITKIND_VFORKED) + if (ecs->ws.kind () == TARGET_WAITKIND_FORKED) { /* This won't actually modify the breakpoint list, but will physically remove the breakpoints from the child. */ @@ -5962,14 +5965,24 @@ handle_inferior_event (struct execution_control_state *ecs) if (!bpstat_causes_stop (ecs->event_thread->control.stop_bpstat)) { bool follow_child - = (follow_fork_mode_string == follow_fork_mode_child); + = (ecs->ws.kind () != TARGET_WAITKIND_THREAD_CLONED + && follow_fork_mode_string == follow_fork_mode_child); ecs->event_thread->set_stop_signal (GDB_SIGNAL_0); process_stratum_target *targ = ecs->event_thread->inf->process_target (); - bool should_resume = follow_fork (); + bool should_resume; + if (ecs->ws.kind () != TARGET_WAITKIND_THREAD_CLONED) + should_resume = follow_fork (); + else + { + should_resume = true; + inferior *inf = ecs->event_thread->inf; + inf->top_target ()->follow_clone (ecs->ws.child_ptid ()); + ecs->event_thread->pending_follow.set_spurious (); + } /* Note that one of these may be an invalid pointer, depending on detach_fork. */ @@ -5980,16 +5993,21 @@ handle_inferior_event (struct execution_control_state *ecs) child is marked stopped. */ /* If not resuming the parent, mark it stopped. */ - if (follow_child && !detach_fork && !non_stop && !sched_multi) + if (ecs->ws.kind () != TARGET_WAITKIND_THREAD_CLONED + && follow_child && !detach_fork && !non_stop && !sched_multi) parent->set_running (false); /* If resuming the child, mark it running. */ - if (follow_child || (!detach_fork && (non_stop || sched_multi))) + if (ecs->ws.kind () == TARGET_WAITKIND_THREAD_CLONED + || (follow_child || (!detach_fork && (non_stop || sched_multi)))) child->set_running (true); /* In non-stop mode, also resume the other branch. */ - if (!detach_fork && (non_stop - || (sched_multi && target_is_non_stop_p ()))) + if ((ecs->ws.kind () == TARGET_WAITKIND_THREAD_CLONED + && target_is_non_stop_p ()) + || (!detach_fork && (non_stop + || (sched_multi + && target_is_non_stop_p ())))) { if (follow_child) switch_to_thread (parent); |