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

Sam Price thesamprice@gmail.com
Tue Aug 18 02:11:57 GMT 2026


lookup_address_in_function_table picks the function with the smallest range
containing the address.  When two candidates have the same range -- an inlined
subroutine covering its containing subprogram exactly -- the tie was broken by
comparing the two funcinfo pointers.  Those come from bfd_zalloc, so the winner
tracked heap layout (a new objalloc chunk can land below the old one), and
addr2line named a different function from run to run on the same binary; only
the function and its inlined frames varied, never file and line.

Order by unit_offset instead.  It does not move with the heap, and it restores
the pre-089e3718bd8 result: that algorithm walked the prepend-built
function_table and kept the last-parsed among equals, i.e. the greater DW_AT
offset.  An inlined subroutine's DIE is a child of its subprogram's, so it has
the greater offset and now wins every time -- the innermost routine, as
intended.  The offsets are unique within the one comp_unit these candidates
share.

This only changes output that was already unstable: funcinfos are allocated in
increasing-offset scan order, so within a single chunk pointer order and offset
order agree and both rules pick the same function -- verified byte-identical
over 164k addresses across ten inlined binaries.  Forcing the divergence needs
a chunk placed below its predecessor, which test input cannot, so the added
dw2-inline-tie test is a regression pin (inverting the compare to < fails it),
not a trigger.

bfd/
	* dwarf2.c (lookup_address_in_function_table): Break equal
	best_fit_len ties on funcinfo->unit_offset rather than on the
	funcinfo pointer.

Signed-off-by: Samuel Price <thesamprice@gmail.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
 bfd/dwarf2.c                                     |   11 +
 binutils/testsuite/binutils-all/addr2line.exp    |    7 +
 binutils/testsuite/binutils-all/dw2-inline-tie.S |  186 ++++++++++++++++++++++
 binutils/testsuite/binutils-all/dw2-inline-tie.d |   12 +
 4 files changed, 214 insertions(+), 2 deletions(-)

diff --git a/bfd/dwarf2.c b/bfd/dwarf2.c
index 4c88add7bc7..208b6db29a5 100644
--- a/bfd/dwarf2.c
+++ b/bfd/dwarf2.c
@@ -3365,9 +3365,16 @@ lookup_address_in_function_table (struct comp_unit *unit,
 	  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.  */
+		 same best fit length.  That algorithm walked the function
+		 table, which is built by prepending, so among equals it kept
+		 the one parsed last.  Order by DW_AT offset, which says that
+		 directly.  Comparing the funcinfo pointers instead made the
+		 result depend on the addresses bfd_zalloc happened to return:
+		 they increase within an objalloc chunk but a new chunk can
+		 land below the old one, so a tie could be resolved either way
+		 from run to run and the reported function changed.  */
 	      || (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..646890f8e85 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 the same range.  Needs
+# hand written DWARF: whether a compiler emits the tie at all depends on its
+# inlining decisions.
+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..4177d9476a4
--- /dev/null
+++ b/binutils/testsuite/binutils-all/dw2-inline-tie.S
@@ -0,0 +1,186 @@
+/* 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/>.  */
+
+/* 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
+.Lptr_begin:
+	.dc.a	0
+.Lptr_end:
+
+	.section .debug_info
+.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 */
+	.dc.a		.Lfunc_begin		/* DW_AT_low_pc */
+	.dc.a		.Lfunc_end		/* DW_AT_high_pc */
+
+	/* The containing function.  */
+	.uleb128	2			/* Abbrev: DW_TAG_subprogram */
+	.ascii		"outer_fn\0"		/* DW_AT_name */
+	.dc.a		.Lfunc_begin		/* DW_AT_low_pc */
+	.dc.a		.Lfunc_end		/* 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 */
+	.dc.a		.Lfunc_begin		/* DW_AT_low_pc */
+	.dc.a		.Lfunc_end		/* 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	0x1			/* DW_FORM_addr */
+	.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	0x1			/* DW_FORM_addr */
+	.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	0x1			/* DW_FORM_addr */
+	.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
+.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
+	.dc.a		.Lfunc_begin
+
+	.byte		3			/* DW_LNS_advance_line */
+	.sleb128	16			/* ... to 17 */
+
+	.byte		1			/* DW_LNS_copy */
+
+	.byte		0			/* DW_LNE_set_address */
+	.uleb128	1 + (.Lptr_end - .Lptr_begin)
+	.byte		2
+	.dc.a		.Lfunc_end
+
+	.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


More information about the Binutils mailing list