gdb: Add source-tracking breakpoints feature

Message ID 20260407092737.85411-1-ahajkova@redhat.com
State New
Headers
Series gdb: Add source-tracking breakpoints feature |

Checks

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

Commit Message

Alexandra Hájková April 7, 2026, 9:22 a.m. UTC
  The breakpoint_source structure stores captured source code lines
around a breakpoint location, along with a reference to the BFD
that was current when the source was captured.

When source tracking is enabled (via 'set breakpoint source-tracking
enabled on'), GDB captures 3 lines of source context
(BREAKPOINT_SRC_CTX_LINES) along with the current BFD when a
breakpoint is first set. On executable reload (detected by comparing
BFDs), it searches within a 12-line window
(BREAKPOINT_SRC_CTX_LINES * BREAKPOINT_SRC_SEARCH_MULTIPLIER) for
the best match and adjusts the breakpoint location if needed.

Tests added:
  gdb.base/adjust_breakpoint.exp
  gdb.base/adjust_breakpoint-missing-source.exp
  gdb.base/source-tracking-inline.exp

adjust_breakpoint.exp covers four scenarios:
  - adjust the breakpoint when lines are deleted
  - adjust the breakpoint when lines are inserted
  - the tracked line disappears entirely
  - verify the tracking can be disabled

adjust_breakpoint-missing-source.exp covers the edge case where source
files are unavailable, verifying GDB falls back to non-tracking breakpoints.

source-tracking-inline.exp covers source tracking with inline functions.

Add maintenance command to print tracked source code
Add documentation for the new source-tracking breakpoints feature.

Limitations of the current implementation:

Source tracking is not enabled for pending breakpoints that become
non-pending.  When a breakpoint is created pending (e.g. with 'set
breakpoint pending on'), source context is not captured at creation
time since no symtab is available yet.  When the breakpoint later
resolves to a location, re_set_default() only updates existing tracked
breakpoints and does not initiate tracking for newly resolved ones.
This could be fixed in the future by initiating source tracking in
re_set_default() when a breakpoint transitions from pending to
non-pending.

Source tracking for ranged breakpoints is not currently supported.
Ranged breakpoints have a start and end location spec, and tracking
both independently raises questions about whether to preserve the
range length or track each end separately.  For now, ranged
breakpoints will never be source-tracked.
---
This is the second version of "Add source-tracking breakpoints feature"
series posted at the end of the last year. I tried to apply all the
feedback and I ended up rewriting big portions of the original work
and also I decided to squash the series into one commit to improve
readability.

 gdb/NEWS                                      |  15 +
 gdb/breakpoint.c                              | 405 +++++++++++++++++-
 gdb/breakpoint.h                              |  48 +++
 gdb/doc/gdb.texinfo                           |  31 ++
 .../gdb.base/adjust_breakpoint-2.cpp          |  39 ++
 .../gdb.base/adjust_breakpoint-3.cpp          |  41 ++
 .../gdb.base/adjust_breakpoint-4.cpp          |  37 ++
 .../adjust_breakpoint-missing-source.exp      |  55 +++
 gdb/testsuite/gdb.base/adjust_breakpoint.cpp  |  40 ++
 gdb/testsuite/gdb.base/adjust_breakpoint.exp  | 118 +++++
 .../gdb.base/source-tracking-inline-1.c       |  50 +++
 .../gdb.base/source-tracking-inline-2.c       |  49 +++
 .../gdb.base/source-tracking-inline.c         |  50 +++
 .../gdb.base/source-tracking-inline.exp       |  77 ++++
 14 files changed, 1054 insertions(+), 1 deletion(-)
 create mode 100644 gdb/testsuite/gdb.base/adjust_breakpoint-2.cpp
 create mode 100644 gdb/testsuite/gdb.base/adjust_breakpoint-3.cpp
 create mode 100644 gdb/testsuite/gdb.base/adjust_breakpoint-4.cpp
 create mode 100644 gdb/testsuite/gdb.base/adjust_breakpoint-missing-source.exp
 create mode 100644 gdb/testsuite/gdb.base/adjust_breakpoint.cpp
 create mode 100644 gdb/testsuite/gdb.base/adjust_breakpoint.exp
 create mode 100644 gdb/testsuite/gdb.base/source-tracking-inline-1.c
 create mode 100644 gdb/testsuite/gdb.base/source-tracking-inline-2.c
 create mode 100644 gdb/testsuite/gdb.base/source-tracking-inline.c
 create mode 100644 gdb/testsuite/gdb.base/source-tracking-inline.exp
  

Comments

Eli Zaretskii April 7, 2026, 9:56 a.m. UTC | #1
> From: Alexandra Hájková <ahajkova@redhat.com>
> Cc: ahajkova@redhat.com
> Date: Tue,  7 Apr 2026 11:22:28 +0200
> 
> This is the second version of "Add source-tracking breakpoints feature"
> series posted at the end of the last year. I tried to apply all the
> feedback and I ended up rewriting big portions of the original work
> and also I decided to squash the series into one commit to improve
> readability.

Thanks, but it looks like only a small part of my previous review
comments in
https://sourceware.org/pipermail/gdb-patches/2025-December/223382.html
were addressed: there are still sentences with just one space between
them, the line in NEWS which I said was too long is still there
unchanged, an additional index entry I suggested is not there, and the
question I said the text was begging is still not answered by the
text.  Are you sure you posted the correct patch?  If so, please
address the review comments from back then.

Reviewed-By: Eli Zaretskii <eliz@gnu.org>
  

Patch

diff --git a/gdb/NEWS b/gdb/NEWS
index 4cf91053c95..7c2b0cf30e5 100644
--- a/gdb/NEWS
+++ b/gdb/NEWS
@@ -3,6 +3,15 @@ 
 
 *** Changes since GDB 17
 
+* Source-tracking breakpoints
+
+  GDB now supports source-tracking breakpoints, which automatically adjust
+  their location when source code changes between rebuilds. When enabled,
+  file and line breakpoints capture the surrounding source code context and
+  use it to adjust the breakpoint line if the source is modified.
+
+  Source tracking can be enabled with 'set breakpoint source-tracking enabled on'.
+
 * Support for .gdb_index sections with version less than 7 has been
   removed.
 
@@ -80,6 +89,12 @@  unset local-environment
   Analogs of the existing "environment" commands that affect GDB's own
   environment.  The local environment is used by "shell", "pipe", and
   other commands that launch a subprocess other than an inferior.
+set breakpoint source-tracking enabled [on|off]
+show breakpoint source-tracking enabled
+  Enable or disable source-tracking for file and line breakpoints.
+  When enabled, breakpoints capture surrounding source code and
+  automatically adjust their location when the source changes between
+  recompilations.
 
 save history FILENAME
   Save the command history to the given file.
diff --git a/gdb/breakpoint.c b/gdb/breakpoint.c
index d7be1b44229..a6c2bcae0f3 100644
--- a/gdb/breakpoint.c
+++ b/gdb/breakpoint.c
@@ -69,6 +69,7 @@ 
 #include "cli/cli-style.h"
 #include "cli/cli-decode.h"
 #include "break-cond-parse.h"
+#include "source-cache.h"
 
 /* readline defines this.  */
 #undef savestring
@@ -574,6 +575,10 @@  show_automatic_hardware_breakpoints (struct ui_file *file, int from_tty,
    processing user input.  */
 static bool always_inserted_mode = false;
 
+/* Control whether file & line breakpoints are created as source-tracking
+   breakpoints.  */
+static bool source_tracking_breakpoints = false;
+
 static void
 show_always_inserted_mode (struct ui_file *file, int from_tty,
 		     struct cmd_list_element *c, const char *value)
@@ -593,6 +598,179 @@  show_debug_breakpoint (struct ui_file *file, int from_tty,
   gdb_printf (file, _("Breakpoint location debugging is %s.\n"), value);
 }
 
+/* Return true if the breakpoint source is being tracked.  */
+
+static bool
+breakpoint_source_is_tracked (const breakpoint_source *src)
+{
+  if (src == nullptr)
+    return false;
+  return src->source_lines.size () > 0 && src->bp_line > 0;
+}
+
+/* Calculate the starting line number for captured source.  */
+
+static int
+breakpoint_source_get_start_line (const breakpoint_source *src)
+{
+  if (!breakpoint_source_is_tracked (src))
+    return 0;
+  return src->bp_line - src->bp_line_stored;
+}
+
+/* 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
+   allow for SPEC to be function or label based.  Most of these
+   restrictions could be lifted, but this would likely require additional
+   work to support these changes, especially when updating the location
+   spec.  */
+
+static bool
+breakpoint_locspec_suitable_for_tracking (const location_spec *spec)
+{
+  if (spec->type () != EXPLICIT_LOCATION_SPEC)
+    return false;
+
+  const explicit_location_spec *explicit_loc
+    = as_explicit_location_spec (spec);
+
+  if (explicit_loc->function_name.get () != nullptr
+      || explicit_loc->label_name.get () != nullptr
+      || explicit_loc->source_filename.get () == nullptr)
+    return false;
+
+  if (explicit_loc->line_offset.sign != LINE_OFFSET_NONE)
+    return false;
+
+  return explicit_loc->line_offset.offset > 0;
+}
+
+/* Print captured source lines to stdout, marking the breakpoint line with '>'.  */
+
+static void
+breakpoint_source_print (const breakpoint_source *src)
+{
+  if (!breakpoint_source_is_tracked (src))
+    return;
+
+  int start_line = breakpoint_source_get_start_line (src);
+  for (int j = 0; j < (int) src->source_lines.size (); j++)
+    {
+      int line_num = start_line + j;
+      char prefix;
+      if (j == src->bp_line_stored)
+	prefix = '>';
+      else
+	prefix = ' ';
+      gdb_printf ("%c %ps %s", prefix,
+		  styled_string (line_number_style.style (),
+				 pulongest (line_num)),
+		  src->source_lines[j].c_str ());
+      if (src->source_lines[j].empty ()
+	  || src->source_lines[j].back () != '\n')
+	gdb_putc ('\n');
+    }
+}
+
+/* Implement the "maintenance info source-tracking-context" command.  */
+
+static void
+maintenance_info_source_tracking_context (const char *args, int from_tty)
+{
+  if (args == NULL || *args == '\0')
+    error (_("Breakpoint number required."));
+
+  /* Parse the breakpoint number.  */
+  const char *end = args;
+  int num = get_number_trailer (&end, 0);
+
+  if (num <= 0)
+    error (_("Invalid breakpoint number '%s'."), args);
+
+  /* Find the breakpoint.  */
+  breakpoint *b = nullptr;
+  for (breakpoint &bp : all_breakpoints ())
+    {
+      if (bp.number == num)
+	{
+	  b = &bp;
+	  break;
+	}
+    }
+
+  if (b == nullptr)
+    error (_("No breakpoint number %d."), num);
+
+  /* Check if source tracking is enabled for this breakpoint.  */
+  if (!breakpoint_source_is_tracked (b->bp_source.get ()))
+    {
+      gdb_printf (_("Breakpoint %d does not have source tracking enabled.\n"), num);
+      return;
+    }
+
+  /* Print the source tracking information.  */
+  breakpoint_source_print (b->bp_source.get ());
+}
+
+/* Capture source lines around a breakpoint location for source tracking.
+   Returns a breakpoint_source structure with the captured lines, or an
+   empty structure if capture fails.  Does not print the lines.  */
+
+static breakpoint_source
+breakpoint_source_capture (gdb::array_view<const symtab_and_line> sals,
+			   int num_of_lines)
+{
+  /* Check if we have a valid symtab - if not, we can't capture source lines.
+     The symtab can be missing if the executable wasn't compiled with
+     debugging symbols.  */
+  if (sals.empty () || sals[0].symtab == nullptr || sals[0].line <= 0)
+    return {};
+
+  breakpoint_source result;
+  result.bp_line = sals[0].line;
+
+  /* Calculate the starting line, centering around the breakpoint line.
+     Avoid going before line 1.  */
+  int lines_to_capture = num_of_lines;
+  int start_line = sals[0].line - (lines_to_capture / 2);
+  if (start_line < 1)
+    start_line = 1;
+
+  /* Get line offsets to check file bounds.  */
+  const std::vector<off_t> *offsets;
+  if (!g_source_cache.get_line_charpos (sals[0].symtab, &offsets))
+      return {};
+
+  /* Adjust number of lines if we'd run past the end of the file.  */
+  if (start_line + lines_to_capture > (int) offsets->size ())
+    lines_to_capture = (int) offsets->size () - start_line;
+
+  /* Get the BFD from the symtab.  */
+  if (sals[0].symtab->compunit()->objfile())
+    result.source_bfd = sals[0].symtab->compunit()->objfile()->obfd;
+
+  /* Capture the source lines.  */
+  auto restore_styling = make_scoped_restore (&source_styling, false);
+  for (int j = 0; j < lines_to_capture; j++)
+    {
+      std::string line;
+      if (!g_source_cache.get_source_lines (sals[0].symtab, start_line + j,
+					    start_line + j, &line))
+	{
+	  /* Failed to read source - return empty structure so this
+	     breakpoint won't be tracked.  */
+	  warning (_("Failed to capture source lines for source tracking."));
+	  return {};
+	}
+      result.source_lines.push_back (line);
+      if (start_line + j == sals[0].line)
+	result.bp_line_stored = j;
+    }
+
+  return result;
+}
+
 /* See breakpoint.h.  */
 
 int
@@ -838,6 +1016,8 @@  static int tracepoint_count;
 
 static struct cmd_list_element *breakpoint_set_cmdlist;
 static struct cmd_list_element *breakpoint_show_cmdlist;
+static struct cmd_list_element *source_tracking_bp_set_cmdlist;
+static struct cmd_list_element *source_tracking_bp_show_cmdlist;
 struct cmd_list_element *save_cmdlist;
 
 /* Return whether a breakpoint is an active enabled breakpoint.  */
@@ -6821,6 +7001,16 @@  print_one_breakpoint_location (struct breakpoint *b,
       uiout->text ("\n");
     }
 
+  if (!part_of_multiple && breakpoint_source_is_tracked (b->bp_source.get ()))
+    {
+      uiout->text ("\tsource-tracking enabled (tracking ");
+      uiout->field_signed ("tracked-lines",
+			   b->bp_source->source_lines.size ());
+      uiout->text (" lines around line ");
+      uiout->field_signed ("original-line", b->bp_source->bp_line);
+      uiout->text (")\n");
+    }
+
   if (!part_of_multiple)
     {
       if (b->hit_count)
@@ -8930,7 +9120,35 @@  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
+      && breakpoint_locspec_suitable_for_tracking (b->locspec.get ()))
+    {
+      /* Capture source if we have valid symtab and line info.
+	 This works for both "b file:line" and "b line" formats.
+	 We capture BREAKPOINT_SRC_CTX_LINES lines to provide
+	 context around the breakpoint location.  */
+      b->bp_source = std::make_unique<breakpoint_source>
+	(breakpoint_source_capture (sals, BREAKPOINT_SRC_CTX_LINES));
+    }
+
+  bool warn_not_tracked = (source_tracking_breakpoints && sals.size () == 1
+			   && sals[0].explicit_line
+			   && !breakpoint_source_is_tracked (b->bp_source.get ()));
   install_breakpoint (internal, std::move (b), 0);
+  if (warn_not_tracked)
+    warning (_("Source file not available; breakpoint will not be "
+	       "source-tracked."));
 }
 
 /* Add SALS.nelts breakpoints to the breakpoint table.  For each
@@ -9213,7 +9431,6 @@  breakpoint_ops_for_location_spec (const location_spec *locspec,
 }
 
 /* See breakpoint.h.  */
-
 int
 create_breakpoint (struct gdbarch *gdbarch,
 		   location_spec *locspec,
@@ -13186,6 +13403,157 @@  code_breakpoint::location_spec_to_sals (location_spec *locspec,
   return sals;
 }
 
+/* Match BREAKPOINT_SRC_CTX_LINES lines of the initially stored source in a
+   BREAKPOINT_SRC_CTX_LINES * BREAKPOINT_SRC_SEARCH_MULTIPLIER lines current
+   source window.
+   Returns new breakpoint line on success or -1 on failure.  */
+static int
+sliding_window_match (breakpoint_source *bp_source,
+		      breakpoint_source *tmp_source)
+{
+  int bp_stored = bp_source->bp_line_stored;
+  int tmp_size = (int) tmp_source->source_lines.size ();
+  int bp_size = (int) bp_source->source_lines.size ();
+
+  for (int i = 0; i < tmp_size; i++)
+    {
+      /* Look for the initial breakpoint line in a stored window.  */
+      if (bp_source->source_lines[bp_stored] == tmp_source->source_lines[i])
+	{
+	  /* Check if the stored lines before and after the breakpoint line
+	     also match, to reduce false positives.  */
+	  if (i > 0 && bp_stored > 0
+	      && (i + 1) < tmp_size
+	      && (bp_stored + 1) < bp_size)
+	    {
+	      if ((bp_source->source_lines[bp_stored - 1] == tmp_source->source_lines[i - 1])
+		  && (bp_source->source_lines[bp_stored + 1] == tmp_source->source_lines[i + 1]))
+		{
+		  return tmp_source->bp_line + i - tmp_source->bp_line_stored;
+		}
+	    }
+	  else if (i == 0 && (i + 1) < tmp_size
+		   && (bp_stored + 1) < bp_size)
+	    {
+	      if (bp_source->source_lines[bp_stored + 1] == tmp_source->source_lines[i + 1])
+		{
+		  return tmp_source->bp_line + i - tmp_source->bp_line_stored;
+		}
+	    }
+	  else if ((i + 1) == tmp_size && i > 0
+		   && bp_stored > 0)
+	    {
+	      if (bp_source->source_lines[bp_stored - 1] == tmp_source->source_lines[i - 1])
+		{
+		  return tmp_source->bp_line + i - tmp_source->bp_line_stored;
+		}
+	    }
+	}
+    }
+
+  return -1;
+}
+
+/* See breakpoint.h.  */
+
+void
+code_breakpoint::adjust_bp_for_source_tracking
+  (program_space *filter_pspace,
+   std::vector<symtab_and_line> &expanded)
+{
+  if (expanded.empty () || expanded[0].symtab == nullptr
+      || !breakpoint_source_is_tracked (bp_source.get ()))
+    return;
+
+  struct compunit_symtab *cust = expanded[0].symtab->compunit ();
+  if (cust == nullptr || cust->objfile () == nullptr)
+    return;
+
+  bfd *current_bfd = cust->objfile ()->obfd.get ();
+  if (bp_source->source_bfd.get () == current_bfd)
+    return;
+
+  /* BFD changed — executable was reloaded.  */
+  if (expanded.size () != 1)
+    {
+      warning (_("Breakpoint %d now has multiple locations after reload, "
+		 "disabling source tracking."), number);
+      bp_source.reset ();
+      return;
+    }
+
+  /* If this fails then the location spec has changed since the
+     breakpoint's source tracking was initially setup.  */
+  gdb_assert (breakpoint_locspec_suitable_for_tracking (locspec.get ()));
+
+  int bp_stored = bp_source->bp_line_stored;
+  std::string line;
+  auto restore_styling = make_scoped_restore (&source_styling, false);
+  if (!g_source_cache.get_source_lines (expanded[0].symtab,
+					expanded[0].line,
+					expanded[0].line, &line))
+    {
+      /* Source is unreadable after reload — drop tracking.  */
+      bp_source.reset ();
+      return;
+    }
+
+  if (line == bp_source->source_lines[bp_stored])
+    {
+      /* Line unchanged — just refresh the capture with the new BFD.  */
+      bp_source = std::make_unique<breakpoint_source>
+	(breakpoint_source_capture (expanded, BREAKPOINT_SRC_CTX_LINES));
+      return;
+    }
+
+  breakpoint_source tmp_source
+    = breakpoint_source_capture (expanded,
+				 BREAKPOINT_SRC_CTX_LINES
+				 * BREAKPOINT_SRC_SEARCH_MULTIPLIER);
+  int new_bp_line = sliding_window_match (bp_source.get (), &tmp_source);
+  if (new_bp_line == -1)
+    {
+      warning (_("Breakpoint %d source code not found "
+		 "after reload, keeping original location."), number);
+      bp_source.reset ();
+      return;
+    }
+
+  location_spec *spec = locspec.get ();
+  std::string bp_string (spec->to_string ());
+  auto pos = bp_string.rfind (':');
+  if (pos == std::string::npos)
+    {
+      warning (_("unable to update location spec for breakpoint %d, "
+		 "disabling source tracking."), number);
+      bp_source.reset ();
+      return;
+    }
+
+  bp_string = bp_string.substr (0, pos + 1);
+  bp_string.append (std::to_string (new_bp_line));
+
+  /* set_string only updates the cached display string, not the parsed
+     spec_string that location_spec_to_sals actually uses.  Replace the
+     location spec entirely by re-parsing the updated string so the new
+     line number takes effect.  */
+  const char *new_spec_str = bp_string.c_str ();
+  locspec = string_to_location_spec (&new_spec_str, current_language);
+  spec = locspec.get ();
+
+  int found;
+  expanded = location_spec_to_sals (spec, filter_pspace, &found);
+  if (found && new_bp_line != bp_source->bp_line)
+    {
+      gdb_printf (_("Breakpoint %d adjusted from line %d to line %d.\n"),
+		  number, bp_source->bp_line, new_bp_line);
+      notify_breakpoint_modified (this);
+    }
+
+  bp_source = std::make_unique<breakpoint_source>
+    (breakpoint_source_capture (expanded, BREAKPOINT_SRC_CTX_LINES));
+}
+
 /* The default re_set method, for typical hardware or software
    breakpoints.  Reevaluate the breakpoint and recreate its
    locations.  */
@@ -13216,6 +13584,9 @@  code_breakpoint::re_set_default (struct program_space *filter_pspace)
 
       if (locspec_range_end != nullptr)
 	{
+	  /* Ranged breakpoints are not currently tracked.  */
+	  gdb_assert (!breakpoint_source_is_tracked (bp_source.get ()));
+
 	  std::vector<symtab_and_line> sals_end
 	    = location_spec_to_sals (locspec_range_end.get (),
 				     filter_pspace, &found);
@@ -13224,6 +13595,8 @@  code_breakpoint::re_set_default (struct program_space *filter_pspace)
 	}
     }
 
+  adjust_bp_for_source_tracking (filter_pspace, expanded);
+
   /* Update the locations for this breakpoint.  For thread-specific
      breakpoints this will remove any old locations that are for the wrong
      program space -- this can happen if the user changes the thread of a
@@ -15007,6 +15380,15 @@  Convenience variable \"$bpnum\" contains the number of the last\n\
 breakpoint set."),
 	   &maintenanceinfolist);
 
+  add_cmd ("source-tracking-context", class_maintenance,
+           maintenance_info_source_tracking_context, _("\
+  Print source tracking context for a breakpoint.\n\
+  Usage: maintenance info source-tracking-context BPNUM\n\
+  \n\
+  Displays the captured source code lines used to track\n\
+  and automatically adjust the breakpoint when source code changes."),
+           &maintenanceinfolist);
+
   add_basic_prefix_cmd ("catch", class_breakpoint, _("\
 Set catchpoints to catch events."),
 			&catch_cmdlist,
@@ -15325,6 +15707,27 @@  Usage: agent-printf \"format string\", ARG1, ARG2, ARG3, ..., ARGN\n\
 This supports most C printf format specifications, like %s, %d, etc.\n\
 This is useful for formatted output in user-defined commands."));
 
+  add_setshow_prefix_cmd ("source-tracking", class_breakpoint,
+			  _("\
+Source tracking breakpoint specific settings."),
+			  _("\
+Source tracking breakpoint specific settings."),
+			  &source_tracking_bp_set_cmdlist,
+			  &source_tracking_bp_show_cmdlist,
+			  &breakpoint_set_cmdlist, &breakpoint_show_cmdlist);
+
+  add_setshow_boolean_cmd ("enabled", class_breakpoint,
+			   &source_tracking_breakpoints, _("\
+Set whether file and line breakpoints use source tracking."), _("\
+Show whether file and line breakpoints use source tracking."), _("\
+When on, breakpoints set with file:line syntax will track the source\n\
+location and automatically adjust when the source changes and the\n\
+inferior is restarted."),
+			   NULL,
+			   NULL,
+			   &source_tracking_bp_set_cmdlist,
+			   &source_tracking_bp_show_cmdlist);
+
   automatic_hardware_breakpoints = true;
 
   gdb::observers::about_to_proceed.attach (breakpoint_about_to_proceed,
diff --git a/gdb/breakpoint.h b/gdb/breakpoint.h
index 3e84f1c99ab..f8308be48c7 100644
--- a/gdb/breakpoint.h
+++ b/gdb/breakpoint.h
@@ -27,6 +27,7 @@ 
 #include "probe.h"
 #include "location.h"
 #include <vector>
+#include <memory>
 #include "gdbsupport/array-view.h"
 #include "gdbsupport/filtered-iterator.h"
 #include "gdbsupport/iterator-range.h"
@@ -34,6 +35,7 @@ 
 #include "gdbsupport/safe-iterator.h"
 #include "cli/cli-script.h"
 #include "target/waitstatus.h"
+#include "gdb_bfd.h"
 
 struct block;
 struct gdbpy_breakpoint_object;
@@ -82,6 +84,17 @@  enum remove_bp_reason
    architecture.  */
 
 #define	BREAKPOINT_MAX	16
+
+/* Number of source lines to capture around a breakpoint for source tracking.
+   This context is used to match and relocate breakpoints when the executable
+   is reloaded.  The window is centered on the breakpoint line, capturing
+   lines both before and after it.  */
+#define BREAKPOINT_SRC_CTX_LINES 3
+
+/* Multiplier for the search window when looking for relocated breakpoints.
+   We search in (BREAKPOINT_SRC_CTX_LINES * BREAKPOINT_SRC_SEARCH_MULTIPLIER)
+   lines to find code that may have moved.  */
+#define BREAKPOINT_SRC_SEARCH_MULTIPLIER 4
 
 
 /* Type of breakpoint.  */
@@ -614,6 +627,28 @@  extern bool target_exact_watchpoints;
 using bp_location_list = intrusive_list<bp_location>;
 using bp_location_iterator = bp_location_list::iterator;
 using bp_location_range = iterator_range<bp_location_iterator>;
+/* Captured source code around a breakpoint location, used for
+   source-tracking breakpoints.  When source tracking is enabled,
+   this structure stores the original source lines around a breakpoint
+   so the breakpoint can be automatically adjusted if the source code
+   changes when the executable is reloaded.  */
+struct breakpoint_source
+{
+  /* The captured source lines as strings.  The number of captured lines
+     is source_lines.size().  */
+  std::vector<std::string> source_lines;
+
+  /* The original line number where the breakpoint was set
+     in the source file.  */
+  int bp_line = 0;
+
+  /* Index into source_lines vector indicating which line
+     contains the breakpoint (0-based).  */
+  int bp_line_stored = 0;
+
+  /* BFD when source was captured.  */
+  gdb_bfd_ref_ptr source_bfd;
+};
 
 /* Note that the ->silent field is not currently used by any commands
    (though the code is in there if it was to be, and set_raw_breakpoint
@@ -863,6 +898,11 @@  struct breakpoint : public intrusive_list_node<breakpoint>
      find the end of the range.  */
   location_spec_up locspec_range_end;
 
+  /* Captured source code around the breakpoint location, used to
+     track source around the breakpoint to automatically adjust the breakpoint
+     when source code changes between recompilations.  */
+  std::unique_ptr<breakpoint_source> bp_source;
+
   /* Architecture we used to set the breakpoint.  */
   struct gdbarch *gdbarch;
   /* Language we used to set the breakpoint.  */
@@ -983,6 +1023,14 @@  struct code_breakpoint : public breakpoint
   /* Helper method that does the basic work of re_set.  */
   void re_set_default (program_space *pspace);
 
+  /* Helper method for re_set_default.  Checks if the executable was
+     reloaded and if so, attempts to adjust the breakpoint location
+     using source tracking.  EXPANDED may be updated if the location
+     is adjusted.  */
+  void adjust_bp_for_source_tracking
+    (program_space *filter_pspace,
+     std::vector<symtab_and_line> &expanded);
+
   /* Find the SaL locations corresponding to the given LOCATION.
      On return, FOUND will be 1 if any SaL was found, zero otherwise.  */
 
diff --git a/gdb/doc/gdb.texinfo b/gdb/doc/gdb.texinfo
index 16cd01aad4d..d85ff72c0de 100644
--- a/gdb/doc/gdb.texinfo
+++ b/gdb/doc/gdb.texinfo
@@ -4659,6 +4659,17 @@  program.
 On some systems, you can set breakpoints in shared libraries before
 the executable is run.
 
+@cindex source-tracking breakpoints
+@value{GDBN} supports @dfn{source-tracking breakpoints}, which
+automatically adjust their location when source code changes between
+recompilations.  When enabled with @code{set breakpoint source-tracking
+enabled on}, breakpoints set by file and line number capture the
+surrounding source code lines.  If the source file is modified and the
+executable is rebuilt, @value{GDBN} attempts to relocate the breakpoint
+by searching for the captured source code.
+Note that breakpoints set by function name or address are
+not affected by source tracking.  @xref{Set Breaks}.
+
 @cindex watchpoints
 @cindex data breakpoints
 @cindex memory tracing
@@ -41980,6 +41991,13 @@  Shared library events.
 
 @end table
 
+@kindex maint info source-tracking-context
+@item maint info source-tracking-context @var{num}
+For source tracking breakpoints (@pxref{Set Breaks}), print the
+tracked source code context for breakpoint @var{num}.  If breakpoint
+@var{num} is not source tracked, or @var{num} is not a valid
+breakpoint number, then the command gives an error.
+
 @kindex maint info btrace
 @item maint info btrace
 Pint information about raw branch tracing data.
@@ -42848,6 +42866,19 @@  Control whether to show all non zero areas within a 1k block starting
 at thread local base, when using the @samp{info w32 thread-information-block}
 command.
 
+@kindex set breakpoint source-tracking enabled
+@kindex show breakpoint source-tracking enabled
+@item set breakpoint source-tracking enabled @r{[}on@r{|}off@r{]}
+@itemx show breakpoint source-tracking enabled
+Control whether to enable source-tracking for breakpoints set by file and
+line number.  Use @code{on} to enable, @code{off} to disable.  When enabled,
+@value{GDBN} captures the surrounding source code lines when creating a
+file:line breakpoint.  When the executable is reloaded, @value{GDBN}
+attempts to relocate the breakpoint by searching for the captured source
+code, allowing breakpoints to automatically adjust their location when source
+files are modified between compilations.  The default is @code{off}.
+Breakpoints set by function name or address are not affected by this setting.
+
 @kindex maint set target-async
 @kindex maint show target-async
 @item maint set target-async
diff --git a/gdb/testsuite/gdb.base/adjust_breakpoint-2.cpp b/gdb/testsuite/gdb.base/adjust_breakpoint-2.cpp
new file mode 100644
index 00000000000..0fb29c4ddac
--- /dev/null
+++ b/gdb/testsuite/gdb.base/adjust_breakpoint-2.cpp
@@ -0,0 +1,39 @@ 
+/* 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/>.  */
+
+int
+foo (void)
+{
+  return 0;
+}
+
+int
+foo (int var)
+{
+  int i = 8;
+  var += 10;
+  var += i;
+
+  return var;
+}
+
+int
+main (void)
+{
+  foo ();
+  return foo (2);
+}
diff --git a/gdb/testsuite/gdb.base/adjust_breakpoint-3.cpp b/gdb/testsuite/gdb.base/adjust_breakpoint-3.cpp
new file mode 100644
index 00000000000..cca12c5e170
--- /dev/null
+++ b/gdb/testsuite/gdb.base/adjust_breakpoint-3.cpp
@@ -0,0 +1,41 @@ 
+/* 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/>.  */
+
+
+int
+foo () {
+  return 0;
+}
+
+int
+foo (int var)
+{
+  int j = 0;
+  int i = 8;
+
+  var += 10;
+  var += i;
+
+  return var;
+}
+
+int
+main (void)
+{
+  foo ();
+  return foo (2);
+}
diff --git a/gdb/testsuite/gdb.base/adjust_breakpoint-4.cpp b/gdb/testsuite/gdb.base/adjust_breakpoint-4.cpp
new file mode 100644
index 00000000000..371f5553bb4
--- /dev/null
+++ b/gdb/testsuite/gdb.base/adjust_breakpoint-4.cpp
@@ -0,0 +1,37 @@ 
+/* This testcase is part of GDB, the GNU debugger.
+
+   Copyright 1992-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/>.  */
+
+int
+foo () {
+  return 0;
+}
+
+int
+foo (int var)
+{
+  int i = 8;
+
+
+  return var;
+}
+
+int
+main (void)
+{
+  foo ();
+  return foo (2);
+}
diff --git a/gdb/testsuite/gdb.base/adjust_breakpoint-missing-source.exp b/gdb/testsuite/gdb.base/adjust_breakpoint-missing-source.exp
new file mode 100644
index 00000000000..8965a7e30e6
--- /dev/null
+++ b/gdb/testsuite/gdb.base/adjust_breakpoint-missing-source.exp
@@ -0,0 +1,55 @@ 
+# Copyright 2025 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/>.
+
+# Test that source-tracking breakpoints handle missing source files correctly.
+# When source tracking is enabled but the source file cannot be found, GDB
+# should create a non-tracking breakpoint and not crash or fail.
+
+standard_testfile adjust_breakpoint.cpp
+set build_srcfile xxx-${testfile}.cpp
+
+set new_source_file [standard_output_file ${build_srcfile}]
+remote_exec build "cp ${srcdir}/${subdir}/adjust_breakpoint.cpp $new_source_file"
+if { [prepare_for_testing "failed to prepare" $testfile $new_source_file] } {
+    return -1
+}
+
+# Enable source tracking
+gdb_test_no_output "set breakpoint source-tracking enabled on" \
+    "enable source tracking breakpoints"
+
+# Now remove the source file before setting the breakpoint
+set bp_line [gdb_get_line_number "return 0;" $new_source_file]
+remote_exec build "rm -f $new_source_file"
+
+# Verify the source is not available
+gdb_test "list" ".*No such file or directory.*" \
+    "verify source file is not available"
+
+# Try to set a breakpoint - should succeed but not be tracked
+gdb_test "break ${build_srcfile}:${bp_line}" "Breakpoint.*at.*" \
+    "create breakpoint when source file missing"
+
+# Check that the breakpoint was created but is not tracked
+# (should not show "source-tracking enabled" message)
+set test "breakpoint not tracked when source missing"
+gdb_test_multiple "info breakpoints" $test {
+    -re "source-tracking enabled.*$gdb_prompt $" {
+        fail $test
+    }
+    -re "breakpoint.*${build_srcfile}:${bp_line}.*$gdb_prompt $" {
+        pass $test
+    }
+}
diff --git a/gdb/testsuite/gdb.base/adjust_breakpoint.cpp b/gdb/testsuite/gdb.base/adjust_breakpoint.cpp
new file mode 100644
index 00000000000..1cedcb05ddc
--- /dev/null
+++ b/gdb/testsuite/gdb.base/adjust_breakpoint.cpp
@@ -0,0 +1,40 @@ 
+/* This testcase is part of GDB, the GNU debugger.
+
+   Copyright 1992-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/>.  */
+
+int
+foo (void)
+{
+  return 0;
+}
+
+int
+foo (int var)
+{
+  int i = 8;
+
+  var += 10;
+  var += i;
+
+  return var;
+}
+
+int
+main (void)
+{
+  foo ();
+  return foo (2);
+}
diff --git a/gdb/testsuite/gdb.base/adjust_breakpoint.exp b/gdb/testsuite/gdb.base/adjust_breakpoint.exp
new file mode 100644
index 00000000000..9f31380d5fc
--- /dev/null
+++ b/gdb/testsuite/gdb.base/adjust_breakpoint.exp
@@ -0,0 +1,118 @@ 
+# Copyright 2025 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/>.
+
+# 1) Set the breakpoint to a certain line in $srcfile. Replace the $srcfile
+# with tmp-$srcfile which is exactly the same except one line is missing,
+# which changes the line where the breakpoint was initially set and moves
+# the breakpoint one line backwards.
+# Check if GDB adjusted the line correctly.
+#
+# 2) Do all the same but move the breakpoint a few lines forward by adding an
+# additional line to the tmp2-$srcfile.
+
+standard_testfile .cpp -2.cpp -3.cpp -4.cpp
+set build_srcfile ${testfile}-xxx.cpp
+
+set new_source_file [standard_output_file ${build_srcfile}]
+remote_exec build "cp ${srcdir}/${subdir}/${srcfile} $new_source_file"
+if { [prepare_for_testing "failed to prepare" $testfile $new_source_file] } {
+    return
+}
+
+# Enable source tracking for breakpoints
+gdb_test_no_output "set breakpoint source-tracking enabled on" \
+    "enable source tracking breakpoints"
+
+# part 1) move the breakpoint backward
+set lineno [gdb_get_line_number "var += i;" $new_source_file]
+gdb_breakpoint ${build_srcfile}:$lineno
+
+# Sleep to ensure timestamp changes when we rebuild
+sleep 1
+remote_exec build "cp ${srcdir}/${subdir}/${srcfile2} $new_source_file"
+if {[build_executable "failed to prepare" $testfile $new_source_file] == -1} {
+    return
+}
+
+set lineno [expr {$lineno - 1}]
+gdb_test "r" "Breakpoint 1,.*$build_srcfile:$lineno\r\n$lineno\t.*" \
+    "run stops at adjusted breakpoint location"
+gdb_test "info breakpoints"\
+    "breakpoint.*keep.*y.*$hex.*$build_srcfile:$lineno.*already hit 1 time"\
+    "info breakpoints show the breakpoint was adjusted one line backward"
+
+# part 2) move the breakpoint forward
+clean_restart ${testfile}
+gdb_test_no_output "set breakpoint source-tracking enabled on" \
+    "enable source tracking breakpoints for part 2"
+gdb_breakpoint ${build_srcfile}:$lineno
+
+# Sleep to ensure timestamp changes when we rebuild
+sleep 1
+remote_exec build "cp ${srcdir}/${subdir}/${srcfile3} $new_source_file"
+if {[build_executable "failed to prepare" $testfile $new_source_file] == -1} {
+    return
+}
+
+set lineno [expr {$lineno + 2}]
+gdb_test "r" "Breakpoint 1,.*$build_srcfile:$lineno\r\n$lineno\t.*" \
+    "run for the second time stops at adjusted breakpoint location"
+gdb_test "info breakpoints"\
+    "breakpoint.*keep.*y.*$hex.*$build_srcfile:$lineno.*already hit 1 time"\
+    "info breakpoints show the breakpoint was adjusted forward"
+
+# part 3) the breakpoint line disappears
+clean_restart ${testfile}
+gdb_test_no_output "set breakpoint source-tracking enabled on" \
+    "enable source tracking breakpoints for part 3"
+set lineno [gdb_get_line_number "var += 10;" $new_source_file]
+gdb_breakpoint ${build_srcfile}:$lineno
+
+# Sleep to ensure timestamp changes when we rebuild
+sleep 1
+remote_exec build "cp ${srcdir}/${subdir}/${srcfile4} $new_source_file"
+if {[build_executable "failed to prepare" $testfile $new_source_file] == -1} {
+    return
+}
+
+# When the original line is removed and cannot be found in the search window,
+# the breakpoint stays at the symbol-resolved location. Line 10 becomes blank
+# in tmp3, so GDB resolves it to line 11 (return var;) or stays at line 10.
+# We test that it doesn't move beyond the reasonable range.
+set lineno_re "(?:$lineno|[expr {$lineno + 1}])"
+gdb_test "r" "Breakpoint 1,.*$build_srcfile:$lineno_re\r\n$lineno_re\t.*" \
+    "run for the third time stops near original location"
+gdb_test "info breakpoints"\
+    "breakpoint.*keep.*y.*$hex.*$build_srcfile:$lineno_re.*already hit 1 time"\
+    "the breakpoint stays near original location when line disappears"
+
+# part 4) source tracking disabled - breakpoint should not adjust
+clean_restart ${testfile}
+# Don't enable source tracking - test that breakpoints don't adjust without it
+set lineno [gdb_get_line_number "return var;" $new_source_file]
+gdb_breakpoint ${build_srcfile}:$lineno
+
+# Sleep to ensure timestamp changes when we rebuild
+sleep 1
+remote_exec build "cp ${srcdir}/${subdir}/${srcfile2} $new_source_file"
+if {[build_executable "failed to prepare" $testfile $new_source_file] == -1} {
+    return
+}
+
+gdb_test "r" "Breakpoint 1,.*$build_srcfile:$lineno\r\n$lineno\t.*" \
+    "run for the fourth time stops at unadjusted location"
+gdb_test "info breakpoints"\
+    "breakpoint.*keep.*y.*$hex.*$build_srcfile:$lineno.*already hit 1 time"\
+    "breakpoint not adjusted when tracking disabled"
diff --git a/gdb/testsuite/gdb.base/source-tracking-inline-1.c b/gdb/testsuite/gdb.base/source-tracking-inline-1.c
new file mode 100644
index 00000000000..cbc1ee993de
--- /dev/null
+++ b/gdb/testsuite/gdb.base/source-tracking-inline-1.c
@@ -0,0 +1,50 @@ 
+/* 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/>.  */
+
+volatile int global_var = 0;
+
+static void __attribute__ ((__always_inline__))
+inline_func (void)
+{
+  ++global_var;		/* Breakpoint here.  */
+}
+
+int __attribute__ ((noinline, noclone))
+foo (int x)
+{
+  inline_func ();
+  return x + global_var;
+}
+
+int __attribute__ ((noinline, noclone))
+bar (int x)
+{
+  inline_func ();
+  return x - global_var;
+}
+
+int
+main (void)
+{
+  ++global_var;
+
+  int ans = foo (42) + bar (10);
+
+  ++global_var;
+
+  return ans - global_var;
+}
diff --git a/gdb/testsuite/gdb.base/source-tracking-inline-2.c b/gdb/testsuite/gdb.base/source-tracking-inline-2.c
new file mode 100644
index 00000000000..724bb78845a
--- /dev/null
+++ b/gdb/testsuite/gdb.base/source-tracking-inline-2.c
@@ -0,0 +1,49 @@ 
+/* 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/>.  */
+volatile int global_var = 0;
+
+static void __attribute__ ((__always_inline__))
+inline_func (void)
+{
+  ++global_var;		/* Breakpoint here.  */
+}
+
+int __attribute__ ((noinline, noclone))
+foo (int x)
+{
+  inline_func ();
+  return x + global_var;
+}
+
+int __attribute__ ((noinline, noclone))
+bar (int x)
+{
+  inline_func ();
+  return x - global_var;
+}
+
+int
+main (void)
+{
+  ++global_var;
+
+  int ans = foo (42) + bar (10);
+
+  ++global_var;
+
+  return ans - global_var;
+}
diff --git a/gdb/testsuite/gdb.base/source-tracking-inline.c b/gdb/testsuite/gdb.base/source-tracking-inline.c
new file mode 100644
index 00000000000..cbc1ee993de
--- /dev/null
+++ b/gdb/testsuite/gdb.base/source-tracking-inline.c
@@ -0,0 +1,50 @@ 
+/* 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/>.  */
+
+volatile int global_var = 0;
+
+static void __attribute__ ((__always_inline__))
+inline_func (void)
+{
+  ++global_var;		/* Breakpoint here.  */
+}
+
+int __attribute__ ((noinline, noclone))
+foo (int x)
+{
+  inline_func ();
+  return x + global_var;
+}
+
+int __attribute__ ((noinline, noclone))
+bar (int x)
+{
+  inline_func ();
+  return x - global_var;
+}
+
+int
+main (void)
+{
+  ++global_var;
+
+  int ans = foo (42) + bar (10);
+
+  ++global_var;
+
+  return ans - global_var;
+}
diff --git a/gdb/testsuite/gdb.base/source-tracking-inline.exp b/gdb/testsuite/gdb.base/source-tracking-inline.exp
new file mode 100644
index 00000000000..6c1b8022d29
--- /dev/null
+++ b/gdb/testsuite/gdb.base/source-tracking-inline.exp
@@ -0,0 +1,77 @@ 
+# 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/>.
+
+standard_testfile .c -1.c -2.c
+
+set build_srcfile [standard_output_file $srcfile]
+
+remote_exec build "cp $srcdir/$subdir/$srcfile2 $build_srcfile"
+if { [prepare_for_testing "failed to prepare" $testfile $build_srcfile {debug nowarnings}] } {
+    return
+}
+
+if {![runto_main]} {
+    return
+}
+
+gdb_test_no_output "set breakpoint source-tracking enabled on" \
+    "enable source tracking breakpoints"
+
+set lineno [gdb_get_line_number "Breakpoint here."]
+
+# The breakpoint is on an inline function called from two places,
+# so it resolves to 2 locations.
+gdb_test "break $srcfile:$lineno" \
+    "^Breakpoint $decimal at $hex: $srcfile:$lineno\\. \\(2 locations\\)"
+
+# 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".
+set test "multi-location breakpoint not tracked"
+gdb_test_multiple "info breakpoints" $test {
+    -re "source-tracking enabled.*$gdb_prompt $" {
+	fail "$test (tracking incorrectly enabled)"
+    }
+    -re "<MULTIPLE>.*$gdb_prompt $" {
+	pass $test
+    }
+}
+
+sleep 1
+remote_exec build "cp $srcdir/$subdir/$srcfile3 $build_srcfile"
+if { [build_executable "failed to build" $testfile $build_srcfile {debug nowarnings}] } {
+    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"
+
+# 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.
+set test "multi-location breakpoint still not tracked after reload"
+gdb_test_multiple "info breakpoints" $test {
+    -re "source-tracking enabled.*$gdb_prompt $" {
+	fail "$test (tracking incorrectly enabled)"
+    }
+    -re "<MULTIPLE>.*$gdb_prompt $" {
+	pass $test
+    }
+}
+
+gdb_test "continue"