public inbox for gcc-patches@gcc.gnu.org
 help / color / mirror / Atom feed
From: Mark Harmstone <mark@harmstone.com>
To: gcc-patches@gcc.gnu.org
Cc: Mark Harmstone <mark@harmstone.com>
Subject: [PATCH 06/11] Handle enums for CodeView
Date: Tue, 18 Jun 2024 01:17:08 +0100	[thread overview]
Message-ID: <20240618001713.24034-7-mark@harmstone.com> (raw)
In-Reply-To: <20240618001713.24034-1-mark@harmstone.com>

Translates DW_TAG_enumeration_type DIEs into LF_ENUM symbols.

    gcc/
            * dwarf2codeview.cc (MAX_FIELDLIST_SIZE): Define.
            (struct codeview_integer): New structure.
            (struct codeview_subtype): Likewise
            (struct codeview_custom_type): Add lf_fieldlist and lf_enum
                to union.
            (write_cv_integer, cv_integer_len): New functions.
            (write_lf_fieldlist, write_lf_enum): Likewise.
            (write_custom_types): Call write_lf_fieldlist and write_lf_enum.
            (add_enum_forward_def): New function.
            (get_type_num_enumeration_type): Likewise.
            (get_type_num): Handle DW_TAG_enumeration_type DIEs.
            * dwarf2codeview.h (LF_FIELDLIST, LF_INDEX, LF_ENUMERATE): Define.
            (LF_ENUM, LF_CHAR, LF_SHORT, LF_USHORT, LF_LONG): Likewise.
            (LF_ULONG, LF_QUADWORD, LF_UQUADWORD): Likewise.
            (CV_ACCESS_PRIVATE, CV_ACCESS_PROTECTED): Likewise.
            (CV_ACCESS_PUBLIC, CV_PROP_FWDREF): Likewise.
---
 gcc/dwarf2codeview.cc | 524 ++++++++++++++++++++++++++++++++++++++++++
 gcc/dwarf2codeview.h  |  17 ++
 2 files changed, 541 insertions(+)

diff --git a/gcc/dwarf2codeview.cc b/gcc/dwarf2codeview.cc
index 05f5f60997e..475a53573e9 100644
--- a/gcc/dwarf2codeview.cc
+++ b/gcc/dwarf2codeview.cc
@@ -63,6 +63,11 @@ along with GCC; see the file COPYING3.  If not see
 #define SYMBOL_START_LABEL	"Lcvsymstart"
 #define SYMBOL_END_LABEL	"Lcvsymend"
 
+/* There's two bytes available for each type's size, but follow MSVC's lead in
+   capping the LF_FIELDLIST size at fb00 (minus 8 bytes for the LF_INDEX
+   pointing to the overflow entry).  */
+#define MAX_FIELDLIST_SIZE	0xfaf8
+
 #define HASH_SIZE 16
 
 struct codeview_string
@@ -170,6 +175,31 @@ struct die_hasher : free_ptr_hash <codeview_type>
   }
 };
 
+struct codeview_integer
+{
+  bool neg;
+  uint64_t num;
+};
+
+struct codeview_subtype
+{
+  struct codeview_subtype *next;
+  uint16_t kind;
+
+  union
+  {
+    struct
+    {
+      char *name;
+      struct codeview_integer value;
+    } lf_enumerate;
+    struct
+    {
+      uint32_t type_num;
+    } lf_index;
+  };
+};
+
 struct codeview_custom_type
 {
   struct codeview_custom_type *next;
@@ -188,6 +218,20 @@ struct codeview_custom_type
       uint32_t base_type;
       uint16_t modifier;
     } lf_modifier;
+    struct
+    {
+      size_t length;
+      codeview_subtype *subtypes;
+      codeview_subtype *last_subtype;
+    } lf_fieldlist;
+    struct
+    {
+      uint16_t count;
+      uint16_t properties;
+      uint32_t underlying_type;
+      uint32_t fieldlist;
+      char *name;
+    } lf_enum;
   };
 };
 
@@ -978,6 +1022,292 @@ write_lf_modifier (codeview_custom_type *t)
   asm_fprintf (asm_out_file, "%LLcv_type%x_end:\n", t->num);
 }
 
+/* Write a CodeView extensible integer.  If the value is non-negative and
+   < 0x8000, the value gets written directly as an uint16_t.  Otherwise, we
+   output two bytes for the integer type (LF_CHAR, LF_SHORT, ...), and the
+   actual value follows.  */
+
+static size_t
+write_cv_integer (codeview_integer *i)
+{
+  if (i->neg)
+    {
+      if (i->num <= 0x80)
+	{
+	  fputs (integer_asm_op (2, false), asm_out_file);
+	  fprint_whex (asm_out_file, LF_CHAR);
+	  putc ('\n', asm_out_file);
+
+	  fputs (integer_asm_op (1, false), asm_out_file);
+	  fprint_whex (asm_out_file, -i->num);
+	  putc ('\n', asm_out_file);
+
+	  return 3;
+	}
+      else if (i->num <= 0x8000)
+	{
+	  fputs (integer_asm_op (2, false), asm_out_file);
+	  fprint_whex (asm_out_file, LF_SHORT);
+	  putc ('\n', asm_out_file);
+
+	  fputs (integer_asm_op (2, false), asm_out_file);
+	  fprint_whex (asm_out_file, -i->num);
+	  putc ('\n', asm_out_file);
+
+	  return 4;
+	}
+      else if (i->num <= 0x80000000)
+	{
+	  fputs (integer_asm_op (2, false), asm_out_file);
+	  fprint_whex (asm_out_file, LF_LONG);
+	  putc ('\n', asm_out_file);
+
+	  fputs (integer_asm_op (4, false), asm_out_file);
+	  fprint_whex (asm_out_file, -i->num);
+	  putc ('\n', asm_out_file);
+
+	  return 6;
+	}
+      else
+	{
+	  fputs (integer_asm_op (2, false), asm_out_file);
+	  fprint_whex (asm_out_file, LF_QUADWORD);
+	  putc ('\n', asm_out_file);
+
+	  fputs (integer_asm_op (8, false), asm_out_file);
+	  fprint_whex (asm_out_file, -i->num);
+	  putc ('\n', asm_out_file);
+
+	  return 10;
+	}
+    }
+  else
+    {
+      if (i->num <= 0x7fff)
+	{
+	  fputs (integer_asm_op (2, false), asm_out_file);
+	  fprint_whex (asm_out_file, i->num);
+	  putc ('\n', asm_out_file);
+
+	  return 2;
+	}
+      else if (i->num <= 0xffff)
+	{
+	  fputs (integer_asm_op (2, false), asm_out_file);
+	  fprint_whex (asm_out_file, LF_USHORT);
+	  putc ('\n', asm_out_file);
+
+	  fputs (integer_asm_op (2, false), asm_out_file);
+	  fprint_whex (asm_out_file, i->num);
+	  putc ('\n', asm_out_file);
+
+	  return 4;
+	}
+      else if (i->num <= 0xffffffff)
+	{
+	  fputs (integer_asm_op (2, false), asm_out_file);
+	  fprint_whex (asm_out_file, LF_ULONG);
+	  putc ('\n', asm_out_file);
+
+	  fputs (integer_asm_op (4, false), asm_out_file);
+	  fprint_whex (asm_out_file, i->num);
+	  putc ('\n', asm_out_file);
+
+	  return 6;
+	}
+      else
+	{
+	  fputs (integer_asm_op (2, false), asm_out_file);
+	  fprint_whex (asm_out_file, LF_UQUADWORD);
+	  putc ('\n', asm_out_file);
+
+	  fputs (integer_asm_op (8, false), asm_out_file);
+	  fprint_whex (asm_out_file, i->num);
+	  putc ('\n', asm_out_file);
+
+	  return 10;
+	}
+    }
+}
+
+/* Return the extra size needed for an extensible integer.  */
+
+static size_t
+cv_integer_len (codeview_integer *i)
+{
+  if (i->neg)
+    {
+      if (i->num <= 0x80)
+	return sizeof (int8_t);
+      else if (i->num <= 0x8000)
+	return sizeof (int16_t);
+      else if (i->num <= 0x80000000)
+	return sizeof (int32_t);
+      else
+	return sizeof (int64_t);
+    }
+  else
+    {
+      if (i->num <= 0x7fff)
+	return 0;
+      else if (i->num <= 0xffff)
+	return sizeof (uint16_t);
+      else if (i->num <= 0xffffffff)
+	return sizeof (uint32_t);
+      else
+	return sizeof (uint64_t);
+    }
+}
+
+/* Write an LF_FIELDLIST type, which is a container for various subtypes.  This
+   has two uses: for the values in an enum, and for the member, operators etc.
+   for a struct, class, or union.  */
+
+static void
+write_lf_fieldlist (codeview_custom_type *t)
+{
+  fputs (integer_asm_op (2, false), asm_out_file);
+  asm_fprintf (asm_out_file, "%LLcv_type%x_end - %LLcv_type%x_start\n",
+	       t->num, t->num);
+
+  asm_fprintf (asm_out_file, "%LLcv_type%x_start:\n", t->num);
+
+  fputs (integer_asm_op (2, false), asm_out_file);
+  fprint_whex (asm_out_file, t->kind);
+  putc ('\n', asm_out_file);
+
+  while (t->lf_fieldlist.subtypes)
+    {
+      codeview_subtype *v = t->lf_fieldlist.subtypes;
+      codeview_subtype *next = v->next;
+      size_t name_len, leaf_len;
+
+      switch (v->kind)
+	{
+	case LF_ENUMERATE:
+	  /* This is lf_enumerate in binutils and lfEnumerate in Microsoft's
+	     cvinfo.h:
+
+	    struct lf_enumerate
+	    {
+	      uint16_t kind;
+	      uint16_t attributes;
+	      uint16_t value;
+	      (then actual value if value >= 0x8000)
+	      char name[];
+	    } ATTRIBUTE_PACKED;
+	  */
+
+	  fputs (integer_asm_op (2, false), asm_out_file);
+	  fprint_whex (asm_out_file, LF_ENUMERATE);
+	  putc ('\n', asm_out_file);
+
+	  fputs (integer_asm_op (2, false), asm_out_file);
+	  fprint_whex (asm_out_file, CV_ACCESS_PUBLIC);
+	  putc ('\n', asm_out_file);
+
+	  leaf_len = 4 + write_cv_integer (&v->lf_enumerate.value);
+
+	  name_len = strlen (v->lf_enumerate.name) + 1;
+	  ASM_OUTPUT_ASCII (asm_out_file, v->lf_enumerate.name, name_len);
+
+	  leaf_len += name_len;
+	  write_cv_padding (4 - (leaf_len % 4));
+
+	  free (v->lf_enumerate.name);
+	  break;
+
+	case LF_INDEX:
+	  /* This is lf_index in binutils and lfIndex in Microsoft's cvinfo.h:
+
+	    struct lf_index
+	    {
+	      uint16_t kind;
+	      uint16_t padding;
+	      uint32_t index;
+	    } ATTRIBUTE_PACKED;
+	  */
+
+	  fputs (integer_asm_op (2, false), asm_out_file);
+	  fprint_whex (asm_out_file, LF_INDEX);
+	  putc ('\n', asm_out_file);
+
+	  fputs (integer_asm_op (2, false), asm_out_file);
+	  fprint_whex (asm_out_file, 0);
+	  putc ('\n', asm_out_file);
+
+	  fputs (integer_asm_op (4, false), asm_out_file);
+	  fprint_whex (asm_out_file, v->lf_index.type_num);
+	  putc ('\n', asm_out_file);
+
+	  break;
+	}
+
+      t->lf_fieldlist.subtypes = next;
+      free (v);
+    }
+
+  asm_fprintf (asm_out_file, "%LLcv_type%x_end:\n", t->num);
+}
+
+/* Write an LF_ENUM type.  */
+
+static void
+write_lf_enum (codeview_custom_type *t)
+{
+  size_t name_len, leaf_len;
+
+  /* This is lf_enum in binutils and lfEnum in Microsoft's cvinfo.h:
+
+    struct lf_enum
+    {
+      uint16_t size;
+      uint16_t kind;
+      uint16_t num_elements;
+      uint16_t properties;
+      uint32_t underlying_type;
+      uint32_t field_list;
+      char name[];
+    } ATTRIBUTE_PACKED;
+  */
+
+  fputs (integer_asm_op (2, false), asm_out_file);
+  asm_fprintf (asm_out_file, "%LLcv_type%x_end - %LLcv_type%x_start\n",
+	       t->num, t->num);
+
+  asm_fprintf (asm_out_file, "%LLcv_type%x_start:\n", t->num);
+
+  fputs (integer_asm_op (2, false), asm_out_file);
+  fprint_whex (asm_out_file, t->kind);
+  putc ('\n', asm_out_file);
+
+  fputs (integer_asm_op (2, false), asm_out_file);
+  fprint_whex (asm_out_file, t->lf_enum.count);
+  putc ('\n', asm_out_file);
+
+  fputs (integer_asm_op (2, false), asm_out_file);
+  fprint_whex (asm_out_file, t->lf_enum.properties);
+  putc ('\n', asm_out_file);
+
+  fputs (integer_asm_op (4, false), asm_out_file);
+  fprint_whex (asm_out_file, t->lf_enum.underlying_type);
+  putc ('\n', asm_out_file);
+
+  fputs (integer_asm_op (4, false), asm_out_file);
+  fprint_whex (asm_out_file, t->lf_enum.fieldlist);
+  putc ('\n', asm_out_file);
+
+  name_len = strlen (t->lf_enum.name) + 1;
+  ASM_OUTPUT_ASCII (asm_out_file, t->lf_enum.name, name_len);
+
+  leaf_len = 14 + name_len;
+  write_cv_padding (4 - (leaf_len % 4));
+
+  free (t->lf_enum.name);
+
+  asm_fprintf (asm_out_file, "%LLcv_type%x_end:\n", t->num);
+}
+
 /* Write the .debug$T section, which contains all of our custom type
    definitions.  */
 
@@ -1003,6 +1333,14 @@ write_custom_types (void)
 	case LF_MODIFIER:
 	  write_lf_modifier (custom_types);
 	  break;
+
+	case LF_FIELDLIST:
+	  write_lf_fieldlist (custom_types);
+	  break;
+
+	case LF_ENUM:
+	  write_lf_enum (custom_types);
+	  break;
 	}
 
       free (custom_types);
@@ -1308,6 +1646,188 @@ get_type_num_volatile_type (dw_die_ref type)
   return ct->num;
 }
 
+/* Add a forward declaration for an enum.  This is legal from C++11 onwards.  */
+
+static uint32_t
+add_enum_forward_def (dw_die_ref type)
+{
+  codeview_custom_type *ct;
+
+  ct = (codeview_custom_type *) xmalloc (sizeof (codeview_custom_type));
+
+  ct->next = NULL;
+  ct->kind = LF_ENUM;
+
+  ct->lf_enum.count = 0;
+  ct->lf_enum.properties = CV_PROP_FWDREF;
+  ct->lf_enum.underlying_type = get_type_num (get_AT_ref (type, DW_AT_type));
+  ct->lf_enum.fieldlist = 0;
+  ct->lf_enum.name = xstrdup (get_AT_string (type, DW_AT_name));
+
+  add_custom_type (ct);
+
+  return ct->num;
+}
+
+/* Process a DW_TAG_enumeration_type DIE, adding an LF_FIELDLIST and an LF_ENUM
+   type, returning the number of the latter.  */
+
+static uint32_t
+get_type_num_enumeration_type (dw_die_ref type)
+{
+  dw_die_ref first_child;
+  codeview_custom_type *ct;
+  uint16_t count = 0;
+  uint32_t last_type;
+
+  if (get_AT_flag (type, DW_AT_declaration))
+    return add_enum_forward_def (type);
+
+  /* First, add an LF_FIELDLIST for the enum's values.  We don't need to worry
+     about deduplication here, as ld will take care of that for us.  If there's
+     a lot of entries, add more LF_FIELDLISTs with LF_INDEXes pointing to
+     the overflow lists.  */
+
+  first_child = dw_get_die_child (type);
+
+  ct = (codeview_custom_type *) xmalloc (sizeof (codeview_custom_type));
+
+  ct->next = NULL;
+  ct->kind = LF_FIELDLIST;
+  ct->lf_fieldlist.length = 0;
+  ct->lf_fieldlist.subtypes = NULL;
+  ct->lf_fieldlist.last_subtype = NULL;
+
+  if (first_child)
+    {
+      dw_die_ref c;
+
+      c = first_child;
+      do
+	{
+	  dw_attr_node *att;
+	  codeview_subtype *el;
+	  size_t el_len;
+
+	  c = dw_get_die_sib (c);
+
+	  if (dw_get_die_tag (c) != DW_TAG_enumerator)
+	    continue;
+
+	  att = get_AT (c, DW_AT_const_value);
+	  if (!att)
+	    continue;
+
+	  el = (codeview_subtype *) xmalloc (sizeof (*el));
+	  el->next = NULL;
+	  el->kind = LF_ENUMERATE;
+
+	  switch (AT_class (att))
+	    {
+	    case dw_val_class_unsigned_const:
+	    case dw_val_class_unsigned_const_implicit:
+	      el->lf_enumerate.value.neg = false;
+	      el->lf_enumerate.value.num = att->dw_attr_val.v.val_unsigned;
+	      break;
+
+	    case dw_val_class_const:
+	    case dw_val_class_const_implicit:
+	      if (att->dw_attr_val.v.val_int < 0)
+		{
+		  el->lf_enumerate.value.neg = true;
+		  el->lf_enumerate.value.num = -att->dw_attr_val.v.val_int;
+		}
+	      else
+		{
+		  el->lf_enumerate.value.neg = false;
+		  el->lf_enumerate.value.num = att->dw_attr_val.v.val_int;
+		}
+	      break;
+
+	    default:
+	      free (el);
+	      continue;
+	    }
+
+	  el->lf_enumerate.name = xstrdup (get_AT_string (c, DW_AT_name));
+
+	  el_len = 7 + strlen (el->lf_enumerate.name);
+	  el_len += cv_integer_len (&el->lf_enumerate.value);
+
+	  if (el_len % 4)
+	    el_len += 4 - (el_len % 4);
+
+	  if (ct->lf_fieldlist.length + el_len > MAX_FIELDLIST_SIZE)
+	    {
+	      codeview_subtype *idx;
+	      codeview_custom_type *ct2;
+
+	      idx = (codeview_subtype *) xmalloc (sizeof (*idx));
+	      idx->next = NULL;
+	      idx->kind = LF_INDEX;
+	      idx->lf_index.type_num = 0;
+
+	      ct->lf_fieldlist.last_subtype->next = idx;
+	      ct->lf_fieldlist.last_subtype = idx;
+
+	      ct2 = (codeview_custom_type *)
+		xmalloc (sizeof (codeview_custom_type));
+
+	      ct2->next = ct;
+	      ct2->kind = LF_FIELDLIST;
+	      ct2->lf_fieldlist.length = 0;
+	      ct2->lf_fieldlist.subtypes = NULL;
+	      ct2->lf_fieldlist.last_subtype = NULL;
+
+	      ct = ct2;
+	    }
+
+	  ct->lf_fieldlist.length += el_len;
+
+	  if (ct->lf_fieldlist.last_subtype)
+	    ct->lf_fieldlist.last_subtype->next = el;
+	  else
+	    ct->lf_fieldlist.subtypes = el;
+
+	  ct->lf_fieldlist.last_subtype = el;
+	  count++;
+	}
+      while (c != first_child);
+    }
+
+  while (ct)
+    {
+      codeview_custom_type *ct2;
+
+      ct2 = ct->next;
+      ct->next = NULL;
+
+      if (ct->lf_fieldlist.last_subtype->kind == LF_INDEX)
+	ct->lf_fieldlist.last_subtype->lf_index.type_num = last_type;
+
+      add_custom_type (ct);
+      last_type = ct->num;
+
+      ct = ct2;
+    }
+
+  /* Now add an LF_ENUM, pointing to the LF_FIELDLIST we just added.  */
+
+  ct = (codeview_custom_type *) xmalloc (sizeof (codeview_custom_type));
+
+  ct->next = NULL;
+  ct->kind = LF_ENUM;
+  ct->lf_enum.count = count;
+  ct->lf_enum.properties = 0;
+  ct->lf_enum.underlying_type = get_type_num (get_AT_ref (type, DW_AT_type));
+  ct->lf_enum.fieldlist = last_type;
+  ct->lf_enum.name = xstrdup (get_AT_string (type, DW_AT_name));
+
+  add_custom_type (ct);
+
+  return ct->num;
+}
+
 /* Process a DIE representing a type definition, add a CodeView type if
    necessary, and return its number.  If it's something we can't handle, return
    0.  We keep a hash table so that we're not adding the same type multiple
@@ -1358,6 +1878,10 @@ get_type_num (dw_die_ref type)
       t->num = get_type_num_volatile_type (type);
       break;
 
+    case DW_TAG_enumeration_type:
+      t->num = get_type_num_enumeration_type (type);
+      break;
+
     default:
       t->num = 0;
       break;
diff --git a/gcc/dwarf2codeview.h b/gcc/dwarf2codeview.h
index d48cfbebc32..3f3695625c4 100644
--- a/gcc/dwarf2codeview.h
+++ b/gcc/dwarf2codeview.h
@@ -63,6 +63,23 @@ along with GCC; see the file COPYING3.  If not see
 /* Constants for type definitions.  */
 #define LF_MODIFIER		0x1001
 #define LF_POINTER		0x1002
+#define LF_FIELDLIST		0x1203
+#define LF_INDEX		0x1404
+#define LF_ENUMERATE		0x1502
+#define LF_ENUM			0x1507
+#define LF_CHAR			0x8000
+#define LF_SHORT		0x8001
+#define LF_USHORT		0x8002
+#define LF_LONG			0x8003
+#define LF_ULONG		0x8004
+#define LF_QUADWORD		0x8009
+#define LF_UQUADWORD		0x800a
+
+#define CV_ACCESS_PRIVATE	1
+#define CV_ACCESS_PROTECTED	2
+#define CV_ACCESS_PUBLIC	3
+
+#define CV_PROP_FWDREF		0x80
 
 /* Debug Format Interface.  Used in dwarf2out.cc.  */
 
-- 
2.44.2


  parent reply	other threads:[~2024-06-18  0:17 UTC|newest]

Thread overview: 25+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2024-06-18  0:17 [PATCH 00/11] CodeView variables and type system Mark Harmstone
2024-06-18  0:17 ` [PATCH 01/11] Output CodeView data about variables Mark Harmstone
2024-06-23 23:50   ` Jeff Law
2024-06-18  0:17 ` [PATCH 02/11] Handle CodeView base types Mark Harmstone
2024-06-24  0:18   ` Jeff Law
2024-06-18  0:17 ` [PATCH 03/11] Handle typedefs for CodeView Mark Harmstone
2024-06-24  0:30   ` Jeff Law
2024-06-18  0:17 ` [PATCH 04/11] Handle pointers " Mark Harmstone
2024-06-24  3:31   ` Jeff Law
2024-06-18  0:17 ` [PATCH 05/11] Handle const and varible modifiers " Mark Harmstone
2024-06-24  3:39   ` Jeff Law
2024-06-25  2:49     ` Mark Harmstone
2024-06-25  5:42       ` Jeff Law
2024-06-18  0:17 ` Mark Harmstone [this message]
2024-06-24  3:49   ` [PATCH 06/11] Handle enums " Jeff Law
2024-06-18  0:17 ` [PATCH 07/11] Handle structs and classes " Mark Harmstone
2024-06-25  5:40   ` Jeff Law
2024-06-18  0:17 ` [PATCH 08/11] Handle unions " Mark Harmstone
2024-06-25 23:29   ` Jeff Law
2024-06-18  0:17 ` [PATCH 09/11] Handle arrays " Mark Harmstone
2024-06-25 23:32   ` Jeff Law
2024-06-18  0:17 ` [PATCH 10/11] Handle bitfields " Mark Harmstone
2024-06-26  2:21   ` Jeff Law
2024-06-18  0:17 ` [PATCH 11/11] Handle subroutine types in CodeView Mark Harmstone
2024-06-26  2:27   ` Jeff Law

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20240618001713.24034-7-mark@harmstone.com \
    --to=mark@harmstone.com \
    --cc=gcc-patches@gcc.gnu.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
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).