[v5] gdb: Add source-tracking breakpoints feature

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

Commit Message

Alexandra Hájková May 5, 2026, 8:29 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.

If source tracking is disabled after breakpoints have been tracked,
all existing source tracking information is discarded and a message
is printed.

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.

Reviewed-By: Eli Zaretskii <eliz@gnu.org>
---
 gdb/NEWS                                      |  14 +
 gdb/breakpoint.c                              | 472 ++++++++++++++++++
 gdb/breakpoint.h                              |  38 ++
 gdb/doc/gdb.texinfo                           |  45 ++
 .../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  | 143 ++++++
 .../gdb.base/source-tracking-inline-1.c       |  50 ++
 .../gdb.base/source-tracking-inline-2.c       |  49 ++
 .../gdb.base/source-tracking-inline.exp       |  80 +++
 13 files changed, 1103 insertions(+)
 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.exp
  

Comments

Kevin Buettner May 10, 2026, 6:52 a.m. UTC | #1
Hi Sasha,

On Tue,  5 May 2026 10:29:03 +0200
Alexandra Hájková <ahajkova@redhat.com> wrote:

> The breakpoint_source structure stores captured source code lines

Instead of jumping right into describing the implementation, I'd
like to see a high level description of the problem that this new
feature is intended to solve.

> around a breakpoint location, along with a reference to the BFD
> that was current when the source was captured.

[...]

> +/* 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)
> +{
> +  /* The index into BP_SOURCE's lines where the breakpoint was placed.
> */
> +  int bp_stored = bp_source->bp_line_stored;
> +  size_t bp_size = bp_source->source_lines.size ();
> +
> +  /* An empty string, used if the breakpoint line is at the start or end
> of
> +     the context window.  */
> +  static std::string empty_string ("");
> +
> +  /* The lines immediately before and after the breakpoint in BP_SOURCE.
> +     If the breakpoint is the first or last line in BP_SOURCE then use
> +     EMPTY_STRING as a stand in.  */
> +  const std::string &bp_prev =
> +    (bp_stored == 0
> +     ? empty_string : bp_source->source_lines[bp_stored - 1]);
> +  const std::string &bp_next =
> +    ((bp_stored + 1) == bp_size

This is a comparison between a signed and unsigned value.  Maybe change
the type of bp_stored to size_t?  (That assignment above will likely
require a cast.)

[...]

> +  /* The updated breakpoint location has not bee found in TMP_SOURCE.  */

Typo: s/bee/been/

> +  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 ();

Instead of using rfind and this rather circuitous approach to making a
new locspec, why not clone the existing locspec and then update its
offset?  (Or you might be able to change it directly, but I'm not
entirely sure that's safe.)

Also, if you do end up keeping that code above, you should probably
use the language specified by the existing breakpoint instead of
the global 'current_language'.

> +
> +  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);
> +    }

What happens here when !found is true?  I expected to see some code
here to handle this case.  If it turns out that !found can't happen,
perhaps use an assert to indicate this.  If it's safe for !found to
fall through (which I don't think it is - expanded would be empty and
update_breakpoint_locations would silently remove all breakpoint
locations), then add a comment here indicating that fact.

> +
> +  bp_source = std::make_unique<breakpoint_source>
> +    (breakpoint_source_capture (expanded, BREAKPOINT_SRC_CTX_LINES));
> +}
> +
[...]

Kevin
  

Patch

diff --git a/gdb/NEWS b/gdb/NEWS
index 480e1854002..0754e38fa9f 100644
--- a/gdb/NEWS
+++ b/gdb/NEWS
@@ -3,6 +3,13 @@ 
 
 *** Changes since GDB 17
 
+* 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.
 
@@ -93,6 +100,13 @@  unset local-environment
   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 101dc57ee6b..017412d53a4 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
@@ -165,6 +166,19 @@  static bool bl_address_is_meaningful (const bp_location *loc);
 
 static int find_loc_num_by_location (const bp_location *loc);
 
+static bool breakpoint_source_is_tracked (const breakpoint_source *src);
+
+/* 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
+
 /* update_global_location_list's modes of operation wrt to whether to
    insert locations now.  */
 enum ugll_insert_mode
@@ -582,6 +596,60 @@  show_always_inserted_mode (struct ui_file *file, int from_tty,
 	      value);
 }
 
+/* When true file and line breakpoints are created as source-tracking
+   breakpoints.  */
+
+static bool source_tracking_breakpoints = false;
+
+/* Implement 'show breakpoint source-tracking enabled'.  */
+
+static void
+show_source_tracking_breakpoints (struct ui_file *file, int from_tty,
+				  struct cmd_list_element *c,
+				  const char *value)
+{
+  gdb_printf (file, _("Source tracking breakpoints is %s.\n"), value);
+}
+
+/* Get the value of 'breakpoint source-tracking enabled' setting.  */
+
+static bool
+get_breakpoint_source_tracking_enabled ()
+{
+  return source_tracking_breakpoints;
+}
+
+/* Set the value of 'breakpoint source-tracking enabled' setting.  When
+   this setting is changed to 'off' then any existing source-tracked
+   breakpoints have their source tracking information removed.  */
+
+static void
+set_breakpoint_source_tracking_enabled (bool value)
+{
+  /* Store the new value.  */
+  source_tracking_breakpoints = value;
+
+  /* When turning source tracking breakpoints on we don't start tracking
+     existing breakpoints, so we're done.  */
+  if (source_tracking_breakpoints)
+    return;
+
+  /* Source tracking has been turned off.  Discard any existing source
+     tracking information.  Print a message if some information was
+     actually discarded.  */
+  bool any_discarded = false;
+  for (struct breakpoint &b : all_breakpoints ())
+    {
+      if (breakpoint_source_is_tracked (b.bp_source.get ()))
+	{
+	  any_discarded = true;
+	  b.bp_source.reset ();
+	}
+    }
+  if (any_discarded)
+    gdb_printf ("Discarding existing source tracking information.\n");
+}
+
 /* See breakpoint.h.  */
 bool debug_breakpoint = false;
 
@@ -593,6 +661,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 +1079,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.  */
@@ -6834,6 +7077,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)
@@ -8943,7 +9196,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
@@ -13200,6 +13481,162 @@  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)
+{
+  /* The index into BP_SOURCE's lines where the breakpoint was placed.  */
+  int bp_stored = bp_source->bp_line_stored;
+  size_t bp_size = bp_source->source_lines.size ();
+
+  /* An empty string, used if the breakpoint line is at the start or end of
+     the context window.  */
+  static std::string empty_string ("");
+
+  /* The lines immediately before and after the breakpoint in BP_SOURCE.
+     If the breakpoint is the first or last line in BP_SOURCE then use
+     EMPTY_STRING as a stand in.  */
+  const std::string &bp_prev =
+    (bp_stored == 0
+     ? empty_string : bp_source->source_lines[bp_stored - 1]);
+  const std::string &bp_next =
+    ((bp_stored + 1) == bp_size
+     ? empty_string : bp_source->source_lines[bp_stored + 1]);
+
+  /* Now search TMP_SOURCE for the breakpoint line.  */
+  size_t tmp_size = tmp_source->source_lines.size ();
+  for (size_t i = 0; i < tmp_size; i++)
+    {
+      if (bp_source->source_lines[bp_stored] == tmp_source->source_lines[i])
+	{
+	  /* Found the breakpoint line.  Capture the lines before and after
+	     the breakpoint line from TMP_SOURCE.  As above, if the
+	     breakpoint is at the start or end of TMP_SOURCE then use
+	     EMPTY_STRING as a stand in.  */
+	  const std::string &tmp_prev =
+	    (i == 0 ? empty_string : tmp_source->source_lines[i - 1]);
+	  const std::string &tmp_next =
+	    ((i + 1) == tmp_size
+	     ? empty_string : tmp_source->source_lines[i + 1]);
+
+	  /* If the previous and next lines match then this is the new
+	     location of the breakpoint, calculate and return the updated
+	     line number.  */
+	  if (bp_prev == tmp_prev && bp_next == tmp_next)
+	    return tmp_source->bp_line + i - tmp_source->bp_line_stored;
+	}
+    }
+
+  /* The updated breakpoint location has not bee found in TMP_SOURCE.  */
+  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.  */
@@ -13230,12 +13667,17 @@  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);
 	  if (found)
 	    expanded_end = std::move (sals_end);
 	}
+
+      adjust_bp_for_source_tracking (filter_pspace, expanded);
     }
 
   /* Update the locations for this breakpoint.  For thread-specific
@@ -15024,6 +15466,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,
@@ -15342,6 +15793,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, _("\
+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."),
+			   set_breakpoint_source_tracking_enabled,
+			   get_breakpoint_source_tracking_enabled,
+			   show_source_tracking_breakpoints,
+			   &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..edab3704f98 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;
@@ -615,6 +617,30 @@  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
    does set it to 0).  I implemented it because I thought it would be
@@ -863,6 +889,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 +1014,13 @@  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 ab0216ff477..f6c16473433 100644
--- a/gdb/doc/gdb.texinfo
+++ b/gdb/doc/gdb.texinfo
@@ -4659,6 +4659,28 @@  program.
 On some systems, you can set breakpoints in shared libraries before
 the executable is run.
 
+@cindex source-tracking breakpoints
+@cindex breakpoints, automatic adjustment when source changes
+@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 a small
+window of surrounding source lines: the line immediately before the
+breakpoint, the breakpoint line itself, and the line immediately after
+it.  If the source file is modified and the executable is rebuilt,
+@value{GDBN} searches a window of approximately 12 lines centered on the
+breakpoint's original position for a match.  A candidate line is
+accepted when it matches the captured breakpoint line and at least one
+of its immediate neighbors also matches, reducing false positives from
+short or repeated lines.  @value{GDBN} uses the first such confirmed
+match found, scanning from the top of the search window.  If the same
+code sequence appears more than once within the search window, the
+earliest occurrence is chosen; code outside the search window is not
+considered.  If no match is found, @value{GDBN} issues a warning and
+keeps the breakpoint at its original location, disabling source tracking
+for that breakpoint.  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
@@ -42017,6 +42039,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{Breakpoints}), 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.
@@ -42885,6 +42914,22 @@  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 a window of source lines around each
+new file:line breakpoint and uses it to relocate the breakpoint if the
+source is modified and the executable is rebuilt.  @xref{Breakpoints},
+for a full description of the matching algorithm and its limitations.
+The default is @code{off}.  Breakpoints set by function name or address
+are not affected by this setting.
+
+If this setting is changed from @code{on} to @code{off}, then any
+existing source tracking information will be discarded.
+
 @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..24daadae1fe
--- /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 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..5490ed40f78
--- /dev/null
+++ b/gdb/testsuite/gdb.base/adjust_breakpoint-missing-source.exp
@@ -0,0 +1,55 @@ 
+# 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/>.
+
+# 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
+}
+
+# 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, this 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,
+# it should not show the "source-tracking enabled" message.
+gdb_test_multiple "info breakpoints" \
+    "breakpoint not tracked when source missing" {
+	-re -wrap "source-tracking enabled.*" {
+	    fail $gdb_test_name
+	}
+	-re -wrap "breakpoint.*${build_srcfile}:${bp_line}.*" {
+	    pass $gdb_test_name
+	}
+    }
diff --git a/gdb/testsuite/gdb.base/adjust_breakpoint.cpp b/gdb/testsuite/gdb.base/adjust_breakpoint.cpp
new file mode 100644
index 00000000000..4d42adb3011
--- /dev/null
+++ b/gdb/testsuite/gdb.base/adjust_breakpoint.cpp
@@ -0,0 +1,40 @@ 
+/* 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.exp b/gdb/testsuite/gdb.base/adjust_breakpoint.exp
new file mode 100644
index 00000000000..5c8ddb8b64e
--- /dev/null
+++ b/gdb/testsuite/gdb.base/adjust_breakpoint.exp
@@ -0,0 +1,143 @@ 
+# 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/>.
+
+# 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"
+
+# If a breakpoint is being source tracked, then turning source
+# tracking off discards the tracking information and prints a message.
+# If no breakpoints are being source tracked then disabling source
+# tracking should be silent.
+with_test_prefix "check disabling is silent" {
+    gdb_test_no_output "set breakpoint source-tracking enabled off" \
+	"disable"
+
+    gdb_test_no_output "set breakpoint source-tracking enabled on" \
+	"enable"
+}
+
+# Test that the breakpoint can be adjusted 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 "run" "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"
+
+# Test that the breakpoint can be adjusted 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 "run" "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"
+
+# Disable source tracking breakpoints, the existing tracking
+# information is discarded.
+gdb_test "set breakpoint source-tracking enabled off" \
+    "^Discarding existing source tracking information\\." \
+    "disable source tracking, existing tracking is discarded"
+
+gdb_test "info breakpoints" \
+    [multi_line \
+	 "1\\s+breakpoint\\s+keep\\s+y\[^\r\n\]+" \
+	 "\\s+breakpoint already hit \[^\r\n\]+"] \
+    "info breakpoints breakpoint no longer tracked"
+
+# Test what happends when 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 "run" "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"
+
+# Test that with source tracking disabled the breakpoint should not be
+# adjusted.
+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 "run" "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.exp b/gdb/testsuite/gdb.base/source-tracking-inline.exp
new file mode 100644
index 00000000000..ab9d2276b4e
--- /dev/null
+++ b/gdb/testsuite/gdb.base/source-tracking-inline.exp
@@ -0,0 +1,80 @@ 
+# 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/>.
+
+# 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.
+
+standard_testfile -1.c -2.c
+
+set build_srcfile [standard_output_file ${testfile}.c]
+
+remote_exec build "cp $srcdir/$subdir/$srcfile $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." $build_srcfile]
+
+# 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\\)"
+
+# 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/$srcfile2 $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
+    }
+}