threads/157755: gdb hardware watchpoints do not work correctly in multi-threaded programs

KUROSAWA Takahiro takahiro.kurosawa at gmail.com
Sat Jun 11 04:50:09 UTC 2011


>Number:         157755
>Category:       threads
>Synopsis:       gdb hardware watchpoints do not work correctly in multi-threaded programs
>Confidential:   no
>Severity:       non-critical
>Priority:       low
>Responsible:    freebsd-threads
>State:          open
>Quarter:        
>Keywords:       
>Date-Required:
>Class:          sw-bug
>Submitter-Id:   current-users
>Arrival-Date:   Sat Jun 11 04:50:09 UTC 2011
>Closed-Date:
>Last-Modified:
>Originator:     KUROSAWA Takahiro
>Release:        9-CURRENT
>Organization:
>Environment:
FreeBSD amdk6 9.0-CURRENT FreeBSD 9.0-CURRENT #4: Fri Jun  3 19:18:20 JST 2011     kurosawa at amdk6:/usr/obj/.world/src/sys/AMDK7  i386

>Description:
Hardware watchpoints are implemented on gdb in FreeBSD/i386 and
FreeBSD/amd64 by setting debug registers with ptrace(PT_SETDBREGS).
But watchpoints are effective only for one thread in the multi-threaded
program because debug registers are only set per process.

>How-To-Repeat:
We can reproduce the problem by the following procedure:

% cat watch.c
#include <pthread.h>
#include <stdio.h>
#include <string.h>
#include <err.h>

static int watchpoint;

void *
thr_main(void *arg)
{

        watchpoint = 2;
        return NULL;
}

int
main(void)
{
        pthread_t pt;
        int ret = 0;

        if ((ret = pthread_create(&pt, NULL, thr_main, NULL)) != 0)
                errx(1, "pthread_create: %s.\n", strerror(ret));

        ret = 2;
        pthread_join(pt, NULL);
        watchpoint = 1;
        ret = 1;

        return 0;
}
% cc -pthread -g watch.c
% gdb a.out
GNU gdb 6.1.1 [FreeBSD]
Copyright 2004 Free Software Foundation, Inc.
   ....

# "break thr_main" sets a breakpoint at "watchpoint = 2"
(gdb) x/i thr_main
0x8048540 <thr_main>:   push   %ebp
(gdb) break *0x8048540
Breakpoint 1 at 0x8048540: file watch.c, line 10.

(gdb) run
Starting program: /home/kurosawa/a.out
[New LWP 100260]
[New Thread 28404300 (LWP 100260/initial thread)]
[New Thread 28404900 (LWP 100693/a.out)]
[Switching to Thread 28404900 (LWP 100693/a.out)]

Breakpoint 1, thr_main (arg=0x0) at watch.c:10
10      {

# set an watchpoint to the "watchpoint" variable
(gdb) watch watchpoint
Hardware watchpoint 2: watchpoint

# watchpoint hit in thr_main
(gdb) c
Continuing.
Hardware watchpoint 2: watchpoint

Old value = 0
New value = 2
thr_main (arg=0x0) at watch.c:13
13              return NULL;

(gdb) c
Continuing.
[Thread 28404900 (LWP 100693/a.out) exited]
[New Thread 28404900 (LWP 100693/a.out)]

Program exited normally.
# watchpoint must be hit also in main, but program runs without stopping.

>Fix:
The attached patch fixes the problem.

----
(gdb) x/i thr_main
0x8048540 <thr_main>:   push   %ebp
(gdb) break *0x8048540
Breakpoint 1 at 0x8048540: file watch.c, line 10.
(gdb) run
Starting program: /home/kurosawa/a.out
[New LWP 100163]
[New Thread 28404300 (LWP 100163/initial thread)]
[New Thread 28404900 (LWP 100694/a.out)]
[Switching to Thread 28404900 (LWP 100694/a.out)]

Breakpoint 1, thr_main (arg=0x0) at watch.c:10
10      {
(gdb) watch watchpoint
Hardware watchpoint 2: watchpoint
(gdb) c
Continuing.
Hardware watchpoint 2: watchpoint

Old value = 0
New value = 2
thr_main (arg=0x0) at watch.c:13
13              return NULL;
(gdb) c
Continuing.
[Thread 28404900 (LWP 100694/a.out) exited]
[New Thread 28404900 (LWP 100694/a.out)]
[Switching to Thread 28404300 (LWP 100163/initial thread)]
Hardware watchpoint 2: watchpoint

Old value = 2
New value = 1
main () at watch.c:28
28              ret = 1;
(gdb) c
Continuing.

Program exited normally.
----

The patch only fixes FreeBSD/i386 but the same approach could be used
for FreeBSD/amd64.


Patch attached with submission follows:

# HG changeset patch
# Parent d95f240e861430967a2cd1ee93e8c1b0d5a3b6ef

diff -r d95f240e8614 gnu/usr.bin/gdb/arch/i386/Makefile
--- a/gnu/usr.bin/gdb/arch/i386/Makefile	Fri Jun 03 15:58:48 2011 +0900
+++ b/gnu/usr.bin/gdb/arch/i386/Makefile	Sat Jun 11 12:50:52 2011 +0900
@@ -9,7 +9,26 @@ LIBSRCS+= solib.c solib-svr4.c
 LIBSRCS+= i386-tdep.c i386bsd-tdep.c i386fbsd-tdep-fixed.c i387-tdep.c
 
 nm.h:
-	echo '#include "i386/nm-fbsd.h"' > ${.TARGET}
+	echo '#ifndef GENSRCS_NM_H' > ${.TARGET}
+	echo '#define GENSRCS_NM_H' >> ${.TARGET}
+	echo '#include "i386/nm-fbsd.h"' >> ${.TARGET}
+	echo '#undef I386_DR_LOW_SET_CONTROL' >> ${.TARGET}
+	echo '#undef I386_DR_LOW_SET_ADDR' >> ${.TARGET}
+	echo '#undef I386_DR_LOW_RESET_ADDR' >> ${.TARGET}
+	echo '#undef I386_DR_LOW_GET_STATUS' >> ${.TARGET}
+	echo '#define I386_DR_LOW_SET_CONTROL(c) \' >> ${.TARGET}
+	echo '  fbsd_thread_dr_set_control (c)' >> ${.TARGET}
+	echo 'void i386bsd_dr_set_control (unsigned long);' >> ${.TARGET}
+	echo '#define I386_DR_LOW_SET_ADDR(r, v) \' >> ${.TARGET}
+	echo '  fbsd_thread_dr_set_addr (r, v)' >> ${.TARGET}
+	echo 'void fbsd_thread_dr_set_addr (int, CORE_ADDR);' >> ${.TARGET}
+	echo '#define I386_DR_LOW_RESET_ADDR(r) \' >> ${.TARGET}
+	echo '  fbsd_thread_dr_reset_addr (r)' >> ${.TARGET}
+	echo 'void fbsd_thread_dr_reset_addr (int);' >> ${.TARGET}
+	echo '#define I386_DR_LOW_GET_STATUS() \' >> ${.TARGET}
+	echo '  fbsd_thread_dr_get_status ()' >> ${.TARGET}
+	echo 'unsigned long fbsd_thread_dr_get_status (void);' >> ${.TARGET}
+	echo '#endif /* GENSRCS_NM_H */' >> ${.TARGET}
 
 tm.h:
 	echo '#include "i386/tm-fbsd.h"' > ${.TARGET}
diff -r d95f240e8614 gnu/usr.bin/gdb/libgdb/fbsd-threads.c
--- a/gnu/usr.bin/gdb/libgdb/fbsd-threads.c	Fri Jun 03 15:58:48 2011 +0900
+++ b/gnu/usr.bin/gdb/libgdb/fbsd-threads.c	Sat Jun 11 12:50:52 2011 +0900
@@ -1797,3 +1797,183 @@ ps_linfo(struct ps_prochandle *ph, lwpid
     return PS_ERR;
   return PS_OK;
 }
+
+
+#if defined(HAVE_PT_GETDBREGS) && defined(I386_DR_LOW_SET_ADDR)
+/* Hardware watchpoint support based on i386bsd-nat.c: */
+/* Native-dependent code for modern i386 BSD's.
+   Copyright 2000, 2001, 2002, 2003 Free Software Foundation, Inc.
+
+   This file is part of GDB.
+
+   This program is free software; you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 2 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program; if not, write to the Free Software
+   Foundation, Inc., 59 Temple Place - Suite 330,
+   Boston, MA 02111-1307, USA.  */
+
+/*
+ * We need to add threads support because debug registers need to be
+ * set per LWP to implement hardware watchpoints.
+ * 
+ * We do not need to care user-level threads contexts for debug registers
+ * since they are not in mcontext.
+ */
+
+/* Not all versions of FreeBSD/i386 that support the debug registers
+   have this macro.  */
+#ifndef DBREG_DRX
+#define DBREG_DRX(d, x) ((&d->dr0)[x])
+#endif
+
+static void
+fbsd_lwp_dr_set (lwpid_t lwpid, int regnum, unsigned long value)
+{
+  struct dbreg dbregs;
+
+  if (ptrace (PT_GETDBREGS, lwpid,
+              (PTRACE_ARG3_TYPE) &dbregs, 0) == -1)
+    perror_with_name ("Couldn't get debug registers");
+
+  /* For some mysterious reason, some of the reserved bits in the
+     debug control register get set.  Mask these off, otherwise the
+     ptrace call below will fail.  */
+  DBREG_DRX ((&dbregs), 7) &= ~(0x0000fc00);
+
+  DBREG_DRX ((&dbregs), regnum) = value;
+
+  if (ptrace (PT_SETDBREGS, lwpid,
+              (PTRACE_ARG3_TYPE) &dbregs, 0) == -1)
+    perror_with_name ("Couldn't write debug registers");
+}
+
+struct dr_set_thr_arg {
+  int regnum;
+  unsigned long value;
+};
+
+static int
+fbsd_thread_dr_set_callback (const td_thrhandle_t *th_p, void *data)
+{
+  struct dr_set_thr_arg *arg = data;
+  td_thrinfo_t ti;
+  td_err_e err;
+
+  err = td_thr_get_info_p (th_p, &ti);
+  if (err != TD_OK)
+    error ("Cannot get thread info: %s", thread_db_err_str (err));
+
+  /* Ignore zombie */
+  if (ti.ti_state == TD_THR_UNKNOWN || ti.ti_state == TD_THR_ZOMBIE)
+    return 0;
+
+  fbsd_lwp_dr_set (ti.ti_lid, arg->regnum, arg->value);
+  return 0;
+}
+
+static void
+fbsd_thread_dr_set (int regnum, unsigned long value)
+{
+  struct dr_set_thr_arg arg;
+  td_err_e err;
+
+  arg.regnum = regnum;
+  arg.value = value;
+
+  /* Iterating over LWPs is sufficient actually. */
+  err = td_ta_thr_iter_p (thread_agent, fbsd_thread_dr_set_callback, &arg,
+          TD_THR_ANY_STATE, TD_THR_LOWEST_PRIORITY,
+          TD_SIGNO_MASK, TD_THR_ANY_USER_FLAGS);
+  if (err != TD_OK)
+    error ("Cannot set debug registers: %s", thread_db_err_str (err));
+}
+
+void
+fbsd_thread_dr_set_control (unsigned long control)
+{
+  if (fbsd_thread_active)
+    fbsd_thread_dr_set (7, control);
+  else
+    fbsd_lwp_dr_set (GET_PID (inferior_ptid), 7, control);
+}
+
+void
+fbsd_thread_dr_set_addr (int regnum, CORE_ADDR addr)
+{
+  gdb_assert (regnum >= 0 && regnum <= 4);
+
+  if (fbsd_thread_active)
+    fbsd_thread_dr_set (regnum, addr);
+  else
+    fbsd_lwp_dr_set (GET_PID (inferior_ptid), regnum, addr);
+}
+
+void
+fbsd_thread_dr_reset_addr (int regnum)
+{
+  gdb_assert (regnum >= 0 && regnum <= 4);
+
+  if (fbsd_thread_active)
+    fbsd_thread_dr_set (regnum, 0);
+  else
+    fbsd_lwp_dr_set (GET_PID (inferior_ptid), regnum, 0);
+}
+
+unsigned long
+fbsd_thread_dr_get_status (void)
+{
+  struct dbreg dbregs;
+  td_thrhandle_t th;
+  td_thrinfo_t ti;
+  td_err_e err;
+  lwpid_t lwpid;
+
+  if (IS_THREAD (inferior_ptid))
+    {
+      err = td_ta_map_id2thr_p (thread_agent, GET_THREAD (inferior_ptid), &th);
+      if (err != TD_OK)
+        error ("Cannot find thread %d: Thread ID=%ld, %s",
+               pid_to_thread_id (inferior_ptid),           
+               GET_THREAD (inferior_ptid), thread_db_err_str (err));
+
+      err = td_thr_get_info_p (&th, &ti);
+      if (err != TD_OK)
+        error ("Cannot get thread info: %s", thread_db_err_str (err));
+
+      lwpid = ti.ti_lid;
+    }
+  else if (IS_LWP (inferior_ptid))
+    {
+      lwpid = GET_LWP (inferior_ptid);
+    }
+  else
+    {
+      lwpid = GET_PID (inferior_ptid);
+    }
+
+  /* FIXME: kettenis/2001-03-31: Calling perror_with_name if the
+     ptrace call fails breaks debugging remote targets.  The correct
+     way to fix this is to add the hardware breakpoint and watchpoint
+     stuff to the target vector.  For now, just return zero if the
+     ptrace call fails.  */
+  if (ptrace (PT_GETDBREGS, GET_PID (inferior_ptid),
+              (PTRACE_ARG3_TYPE) & dbregs, 0) == -1)
+#if 0
+    perror_with_name ("Couldn't read debug registers");
+#else
+    return 0;
+#endif
+
+  return DBREG_DRX ((&dbregs), 6);
+}
+
+#endif /* PT_GETDBREGS && I386_DR_LOW_SET_ADDR */


>Release-Note:
>Audit-Trail:
>Unformatted:


More information about the freebsd-threads mailing list