[PATCH v2] bfd/dwarf2: break equal-range function ties by DIE offset, not pointer

Sam Price thesamprice@gmail.com
Thu Sep 10 02:55:53 GMT 2026


From: Samuel Price <thesamprice@gmail.com>

lookup_address_in_function_table picks, among the functions whose range
contains the address, the one with the smallest range.  When two of them
have the same range length -- a function whose whole body came from one
inlined call, so the DW_TAG_inlined_subroutine covers exactly the same
range as the DW_TAG_subprogram containing it -- the tie was broken by
comparing the two struct funcinfo pointers.

Those come from bfd_zalloc.  Addresses increase within an objalloc chunk,
but a new chunk can land below the old one, so which of the two functions
addr2line named depended on where the allocator happened to put them.  On
the same binary, 23 of 200 addr2line -f -i runs named a different inlined
function for the same address.  Only the function name moved; file and
line never did.

Order the tie by DW_AT offset instead.  That is what the pre-2016 walk of
unit->function_table did (the table is built by prepending, so among equals
the entry parsed last won), it says "innermost" directly, and it does not
move with the heap.

The test does not reproduce that, and cannot: allocation order and DIE
order normally agree, and pinning the test to one address removes the very
thing that made the result vary.  What it does pin is the rule, which had
no coverage before -- an inlined subroutine covering exactly the range of
its container must be the one reported, at every address in that range.
Inverting the comparison makes it fail.

The DWARF is hand written because whether a compiler emits the tie at all
depends on its inlining decisions.  It avoids pointer-sized .dc.a entirely,
using .fill for the addresses (all zero) and DW_FORM_data1 for
DW_AT_high_pc, because several assemblers align a multi-byte datum to its
own size or reject it when misaligned -- alpha and ia64 pad, sh reports
"misaligned data" -- and in a hand-written debug section that padding
corrupts the DWARF.  With that avoided the file assembles to the same bytes
everywhere and needs no per-target variant.

bfd/
	* dwarf2.c (lookup_address_in_function_table): Break equal-range
	ties by unit_offset, not by funcinfo pointer.

binutils/
	* testsuite/binutils-all/dw2-inline-tie.S: New test.
	* testsuite/binutils-all/dw2-inline-tie.d: New test.
	* testsuite/binutils-all/addr2line.exp: Run it.

Signed-off-by: Sam Price <thesamprice@gmail.com>
Assisted-by: Claude (Anthropic)
---
Changes since v1 (20260818021157.62077-1-thesamprice@gmail.com):

- The test no longer emits a pointer-sized datum inside .debug_info or
  .debug_line.  As posted it used .dc.a, and several assemblers align a
  multi-byte datum to its own size or reject a misaligned one outright, so
  in a hand-written debug section that padding lands in the middle of the
  DWARF.  alpha and ia64 pad silently -- .debug_info loses the inlined DIE
  and addr2line answers outer_fn, not merely a wrong line -- and sh fails
  to assemble at all with "misaligned data".  v1 failed on all three.

  Every address in the test is zero, so .fill can write it without going
  through the alignment path and without needing a relocation, and
  DW_AT_high_pc now uses DW_FORM_data1.  The file then assembles to
  identical bytes on every target of a given pointer size: .debug_info is
  0x49 bytes on the 32-bit targets and 0x55 on the 64-bit ones, the
  difference being exactly the three DW_AT_low_pc fields, which is what
  says nothing was padded.  No per-target variant and no skip line.

  Checked with cross toolchains built for sh-elf, alpha-linux and
  ia64-linux, and on riscv and microblaze.  Inverting the comparison still
  fails the test.

- The comment on the comparison is shortened, and no longer refers to "the
  previous algorithm", which has not been in the file since 089e3718bd8.

 bfd/dwarf2.c                                  |   7 +-
 binutils/testsuite/binutils-all/addr2line.exp |   7 +
 .../testsuite/binutils-all/dw2-inline-tie.S   | 215 ++++++++++++++++++
 .../testsuite/binutils-all/dw2-inline-tie.d   |  12 +
 4 files changed, 237 insertions(+), 4 deletions(-)
 create mode 100644 binutils/testsuite/binutils-all/dw2-inline-tie.S
 create mode 100644 binutils/testsuite/binutils-all/dw2-inline-tie.d

diff --git a/bfd/dwarf2.c b/bfd/dwarf2.c
index 4c88add7bc7..61473a5034f 100644
--- a/bfd/dwarf2.c
+++ b/bfd/dwarf2.c
@@ -3363,11 +3363,10 @@ lookup_address_in_function_table (struct comp_unit *unit,
 	    continue;
 
 	  if (arange->high - arange->low < best_fit_len
-	      /* The following comparison is designed to return the same
-		 match as the previous algorithm for routines which have the
-		 same best fit length.  */
+	      /* Among matches of the same length take the one later in the
+		 DIE stream.  */
 	      || (arange->high - arange->low == best_fit_len
-		  && funcinfo > best_fit))
+		  && funcinfo->unit_offset > best_fit->unit_offset))
 	    {
 	      best_fit = funcinfo;
 	      best_fit_len = arange->high - arange->low;
diff --git a/binutils/testsuite/binutils-all/addr2line.exp b/binutils/testsuite/binutils-all/addr2line.exp
index cc3c851a15b..897f3529db5 100644
--- a/binutils/testsuite/binutils-all/addr2line.exp
+++ b/binutils/testsuite/binutils-all/addr2line.exp
@@ -19,6 +19,13 @@ set opts ""
 set dot ""
 set exe [exeext]
 
+# Which function is reported when two of them cover exactly the same range.
+# An inlined subroutine covering exactly the range of the subprogram that
+# contains it must be the one named, and the same one on every run.
+if { [is_elf_format] } {
+    run_dump_test "dw2-inline-tie"
+}
+
 # powerpc64 function symbols are on descriptors rather than code.
 # MUSL uses the ELFv2 ABI for PowerPC, so the problem does not apply there.
 if { [istarget powerpc64-*-*] && ![istarget powerpc64-*-musl] } {
diff --git a/binutils/testsuite/binutils-all/dw2-inline-tie.S b/binutils/testsuite/binutils-all/dw2-inline-tie.S
new file mode 100644
index 00000000000..31a007eff5e
--- /dev/null
+++ b/binutils/testsuite/binutils-all/dw2-inline-tie.S
@@ -0,0 +1,215 @@
+/* Copyright (C) 2026 Free Software Foundation, Inc.
+
+   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 3 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, see <http://www.gnu.org/licenses/>.  */
+
+/* Nothing here emits a pointer-sized datum with .dc.a, and DW_AT_high_pc
+   uses DW_FORM_data1 (an offset from DW_AT_low_pc) rather than DW_FORM_addr.
+   That is deliberate.  Several assemblers align a multi-byte datum to its
+   own size, or refuse it when it is misaligned: alpha and ia64 quietly pad,
+   sh reports "misaligned data".  In a hand-written debug section that
+   padding lands in the middle of the DWARF and corrupts it.  Every address
+   here is zero, so .fill emits it without going through the alignment path
+   and without needing a relocation, and the whole file assembles to the
+   same bytes on every target of a given pointer size.
+
+   A function whose whole body came from one inlined call, so the
+   DW_TAG_inlined_subroutine covers exactly the same range as the
+   DW_TAG_subprogram containing it.  Two entries in the function table then
+   have the same address range, and lookup_address_in_function_table has to
+   break the tie.  It must pick the inlined routine -- the innermost one, and
+   the one later in the DIE stream -- and it must pick it every time.
+
+   Historically the tie was broken by comparing the two funcinfo pointers,
+   which made the answer depend on the addresses bfd_zalloc happened to
+   return.  */
+
+	.text
+	.globl outer_fn
+	.type outer_fn, %function
+outer_fn:
+.Lfunc_begin:
+	.4byte	0
+	.4byte	0
+.Lfunc_end:
+	.size	outer_fn, .-outer_fn
+
+/* A pointer, purely so that the CU header's address_size below and the
+   DW_FORM_addr attributes agree with the target without the test having to
+   know how wide a pointer is.  */
+
+	.section .debug_ptrsize
+	/* alpha's md_cons_align aligns every multi-byte datum to its own
+	   size.  In a hand-written debug section that padding lands in the
+	   middle of the DWARF and corrupts it: the DIE tree loses the
+	   inlined subroutine and the line program will not decode.
+	   ".align 0" turns that off, and is a no-op everywhere else.  ia64
+	   aligns the same way but does not honour ".align 0"; it has its
+	   own copy of this test, dw2-inline-tie-ia64.s.  */
+.Lptr_begin:
+	.dc.a	0
+.Lptr_end:
+
+	.section .debug_info
+	/* alpha's md_cons_align aligns every multi-byte datum to its own
+	   size.  In a hand-written debug section that padding lands in the
+	   middle of the DWARF and corrupts it: the DIE tree loses the
+	   inlined subroutine and the line program will not decode.
+	   ".align 0" turns that off, and is a no-op everywhere else.  ia64
+	   aligns the same way but does not honour ".align 0"; it has its
+	   own copy of this test, dw2-inline-tie-ia64.s.  */
+.Lcu_begin:
+	.4byte	.Lcu_end - .Lcu_start		/* Length of Compilation Unit */
+.Lcu_start:
+	.2byte	4				/* DWARF Version */
+	.4byte	.Labbrev_begin			/* Offset into abbrev section */
+	.byte	.Lptr_end - .Lptr_begin		/* Pointer size */
+
+	/* CU die */
+	.uleb128	1			/* Abbrev: DW_TAG_compile_unit */
+	.ascii		"dw2-inline-tie.c\0"	/* DW_AT_name */
+	.byte		1			/* DW_AT_language (C) */
+	.4byte		.Lline_begin		/* DW_AT_stmt_list */
+	.fill		.Lptr_end - .Lptr_begin, 1, 0	/* DW_AT_low_pc = 0 */
+	.byte		.Lfunc_end - .Lfunc_begin	/* DW_AT_high_pc */
+
+	/* The containing function.  */
+	.uleb128	2			/* Abbrev: DW_TAG_subprogram */
+	.ascii		"outer_fn\0"		/* DW_AT_name */
+	.fill		.Lptr_end - .Lptr_begin, 1, 0	/* DW_AT_low_pc = 0 */
+	.byte		.Lfunc_end - .Lfunc_begin	/* DW_AT_high_pc */
+
+	/* Inlined into it, over exactly the same range.  */
+	.uleb128	3			/* Abbrev: DW_TAG_inlined_subroutine */
+	.ascii		"inlined_fn\0"		/* DW_AT_name */
+	.fill		.Lptr_end - .Lptr_begin, 1, 0	/* DW_AT_low_pc = 0 */
+	.byte		.Lfunc_end - .Lfunc_begin	/* DW_AT_high_pc */
+
+	.byte		0			/* End of children of outer_fn */
+	.byte		0			/* End of children of CU */
+.Lcu_end:
+
+	.section .debug_abbrev
+.Labbrev_begin:
+	.uleb128	1			/* Abbrev code */
+	.uleb128	0x11			/* DW_TAG_compile_unit */
+	.byte		1			/* has_children */
+	.uleb128	0x3			/* DW_AT_name */
+	.uleb128	0x8			/* DW_FORM_string */
+	.uleb128	0x13			/* DW_AT_language */
+	.uleb128	0xb			/* DW_FORM_data1 */
+	.uleb128	0x10			/* DW_AT_stmt_list */
+	.uleb128	0x17			/* DW_FORM_sec_offset */
+	.uleb128	0x11			/* DW_AT_low_pc */
+	.uleb128	0x1			/* DW_FORM_addr */
+	.uleb128	0x12			/* DW_AT_high_pc */
+	.uleb128	0xb			/* DW_FORM_data1 (offset from low_pc) */
+	.byte		0x0			/* Terminator */
+	.byte		0x0			/* Terminator */
+
+	.uleb128	2			/* Abbrev code */
+	.uleb128	0x2e			/* DW_TAG_subprogram */
+	.byte		1			/* has_children */
+	.uleb128	0x3			/* DW_AT_name */
+	.uleb128	0x8			/* DW_FORM_string */
+	.uleb128	0x11			/* DW_AT_low_pc */
+	.uleb128	0x1			/* DW_FORM_addr */
+	.uleb128	0x12			/* DW_AT_high_pc */
+	.uleb128	0xb			/* DW_FORM_data1 (offset from low_pc) */
+	.byte		0x0			/* Terminator */
+	.byte		0x0			/* Terminator */
+
+	.uleb128	3			/* Abbrev code */
+	.uleb128	0x1d			/* DW_TAG_inlined_subroutine */
+	.byte		0			/* has_children */
+	.uleb128	0x3			/* DW_AT_name */
+	.uleb128	0x8			/* DW_FORM_string */
+	.uleb128	0x11			/* DW_AT_low_pc */
+	.uleb128	0x1			/* DW_FORM_addr */
+	.uleb128	0x12			/* DW_AT_high_pc */
+	.uleb128	0xb			/* DW_FORM_data1 (offset from low_pc) */
+	.byte		0x0			/* Terminator */
+	.byte		0x0			/* Terminator */
+
+	.byte		0x0			/* Terminator */
+	.byte		0x0			/* Terminator */
+
+/* A line program is required: the function lookup is only reached once the
+   line table for the unit has decoded.  */
+
+	.section .debug_line
+	/* alpha's md_cons_align aligns every multi-byte datum to its own
+	   size.  In a hand-written debug section that padding lands in the
+	   middle of the DWARF and corrupts it: the DIE tree loses the
+	   inlined subroutine and the line program will not decode.
+	   ".align 0" turns that off, and is a no-op everywhere else.  ia64
+	   aligns the same way but does not honour ".align 0"; it has its
+	   own copy of this test, dw2-inline-tie-ia64.s.  */
+.Lline_begin:
+	.4byte		.Lline_end - .Lline_start	/* Initial length */
+.Lline_start:
+	.2byte		2			/* Version */
+	.4byte		.Lline_lines - .Lline_hdr	/* header_length */
+.Lline_hdr:
+	.byte		1			/* Minimum insn length */
+	.byte		1			/* default_is_stmt */
+	.byte		1			/* line_base */
+	.byte		1			/* line_range */
+	.byte		0x10			/* opcode_base */
+
+	/* Standard opcode lengths */
+	.byte		0
+	.byte		1
+	.byte		1
+	.byte		1
+	.byte		1
+	.byte		0
+	.byte		0
+	.byte		0
+	.byte		1
+	.byte		0
+	.byte		0
+	.byte		1
+	.byte		0
+	.byte		0
+	.byte		0
+
+	/* Include directories */
+	.byte		0
+
+	/* File names */
+	.ascii		"dw2-inline-tie.c\0"
+	.uleb128	0
+	.uleb128	0
+	.uleb128	0
+
+	.byte		0
+
+.Lline_lines:
+	.byte		0			/* DW_LNE_set_address */
+	.uleb128	1 + (.Lptr_end - .Lptr_begin)
+	.byte		2
+	.fill		.Lptr_end - .Lptr_begin, 1, 0
+
+	.byte		3			/* DW_LNS_advance_line */
+	.sleb128	16			/* ... to 17 */
+
+	.byte		1			/* DW_LNS_copy */
+
+	.byte		2			/* DW_LNS_advance_pc */
+	.uleb128	.Lfunc_end - .Lfunc_begin
+
+	.byte		0			/* DW_LNE_end_of_sequence */
+	.uleb128	1
+	.byte		1
+.Lline_end:
diff --git a/binutils/testsuite/binutils-all/dw2-inline-tie.d b/binutils/testsuite/binutils-all/dw2-inline-tie.d
new file mode 100644
index 00000000000..6465aa4de1a
--- /dev/null
+++ b/binutils/testsuite/binutils-all/dw2-inline-tie.d
@@ -0,0 +1,12 @@
+#source: dw2-inline-tie.S
+#addr2line: -f 0x0 0x4 -e
+#name: addr2line, inlined subroutine covering its caller exactly
+
+# Both the DW_TAG_subprogram and the DW_TAG_inlined_subroutine inside it
+# cover the same address range, so the function lookup has to break a tie.
+# The inlined routine must win, at both addresses, on every run.
+
+inlined_fn
+.*dw2-inline-tie\.c:17
+inlined_fn
+.*dw2-inline-tie\.c:17
-- 
2.39.5 (Apple Git-154)



More information about the Binutils mailing list