gdb: support source tracking for multi-location breakpoints

Message ID 5d354be53ea21f4da7b446c6337d694c06c475f3.1791027408.git.aburgess@redhat.com
State New
Headers
Series gdb: support source tracking for multi-location breakpoints |

Checks

Context Check Description
linaro-tcwg-bot/tcwg_gdb_build--master-arm success Build passed
linaro-tcwg-bot/tcwg_gdb_build--master-aarch64 success Build passed
linaro-tcwg-bot/tcwg_gdb_check--master-arm success Test passed
linaro-tcwg-bot/tcwg_gdb_check--master-aarch64 success Test passed

Commit Message

Andrew Burgess Oct. 3, 2026, 11:37 a.m. UTC
  Currently, when breakpoint source tracking is enabled (see 'set
breakpoint source-tracking enabled on'), GDB will not source track
multi-location breakpoints.

As a first take this was fine, but I think that we can support source
tracking for some multi-location breakpoints.

Source tracking only applies to breakpoints set based on a file and
line number, so the rest of this discussion is only concerned with
this case.  This includes things like 'break file:line' and also
'break line'.  In the latter case GDB uses the current source file to
provide the filename, so the two cases are really equivalent.

If we consider 'break file:line' then there are two ways that this can
resolve to multiple locations.

First, if 'file:line' lands in an inline function, and the inline
function is inlined multiple times (or has an out-of-line
instantiation) then there can be multiple locations.  However, there
is only a single source line for all of these breakpoint locations,
the original line in the single original file.

The second way that 'file:line' can result in multiple locations is if
file is not an absolute filename, and the partial filename matches
multiple source files in the executable, and each source file has
source code on the requested line number.  In this case we have a
single breakpoint, but multiple source files.  GDB can only track a
single source line per breakpoint, so in this case source tracking is
not possible.

Now, you might think that this second case indicates that the source
tracking information should be per-breakpoint location, not per
breakpoint.  However, this would not be a good idea.

The breakpoint spec, i.e. the 'file:line' is part of the breakpoint
itself.  When source tracking indicates that a source line has moved
this spec is updated.  If we source tracked per-location what would we
do when one source file changed but not the other?  This would leave
us in a situation where one location might be left at 'file:line'
while another needs to change to 'file:(line + 1)'.  Supporting this
would require the actual breakpoint spec to be per-location, and
would, I think have much bigger implications for how GDB handles
breakpoints.

For now then, I propose sticking with the current approach, the
breakpoint spec and the source tracking are per-breakpoint.

But, we can still support the case where a single source file line is
an inline function with multiple locations.

To support this, after generating the breakpoint symtab_and_line
vector, instead of rejecting any vector with more than one entry, we
look through all of the entries.  If every entry is from the same
source file and line then we will now source track this breakpoint.

I've updated an existing test which had an inline function; this test
now correctly source tracks the breakpoint.  I extended the test to
cover two cases, the inline function is defined and used in a single
source file, and the inline function is in a header file and is used
in multiple source files.

I then added a new test, this test covers the case where a 'file:line'
breakpoint matches multiple different source files.  In this case the
breakpoint should not be source tracked.
---
 gdb/breakpoint.c                              |  82 +++++++--
 .../gdb.base/source-tracking-inline-1.c       |   9 +-
 .../gdb.base/source-tracking-inline-2.c       |  10 +-
 .../gdb.base/source-tracking-inline-hdr-1.h   |  33 ++++
 .../gdb.base/source-tracking-inline-hdr-2.h   |  34 ++++
 .../gdb.base/source-tracking-inline-other.c   |  25 +++
 .../gdb.base/source-tracking-inline.exp       | 163 ++++++++++++++----
 .../source-tracking-multi-loc-other.c         |  36 ++++
 .../gdb.base/source-tracking-multi-loc.c      |  35 ++++
 .../gdb.base/source-tracking-multi-loc.exp    | 101 +++++++++++
 10 files changed, 472 insertions(+), 56 deletions(-)
 create mode 100644 gdb/testsuite/gdb.base/source-tracking-inline-hdr-1.h
 create mode 100644 gdb/testsuite/gdb.base/source-tracking-inline-hdr-2.h
 create mode 100644 gdb/testsuite/gdb.base/source-tracking-inline-other.c
 create mode 100644 gdb/testsuite/gdb.base/source-tracking-multi-loc-other.c
 create mode 100644 gdb/testsuite/gdb.base/source-tracking-multi-loc.c
 create mode 100644 gdb/testsuite/gdb.base/source-tracking-multi-loc.exp


base-commit: 36dd3acb47b80e430b712281cf55a02777677242
  

Comments

Alexandra Petlanova Hajkova Oct. 6, 2026, 9:41 a.m. UTC | #1
On Sat, Oct 3, 2026 at 1:38 PM Andrew Burgess <aburgess@redhat.com> wrote:

> Currently, when breakpoint source tracking is enabled (see 'set
> breakpoint source-tracking enabled on'), GDB will not source track
> multi-location breakpoints.
>
> As a first take this was fine, but I think that we can support source
> tracking for some multi-location breakpoints.
>
> Source tracking only applies to breakpoints set based on a file and
> line number, so the rest of this discussion is only concerned with
> this case.  This includes things like 'break file:line' and also
> 'break line'.  In the latter case GDB uses the current source file to
> provide the filename, so the two cases are really equivalent.
>
> If we consider 'break file:line' then there are two ways that this can
> resolve to multiple locations.
>
> First, if 'file:line' lands in an inline function, and the inline
> function is inlined multiple times (or has an out-of-line
> instantiation) then there can be multiple locations.  However, there
> is only a single source line for all of these breakpoint locations,
> the original line in the single original file.
>
>
> Hi Andrew,

 I like your approach. My initial restriction on all multi-threaded
breakpoints was too broad.
It's great the source-tracking actually works for the inlined functions.

Thank you for your improvement,
Alexandra
  

Patch

diff --git a/gdb/breakpoint.c b/gdb/breakpoint.c
index 0f5360c4ce5..5232ae8c3a8 100644
--- a/gdb/breakpoint.c
+++ b/gdb/breakpoint.c
@@ -706,6 +706,66 @@  breakpoint_source_get_start_line (const breakpoint_source *src)
   return src->bp_line - src->bp_line_stored;
 }
 
+/* Are SALS suitable for breakpoint source tracking?
+
+   SALS are the locations that GDB found for the location spec the user
+   provided for the breakpoint.
+
+   If SALS is a single location which was created with an explicit line
+   number, e.g. 'break file:line' or just 'break line' then we can source
+   track this location.
+
+   If there are no locations in SALS then obviously we cannot track the
+   source.
+
+   Also if the locations were not created from an explicit line number,
+   e.g. 'break function_name' then we don't want to source track this
+   breakpoint.
+
+   If there are multiple locations in SALS, and they are all created using
+   an explicit line, then we can source track this breakpoint so long as the
+   same source file is used for each location.  This distinguishes between
+   'break foo.c:10' matching a single inline function in foo.c which is
+   inlined at multiple locations, and 'break foo.c:10' which matches
+   multiple files called 'foo.c' each of which has a line 10 that we can
+   break on.  The former case can be source tracked, while the latter can
+   not.  */
+
+static bool
+breakpoint_sals_suitable_for_tracking
+	(gdb::array_view<const symtab_and_line> sals)
+{
+  /* Easy case first.  If there is a single sal, and it uses an explicit
+     line number, then we can potentially track this breakpoint.  */
+  if (sals.size () == 1 && sals[0].explicit_line)
+    return true;
+
+  /* Cannot track this breakpoint if it has no locations.  Or if we have
+     multiple locations, but the first doesn't use an explicit line
+     number, then we cannot track this breakpoint.  */
+  if (sals.empty () || !sals[0].explicit_line)
+    return false;
+
+  /* For multiple locations, we can track the breakpoint so long as every
+     location refers to the same symtab and line as the first sal.  This
+     would correspond to a single inline function in the source code being
+     inlined at multiple locations.  */
+  for (std::size_t i = 1; i < sals.size (); ++i)
+    {
+      /* If any sal is different to the first then we cannot track this
+	 breakpoint.  */
+      if ((sals[i].symtab != sals[0].symtab
+	   && strcmp (symtab_to_fullname (sals[i].symtab),
+		      symtab_to_fullname (sals[0].symtab)) != 0)
+	  || sals[i].explicit_line != sals[0].explicit_line
+	  || sals[i].line != sals[0].line)
+	return false;
+    }
+
+  /* All sals match.  */
+  return true;
+}
+
 /* Return true if SPEC is suitable for source tracking, otherwise false.  A
    location spec is suitable for tracking if it is an explicit location
    spec, and the line offset is an absolute line number.  We also don't
@@ -9186,18 +9246,10 @@  create_breakpoint_sal (struct gdbarch *gdbarch,
 				enabled, flags,
 				display_canonical);
 
-  /* Only capture source lines for file:line breakpoints when source
-     tracking is enabled.  We check explicit_line to ensure the user
-     explicitly specified a line number (e.g., "break file.c:23" or
-     "break 23"), as opposed to "break function_name" or temporary
-     breakpoints set by commands like "start".
-
-     We also only track single-location breakpoints.  Multi-location
-     breakpoints (e.g., breakpoints on inline functions that are inlined
-     in multiple places) are too complex to track reliably as each location
-     may have moved differently.  */
-  if (source_tracking_breakpoints && sals.size () == 1
-      && sals[0].explicit_line
+  /* Are source tracking breakpoints enabled?  If they are, is this
+     breakpoint suitable for source tracking?  */
+  if (source_tracking_breakpoints
+      && breakpoint_sals_suitable_for_tracking (sals)
       && breakpoint_locspec_suitable_for_tracking (b->locspec.get ()))
     {
       /* Capture source if we have valid symtab and line info.
@@ -13541,10 +13593,10 @@  code_breakpoint::adjust_bp_for_source_tracking
     return;
 
   /* BFD changed - executable was reloaded.  */
-  if (expanded.size () != 1)
+  if (!breakpoint_sals_suitable_for_tracking (expanded))
     {
-      warning (_("Breakpoint %d now has multiple locations after reload, "
-		 "disabling source tracking."), number);
+      warning (_("Breakpoint %d is no longer suitable for source tracking "
+		 "after reload, disabling source tracking."), number);
       bp_source.reset ();
       return;
     }
diff --git a/gdb/testsuite/gdb.base/source-tracking-inline-1.c b/gdb/testsuite/gdb.base/source-tracking-inline-1.c
index 1f096af225f..16f581a2dda 100644
--- a/gdb/testsuite/gdb.base/source-tracking-inline-1.c
+++ b/gdb/testsuite/gdb.base/source-tracking-inline-1.c
@@ -15,12 +15,16 @@ 
    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.  */
 
+#include "source-tracking-inline-hdr.h"
+
 volatile int global_var = 0;
 
 static inline void __attribute__ ((__always_inline__))
 inline_func (void)
 {
-  ++global_var;		/* Breakpoint here.  */
+  global_var = global_var + 1;
+  global_var = global_var + 2;		/* Breakpoint here.  */
+  global_var = global_var + 3;
 }
 
 int __attribute__ ((noinline, noclone))
@@ -46,5 +50,6 @@  main (void)
 
   ++global_var;
 
-  return answer - global_var;
+  header_func ();
+  return answer - other_func (global_var);
 }
diff --git a/gdb/testsuite/gdb.base/source-tracking-inline-2.c b/gdb/testsuite/gdb.base/source-tracking-inline-2.c
index 451593f3b54..c7b58752fd5 100644
--- a/gdb/testsuite/gdb.base/source-tracking-inline-2.c
+++ b/gdb/testsuite/gdb.base/source-tracking-inline-2.c
@@ -15,12 +15,17 @@ 
    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.  */
 
+#include "source-tracking-inline-hdr.h"
+
 volatile int global_var = 0;
 
 static inline void __attribute__ ((__always_inline__))
 inline_func (void)
 {
-  global_var += 2;		/* Breakpoint here.  */
+  global_var = global_var + 5;
+  global_var = global_var + 1;
+  global_var = global_var + 2;		/* Breakpoint here.  */
+  global_var = global_var + 3;
 }
 
 int __attribute__ ((noinline, noclone))
@@ -46,5 +51,6 @@  main (void)
 
   ++global_var;
 
-  return answer - global_var;
+  header_func ();
+  return answer - other_func (global_var);
 }
diff --git a/gdb/testsuite/gdb.base/source-tracking-inline-hdr-1.h b/gdb/testsuite/gdb.base/source-tracking-inline-hdr-1.h
new file mode 100644
index 00000000000..42e8d051aaf
--- /dev/null
+++ b/gdb/testsuite/gdb.base/source-tracking-inline-hdr-1.h
@@ -0,0 +1,33 @@ 
+/* This testcase is part of GDB, the GNU debugger.
+
+   Copyright 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/>.  */
+
+#ifndef SOURCE_TRACKING_INLINE_HDR_H
+#define SOURCE_TRACKING_INLINE_HDR_H
+
+static inline void __attribute__ ((__always_inline__))
+header_func (void)
+{
+  static int header_var = 0;
+
+  header_var = header_var + 1;
+  header_var = header_var + 2;		/* Header breakpoint location.  */
+  header_var = header_var + 3;
+}
+
+extern int other_func (int arg);
+
+#endif /* SOURCE_TRACKING_INLINE_HDR_H */
diff --git a/gdb/testsuite/gdb.base/source-tracking-inline-hdr-2.h b/gdb/testsuite/gdb.base/source-tracking-inline-hdr-2.h
new file mode 100644
index 00000000000..ac68e9949be
--- /dev/null
+++ b/gdb/testsuite/gdb.base/source-tracking-inline-hdr-2.h
@@ -0,0 +1,34 @@ 
+/* This testcase is part of GDB, the GNU debugger.
+
+   Copyright 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/>.  */
+
+#ifndef SOURCE_TRACKING_INLINE_HDR_H
+#define SOURCE_TRACKING_INLINE_HDR_H
+
+static inline void __attribute__ ((__always_inline__))
+header_func (void)
+{
+  static int header_var = 0;
+
+  header_var = header_var + 5;
+  header_var = header_var + 1;
+  header_var = header_var + 2;		/* Header breakpoint location.  */
+  header_var = header_var + 3;
+}
+
+extern int other_func (int arg);
+
+#endif /* SOURCE_TRACKING_INLINE_HDR_H */
diff --git a/gdb/testsuite/gdb.base/source-tracking-inline-other.c b/gdb/testsuite/gdb.base/source-tracking-inline-other.c
new file mode 100644
index 00000000000..7eb324a02a9
--- /dev/null
+++ b/gdb/testsuite/gdb.base/source-tracking-inline-other.c
@@ -0,0 +1,25 @@ 
+/* This testcase is part of GDB, the GNU debugger.
+
+   Copyright 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/>.  */
+
+#include "source-tracking-inline-hdr.h"
+
+int
+other_func (int arg)
+{
+  header_func ();
+  return arg + 1;
+}
diff --git a/gdb/testsuite/gdb.base/source-tracking-inline.exp b/gdb/testsuite/gdb.base/source-tracking-inline.exp
index 96849dd1bb6..9e4354a0317 100644
--- a/gdb/testsuite/gdb.base/source-tracking-inline.exp
+++ b/gdb/testsuite/gdb.base/source-tracking-inline.exp
@@ -13,17 +13,50 @@ 
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-# Test of the source tracking breakpoint feature when trying to place
-# a breakpoint on inline functions.  As the breakpoint resolves to
-# multiple locations we don't currently source track these
-# breakpoints.
+# Place a breakpoint on an inline function.  As the inline function
+# comes from a single source location then GDB will source track this
+# location; when the source line within the inline function moves GDB
+# will update which line the breakpoint is on.
+#
+# This test covers the case where the inline function and its uses are
+# within a single source file, and the case where the inline function
+# is in a header file and used in multiple source files.
 
-standard_testfile -1.c -2.c
+# This test restarts the inferior with 'start'.  This doesn't work
+# when using plain 'gdbserver'.
+require !use_gdb_stub
 
-set build_srcfile [standard_output_file ${testfile}.c]
+# This test has a main source file, there are two copies of this (-1.c
+# and -2.c), then a header file of which there are also two copies
+# (-hdr-1.h and -hdr-2.h).  Then there is the "other" source file,
+# this never changes, there is just one copy of this.
+standard_testfile -1.c -2.c -hdr-1.h -hdr-2.h -other.c
 
+# We need to copy the main source file and the header file into the
+# build directory so we can rename them.  As both these files and the
+# "other" source file all need to include the renamed header file it
+# is easier if the "other" source file is also copied to the build
+# directory, even though this file is never renamed.  The alternative
+# would be to setup include search paths.
+set build_other_srcfile [standard_output_file [file tail $srcfile5]]
+
+# These are the names given to the main source file and the header
+# file within the build directory.
+set testfile_src ${testfile}.c
+set testfile_hdr ${testfile}-hdr.h
+set build_srcfile [standard_output_file $testfile_src]
+set build_header [standard_output_file $testfile_hdr]
+
+# Copy the "other" source file into the build directory now.  This
+# only needs doing once as this file never changes.
+remote_exec build "cp $srcdir/$subdir/$srcfile5 $build_other_srcfile"
+
+# Copy the original source files into the build directory.
 remote_exec build "cp $srcdir/$subdir/$srcfile $build_srcfile"
-if { [prepare_for_testing "failed to prepare" $testfile $build_srcfile] } {
+remote_exec build "cp $srcdir/$subdir/$srcfile3 $build_header"
+
+if { [prepare_for_testing "failed to prepare" $testfile \
+	  [list $build_srcfile $build_other_srcfile]] } {
     return
 }
 
@@ -34,47 +67,103 @@  if {![runto_main]} {
 gdb_test_no_output "set breakpoint source-tracking enabled on" \
     "enable source tracking breakpoints"
 
-set lineno [gdb_get_line_number "Breakpoint here." $build_srcfile]
+# Get initial breakpoint line numbers.
+set lineno_src [gdb_get_line_number "Breakpoint here." $build_srcfile]
+set lineno_hdr [gdb_get_line_number "Header breakpoint location." \
+		    $build_header]
 
-# The breakpoint is on an inline function called from two places,
-# so it resolves to 2 locations.
-gdb_test "break ${testfile}.c:$lineno" \
-    "^Breakpoint $decimal at $hex: ${testfile}.c:$lineno\\. \\(2 locations\\)"
+# Create a breakpoint on an inline function within the main source
+# file.  This breakpoint is called from two places, so it resolves to
+# 2 locations.
+gdb_test "break ${testfile_src}:$lineno_src" \
+    "^Breakpoint $decimal at $hex: ${testfile_src}:$lineno_src\\. \\(2 locations\\)"
+set bp_num_src [get_integer_valueof "\$bpnum" "*UNKNOWN*" \
+		   "get number for main source breakpoint"]
 
-# Check that we are tracking the expected number of lines.
-#
-# With the multi-location fix, this breakpoint should NOT be tracked
-# because it has 2 locations (inline function called from two places).
-# The output should show <MULTIPLE> but NOT "source-tracking enabled".
-gdb_test_multiple "info breakpoints" \
-  "multi-location breakpoint not tracked" {
-    -re -wrap "source-tracking enabled.*" {
-      fail "$gdb_test_name (tracking incorrectly enabled)"
-    }
-    -re -wrap "<MULTIPLE>.*" {
-      pass $gdb_test_name
+# Create a breakpoint on an inline function within the header file.
+# This breakpoint is also called from two places, so it also resolves
+# to 2 locations.
+gdb_test "break ${testfile_hdr}:$lineno_hdr" \
+    "^Breakpoint $decimal at $hex: ${testfile_hdr}:$lineno_hdr\\. \\(2 locations\\)"
+set bp_num_hdr [get_integer_valueof "\$bpnum" "*UNKNOWN*" \
+		   "get number for header breakpoint"]
+
+# Run 'info breakpoints BP_NUM' and check that the breakpoint is
+# source tracked around LINENO.  Use TESTNAME as the name of the test
+# being run.
+proc check_bp_tracked { bp_num lineno testname } {
+    gdb_test_multiple "info breakpoints $bp_num" $testname {
+	-re -wrap "source-tracking enabled \\(tracking $::decimal lines around line $lineno\\).*" {
+	    pass $gdb_test_name
+	}
+	-re -wrap "" {
+	    fail "$gdb_test_name (source tracking not enabled)"
+	}
     }
 }
 
+# Check that the breakpoint on the inline function from the main
+# source file is being source tracked.
+check_bp_tracked $bp_num_src $lineno_src \
+    "inline function within source file is tracked"
+
+# And check that the breakpoint on the inline function from the header
+# file is also being source tracked.
+check_bp_tracked $bp_num_hdr $lineno_hdr \
+  "inline function within header file is tracked"
+
+# Change the main source file, this changes the inline function within
+# that source file, but the header breakpoint is unchanged.
+
 sleep 1
 remote_exec build "cp $srcdir/$subdir/$srcfile2 $build_srcfile"
-if { [build_executable "failed to build" $testfile $build_srcfile] } {
+if { [build_executable "failed to build" $testfile \
+	  [list $build_srcfile $build_other_srcfile]] } {
     return
 }
 
 # Reload the executable.  'start' internally uses 'tbreak main', which
 # is not a file:line breakpoint, so it will not be source-tracked.
-gdb_test "with confirm off -- start"
+gdb_test "with confirm off -- start" ".*" \
+    "start after changing main source file"
 
-# After reload, the breakpoint should still NOT be tracked because it
-# still has 2 locations.  This verifies we don't try to track
-# multi-location breakpoints even after reload.
-gdb_test_multiple "info breakpoints" \
-    "multi-location breakpoint still not tracked after reload" {
-    -re -wrap "source-tracking enabled.*" {
-	fail "$gdb_test_name (tracking incorrectly enabled)"
-    }
-    -re -wrap "<MULTIPLE>.*" {
-	pass $gdb_test_name
-    }
+# Update our understanding of where the breakpoint should be.
+set lineno_src [gdb_get_line_number "Breakpoint here." $build_srcfile]
+
+# The breakpoint should still be source tracked, but its location
+# should have been updated.
+check_bp_tracked $bp_num_src $lineno_src \
+    "main source file breakpoint has moved"
+
+# The header file breakpoint should not have moved, but should still
+# be tracked.
+check_bp_tracked $bp_num_hdr $lineno_hdr \
+    "header file breakpoint has not moved"
+
+# Change the header file, this moves the breakpoint line for the
+# inline function in the header file.
+sleep 1
+remote_exec build "cp $srcdir/$subdir/$srcfile4 $build_header"
+if { [build_executable "failed to build" $testfile \
+	  [list $build_srcfile $build_other_srcfile]] } {
+    return
 }
+
+# Reload the executable.  'start' internally uses 'tbreak main', which
+# is not a file:line breakpoint, so it will not be source-tracked.
+gdb_test "with confirm off -- start" ".*" \
+    "start after changing header file"
+
+# Update our understanding of where the breakpoint should be.
+set lineno_hdr [gdb_get_line_number "Header breakpoint location." \
+		    $build_header]
+
+# The main file breakpoint should still be tracked, but should not
+# have moved.
+check_bp_tracked $bp_num_src $lineno_src \
+    "main source file breakpoint has not moved"
+
+# The header file breakpoint should have moved, and should still be
+# tracked.
+check_bp_tracked $bp_num_hdr $lineno_hdr \
+    "header file breakpoint has moved"
diff --git a/gdb/testsuite/gdb.base/source-tracking-multi-loc-other.c b/gdb/testsuite/gdb.base/source-tracking-multi-loc-other.c
new file mode 100644
index 00000000000..1b086147f6e
--- /dev/null
+++ b/gdb/testsuite/gdb.base/source-tracking-multi-loc-other.c
@@ -0,0 +1,36 @@ 
+/* This testcase is part of GDB, the GNU debugger.
+
+   Copyright 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/>.  */
+
+extern int other_func (void);
+
+volatile int other_var = 0;
+
+/* This comment exists just to force the function below
+   into the correct location.  It has no other job.  */
+
+int
+other_func (void)
+{
+  other_var = other_var + 1;
+  other_var = other_var + 1;
+  other_var = other_var + 1;	/* Breakpoint here.  */
+  other_var = other_var + 1;
+  other_var = other_var + 1;
+  other_var = other_var + 1;
+
+  return other_var;
+}
diff --git a/gdb/testsuite/gdb.base/source-tracking-multi-loc.c b/gdb/testsuite/gdb.base/source-tracking-multi-loc.c
new file mode 100644
index 00000000000..fbf02178844
--- /dev/null
+++ b/gdb/testsuite/gdb.base/source-tracking-multi-loc.c
@@ -0,0 +1,35 @@ 
+/* This testcase is part of GDB, the GNU debugger.
+
+   Copyright 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/>.  */
+
+extern int other_func (void);
+
+volatile int global_var = 0;
+
+int
+main (void)
+{
+  global_var = global_var + 1;
+  global_var = global_var + 1;
+  global_var = global_var + 1;
+  global_var = global_var + 1;
+  global_var = global_var + 1;
+  global_var = global_var + 1;		/* Breakpoint here.  */
+  global_var = global_var + 1;
+  global_var = global_var + 1;
+
+  return other_func ();
+}
diff --git a/gdb/testsuite/gdb.base/source-tracking-multi-loc.exp b/gdb/testsuite/gdb.base/source-tracking-multi-loc.exp
new file mode 100644
index 00000000000..1d875ff4bf4
--- /dev/null
+++ b/gdb/testsuite/gdb.base/source-tracking-multi-loc.exp
@@ -0,0 +1,101 @@ 
+# Copyright 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/>.
+
+# Create a multi-location breakpoint using file and line syntax where
+# the multiple locations come from different source files.  This
+# requires having two source files aa/foo.c and bb/foo.c, then placing
+# a breakpoint on 'foo.c:10' or similar.  GDB will find both 'foo.c'
+# files, and try to create breakpoints on line 10 in both.
+#
+# The point of this test is that breakpoint source tracking should not
+# activate for this breakpoint due to the locations coming from
+# different source files.
+
+standard_testfile .c -other.c
+
+# Create two sub-directories of the build directory in which our two
+# source files can be placed.
+set directory_aa [standard_output_file aa]
+set directory_bb [standard_output_file bb]
+remote_exec build "mkdir -p \"$directory_aa\" \"$directory_bb\""
+
+# Now copy the two source files into the two directories, give them
+# the same basename.
+remote_exec build "cp $srcdir/$subdir/$srcfile $directory_aa/$srcfile"
+remote_exec build "cp $srcdir/$subdir/$srcfile2 $directory_bb/$srcfile"
+
+# Now build the executable.
+if { [prepare_for_testing "failed to prepare" $testfile \
+	  [list $directory_aa/$srcfile $directory_bb/$srcfile]] } {
+    return
+}
+
+if {![runto_main]} {
+    return
+}
+
+gdb_test_no_output "set breakpoint source-tracking enabled on" \
+    "enable source tracking breakpoints"
+
+# Sanity check our source files.  Both have a 'Breakpoint here'
+# marker, these should be on the same line number within the two
+# files.  This isn't a "test" as this isn't checking GDB
+# functionality, this is checking a developer hasn't broken the test.
+set lineno1 [gdb_get_line_number "Breakpoint here." $directory_aa/$srcfile]
+set lineno2 [gdb_get_line_number "Breakpoint here." $directory_bb/$srcfile]
+if { $lineno1 != $lineno2 } {
+    error "line numbers don't match between source files"
+}
+set lineno $lineno1
+
+# Create a breakpoint using the filename and line number.  The
+# filename will match two files, so this will result in two locations
+# being created.
+gdb_test "break ${srcfile}:$lineno" \
+    "^Breakpoint $decimal at $hex: ${srcfile}:$lineno\\. \\(2 locations\\)"
+set bp_num [get_integer_valueof "\$bpnum" "*UNKNOWN*" \
+		"get bp_num for multi-location breakpoint"]
+
+# As the two locations are from two different files this breakpoint
+# should not be source tracked.
+gdb_test_multiple "info breakpoints $bp_num" \
+    "multi-location breakpoint not tracked" {
+	-re -wrap "source-tracking enabled.*" {
+	    fail "$gdb_test_name (tracking incorrectly enabled)"
+	}
+	-re -wrap "" {
+	    pass $gdb_test_name
+	}
+    }
+
+# Now for a sanity check, lets make sure we can create source tracked
+# breakpoints on the files individually.
+foreach_with_prefix dir { aa bb } {
+    delete_breakpoints
+    gdb_test "break $dir/${srcfile}:$lineno" \
+	"^Breakpoint $decimal at $hex: file \[^\r\n\]+/$dir/${srcfile}, line $lineno\\."
+    set bp_num [get_integer_valueof "\$bpnum" "*UNKNOWN*" \
+		    "get bp_num for breakpoint in $dir/$srcfile"]
+
+    gdb_test_multiple "info breakpoints $bp_num" \
+	"single location breakpoint from $dir/$srcfile is tracked" {
+	    -re -wrap "source-tracking enabled \\(tracking $::decimal lines around line $lineno\\).*" {
+		pass $gdb_test_name
+	    }
+	    -re -wrap "" {
+		fail $gdb_test_name
+	    }
+	}
+}