[RFC] gdb/dap: add support for opening core files

Message ID 77d2d959dbadd7112b3dae58679913fabd1d5bbb.1774351223.git.aburgess@redhat.com
State New
Headers
Series [RFC] gdb/dap: add support for opening core files |

Checks

Context Check Description
linaro-tcwg-bot/tcwg_gdb_build--master-aarch64 warning Skipped because it is an RFC
linaro-tcwg-bot/tcwg_gdb_build--master-arm warning Skipped because it is an RFC

Commit Message

Andrew Burgess March 24, 2026, 11:21 a.m. UTC
  This is a rough attempt at adding core file support to GDB's DAP
interface.

I'm pretty new to DAP, so some of what I say here might be wrong.
That's why this is RFC: I'd like to know what I've gotten wrong.  As
such this patch is still a very early draft.  Later in this commit
message I detail all of the things which I know need cleaning up and
adding.  What I'm looking for right now is thoughts on the general
approach taken here, and a review of my 'todo' list below, is there
anything else I need to do?

As I understand it, DAP has two ways to start an inferior, 'launch' or
'attach'.  Of the two, 'attach' seems closer to how we might think
about a core file.  The 'attach' is for attaching to an already
running inferior, which, upon attach, will be stopped at some point.

This is kind-of like a core file.  We "attach" (open) a core file, and
we have a view into an inferior which is part way through running.
The only strange thing is that, for a core file inferior, we cannot
resume the inferior, or edit it's memory or registers.

My plan then is to make opening a core file, a variation of "attach".

To do this I add "coreFile" as a new argument that can be passed to
the "attach" request, this triggers the 'core-file' command to open
the core file.

The todo list:

  1. The send_corefile_stop_event function is needed because we don't
     have any event that triggers when a new core file is loaded.  We
     do already have an observer though (core_file_changed), so I plan
     to make this visible via the Python API, and then in the DAP code
     this will emit a DAP stop event.  In this way, when we open a new
     core file the DAP client will see the inferior stop.

     I did toy with the idea of having GDB emit a stop event when
     opening a core file; an actual 'normal_stop' observable event,
     but this seemed like overkill given we already have the
     core_file_changed observer, and I think that can be made to do
     everything we need.

     This will cleanup the changes in launch.py.

  2. In server.py, we should probably unload the core file in
     _disconnect_or_kill rather than just doing nothing.  The current
     bodge leaves the core file loaded, which will, I think, mean that
     future attempts to reuse this debug session might fail, or act
     weird, as the core file will already be loaded.

  3. In the test I have to override dap_check_log_file because calling
     dap_shutdown checks the log file for assertions.  However, one of
     my tests is that an attempt to write to the inferior when we are
     debugging a core file, will fail cleanly.  The test passes, but
     GDB still logs the memory error exception into the log file,
     which causes dap_shutdown to throw an exception.

     What I'd really like is a way to tag some exceptions in the log
     as "expected" so dap_shutdown will still give a fail if there are
     any unexpected exceptions.

     But if that's too tricky then I'll settle for adding an extra
     flag to dap_shutdown to just say don't check for exceptions.  Or
     maybe an "expected" exception count, that might work.

Prior to starting this work I took a look at what lldb does.  The
documentation is not super clear, but this page seems to indicate that
lldb might also use the 'coreFile' argument to 'attach':

  https://lldb.llvm.org/use/lldbdap.html#configuration-settings-reference

Like I said, it's not very clear, but search for "coreFile" and you'll
see it mentioned, just once, under the "attach" header.  I'd already
decided that supporting core files through attach made the most sense
to me, but I chose 'coreFile' as the name, with that capitalisation,
in order to be compatible with lldb.
---
 gdb/python/lib/gdb/dap/events.py   | 13 +++++
 gdb/python/lib/gdb/dap/launch.py   | 12 +++-
 gdb/python/lib/gdb/dap/server.py   |  2 +-
 gdb/testsuite/gdb.dap/corefile.c   | 45 +++++++++++++++
 gdb/testsuite/gdb.dap/corefile.exp | 92 ++++++++++++++++++++++++++++++
 gdb/testsuite/lib/dap-support.exp  | 16 ++++++
 6 files changed, 178 insertions(+), 2 deletions(-)
 create mode 100644 gdb/testsuite/gdb.dap/corefile.c
 create mode 100644 gdb/testsuite/gdb.dap/corefile.exp


base-commit: 37af6f1ea8184510a479ccf018e19c844fb9a338
  

Comments

Tom Tromey March 24, 2026, 1:51 p.m. UTC | #1
>>>>> "Andrew" == Andrew Burgess <aburgess@redhat.com> writes:

Andrew> This is a rough attempt at adding core file support to GDB's DAP
Andrew> interface.

Thanks.

Andrew> I'm pretty new to DAP, so some of what I say here might be wrong.
Andrew> That's why this is RFC: I'd like to know what I've gotten wrong.  As
Andrew> such this patch is still a very early draft.  Later in this commit
Andrew> message I detail all of the things which I know need cleaning up and
Andrew> adding.  What I'm looking for right now is thoughts on the general
Andrew> approach taken here, and a review of my 'todo' list below, is there
Andrew> anything else I need to do?

Yeah, new launch/attach parameters should be documented in the "Debugger
Adapter Protocol" node of the manual.

Andrew> My plan then is to make opening a core file, a variation of "attach".

Sounds good.

Andrew>   1. The send_corefile_stop_event function is needed because we don't
Andrew>      have any event that triggers when a new core file is loaded.  We
Andrew>      do already have an observer though (core_file_changed), so I plan
Andrew>      to make this visible via the Python API, and then in the DAP code
Andrew>      this will emit a DAP stop event.  In this way, when we open a new
Andrew>      core file the DAP client will see the inferior stop.

Andrew>      I did toy with the idea of having GDB emit a stop event when
Andrew>      opening a core file; an actual 'normal_stop' observable event,
Andrew>      but this seemed like overkill given we already have the
Andrew>      core_file_changed observer, and I think that can be made to do
Andrew>      everything we need.

Agreeing with the second paragraph, emitting a stop event internally
here seems weird, I wouldn't probably do that.  My thinking is that
nothing actually stopped.

Having send_corefile_stop_event handled explicitly doesn't sound so
terrible.  I mean, it's fine if you want to do it by wiring up the
observer, too.  This might be useful to Python script writers.

At the same time in DAP I am not sure there's a way to have it load a
different core file.  At least, not in DAP itself, the DAP client could
probably send an evaluate request with the "core" command to do whatever
behind DAP's back.

It's unclear how much behind-the-back stuff DAP really supports.
There's a handful of upstream protocol bugs in this area but the DAP
maintainers don't really seem to engage with this concept.

Andrew>   2. In server.py, we should probably unload the core file in
Andrew>      _disconnect_or_kill rather than just doing nothing.  The current
Andrew>      bodge leaves the core file loaded, which will, I think, mean that
Andrew>      future attempts to reuse this debug session might fail, or act
Andrew>      weird, as the core file will already be loaded.

Makes sense, though I don't know if re-run/re-attach is really done.
My impression is that DAP clients tend to just start a new adapter.

Andrew>      What I'd really like is a way to tag some exceptions in the log
Andrew>      as "expected" so dap_shutdown will still give a fail if there are
Andrew>      any unexpected exceptions.

Andrew>      But if that's too tricky then I'll settle for adding an extra
Andrew>      flag to dap_shutdown to just say don't check for exceptions.  Or
Andrew>      maybe an "expected" exception count, that might work.

Maybe dap_shutdown could take an optional argument that would match the
text of any expected exception?

Another option would be to wrap the exception in a DAPException.  These
aren't logged by default and represent "expected failures".  This
requires a bit of care because maybe in some cases this would be a true
failure, I suppose it would depend on exactly where the wrapping would
have to be done.

Andrew> -from .events import exec_and_expect_stop, expect_process, expect_stop
Andrew> +from .events import exec_and_expect_stop, expect_process, expect_stop, send_corefile_stop_event

black will probably complain about this and maybe some other code FWIW.

Andrew> @@ -149,11 +150,20 @@ def attach(
Andrew>              cmd = "attach " + str(pid)
Andrew>          elif target is not None:
Andrew>              cmd = "target remote " + target
Andrew> +        elif coreFile is not None:
Andrew> +            cmd = "core-file " + coreFile
Andrew>          else:
Andrew>              raise DAPException("attach requires either 'pid' or 'target'")
Andrew>          expect_process("attach")
Andrew>          expect_stop("attach")
Andrew>          exec_and_log(cmd)
Andrew> +
Andrew> +        # Report a stop event when connecting to a core file.
Andrew> +        # Currently GDB doesn't trigger a stop event when opening a
Andrew> +        # new core file, so we need to emit one now.
Andrew> +        if coreFile is not None:
Andrew> +            send_corefile_stop_event()

Maybe the logic has to be a little different here if the client
erroneously passes both a target and a core file.  Or maybe we should
validate the arguments a bit more here.

thanks,
Tom
  

Patch

diff --git a/gdb/python/lib/gdb/dap/events.py b/gdb/python/lib/gdb/dap/events.py
index 91d75c81610..f6e1e95c238 100644
--- a/gdb/python/lib/gdb/dap/events.py
+++ b/gdb/python/lib/gdb/dap/events.py
@@ -275,6 +275,19 @@  def _on_inferior_call(event):
             _expected_pause = False
             send_event("stopped", obj)
 
+@in_gdb_thread
+def send_corefile_stop_event():
+    global inferior_running
+    inferior_running = False
+
+    obj = {
+        "threadId": gdb.selected_thread().global_num,
+        "allThreadsStopped": True,
+        "reason": "attach",
+    }
+    global _expected_pause
+    _expected_pause = False
+    send_event("stopped", obj)
 
 gdb.events.stop.connect(_on_stop)
 gdb.events.exited.connect(_on_exit)
diff --git a/gdb/python/lib/gdb/dap/launch.py b/gdb/python/lib/gdb/dap/launch.py
index 6fde3396ee9..6b0cb1d5861 100644
--- a/gdb/python/lib/gdb/dap/launch.py
+++ b/gdb/python/lib/gdb/dap/launch.py
@@ -20,7 +20,7 @@  from typing import Mapping, Optional, Sequence
 
 import gdb
 
-from .events import exec_and_expect_stop, expect_process, expect_stop
+from .events import exec_and_expect_stop, expect_process, expect_stop, send_corefile_stop_event
 from .server import (
     DeferredRequest,
     call_function_later,
@@ -136,6 +136,7 @@  def attach(
     pid: Optional[int] = None,
     target: Optional[str] = None,
     adaSourceCharset: Optional[str] = None,
+    coreFile: Optional[str] = None,
     **args,
 ):
     # The actual attach is handled by this function.
@@ -149,11 +150,20 @@  def attach(
             cmd = "attach " + str(pid)
         elif target is not None:
             cmd = "target remote " + target
+        elif coreFile is not None:
+            cmd = "core-file " + coreFile
         else:
             raise DAPException("attach requires either 'pid' or 'target'")
         expect_process("attach")
         expect_stop("attach")
         exec_and_log(cmd)
+
+        # Report a stop event when connecting to a core file.
+        # Currently GDB doesn't trigger a stop event when opening a
+        # new core file, so we need to emit one now.
+        if coreFile is not None:
+            send_corefile_stop_event()
+
         # Attach response does not have a body.
         return None
 
diff --git a/gdb/python/lib/gdb/dap/server.py b/gdb/python/lib/gdb/dap/server.py
index 01c190c797e..e7d08ce662c 100644
--- a/gdb/python/lib/gdb/dap/server.py
+++ b/gdb/python/lib/gdb/dap/server.py
@@ -622,7 +622,7 @@  def terminate(**args):
 @in_gdb_thread
 def _disconnect_or_kill(terminate: Optional[bool]):
     inf = gdb.selected_inferior()
-    if inf.connection is None:
+    if inf.connection is None or inf.corefile is not None:
         # Nothing to do here.
         return
     if terminate is None:
diff --git a/gdb/testsuite/gdb.dap/corefile.c b/gdb/testsuite/gdb.dap/corefile.c
new file mode 100644
index 00000000000..ce1d43b5e51
--- /dev/null
+++ b/gdb/testsuite/gdb.dap/corefile.c
@@ -0,0 +1,45 @@ 
+/* This testcase is part of GDB, the GNU debugger.
+
+   Copyright 2026 Free Software Foundation, Inc.
+
+   This program is free software; you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>.  */
+
+#include <stdlib.h>
+
+int global_var = 0;
+
+void
+baz (void)
+{
+  abort ();
+}
+
+void
+bar (void)
+{
+  baz ();
+}
+
+void
+foo (void)
+{
+  bar ();
+}
+
+int
+main (void)
+{
+  foo ();
+  return 0;
+}
diff --git a/gdb/testsuite/gdb.dap/corefile.exp b/gdb/testsuite/gdb.dap/corefile.exp
new file mode 100644
index 00000000000..83772f7f511
--- /dev/null
+++ b/gdb/testsuite/gdb.dap/corefile.exp
@@ -0,0 +1,92 @@ 
+# Copyright 2023-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 using "attach" in DAP for opening a core file.
+
+require allow_dap_tests
+
+load_lib dap-support.exp
+
+standard_testfile
+
+if {[build_executable ${testfile}.exp $testfile] == -1} {
+    return
+}
+
+set corefile [core_find $binfile {}]
+if {$corefile == ""} {
+    untested "unable to create or find corefile"
+    return
+}
+
+# Test that attaching to a core file works at all.
+set attach_id [dap_corefile $corefile $binfile]
+
+dap_check_request_and_response "configurationDone" configurationDone
+
+dap_check_response "attach response" attach $attach_id
+
+dap_wait_for_event_and_check "stopped" stopped \
+    "body reason" attach
+
+# Try 'continue', this should fail.
+set obj [dap_request_and_response continue \
+	     {o threadId [i 1]}]
+set response [lindex $obj 0]
+gdb_assert { [dict get $response success] == "false" } \
+    "continue with core file target"
+
+# Get a backtrace from the core file.
+set bt [lindex [dap_check_request_and_response "backtrace" stackTrace \
+		    {o threadId [i 1]}] 0]
+set frame_id [dict get [lindex [dict get $bt body stackFrames] 0] id]
+
+# Get all scopes for frame 0.  Search through scopes to find the
+# register scope.
+set scopes [dap_check_request_and_response "get scopes" scopes \
+		[format {o frameId [i %d]} $frame_id]]
+set scopes [dict get [lindex $scopes 0] body scopes]
+set reg_scope ""
+foreach s $scopes {
+    if {[dict get $s name] == "Registers"} {
+	set reg_scope $s
+    }
+}
+gdb_assert { $reg_scope ne "" } "found register scope"
+
+# Read all the registers from the register scope.
+set num [dict get $reg_scope variablesReference]
+lassign [dap_check_request_and_response "fetch all registers" \
+	     "variables" \
+	     [format {o variablesReference [i %d] count [i %d]} $num\
+		  [dict get $reg_scope namedVariables]]] \
+    val events
+
+set obj [dap_request_and_response setExpression \
+	     {o expression [s global_var] value [s 23]}]
+set response [lindex $obj 0]
+gdb_assert { [dict get $response success] == "false" } \
+    "set global variable fails"
+
+# The call to dap_shutdown will check the log file for exceptions.
+# The problem is that the attempt to write to a global variable above
+# will trigger an exception, which is recorded in the log file, which
+# then causes the log file check to fail.
+#
+# We really need a better way to handle this.
+proc empty_dap_check_log_file {} {}
+with_override dap_check_log_file empty_dap_check_log_file {
+    dap_shutdown true
+}
diff --git a/gdb/testsuite/lib/dap-support.exp b/gdb/testsuite/lib/dap-support.exp
index 54d9178648f..926d5891b0b 100644
--- a/gdb/testsuite/lib/dap-support.exp
+++ b/gdb/testsuite/lib/dap-support.exp
@@ -369,6 +369,22 @@  proc dap_attach {pid {prog ""}} {
     return [dap_send_request attach $args]
 }
 
+# Start gdb, send a DAP initialize request, and then an attach request
+# specifying COREFILE as the core file to attach too.  Returns the
+# empty string on failure, or the attach request sequence ID.
+proc dap_corefile {corefile {prog ""}} {
+    if {[dap_initialize "startup - initialize"] == ""} {
+	return ""
+    }
+
+    set args [format {o coreFile [s %s]} $corefile]
+    if {$prog != ""} {
+	append args [format { program [s %s]} $prog]
+    }
+
+    return [dap_send_request attach $args]
+}
+
 # Start gdb, send a DAP initialize request, and then an attach request
 # specifying TARGET as the remote target.  Returns the empty string on
 # failure, or the attach request sequence ID.