RFA: Actual support for tracing forks on GNU/Linux

Daniel Jacobowitz
2003-06-18T23:29:46Z

This patch enables "catch fork" and "set follow-fork-mode child" for fork().

Other things from my working directory that doesn't include:
- vfork support - I'll do that separately once this in
- exec support - the code is still a mess
- exit event support - i.e. stopping the process just before it exits,
to take a look around - this confuses GDB very badly
- gdbserver support for any of the above
- enabling the testsuite checks for this - even worse of a mess
I hope to get at least native vfork support into GDB 6.0.

This patch works by adding a hook every time we attach to an LWP or fork a
new LWP, to set tracing flags on it. We enable event reporting for fork()
[which requires ~ 2.5.34 kernel; I heard that one of RH's backport kernels
included this, but I don't know if it still does.] Then, when we get a
fork, we end up attached to both parent and child. We remove breakpoints in
whichever one we don't care about, and then we detach it.

This both lets us choose which one to trace, and also fixes the problem
where breakpoints would be left in the inferior after it forked, causing the
child to die with SIGTRAP.

I had to override kill_inferior, for an issue discovered in testing: when
we're stopped with both processes attached, we have to make sure to kill
them both. We have to deal with this because the user could "catch fork",
and when they see the fork decide which one to debug.

I think that's everything. I'd like at least a nod from the threading
maintainers, since I had to hook into lin-lwp.c. OK?

--
Daniel Jacobowitz
MontaVista Software Debian GNU/Linux Developer

2003-06-18 Daniel Jacobowitz <drow@mvista.com>

* config/i386/nm-linux.h (LINUX_CHILD_POST_STARTUP_INFERIOR): Define.
* config/nm-linux.h (linux_enable_event_reporting)
(linux_handle_extended_wait, linux_child_post_startup_inferior): New
prototypes.
(CHILD_POST_STARTUP_INFERIOR, CHILD_POST_ATTACH, CHILD_FOLLOW_FORK)
(KILL_INFERIOR): Define.
* i386-linux-nat.c (child_post_startup_inferior): New function.
* i386-nat.c (child_post_startup_inferior): Wrap in #ifdef.
* infptrace.c (kill_inferior): Wrap in #ifdef.
* lin-lwp.c (lin_lwp_attach_lwp): Call child_post_attach after
attaching to each LWP.
(child_wait, lin_lwp_wait): Call linux_handle_extended_wait.
(init_lin_lwp_ops): Fill in some more operations.
* linux-nat.c (linux_enable_event_reporting): New function.
(child_post_attach, linux_child_post_startup_inferior)
(child_post_startup_inferior, child_follow_fork)
(linux_handle_extended_wait, kill_inferior): New functions.

diff -Nurp -x '*.gmo' src-one/gdb/config/i386/nm-linux.h src-two/gdb/config/i386/nm-linux.h
--- src-one/gdb/config/i386/nm-linux.h 2002-11-09 16:31:12.000000000 -0500
+++ src-two/gdb/config/i386/nm-linux.h 2003-06-18 17:54:59.000000000 -0400
@@ -82,4 +82,9 @@ extern int cannot_store_register (int re
/* Override child_resume in `infptrace.c'. */
#define CHILD_RESUME

+/* `linux-nat.c' and `i386-nat.c' have their own versions of
+ child_post_startup_inferior. Define this to use the copy in
+ `i386-linux-nat.c' instead, which calls both. */
+#define LINUX_CHILD_POST_STARTUP_INFERIOR
+
#endif /* nm-linux.h */
diff -Nurp -x '*.gmo' src-one/gdb/config/nm-linux.h src-two/gdb/config/nm-linux.h
--- src-one/gdb/config/nm-linux.h 2003-06-18 18:27:58.000000000 -0400
+++ src-two/gdb/config/nm-linux.h 2003-06-18 18:10:47.000000000 -0400
@@ -79,7 +79,15 @@ extern int linux_proc_xfer_memory (CORE_
struct target_ops *target);

extern void linux_record_stopped_pid (int pid);
+extern void linux_enable_event_reporting (ptid_t ptid);
+extern ptid_t linux_handle_extended_wait (int pid, int status,
+ struct target_waitstatus *ourstatus);
+extern void linux_child_post_startup_inferior (ptid_t ptid);

#define CHILD_INSERT_FORK_CATCHPOINT
#define CHILD_INSERT_VFORK_CATCHPOINT
#define CHILD_INSERT_EXEC_CATCHPOINT
+#define CHILD_POST_STARTUP_INFERIOR
+#define CHILD_POST_ATTACH
+#define CHILD_FOLLOW_FORK
+#define KILL_INFERIOR
diff -Nurp -x '*.gmo' src-one/gdb/i386-linux-nat.c src-two/gdb/i386-linux-nat.c
--- src-one/gdb/i386-linux-nat.c 2003-06-04 16:51:29.000000000 -0400
+++ src-two/gdb/i386-linux-nat.c 2003-06-18 17:16:59.000000000 -0400
@@ -890,6 +890,13 @@ child_resume (ptid_t ptid, int step, enu
if (ptrace (request, pid, 0, target_signal_to_host (signal)) == -1)
perror_with_name ("ptrace");
}
+
+void
+child_post_startup_inferior (ptid_t ptid)
+{
+ i386_cleanup_dregs ();
+ linux_child_post_startup_inferior (ptid);
+}
\f

/* Register that we are able to handle GNU/Linux ELF core file
diff -Nurp -x '*.gmo' src-one/gdb/i386-nat.c src-two/gdb/i386-nat.c
--- src-one/gdb/i386-nat.c 2002-07-04 08:32:28.000000000 -0400
+++ src-two/gdb/i386-nat.c 2003-06-18 17:17:40.000000000 -0400
@@ -230,6 +230,7 @@ i386_cleanup_dregs (void)
dr_status_mirror = 0;
}

+#ifndef LINUX_CHILD_POST_STARTUP_INFERIOR
/* Reset all debug registers at each new startup
to avoid missing watchpoints after restart. */
void
@@ -237,6 +238,7 @@ child_post_startup_inferior (ptid_t ptid
{
i386_cleanup_dregs ();
}
+#endif /* LINUX_CHILD_POST_STARTUP_INFERIOR */

/* Print the values of the mirrored debug registers.
This is called when maint_show_dr is non-zero. To set that
diff -Nurp -x '*.gmo' src-one/gdb/infptrace.c src-two/gdb/infptrace.c
--- src-one/gdb/infptrace.c 2003-06-18 17:18:44.000000000 -0400
+++ src-two/gdb/infptrace.c 2003-06-18 17:19:07.000000000 -0400
@@ -208,6 +208,7 @@ ptrace_wait (ptid_t ptid, int *status)
return wstate;
}

+#ifndef KILL_INFERIOR
void
kill_inferior (void)
{
@@ -229,6 +230,7 @@ kill_inferior (void)
ptrace_wait (null_ptid, &status);
target_mourn_inferior ();
}
+#endif /* KILL_INFERIOR */

#ifndef CHILD_RESUME

diff -Nurp -x '*.gmo' src-one/gdb/lin-lwp.c src-two/gdb/lin-lwp.c
--- src-one/gdb/lin-lwp.c 2003-06-18 18:27:58.000000000 -0400
+++ src-two/gdb/lin-lwp.c 2003-06-18 17:48:29.000000000 -0400
@@ -399,6 +399,8 @@ lin_lwp_attach_lwp (ptid_t ptid, int ver
gdb_assert (pid == GET_LWP (ptid)
&& WIFSTOPPED (status) && WSTOPSIG (status));

+ child_post_attach (pid);
+
lp->stopped = 1;

if (debug_lin_lwp)
@@ -1142,6 +1144,10 @@ child_wait (ptid_t ptid, struct target_w
return minus_one_ptid;
}

+ /* Handle GNU/Linux's extended waitstatus for trace events. */
+ if (WIFSTOPPED (status) && WSTOPSIG (status) == SIGTRAP && status >> 16 != 0)
+ return linux_handle_extended_wait (pid, status, ourstatus);
+
store_waitstatus (ourstatus, status);
return pid_to_ptid (pid);
}
@@ -1563,6 +1569,14 @@ retry:
else
trap_ptid = null_ptid;

+ /* Handle GNU/Linux's extended waitstatus for trace events. */
+ if (WIFSTOPPED (status) && WSTOPSIG (status) == SIGTRAP && status >> 16 != 0)
+ {
+ linux_handle_extended_wait (ptid_get_pid (trap_ptid),
+ status, ourstatus);
+ return trap_ptid;
+ }
+
store_waitstatus (ourstatus, status);
return (threaded ? lp->ptid : pid_to_ptid (GET_LWP (lp->ptid)));
}
@@ -1732,6 +1746,12 @@ init_lin_lwp_ops (void)
lin_lwp_ops.to_mourn_inferior = lin_lwp_mourn_inferior;
lin_lwp_ops.to_thread_alive = lin_lwp_thread_alive;
lin_lwp_ops.to_pid_to_str = lin_lwp_pid_to_str;
+ lin_lwp_ops.to_post_startup_inferior = child_post_startup_inferior;
+ lin_lwp_ops.to_post_attach = child_post_attach;
+ lin_lwp_ops.to_insert_fork_catchpoint = child_insert_fork_catchpoint;
+ lin_lwp_ops.to_insert_vfork_catchpoint = child_insert_vfork_catchpoint;
+ lin_lwp_ops.to_insert_exec_catchpoint = child_insert_exec_catchpoint;
+
lin_lwp_ops.to_stratum = thread_stratum;
lin_lwp_ops.to_has_thread_control = tc_schedlock;
lin_lwp_ops.to_magic = OPS_MAGIC;
diff -Nurp -x '*.gmo' src-one/gdb/linux-nat.c src-two/gdb/linux-nat.c
--- src-one/gdb/linux-nat.c 2003-06-18 18:27:58.000000000 -0400
+++ src-two/gdb/linux-nat.c 2003-06-18 18:30:10.000000000 -0400
@@ -54,6 +54,8 @@
#define __WALL 0x40000000 /* Wait for any child. */
#endif

+extern struct target_ops child_ops;
+
struct simple_pid_list
{
int pid;
@@ -187,13 +189,147 @@ linux_supports_tracefork (void)
}

\f
+void
+linux_enable_event_reporting (ptid_t ptid)
+{
+ int pid = ptid_get_pid (ptid);
+ int options;
+
+ if (! linux_supports_tracefork ())
+ return;
+
+ options = PTRACE_O_TRACEFORK;
+
+ ptrace (PTRACE_SETOPTIONS, pid, 0, options);
+}
+
+void
+child_post_attach (int pid)
+{
+ linux_enable_event_reporting (pid_to_ptid (pid));
+}
+
+void
+linux_child_post_startup_inferior (ptid_t ptid)
+{
+ linux_enable_event_reporting (ptid);
+}
+
+#ifndef LINUX_CHILD_POST_STARTUP_INFERIOR
+void
+child_post_startup_inferior (ptid_t ptid)
+{
+ linux_child_post_startup_inferior (ptid);
+}
+#endif
+
int
-child_insert_fork_catchpoint (int pid)
+child_follow_fork (int follow_child)
{
- if (linux_supports_tracefork ())
- error ("Fork catchpoints have not been implemented yet.");
+ ptid_t last_ptid;
+ struct target_waitstatus last_status;
+ int parent_pid, child_pid;
+
+ get_last_target_status (&last_ptid, &last_status);
+ parent_pid = ptid_get_pid (last_ptid);
+ child_pid = last_status.value.related_pid;
+
+ if (! follow_child)
+ {
+ /* We're already attached to the parent, by default. */
+
+ /* Before detaching from the child, remove all breakpoints from
+ it. (This won't actually modify the breakpoint list, but will
+ physically remove the breakpoints from the child.) */
+ detach_breakpoints (child_pid);
+
+ fprintf_filtered (gdb_stdout,
+ "Detaching after fork from child process %d.\n",
+ child_pid);
+
+ ptrace (PTRACE_DETACH, child_pid, 0, 0);
+ }
else
+ {
+ char child_pid_spelling[40];
+
+ /* Needed to keep the breakpoint lists in sync. */
+ detach_breakpoints (child_pid);
+
+ /* Before detaching from the parent, remove all breakpoints from it. */
+ remove_breakpoints ();
+
+ fprintf_filtered (gdb_stdout,
+ "Attaching after fork to child process %d.\n",
+ child_pid);
+
+ target_detach (NULL, 0);
+
+ inferior_ptid = pid_to_ptid (child_pid);
+ push_target (&child_ops);
+
+ /* Reset breakpoints in the child as appropriate. */
+ follow_inferior_reset_breakpoints ();
+ }
+
+ return 0;
+}
+
+ptid_t
+linux_handle_extended_wait (int pid, int status,
+ struct target_waitstatus *ourstatus)
+{
+ int event = status >> 16;
+
+ if (event == PTRACE_EVENT_CLONE)
+ internal_error (__FILE__, __LINE__,
+ "unexpected clone event");
+
+ if (event == PTRACE_EVENT_FORK)
+ {
+ unsigned long new_pid;
+ int ret;
+
+ ptrace (PTRACE_GETEVENTMSG, pid, 0, &new_pid);
+
+ /* If we haven't already seen the new PID stop, wait for it now. */
+ if (! pull_pid_from_list (&stopped_pids, new_pid))
+ {
+ /* The new child has a pending SIGSTOP. We can't affect it until it
+ hits the SIGSTOP, but we're already attached.
+
+ It won't be a clone (we didn't ask for clones in the event mask)
+ so we can just call waitpid and wait for the SIGSTOP. */
+ do {
+ ret = waitpid (new_pid, &status, 0);
+ } while (ret == -1 && errno == EINTR);
+ if (ret == -1)
+ perror_with_name ("waiting for new child");
+ else if (ret != new_pid)
+ internal_error (__FILE__, __LINE__,
+ "wait returned unexpected PID %d", ret);
+ else if (!WIFSTOPPED (status) || WSTOPSIG (status) != SIGSTOP)
+ internal_error (__FILE__, __LINE__,
+ "wait returned unexpected status 0x%x", status);
+ }
+
+ ourstatus->kind = TARGET_WAITKIND_FORKED;
+ ourstatus->value.related_pid = new_pid;
+ return inferior_ptid;
+ }
+
+ internal_error (__FILE__, __LINE__,
+ "unknown ptrace event %d", event);
+}
+
+\f
+int
+child_insert_fork_catchpoint (int pid)
+{
+ if (! linux_supports_tracefork ())
error ("Your system does not support fork catchpoints.");
+
+ return 0;
}

int
@@ -214,4 +350,43 @@ child_insert_exec_catchpoint (int pid)
error ("Your system does not support exec catchpoints.");
}

+void
+kill_inferior (void)
+{
+ int status;
+ int pid = PIDGET (inferior_ptid);
+ struct target_waitstatus last;
+ ptid_t last_ptid;
+ int ret;
+
+ if (pid == 0)
+ return;
+
+ /* If we're stopped while forking and we haven't followed yet, kill the
+ other task. We need to do this first because the parent will be
+ sleeping if this is a vfork. */
+
+ get_last_target_status (&last_ptid, &last);

+ if (last.kind == TARGET_WAITKIND_FORKED
+ || last.kind == TARGET_WAITKIND_VFORKED)
+ {
+ ptrace (PT_KILL, last.value.related_pid);
+ ptrace_wait (null_ptid, &status);
+ }
+
+ /* Kill the current process. */
+ ptrace (PT_KILL, pid, (PTRACE_ARG3_TYPE) 0, 0);
+ ret = ptrace_wait (null_ptid, &status);
+
+ /* We might get a SIGCHLD instead of an exit status. This is
+ aggravated by the first kill above - a child has just died. */
+
+ while (ret == pid && WIFSTOPPED (status))
+ {
+ ptrace (PT_KILL, pid, (PTRACE_ARG3_TYPE) 0, 0);
+ ret = ptrace_wait (null_ptid, &status);
+ }
+
+ target_mourn_inferior ();
+}