[RFC] gdb/dap: add support for opening core files
Checks
Commit Message
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
>>>>> "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
@@ -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)
@@ -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
@@ -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:
new file mode 100644
@@ -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;
+}
new file mode 100644
@@ -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
+}
@@ -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.