public inbox for dwz@sourceware.org
 help / color / mirror / Atom feed
* [PATCH] Support updating DWARF5 .debug_loclists.
@ 2020-09-28  8:07 Mark Wielaard
  2020-09-29  8:42 ` Jakub Jelinek
  0 siblings, 1 reply; 3+ messages in thread
From: Mark Wielaard @ 2020-09-28  8:07 UTC (permalink / raw)
  To: dwz; +Cc: Mark Wielaard

This patch updates dwz to handle .debug_loclists as it updates .debug_locs.
Both sections can be present at the same time (if there are both DWARF5 CUs
and older DWARF CUs). So we need to keep both a loc_htab and loclists_htab.

	* dwz.c (read_loclist_low_mem_phase1): Add cu as argument.
	Track which section we are reading based on cu->cu_version.
	Read DWARF5 loclists entries setting ptr and len up to the
	start and length of the location lists to read.
	(add_locexpr_dummy_dies): Pass cu to read_loclist_low_mem_phase1.
	(loclists_htab): New static htab variable.
	(read_loclist): Update like read_loclist_low_mem_phase1 and create
	and update loclists_htab for DEBUG_LOCLISTS.
	(checksum_die): Pass cu to read_loclists.
	(adjust_loclists): New function like adjust_loclist for
	DEBUG_LOCLISTS.
	(write_loclists): New function like write_locs.
	(cleanup): Delete loclists_htab.
	(dwz): Call write_loclists.

At this time binutils readelf doesn't display .debug_loclists with view
pairs correctly.  To test the updates are correct when GCC emits view
pairs as GNU extension you'll need elfutils eu-readelf with the following
patch: https://sourceware.org/pipermail/elfutils-devel/2020q3/002900.html
---
 dwz.c | 356 +++++++++++++++++++++++++++++++++++++++++++++++++---------
 1 file changed, 302 insertions(+), 54 deletions(-)

diff --git a/dwz.c b/dwz.c
index e772954..588ee50 100644
--- a/dwz.c
+++ b/dwz.c
@@ -2374,50 +2374,114 @@ read_exprloc_low_mem_phase1 (DSO *dso, dw_die_ref die, unsigned char *ptr,
 
 /* Add dummy DIEs for loclist at OFFSET.  */
 static int
-read_loclist_low_mem_phase1 (DSO *dso, dw_die_ref die, GElf_Addr offset)
+read_loclist_low_mem_phase1 (DSO *dso, dw_cu_ref cu, dw_die_ref die,
+			     GElf_Addr offset)
 {
   unsigned char *ptr, *endsec;
   GElf_Addr low, high;
-  size_t len;
+  size_t len = 0;
+  int sec;
 
-  ptr = debug_sections[DEBUG_LOC].data;
+  sec = cu->cu_version < 5 ? DEBUG_LOC : DEBUG_LOCLISTS;
+  ptr = debug_sections[sec].data;
   if (ptr == NULL)
     {
-      error (0, 0, "%s: loclistptr attribute, yet no .debug_loc section",
-	     dso->filename);
+      error (0, 0, "%s: loclistptr attribute, yet no %s section",
+	     dso->filename, debug_sections[sec].name);
       return 1;
     }
-  if (offset >= debug_sections[DEBUG_LOC].size)
+  if (offset >= debug_sections[sec].size)
     {
       error (0, 0,
-	     "%s: loclistptr offset %Ld outside of .debug_loc section",
-	     dso->filename, (long long) offset);
+	     "%s: loclistptr offset %Ld outside of %s section",
+	     dso->filename, (long long) offset, debug_sections[sec].name);
       return 1;
     }
-  endsec = ptr + debug_sections[DEBUG_LOC].size;
+  endsec = ptr + debug_sections[sec].size;
   ptr += offset;
   while (ptr < endsec)
     {
-      low = read_size (ptr, ptr_size);
-      high = read_size (ptr + ptr_size, ptr_size);
-      ptr += 2 * ptr_size;
-      if (low == 0 && high == 0)
-	break;
+      if (cu->cu_version < 5)
+	{
+	  low = read_size (ptr, ptr_size);
+	  high = read_size (ptr + ptr_size, ptr_size);
+	  ptr += 2 * ptr_size;
+	  if (low == 0 && high == 0)
+	    break;
 
-      if (low == ~ (GElf_Addr) 0 || (ptr_size == 4 && low == 0xffffffff))
-	continue;
+	  if (low == ~ (GElf_Addr) 0 || (ptr_size == 4 && low == 0xffffffff))
+	    continue;
+
+	  len = read_16 (ptr);
+	}
+      else
+	{
+	  uint8_t lle = *ptr++;
+	  switch (lle)
+	    {
+	    case DW_LLE_end_of_list:
+	      return 0;
+
+	    case DW_LLE_base_addressx:
+	      skip_leb128 (ptr);
+	      continue;
+
+	    case DW_LLE_startx_endx:
+	      skip_leb128 (ptr);
+	      skip_leb128 (ptr);
+	      len = read_uleb128 (ptr);
+	      break;
+
+	    case DW_LLE_startx_length:
+	      skip_leb128 (ptr);
+	      skip_leb128 (ptr);
+	      len = read_uleb128 (ptr);
+	      break;
+
+	    case DW_LLE_offset_pair:
+	      skip_leb128 (ptr);
+	      skip_leb128 (ptr);
+	      len = read_uleb128 (ptr);
+	      break;
+
+	    case DW_LLE_default_location:
+	      len = read_uleb128 (ptr);
+	      break;
+
+	    case DW_LLE_base_address:
+	      ptr += ptr_size;
+	      continue;
+
+	    case DW_LLE_start_end:
+	      ptr += 2 * ptr_size;
+	      len = read_uleb128 (ptr);
+	      break;
+
+	    case DW_LLE_start_length:
+	      ptr += ptr_size;
+	      skip_leb128 (ptr);
+	      len = read_uleb128 (ptr);
+	      break;
+
+	    default:
+	      error (0, 0,
+		     "%s: unhandled location list entry 0x%x in %s section",
+		     dso->filename, lle, debug_sections[sec].name);
+	      return 1;
+	    }
+	}
 
-      len = read_16 (ptr);
       if (unlikely (!(ptr + len <= endsec)))
 	{
 	  error (0, 0,
-		 "%s: locexpr length 0x%Lx exceeds .debug_loc section",
-		 dso->filename, (long long) len);
+		 "%s: locexpr length 0x%Lx exceeds %s section",
+		 dso->filename, (long long) len, debug_sections[sec].name);
 	  return 1;
 	}
 
-      if (read_exprloc_low_mem_phase1 (dso, die, ptr, len))
-	return 1;
+      if (len > 0)
+	if (read_exprloc_low_mem_phase1 (dso, die, ptr, len))
+	  return 1;
 
       ptr += len;
     }
@@ -2492,13 +2556,13 @@ add_locexpr_dummy_dies (DSO *dso, dw_cu_ref cu, dw_die_ref die,
       if ((cu->cu_version < 4 && form == DW_FORM_data4)
 	  || form == DW_FORM_sec_offset)
 	{
-	  if (read_loclist_low_mem_phase1 (dso, die, do_read_32 (ptr)))
+	  if (read_loclist_low_mem_phase1 (dso, cu, die, do_read_32 (ptr)))
 	    return 1;
 	  break;
 	}
       else if (cu->cu_version < 4 && form == DW_FORM_data8)
 	{
-	  if (read_loclist_low_mem_phase1 (dso, die, do_read_64 (ptr)))
+	  if (read_loclist_low_mem_phase1 (dso, cu, die, do_read_64 (ptr)))
 	    return 1;
 	  break;
 	}
@@ -2526,10 +2590,12 @@ struct debug_loc_adjust
 };
 ALIGN_STRUCT (debug_loc_adjust)
 
-/* Hash table and obstack for recording .debug_loc adjustment ranges.  */
+/* Hash table and obstack for recording .debug_loc and .debug_loclists
+   adjustment ranges.  */
 static htab_t loc_htab;
+static htab_t loclists_htab;
 
-/* Hash function for loc_htab.  */
+/* Hash function for loc[lists]_htab.  */
 static hashval_t
 loc_hash (const void *p)
 {
@@ -2538,7 +2604,7 @@ loc_hash (const void *p)
   return a->end_offset;
 }
 
-/* Equality function for loc_htab.  */
+/* Equality function for loc[lists]_htab.  */
 static int
 loc_eq (const void *p, const void *q)
 {
@@ -2552,47 +2618,109 @@ loc_eq (const void *p, const void *q)
    DIE.  Call read_exprloc on each of the DWARF expressions
    contained in it.  */
 static int
-read_loclist (DSO *dso, dw_die_ref die, GElf_Addr offset)
+read_loclist (DSO *dso, dw_cu_ref cu, dw_die_ref die, GElf_Addr offset)
 {
   unsigned char *ptr, *endsec;
   GElf_Addr low, high;
   size_t len;
+  int sec;
   bool need_adjust = false;
 
   die->die_ck_state = CK_BAD;
-  ptr = debug_sections[DEBUG_LOC].data;
+  sec = cu->cu_version < 5 ? DEBUG_LOC : DEBUG_LOCLISTS;
+  ptr = debug_sections[sec].data;
   if (ptr == NULL)
     {
-      error (0, 0, "%s: loclistptr attribute, yet no .debug_loc section",
-	     dso->filename);
+      error (0, 0, "%s: loclistptr attribute, yet no %s section",
+	     dso->filename, debug_sections[sec].name);
       return 1;
     }
-  if (offset >= debug_sections[DEBUG_LOC].size)
+  if (offset >= debug_sections[sec].size)
     {
       error (0, 0,
-	     "%s: loclistptr offset %Ld outside of .debug_loc section",
-	     dso->filename, (long long) offset);
+	     "%s: loclistptr offset %Ld outside of %s section",
+	     dso->filename, (long long) offset, debug_sections[sec].name);
       return 1;
     }
-  endsec = ptr + debug_sections[DEBUG_LOC].size;
+  endsec = ptr + debug_sections[sec].size;
   ptr += offset;
   while (ptr < endsec)
     {
-      low = read_size (ptr, ptr_size);
-      high = read_size (ptr + ptr_size, ptr_size);
-      ptr += 2 * ptr_size;
-      if (low == 0 && high == 0)
-	break;
+      if (cu->cu_version < 5)
+	{
+	  low = read_size (ptr, ptr_size);
+	  high = read_size (ptr + ptr_size, ptr_size);
+	  ptr += 2 * ptr_size;
+	  if (low == 0 && high == 0)
+	    break;
 
-      if (low == ~ (GElf_Addr) 0 || (ptr_size == 4 && low == 0xffffffff))
-	continue;
+	  if (low == ~ (GElf_Addr) 0 || (ptr_size == 4 && low == 0xffffffff))
+	    continue;
+
+	  len = read_16 (ptr);
+	}
+      else
+	{
+	  uint8_t lle = *ptr++;
+	  switch (lle)
+	    {
+	    case DW_LLE_end_of_list:
+	      return 0;
+
+	    case DW_LLE_base_addressx:
+	      skip_leb128 (ptr);
+	      continue;
+
+	    case DW_LLE_startx_endx:
+	      skip_leb128 (ptr);
+	      skip_leb128 (ptr);
+	      len = read_uleb128 (ptr);
+	      break;
+
+	    case DW_LLE_startx_length:
+	      skip_leb128 (ptr);
+	      skip_leb128 (ptr);
+	      len = read_uleb128 (ptr);
+	      break;
+
+	    case DW_LLE_offset_pair:
+	      skip_leb128 (ptr);
+	      skip_leb128 (ptr);
+	      len = read_uleb128 (ptr);
+	      break;
+
+	    case DW_LLE_default_location:
+	      len = read_uleb128 (ptr);
+	      break;
+
+	    case DW_LLE_base_address:
+	      ptr += ptr_size;
+	      continue;
+
+	    case DW_LLE_start_end:
+	      ptr += 2 * ptr_size;
+	      len = read_uleb128 (ptr);
+	      break;
+
+	    case DW_LLE_start_length:
+	      ptr += ptr_size;
+	      skip_leb128 (ptr);
+	      len = read_uleb128 (ptr);
+	      break;
+
+	    default:
+	      error (0, 0,
+		     "%s: unhandled location list entry 0x%x in %s section",
+		     dso->filename, lle, debug_sections[sec].name);
+	      return 1;
+	    }
+	}
 
-      len = read_16 (ptr);
       if (unlikely (!(ptr + len <= endsec)))
 	{
 	  error (0, 0,
-		 "%s: locexpr length 0x%Lx exceeds .debug_loc section",
-		 dso->filename, (long long) len);
+		 "%s: locexpr length 0x%Lx exceeds %s section",
+		 dso->filename, (long long) len, debug_sections[sec].name);
 	  return 1;
 	}
 
@@ -2608,15 +2736,30 @@ read_loclist (DSO *dso, dw_die_ref die, GElf_Addr offset)
       void **slot;
 
       adj.start_offset = offset;
-      adj.end_offset = ptr - debug_sections[DEBUG_LOC].data;
-      adj.cu = die_cu (die);
-      if (loc_htab == NULL)
+      adj.end_offset = ptr - debug_sections[sec].data;
+      adj.cu = cu;
+      if (sec == DEBUG_LOC)
 	{
-	  loc_htab = htab_try_create (50, loc_hash, loc_eq, NULL);
 	  if (loc_htab == NULL)
-	    dwz_oom ();
+	    {
+	      loc_htab = htab_try_create (50, loc_hash, loc_eq, NULL);
+	      if (loc_htab == NULL)
+		dwz_oom ();
+	    }
+	  slot = htab_find_slot_with_hash (loc_htab, &adj, adj.end_offset,
+					   INSERT);
+	}
+      else
+	{
+	  if (loclists_htab == NULL)
+	    {
+	      loclists_htab = htab_try_create (50, loc_hash, loc_eq, NULL);
+	      if (loclists_htab == NULL)
+		dwz_oom ();
+	    }
+	  slot = htab_find_slot_with_hash (loclists_htab, &adj, adj.end_offset,
+					   INSERT);
 	}
-      slot = htab_find_slot_with_hash (loc_htab, &adj, adj.end_offset, INSERT);
       if (slot == NULL)
 	dwz_oom ();
       if (*slot == NULL)
@@ -2627,8 +2770,8 @@ read_loclist (DSO *dso, dw_die_ref die, GElf_Addr offset)
 	}
       else if (((struct debug_loc_adjust *)*slot)->cu != adj.cu)
 	{
-	  error (0, 0, "%s: can't adjust .debug_loc section because multiple "
-		       "CUs refer to it", dso->filename);
+	  error (0, 0, "%s: can't adjust %s section because multiple "
+		 "CUs refer to it", dso->filename, debug_sections[sec].name);
 	  return 1;
 	}
       else if (((struct debug_loc_adjust *)*slot)->start_offset > offset)
@@ -2848,14 +2991,14 @@ checksum_die (DSO *dso, dw_cu_ref cu, dw_die_ref top_die, dw_die_ref die)
 	  if ((cu->cu_version < 4 && form == DW_FORM_data4)
 	      || form == DW_FORM_sec_offset)
 	    {
-	      if (read_loclist (dso, die, read_32 (ptr)))
+	      if (read_loclist (dso, cu, die, read_32 (ptr)))
 		return 1;
 	      ptr = old_ptr;
 	      break;
 	    }
 	  else if (cu->cu_version < 4 && form == DW_FORM_data8)
 	    {
-	      if (read_loclist (dso, die, read_64 (ptr)))
+	      if (read_loclist (dso, cu, die, read_64 (ptr)))
 		return 1;
 	      ptr = old_ptr;
 	      break;
@@ -12128,6 +12271,85 @@ adjust_loclist (void **slot, void *data)
   return 1;
 }
 
+/* Adjust .debug_loclists range determined by *SLOT, called through
+   htab_traverse.  */
+static int
+adjust_loclists (void **slot, void *data)
+{
+  struct debug_loc_adjust *adj = (struct debug_loc_adjust *) *slot;
+  unsigned char *ptr, *endsec;
+  size_t len = 0;
+
+  (void)data;
+
+  ptr = debug_sections[DEBUG_LOCLISTS].new_data + adj->start_offset;
+  endsec = ptr + debug_sections[DEBUG_LOCLISTS].size;
+
+  while (ptr < endsec)
+    {
+      uint8_t lle = *ptr++;
+      switch (lle)
+	{
+	case DW_LLE_end_of_list:
+	  return 1;
+
+	case DW_LLE_base_addressx:
+	  skip_leb128 (ptr);
+	  continue;
+
+	case DW_LLE_startx_endx:
+	  skip_leb128 (ptr);
+	  skip_leb128 (ptr);
+	  len = read_uleb128 (ptr);
+	  break;
+
+	case DW_LLE_startx_length:
+	  skip_leb128 (ptr);
+	  skip_leb128 (ptr);
+	  len = read_uleb128 (ptr);
+	  break;
+
+	case DW_LLE_offset_pair:
+	  skip_leb128 (ptr);
+	  skip_leb128 (ptr);
+	  len = read_uleb128 (ptr);
+	  break;
+
+	case DW_LLE_default_location:
+	  len = read_uleb128 (ptr);
+	  break;
+
+	case DW_LLE_base_address:
+	  ptr += ptr_size;
+	  continue;
+
+	case DW_LLE_start_end:
+	  ptr += 2 * ptr_size;
+	  len = read_uleb128 (ptr);
+	  break;
+
+	case DW_LLE_start_length:
+	  ptr += ptr_size;
+	  skip_leb128 (ptr);
+	  len = read_uleb128 (ptr);
+	  break;
+
+	default:
+	  error (0, 0, "unhandled location list entry 0x%x", lle);
+	  return 1;
+	}
+
+      assert (ptr + len <= endsec);
+
+      adjust_exprloc (adj->cu, adj->cu->cu_die, adj->cu, adj->cu->cu_die,
+		      ptr, len);
+
+      ptr += len;
+    }
+
+  return 1;
+}
+
 /* Create new .debug_loc section in malloced memory if .debug_loc
    needs to be adjusted.  */
 static void
@@ -12144,6 +12366,23 @@ write_loc (void)
   htab_traverse (loc_htab, adjust_loclist, NULL);
 }
 
+/* Create new .debug_loclists section in malloced memory if .debug_loclists
+   needs to be adjusted.  */
+static void
+write_loclists (void)
+{
+  unsigned char *loc;
+  if (loclists_htab == NULL)
+    return;
+  loc = malloc (debug_sections[DEBUG_LOCLISTS].size);
+  if (loc == NULL)
+    dwz_oom ();
+  memcpy (loc, debug_sections[DEBUG_LOCLISTS].data,
+	  debug_sections[DEBUG_LOCLISTS].size);
+  debug_sections[DEBUG_LOCLISTS].new_data = loc;
+  htab_traverse (loclists_htab, adjust_loclists, NULL);
+}
+
 /* Create new .debug_types section in malloced memory.  */
 static void
 write_types (void)
@@ -13439,6 +13678,9 @@ cleanup (void)
   if (loc_htab != NULL)
     htab_delete (loc_htab);
   loc_htab = NULL;
+  if (loclists_htab != NULL)
+    htab_delete (loclists_htab);
+  loclists_htab = NULL;
   if (dup_htab != NULL)
     htab_delete (dup_htab);
   dup_htab = NULL;
@@ -14440,6 +14682,12 @@ dwz (const char *file, const char *outfile, struct file_result *res,
 	      fprintf (stderr, "write_loc\n");
 	    }
 	  write_loc ();
+	  if (unlikely (progress_p))
+	    {
+	      report_progress ();
+	      fprintf (stderr, "write_loclists\n");
+	    }
+	  write_loclists ();
 	  if (unlikely (progress_p))
 	    {
 	      report_progress ();
-- 
2.18.4


^ permalink raw reply	[flat|nested] 3+ messages in thread

* Re: [PATCH] Support updating DWARF5 .debug_loclists.
  2020-09-28  8:07 [PATCH] Support updating DWARF5 .debug_loclists Mark Wielaard
@ 2020-09-29  8:42 ` Jakub Jelinek
  2020-09-29 17:59   ` Mark Wielaard
  0 siblings, 1 reply; 3+ messages in thread
From: Jakub Jelinek @ 2020-09-29  8:42 UTC (permalink / raw)
  To: Mark Wielaard; +Cc: dwz

On Mon, Sep 28, 2020 at 10:07:59AM +0200, Mark Wielaard wrote:
>    ptr += offset;
>    while (ptr < endsec)
>      {
> -      low = read_size (ptr, ptr_size);
> -      high = read_size (ptr + ptr_size, ptr_size);
> -      ptr += 2 * ptr_size;
> -      if (low == 0 && high == 0)
> -	break;
> +      if (cu->cu_version < 5)

I wonder if it wouldn't be clearer to use sec == DEBUG_LOC
in this spot (i.e. leave the decision based on cu_version just to the
start of the function).

> +	    case DW_LLE_start_length:
> +	      ptr += ptr_size;
> +	      skip_leb128 (ptr);
> +	      len = read_uleb128 (ptr);
> +	      break;

Wonder if we shouldn't handle case DW_LLE_GNU_view_part here, but perhaps
only for cu->cu_version == 5 because it isn't in a separate vendor code
space and thus in DWARF 6 it could mean something different.
>    while (ptr < endsec)
>      {
> -      low = read_size (ptr, ptr_size);
> -      high = read_size (ptr + ptr_size, ptr_size);
> -      ptr += 2 * ptr_size;
> -      if (low == 0 && high == 0)
> -	break;
> +      if (cu->cu_version < 5)

Ditto.

> +	    case DW_LLE_start_length:
> +	      ptr += ptr_size;
> +	      skip_leb128 (ptr);
> +	      len = read_uleb128 (ptr);
> +	      break;

Ditto.

Otherwise LGTM.

	Jakub


^ permalink raw reply	[flat|nested] 3+ messages in thread

* Re: [PATCH] Support updating DWARF5 .debug_loclists.
  2020-09-29  8:42 ` Jakub Jelinek
@ 2020-09-29 17:59   ` Mark Wielaard
  0 siblings, 0 replies; 3+ messages in thread
From: Mark Wielaard @ 2020-09-29 17:59 UTC (permalink / raw)
  To: Jakub Jelinek; +Cc: dwz

Hi Jakub,

On Tue, 2020-09-29 at 10:42 +0200, Jakub Jelinek wrote:
> On Mon, Sep 28, 2020 at 10:07:59AM +0200, Mark Wielaard wrote:
> >    ptr += offset;
> >    while (ptr < endsec)
> >      {
> > -      low = read_size (ptr, ptr_size);
> > -      high = read_size (ptr + ptr_size, ptr_size);
> > -      ptr += 2 * ptr_size;
> > -      if (low == 0 && high == 0)
> > -	break;
> > +      if (cu->cu_version < 5)
> 
> I wonder if it wouldn't be clearer to use sec == DEBUG_LOC
> in this spot (i.e. leave the decision based on cu_version just to the
> start of the function).

Yes, it would be clearer. Changed.

> > +	    case DW_LLE_start_length:
> > +	      ptr += ptr_size;
> > +	      skip_leb128 (ptr);
> > +	      len = read_uleb128 (ptr);
> > +	      break;
> 
> Wonder if we shouldn't handle case DW_LLE_GNU_view_part here, but perhaps
> only for cu->cu_version == 5 because it isn't in a separate vendor code
> space and thus in DWARF 6 it could mean something different.

Yes, it would. gcc doesn't emit it by default at the moment. But
handling it is easy:

            case DW_LLE_GNU_view_pair:                                          
              if (cu->cu_version != 5)                                          
                error (0, 0,                                                    
                       "%s: DW_LLE_GNU_view_pair used with DWARF version %u\n", 
                       dso->filename, cu->cu_version);                          
              skip_leb128 (ptr);                                                
              skip_leb128 (ptr);                                                
              continue;

It will warn, but still try to handle it as is.
We probably never hit
this because dwz doesn't actually accept
DWARFv6 CUs, because they don't
exist yet :)

> Otherwise LGTM.

Thanks. Pushed with those changes. Tested against a
gcc -gvariable-location-views=incompat5 build.

BTW. I fixed binutils readelf so that it now also correctly shows
.debug_loclists:
https://sourceware.org/pipermail/binutils/2020-September/113510.html

Cheers,

Mark

^ permalink raw reply	[flat|nested] 3+ messages in thread

end of thread, other threads:[~2020-09-29 17:59 UTC | newest]

Thread overview: 3+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2020-09-28  8:07 [PATCH] Support updating DWARF5 .debug_loclists Mark Wielaard
2020-09-29  8:42 ` Jakub Jelinek
2020-09-29 17:59   ` Mark Wielaard

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).