From: Tom de Vries via Gdb-patches <gdb-patches@sourceware.org>
To: Simon Marchi <simon.marchi@polymtl.ca>, gdb-patches@sourceware.org
Cc: Tom Tromey <tom@tromey.com>
Subject: Re: [PATCH][gdb/symtab] Relocate call_site_htab
Date: Fri, 1 Oct 2021 01:47:32 +0200 [thread overview]
Message-ID: <9a605b9e-267b-e9f5-4122-c407913e92f7@suse.de> (raw)
In-Reply-To: <5940137d-6a6f-2309-673f-d9491ed654b5@polymtl.ca>
On 9/30/21 4:26 PM, Simon Marchi wrote:
> On 2021-09-30 05:56, Tom de Vries via Gdb-patches wrote:
>> [ CC-ing maintainers that reviewed previous submission. ]
>>
>> Hi,
>>
>> When running test-case gdb.arch/amd64-entry-value-inline.exp with target board
>> unix/-no-pie/-fno-PIE we have:
>> ...
>> (gdb) continue^M
>> Continuing.^M
>> ^M
>> Breakpoint 2, fn2 (y=y@entry=25, x=x@entry=6) at \
>> gdb.arch/amd64-entry-value-inline.c:32^M
>> 32 y = -2 + x; /* break-here */^M
>> (gdb) PASS: gdb.arch/amd64-entry-value-inline.exp: \
>> continue to breakpoint: break-here
>> p y^M
>> $1 = 25^M
>> (gdb) PASS: gdb.arch/amd64-entry-value-inline.exp: p y
>> ...
>>
>> But with target board unix/-pie/-fPIE we have instead:
>> ...
>> p y^M
>> $1 = <optimized out>^M
>> (gdb) FAIL: gdb.arch/amd64-entry-value-inline.exp: p y
>> ...
>>
>> The test-case uses a .S file, which was generated using gcc 4.8.0, but I can
>> reproduce the same problem using the original C file and gcc 4.8.5.
>>
>> The problem is that in order to access the value, call_site information is
>> accessed, which is both:
>> - unrelocated, and
>> - accessed as if it were relocated.
>>
>> I've submitted an attempt at fixing this before, trying to handle this at all
>> points where the information is used (
>> https://sourceware.org/pipermail/gdb-patches/2019-August/159631.html ).
>>
>> Instead, fix this more reliably by relocating the call_site information.
>>
>> This fixes for me all remaining regressions for unix/-pie/-fPIE vs
>> unix/-no-pie/-fno-PIE (not counting ada compilation FAILs).
>>
>> Tested on x86_64-linux.
>>
>> Bug: https://sourceware.org/bugzilla/show_bug.cgi?id=24892
>>
>> Any comments?
>
> Instead of relocating the data structure, isn't the tendency now (to
> make sharing between objfiles easier) to keep the data structure
> unrelocated, and relocate things when looking things up?
>
Hmm, that makes sense, I didn't think of that. Anyway, I'm still happy
to have written this, since it gives me a working solution to compare to.
> I could imagine a compunit_symtab method called "find_call_site",
> where you pass a relocated PC. The method internally unrelocates the PC
> to do the lookup in the htab. call_site would store an unrelocated pc,
> and a call_site::pc method would return the (computed) relocated pc.
> Simiarly, the call_site's target would keep holding an unrelocated pc,
> and it would be computed on the fly.
>
> See crude patch implementing this at the bottom of this message.
>
Ack, thanks. I've put this through testing and ran into only two
regressions (so, this used to pass for me with unix/-fPIE/-pie):
...
FAIL: gdb.trace/entry-values.exp: bt (1) (pattern 1)
FAIL: gdb.trace/entry-values.exp: bt (2) (pattern 1)
...
while still fixing all the unix/-fno-PIE/-no-pie vs unix/-fPIE/-pie
regressions.
Fixed by:
...
diff --git a/gdb/dwarf2/read.c b/gdb/dwarf2/read.c
index bb5767aae67..fa775722afb 100644
--- a/gdb/dwarf2/read.c
+++ b/gdb/dwarf2/read.c
@@ -13517,7 +13517,8 @@ read_call_site_scope
sect_offset_str (die->sect_off), objfile_name
(objfile));
else
{
- lowpc = gdbarch_adjust_dwarf2_addr (gdbarch, lowpc +
baseaddr);
+ lowpc = (gdbarch_adjust_dwarf2_addr (gdbarch, lowpc +
baseaddr)
+ - baseaddr);
SET_FIELD_PHYSADDR (call_site->target, lowpc);
}
}
...
I'll put this through some testing.
Thanks,
- Tom
> Some observations I made while reading about this.
>
> - In call_site_to_target_addr, when the target is defined using a DWARF
> expression (FIELD_LOC_KIND_DWARF_BLOCK), is the result of computing
> that expression relocated or not? I would presume that it's not
> relocated. Do we need to relocate it at that point? That looks like
> a bug to me, it would probably be caught by an appropriate test.
>
> - In read_call_site_scope, we do an htab lookup to look for
> duplicates. We do:
>
> slot = htab_find_slot (cu->call_site_htab, &call_site_local, INSERT);
>
> where call_site_local is a call_site object. However, the htab works
> using the address as index, it passes core_addr_hash and core_addr_eq
> as hash/eq functions. And in call_site_for_pc, the lookup is done
> by passing a pointer to a CORE_ADDR. Therefore, the lookup in
> read_call_site_scope looks wrong to me. It just works because
> call_site::pc happens to be the first field of call_site.
>
> Otherwise, comments on this version:
>
>>
>> Thanks,
>> - Tom
>>
>> [gdb/symtab] Relocate call_site_htab
>>
>> ---
>> gdb/objfiles.c | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
>> 1 file changed, 58 insertions(+)
>>
>> diff --git a/gdb/objfiles.c b/gdb/objfiles.c
>> index b65fa8820ca..ceff0fcfba4 100644
>> --- a/gdb/objfiles.c
>> +++ b/gdb/objfiles.c
>> @@ -626,6 +626,60 @@ relocate_one_symbol (struct symbol *sym, struct objfile *objfile,
>> }
>> }
>>
>> +/* Relocate call_site S using offset DELTA. */
>> +
>> +static void
>> +call_site_relocate (struct call_site *s, CORE_ADDR delta)
>> +{
>> + s->pc += delta;
>> + if (FIELD_LOC_KIND (s->target) == FIELD_LOC_KIND_PHYSADDR)
>> + FIELD_STATIC_PHYSADDR (s->target) += delta;
>> +}
>> +
>> +/* Relocate HTAB, which is a COMPUNIT_CALL_SITE_HTAB using offset DELTA. */
>> +
>> +static void
>> +compunit_call_site_htab_relocate (htab_t htab, CORE_ADDR delta)
>> +{
>> + /* Changing the pc field changes the hashcode, so we can't just update the
>> + elements. Instead, we move them to this var, and then reinsert them. */
>> + std::vector<struct call_site *> tmp;
>
> Instead of using a temporary vector, would it be simpler to just create
> a new htab, fill it while traversing the original one, and move it in
> place of the original one? The original symtab might still be
> referenced in dwarf2_cu, but it's probably not needed there anymore
> after we have moved it to compunit_symtab. So in
> process_full_comp_unit, after moving the htab, we could set
> dwarf2_cu::call_site_htab to nullptr to make sure we don't ever use it
> again.
>
>> +
>> + /* Copy elements to tmp. */
>> + auto visitor_func
>> + = [] (void **slot, void *info) -> int
>> + {
>> + /* Copy element to tmp. */
>> + struct call_site *s = (struct call_site *) *slot;
>> + std::vector<struct call_site *> *tmp_ptr
>> + = (std::vector<struct call_site *> *)info;
>> + tmp_ptr->push_back (s);
>> +
>> + /* Keep going. */
>> + return 1;
>> + };
>> + htab_traverse (htab, visitor_func, &tmp);
>> +
>> + /* Make hashtable empty. This does not destroy the elements because the
>> + hashtable is created with del_f == nullptr. */
>> + htab_empty (htab);
>> +
>> + /* Relocate and reinsert elements. */
>> + for (struct call_site *s : tmp) {
>> + /* Relocate element. */
>> + call_site_relocate (s, delta);
>> +
>> + /* Reinsert element. */
>> + struct call_site call_site_local;
>> + call_site_local.pc = s->pc;
>> + void **slot
>> + = htab_find_slot (htab, &call_site_local, INSERT);
>
> As explained above, since the htab functions are
> core_addr_eq/core_addr_hash, we should pass a pointer to a CORE_ADDR.
>
>> + gdb_assert (slot != NULL);
>> + gdb_assert (*slot == NULL);
>> + *slot = s;
>> + }
>
> Watch the formatting above.
>
>
> From 8e8b9679da453bd6e5f4573412da275038a945dc Mon Sep 17 00:00:00 2001
> From: Simon Marchi <simon.marchi@polymtl.ca>
> Date: Thu, 30 Sep 2021 09:08:43 -0400
> Subject: [PATCH] blah
>
> Change-Id: I6c73686653d6c4daad331f3536a257cbcea77c64
> ---
> gdb/block.c | 13 ++++++-------
> gdb/dwarf2/frame-tailcall.c | 4 ++--
> gdb/dwarf2/loc.c | 27 +++++++++++++++++----------
> gdb/dwarf2/read.c | 10 +++++-----
> gdb/gdbtypes.c | 10 ++++++++++
> gdb/gdbtypes.h | 7 ++++---
> gdb/symtab.c | 24 ++++++++++++++++++++++++
> gdb/symtab.h | 6 ++++--
> 8 files changed, 72 insertions(+), 29 deletions(-)
>
> diff --git a/gdb/block.c b/gdb/block.c
> index 4cb957313960..426ccb630e25 100644
> --- a/gdb/block.c
> +++ b/gdb/block.c
> @@ -224,16 +224,15 @@ blockvector_contains_pc (const struct blockvector *bv, CORE_ADDR pc)
> struct call_site *
> call_site_for_pc (struct gdbarch *gdbarch, CORE_ADDR pc)
> {
> - struct compunit_symtab *cust;
> - void **slot = NULL;
> + call_site *cs = nullptr;
>
> /* -1 as tail call PC can be already after the compilation unit range. */
> - cust = find_pc_compunit_symtab (pc - 1);
> + compunit_symtab *cust = find_pc_compunit_symtab (pc - 1);
>
> - if (cust != NULL && COMPUNIT_CALL_SITE_HTAB (cust) != NULL)
> - slot = htab_find_slot (COMPUNIT_CALL_SITE_HTAB (cust), &pc, NO_INSERT);
> + if (cust != nullptr)
> + cs = cust->find_call_site (pc);
>
> - if (slot == NULL)
> + if (cs == nullptr)
> {
> struct bound_minimal_symbol msym = lookup_minimal_symbol_by_pc (pc);
>
> @@ -247,7 +246,7 @@ call_site_for_pc (struct gdbarch *gdbarch, CORE_ADDR pc)
> : msym.minsym->print_name ()));
> }
>
> - return (struct call_site *) *slot;
> + return cs;
> }
>
> /* Return the blockvector immediately containing the innermost lexical block
> diff --git a/gdb/dwarf2/frame-tailcall.c b/gdb/dwarf2/frame-tailcall.c
> index f112b4ecca48..9fe498b0924e 100644
> --- a/gdb/dwarf2/frame-tailcall.c
> +++ b/gdb/dwarf2/frame-tailcall.c
> @@ -240,14 +240,14 @@ pretend_pc (struct frame_info *this_frame, struct tailcall_cache *cache)
> gdb_assert (next_levels >= 0);
>
> if (next_levels < chain->callees)
> - return chain->call_site[chain->length - next_levels - 1]->pc;
> + return chain->call_site[chain->length - next_levels - 1]->pc ();
> next_levels -= chain->callees;
>
> /* Otherwise CHAIN->CALLEES are already covered by CHAIN->CALLERS. */
> if (chain->callees != chain->length)
> {
> if (next_levels < chain->callers)
> - return chain->call_site[chain->callers - next_levels - 1]->pc;
> + return chain->call_site[chain->callers - next_levels - 1]->pc ();
> next_levels -= chain->callers;
> }
>
> diff --git a/gdb/dwarf2/loc.c b/gdb/dwarf2/loc.c
> index 2322a01f396a..16e0be73d027 100644
> --- a/gdb/dwarf2/loc.c
> +++ b/gdb/dwarf2/loc.c
> @@ -654,10 +654,10 @@ call_site_to_target_addr (struct gdbarch *call_site_gdbarch,
> {
> struct bound_minimal_symbol msym;
>
> - msym = lookup_minimal_symbol_by_pc (call_site->pc - 1);
> + msym = lookup_minimal_symbol_by_pc (call_site->pc () - 1);
> throw_error (NO_ENTRY_VALUE_ERROR,
> _("DW_AT_call_target is not specified at %s in %s"),
> - paddress (call_site_gdbarch, call_site->pc),
> + paddress (call_site_gdbarch, call_site->pc ()),
> (msym.minsym == NULL ? "???"
> : msym.minsym->print_name ()));
>
> @@ -666,12 +666,12 @@ call_site_to_target_addr (struct gdbarch *call_site_gdbarch,
> {
> struct bound_minimal_symbol msym;
>
> - msym = lookup_minimal_symbol_by_pc (call_site->pc - 1);
> + msym = lookup_minimal_symbol_by_pc (call_site->pc () - 1);
> throw_error (NO_ENTRY_VALUE_ERROR,
> _("DW_AT_call_target DWARF block resolving "
> "requires known frame which is currently not "
> "available at %s in %s"),
> - paddress (call_site_gdbarch, call_site->pc),
> + paddress (call_site_gdbarch, call_site->pc ()),
> (msym.minsym == NULL ? "???"
> : msym.minsym->print_name ()));
>
> @@ -700,11 +700,11 @@ call_site_to_target_addr (struct gdbarch *call_site_gdbarch,
> msym = lookup_minimal_symbol (physname, NULL, NULL);
> if (msym.minsym == NULL)
> {
> - msym = lookup_minimal_symbol_by_pc (call_site->pc - 1);
> + msym = lookup_minimal_symbol_by_pc (call_site->pc () - 1);
> throw_error (NO_ENTRY_VALUE_ERROR,
> _("Cannot find function \"%s\" for a call site target "
> "at %s in %s"),
> - physname, paddress (call_site_gdbarch, call_site->pc),
> + physname, paddress (call_site_gdbarch, call_site->pc ()),
> (msym.minsym == NULL ? "???"
> : msym.minsym->print_name ()));
>
> @@ -713,7 +713,14 @@ call_site_to_target_addr (struct gdbarch *call_site_gdbarch,
> }
>
> case FIELD_LOC_KIND_PHYSADDR:
> - return FIELD_STATIC_PHYSADDR (call_site->target);
> + {
> + dwarf2_per_objfile *per_objfile = call_site->per_objfile;
> + compunit_symtab *cust = per_objfile->get_symtab (call_site->per_cu);
> + int sect_idx = COMPUNIT_BLOCK_LINE_SECTION (cust);
> + CORE_ADDR delta = per_objfile->objfile->section_offsets[sect_idx];
> +
> + return FIELD_STATIC_PHYSADDR (call_site->target) + delta;
> + }
>
> default:
> internal_error (__FILE__, __LINE__, _("invalid call site target kind"));
> @@ -810,7 +817,7 @@ func_verify_no_selftailcall (struct gdbarch *gdbarch, CORE_ADDR verify_addr)
> static void
> tailcall_dump (struct gdbarch *gdbarch, const struct call_site *call_site)
> {
> - CORE_ADDR addr = call_site->pc;
> + CORE_ADDR addr = call_site->pc ();
> struct bound_minimal_symbol msym = lookup_minimal_symbol_by_pc (addr - 1);
>
> fprintf_unfiltered (gdb_stdlog, " %s(%s)", paddress (gdbarch, addr),
> @@ -986,7 +993,7 @@ call_site_find_chain_1 (struct gdbarch *gdbarch, CORE_ADDR caller_pc,
>
> if (target_call_site)
> {
> - if (addr_hash.insert (target_call_site->pc).second)
> + if (addr_hash.insert (target_call_site->pc ()).second)
> {
> /* Successfully entered TARGET_CALL_SITE. */
>
> @@ -1005,7 +1012,7 @@ call_site_find_chain_1 (struct gdbarch *gdbarch, CORE_ADDR caller_pc,
> call_site = chain.back ();
> chain.pop_back ();
>
> - size_t removed = addr_hash.erase (call_site->pc);
> + size_t removed = addr_hash.erase (call_site->pc ());
> gdb_assert (removed == 1);
>
> target_call_site = call_site->tail_call_next;
> diff --git a/gdb/dwarf2/read.c b/gdb/dwarf2/read.c
> index 00aa64dd0abc..bb5767aae673 100644
> --- a/gdb/dwarf2/read.c
> +++ b/gdb/dwarf2/read.c
> @@ -9510,7 +9510,7 @@ process_full_comp_unit (dwarf2_cu *cu, enum language pretend_language)
> if (gcc_4_minor >= 5)
> cust->epilogue_unwind_valid = 1;
>
> - cust->call_site_htab = cu->call_site_htab;
> + cust->set_call_site_htab (cu->call_site_htab);
> }
>
> per_objfile->set_symtab (cu->per_cu, cust);
> @@ -13341,7 +13341,7 @@ read_call_site_scope (struct die_info *die, struct dwarf2_cu *cu)
> struct gdbarch *gdbarch = objfile->arch ();
> CORE_ADDR pc, baseaddr;
> struct attribute *attr;
> - struct call_site *call_site, call_site_local;
> + struct call_site *call_site;
> void **slot;
> int nparams;
> struct die_info *child_die;
> @@ -13364,13 +13364,13 @@ read_call_site_scope (struct die_info *die, struct dwarf2_cu *cu)
> }
> pc = attr->as_address () + baseaddr;
> pc = gdbarch_adjust_dwarf2_addr (gdbarch, pc);
> + pc -= baseaddr;
>
> if (cu->call_site_htab == NULL)
> cu->call_site_htab = htab_create_alloc_ex (16, core_addr_hash, core_addr_eq,
> NULL, &objfile->objfile_obstack,
> hashtab_obstack_allocate, NULL);
> - call_site_local.pc = pc;
> - slot = htab_find_slot (cu->call_site_htab, &call_site_local, INSERT);
> + slot = htab_find_slot (cu->call_site_htab, &pc, INSERT);
> if (*slot != NULL)
> {
> complaint (_("Duplicate PC %s for DW_TAG_call_site "
> @@ -13406,7 +13406,7 @@ read_call_site_scope (struct die_info *die, struct dwarf2_cu *cu)
> + (sizeof (*call_site->parameter) * (nparams - 1))));
> *slot = call_site;
> memset (call_site, 0, sizeof (*call_site) - sizeof (*call_site->parameter));
> - call_site->pc = pc;
> + call_site->m_unrelocated_pc = pc;
>
> if (dwarf2_flag_true_p (die, DW_AT_call_tail_call, cu)
> || dwarf2_flag_true_p (die, DW_AT_GNU_tail_call, cu))
> diff --git a/gdb/gdbtypes.c b/gdb/gdbtypes.c
> index be7c74ac6cfd..2585fe03a138 100644
> --- a/gdb/gdbtypes.c
> +++ b/gdb/gdbtypes.c
> @@ -37,6 +37,7 @@
> #include "cp-support.h"
> #include "bcache.h"
> #include "dwarf2/loc.h"
> +#include "dwarf2/read.h"
> #include "gdbcore.h"
> #include "floatformat.h"
> #include "f-lang.h"
> @@ -6309,6 +6310,15 @@ objfile_type (struct objfile *objfile)
> return objfile_type;
> }
>
> +CORE_ADDR
> +call_site::pc () const
> +{
> + compunit_symtab *cust = this->per_objfile->get_symtab (this->per_cu);
> + CORE_ADDR delta
> + = this->per_objfile->objfile->section_offsets[COMPUNIT_BLOCK_LINE_SECTION (cust)];
> + return m_unrelocated_pc + delta;
> +}
> +
> void _initialize_gdbtypes ();
> void
> _initialize_gdbtypes ()
> diff --git a/gdb/gdbtypes.h b/gdb/gdbtypes.h
> index 2a641122aec0..e47bc46374d3 100644
> --- a/gdb/gdbtypes.h
> +++ b/gdb/gdbtypes.h
> @@ -1783,11 +1783,12 @@ struct call_site_parameter
>
> struct call_site
> {
> - /* * Address of the first instruction after this call. It must be
> + CORE_ADDR pc () const;
> +
> + /* Unrelocated address of the first instruction after this call. It must be
> the first field as we overload core_addr_hash and core_addr_eq
> for it. */
> -
> - CORE_ADDR pc;
> + CORE_ADDR m_unrelocated_pc;
>
> /* * List successor with head in FUNC_TYPE.TAIL_CALL_LIST. */
>
> diff --git a/gdb/symtab.c b/gdb/symtab.c
> index a30d900cf8d6..e536754db02e 100644
> --- a/gdb/symtab.c
> +++ b/gdb/symtab.c
> @@ -329,6 +329,30 @@ search_domain_name (enum search_domain e)
> }
> }
>
> +call_site *
> +compunit_symtab::find_call_site (CORE_ADDR pc) const
> +{
> + if (m_call_site_htab == nullptr)
> + return nullptr;
> +
> + CORE_ADDR delta
> + = this->objfile->section_offsets[COMPUNIT_BLOCK_LINE_SECTION (this)];
> + CORE_ADDR unrelocated_pc = pc - delta;
> +
> + void **slot = htab_find_slot (m_call_site_htab, &unrelocated_pc, NO_INSERT);
> + if (slot == nullptr)
> + return nullptr;
> +
> + return (call_site *) *slot;
> +}
> +
> +void
> +compunit_symtab::set_call_site_htab (htab_t call_site_htab)
> +{
> + gdb_assert (m_call_site_htab == nullptr);
> + m_call_site_htab = call_site_htab;
> +}
> +
> /* See symtab.h. */
>
> struct symtab *
> diff --git a/gdb/symtab.h b/gdb/symtab.h
> index 5182f51672e3..748f49c42435 100644
> --- a/gdb/symtab.h
> +++ b/gdb/symtab.h
> @@ -1449,6 +1449,9 @@ struct symtab
>
> struct compunit_symtab
> {
> + call_site *find_call_site (CORE_ADDR pc) const;
> + void set_call_site_htab (htab_t call_site_htab);
> +
> /* Unordered chain of all compunit symtabs of this objfile. */
> struct compunit_symtab *next;
>
> @@ -1503,7 +1506,7 @@ struct compunit_symtab
> unsigned int epilogue_unwind_valid : 1;
>
> /* struct call_site entries for this compilation unit or NULL. */
> - htab_t call_site_htab;
> + htab_t m_call_site_htab;
>
> /* The macro table for this symtab. Like the blockvector, this
> is shared between different symtabs in a given compilation unit.
> @@ -1538,7 +1541,6 @@ using compunit_symtab_range = next_range<compunit_symtab>;
> #define COMPUNIT_BLOCK_LINE_SECTION(cust) ((cust)->block_line_section)
> #define COMPUNIT_LOCATIONS_VALID(cust) ((cust)->locations_valid)
> #define COMPUNIT_EPILOGUE_UNWIND_VALID(cust) ((cust)->epilogue_unwind_valid)
> -#define COMPUNIT_CALL_SITE_HTAB(cust) ((cust)->call_site_htab)
> #define COMPUNIT_MACRO_TABLE(cust) ((cust)->macro_table)
>
> /* A range adapter to allowing iterating over all the file tables
>
next prev parent reply other threads:[~2021-09-30 23:47 UTC|newest]
Thread overview: 7+ messages / expand[flat|nested] mbox.gz Atom feed top
2021-09-30 9:56 Tom de Vries via Gdb-patches
2021-09-30 14:26 ` Simon Marchi via Gdb-patches
2021-09-30 18:14 ` Tom Tromey
2021-09-30 23:47 ` Tom de Vries via Gdb-patches [this message]
2021-10-01 1:15 ` Simon Marchi via Gdb-patches
2021-10-01 8:25 ` Tom de Vries via Gdb-patches
2021-10-01 12:37 ` Tom de Vries via Gdb-patches
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=9a605b9e-267b-e9f5-4122-c407913e92f7@suse.de \
--to=gdb-patches@sourceware.org \
--cc=simon.marchi@polymtl.ca \
--cc=tdevries@suse.de \
--cc=tom@tromey.com \
/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