[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