]> git.ipfire.org Git - thirdparty/binutils-gdb.git/commitdiff
gdb/dap: add support for opening core files
authorAndrew Burgess <aburgess@redhat.com>
Mon, 23 Mar 2026 22:25:17 +0000 (22:25 +0000)
committerAndrew Burgess <aburgess@redhat.com>
Thu, 23 Apr 2026 08:58:02 +0000 (09:58 +0100)
This patch adds core file support to GDB's DAP interface.

Core files are supported as a GDB specific argument to 'attach', the
new argument is 'coreFile', the name of the core file to debug.

I think handling core files via attach makes the most sense; attach is
for connecting to existing processes, but these targets are (usually)
stopped as soon as GDB attaches, and that's what a core file looks
like, a target that was running, but is now stopped.  It just happens
that core file targets are special in that the target cannot be
resumed again, nor can the user modify the program state (e.g. write
to memory or registers).

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.  In order to
be compatible with lldb I used the same argument name with the same
capitalisation.

The new argument is added to the documentation and mentioned in NEWS.

I had to make some changes to testsuite/lib/dap-support.exp to support
this new feature.  There's a new dap_corefile proc to handle setting
up the initial connection.  This seemed cleaner that overloading
dap_attach, even though under the hood it is still an 'attach' request
that gets sent.

The new test tries to write to memory and registers with the core file
target in place, neither of these requests succeed, which is what we
want, but the exceptions are logged into the dap log file.  The
dap_shutdown proc calls dap_check_log_file to check the log for
exceptions, and these two exceptions are spotted and trigger a FAIL.
To avoid this I've added a new "expected_exception_count" argument
for dap_shutdown.  Now we check that we see the expected number of
exceptions.  We don't check for the specific exception types right
now, but as the test is already checking that the expected requests
fail, I think we're OK.

Approved-By: Tom Tromey <tom@tromey.com>
gdb/NEWS
gdb/doc/gdb.texinfo
gdb/python/lib/gdb/dap/events.py
gdb/python/lib/gdb/dap/launch.py
gdb/python/lib/gdb/dap/server.py
gdb/testsuite/gdb.dap/corefile.c [new file with mode: 0644]
gdb/testsuite/gdb.dap/corefile.exp [new file with mode: 0644]
gdb/testsuite/lib/dap-support.exp

index 6b5c2f6a46a374de230203f319e699ea4cb46918..18f1b6308da74bac076178765c559a188e3c1a99 100644 (file)
--- a/gdb/NEWS
+++ b/gdb/NEWS
@@ -155,6 +155,8 @@ maint print psymbols
   ** The launch and attach requests now accept the adaSourceCharset
      parameter.
 
+  ** The attach request now accepts the coreFile parameter.
+
   ** Constants are now returned in scopes.
 
 * Changed remote packets
index 1fc052bc1e651d82c5b8d4e04d8177db813d3ee1..82306072e8c5e3c1341c39ce0463aa6f464d6a05 100644 (file)
@@ -40371,9 +40371,9 @@ the same approach as the @code{starti} command.  @xref{Starting}.
 @end table
 
 @value{GDBN} defines some additional parameters that can be passed to
-the @code{attach} request.  Either @code{pid} or @code{target} must be
-specified, but if both are specified then @code{target} will be
-ignored.
+the @code{attach} request.  One of @code{pid}, @code{target}, or
+@code{coreFile} must be specified.  If multiple are specified, they
+are checked for in that order, and the first one found is used.
 
 @table @code
 @item pid
@@ -40382,6 +40382,10 @@ The process ID to which @value{GDBN} should attach.  @xref{Attach}.
 @item target
 The target to which @value{GDBN} should connect.  This is a string and
 is passed to the @code{target remote} command.  @xref{Connecting}.
+
+@item coreFile
+A string that specifies a core file to use.  This corresponds to the
+@kbd{core-file} command.  @xref{core-file command}.
 @end table
 
 In response to the @code{disassemble} request, DAP allows the client
index 91d75c81610019fa494b50360e678741880e58d1..27e3b5ab8bc7f8898b37918562ec62ac7002f188 100644 (file)
@@ -276,6 +276,31 @@ def _on_inferior_call(event):
             send_event("stopped", obj)
 
 
+@in_gdb_thread
+def _on_corefile_changed(event):
+    # Ignore events relating to corefile being unloaded.
+    if event.inferior.corefile is None:
+        return
+
+    # Corefiles are usually attached via the 'attach' request, which
+    # sets the global _expected_stop_reason to 'attach'.  It is
+    # because of this that it is safe to forward to _on_stop, as when
+    # _expected_stop_reason is set _on_stop doesn't read the
+    # event.details, which EVENT doesn't have.
+    #
+    # However, if the user loads a core file via some mechanism other
+    # than the 'attach' request, e.g. they use the repl to issue a GDB
+    # 'core-file' command, then when we get here _expected_stop_reason
+    # will not be set.
+    #
+    # So, in either case, set _expected_stop_reason now.
+    global _expected_stop_reason
+    _expected_stop_reason = "attach"
+
+    # A corefile was loaded, announce that the inferior has stopped.
+    _on_stop(event)
+
+
 gdb.events.stop.connect(_on_stop)
 gdb.events.exited.connect(_on_exit)
 gdb.events.new_thread.connect(_new_thread)
@@ -284,3 +309,4 @@ gdb.events.cont.connect(_cont)
 gdb.events.new_objfile.connect(_new_objfile)
 gdb.events.free_objfile.connect(_objfile_removed)
 gdb.events.inferior_call.connect(_on_inferior_call)
+gdb.events.corefile_changed.connect(_on_corefile_changed)
index 6fde3396ee994b28d09a61b2c59a6713022e2ac0..1ad5c693f8cba365c0aada62684b5f2635e86ad6 100644 (file)
@@ -59,12 +59,16 @@ class _LaunchOrAttachDeferredRequest(DeferredRequest):
         super().reschedule()
 
 
+# Handle whitespace, quotes, and backslashes here.  Exactly what
+# to quote depends on libiberty's buildargv and safe-ctype.
+def escape_filename(filename):
+    return re.sub("[ \t\n\r\f\v\\\\'\"]", "\\\\\\g<0>", filename)
+
+
 # A wrapper for the 'file' command that correctly quotes its argument.
 @in_gdb_thread
 def file_command(program):
-    # Handle whitespace, quotes, and backslashes here.  Exactly what
-    # to quote depends on libiberty's buildargv and safe-ctype.
-    program = re.sub("[ \t\n\r\f\v\\\\'\"]", "\\\\\\g<0>", program)
+    program = escape_filename(program)
     exec_and_log("file " + program)
 
 
@@ -136,6 +140,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 +154,14 @@ def attach(
             cmd = "attach " + str(pid)
         elif target is not None:
             cmd = "target remote " + target
+        elif coreFile is not None:
+            cmd = "core-file " + escape_filename(coreFile)
         else:
-            raise DAPException("attach requires either 'pid' or 'target'")
+            raise DAPException("attach requires either 'pid', 'target', or 'coreFile'")
         expect_process("attach")
         expect_stop("attach")
         exec_and_log(cmd)
+
         # Attach response does not have a body.
         return None
 
index 01c190c797e41a17eb7887181a04f5da02db6625..d94036e03e5d89d4918b9c891e40176a8ca15d23 100644 (file)
@@ -629,7 +629,10 @@ def _disconnect_or_kill(terminate: Optional[bool]):
         # The default depends on whether the inferior was attached or
         # launched.
         terminate = not inf.was_attached
-    if terminate:
+
+    if inf.corefile is not None:
+        exec_and_log("core-file")
+    elif terminate:
         exec_and_log("kill")
     elif inf.was_attached:
         exec_and_log("detach")
diff --git a/gdb/testsuite/gdb.dap/corefile.c b/gdb/testsuite/gdb.dap/corefile.c
new file mode 100644 (file)
index 0000000..ce1d43b
--- /dev/null
@@ -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 (file)
index 0000000..088cc82
--- /dev/null
@@ -0,0 +1,199 @@
+# 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 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
+}
+
+set other_corefile [standard_output_file "${testfile} with spaces.core"]
+remote_exec build "cp \"$corefile\" \"$other_corefile\""
+
+# 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]
+set reply [lindex [dap_check_request_and_response "fetch all registers" \
+                     "variables" \
+                     [format {o variablesReference [i %d] count [i %d]} $num\
+                          [dict get $reg_scope namedVariables]]] 0]
+
+# Find the name and value of a register, we'll use this to try setting
+# a register below.
+set regs [dict get $reply body variables]
+gdb_assert {[llength $regs] > 0} "got at least one register"
+set reg_name [dict get [lindex $regs 0] name]
+set reg_value [dict get [lindex $regs 0] value]
+
+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"
+set expected_exception_count 1
+
+# Try setting a register, this should fail as registers are not
+# writable for a core file target.  We need to write back a different
+# register value, so we add one to the current value.  This means we
+# can only run the test if the current register value is an integer.
+if {[string is integer -strict $reg_value]} {
+    set new_value [expr {$reg_value + 1}]
+    set obj [dap_request_and_response setExpression \
+                {o expression [s \$$reg_name] value [s $new_value]}]
+    set response [lindex $obj 0]
+    gdb_assert { [dict get $response success] == "false" } \
+       "set register fails"
+    incr expected_exception_count
+}
+
+dap_shutdown false $expected_exception_count
+
+# Reconnect to the core file.  This time when we shutdown we will
+# request that the target be terminated, GDB should still just
+# disconnect though as core file targets cannot be killed.
+with_test_prefix "reattach" {
+    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
+
+    # Request the target be terminated.  This doesn't make sense for
+    # core file targets (which cannot be killed), but GDB should
+    # handle this gracefully and just disconnect.
+    dap_shutdown true
+}
+
+# Test loading a core file with spaces in its name.
+with_test_prefix "core file with spaces" {
+    gdb_exit
+
+    # Test that attaching to a core file works at all.
+    set attach_id [dap_corefile $other_corefile]
+
+    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
+
+    # Use the repl to issue an 'info inferiors' command.
+    set obj [dap_check_request_and_response "command repl" \
+                evaluate {o expression [s "info inferiors"] context [s repl]}]
+    set response [lindex $obj 0]
+    set result [dict get $response body result]
+    set result [string map {\\n \n \\t \t} $result]
+    verbose -log "Info Inferiors Output:\n$result\n\n"
+
+    # Check that the output contains the header line, the executable
+    # name, and the core file name.  We didn't pass the executable
+    # name when loading the core file, but GDB should have been able
+    # to find the executable from the core file.
+    gdb_assert { [regexp "Num\\s+Description\\s+Connection\\s+Executable" $result] } \
+       "info inferiors column headers found"
+
+    gdb_assert { [regexp "[string_to_regexp $binfile]\\s*\n" $result] } \
+       "executable name was set during core file load"
+
+    gdb_assert { [regexp "core file [string_to_regexp $other_corefile]\\s*$" $result] } \
+       "core file name displayed in info inferiors output"
+
+    dap_shutdown
+}
+
+# Test loading a core file via the repl.
+with_test_prefix "load core file via repl" {
+    gdb_exit
+
+    if {[dap_initialize] == ""} {
+       return
+    }
+
+    # Use the repl to issue a 'core-file' command.
+    set obj [dap_check_request_and_response "command repl" \
+                evaluate [format {o expression [s "core-file %s"] context [s repl]} $corefile]]
+    set response [lindex $obj 0]
+    set result [dict get $response body result]
+    set result [string map {\\n \n \\t \t} $result]
+    verbose -log "Core-File Command Output:\n$result\n\n"
+
+    # By the time we got the response from the 'core-file' command,
+    # the stopped event, sent when we attach to a core file, should
+    # already have been seen, check for it now.
+    set ok false
+    foreach d [lindex $obj 1] {
+       if {[dict get $d type] != "event"
+           || [dict get $d event] != "stopped"} {
+           continue
+       }
+       if {[dict get $d body reason] == "attach"} {
+           set ok true
+           break
+       }
+    }
+    gdb_assert { $ok } "saw stopped event"
+
+    dap_shutdown
+}
index 54d9178648facb0d5512551f58612b835d6fd5d7..1d9d0d1f40a349cf99a88f7ead80458eb4b1945e 100644 (file)
@@ -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 to.  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.
@@ -379,26 +395,33 @@ proc dap_target_remote {target} {
     return [dap_send_request attach [format {o target [s %s]} $target]]
 }
 
-# Read the most recent DAP log file and check it for exceptions.
-proc dap_check_log_file {} {
+# Read the most recent DAP log file and check it for exceptions.  We
+# expect to see exactly EXPECTED_EXCEPTION_COUNT exceptions in the log.
+proc dap_check_log_file { {expected_exception_count 0} } {
     set fd [open [current_dap_log_file]]
     set contents [read $fd]
     close $fd
 
-    set ok 1
+    set exception_count 0
     foreach line [split $contents "\n"] {
        if {[regexp "^Traceback" $line]} {
-           set ok 0
-           break
+           incr exception_count
+           if { $exception_count > $expected_exception_count} {
+               break
+           }
        }
     }
 
-    if {$ok} {
+    if {$exception_count == $expected_exception_count} {
        pass "exceptions in log file"
     } else {
        verbose -log --  "--- DAP LOG START ---"
        verbose -log -- $contents
        verbose -log --  "--- DAP LOG END ---"
+       if { $expected_exception_count > 0 } {
+           verbose -log -- [join [list "Expected $expected_exception_count" \
+                                       "exception(s), saw $exception_count"]]
+       }
        fail "exceptions in log file"
     }
 }
@@ -414,8 +437,9 @@ proc dap_check_log_file_re { re } {
 }
 
 # Cleanly shut down gdb.  TERMINATE is passed as the terminateDebuggee
-# parameter to the request.
-proc dap_shutdown {{terminate false}} {
+# parameter to the request.  The EXPECTED_EXCEPTION_COUNT is the
+# number of exceptions that we expect to see in the latest DAP log.
+proc dap_shutdown {{terminate false} {expected_exception_count 0}} {
     dap_check_request_and_response "shutdown" disconnect \
        [format {o terminateDebuggee [l %s]} $terminate]
 
@@ -427,7 +451,7 @@ proc dap_shutdown {{terminate false}} {
 
     clear_gdb_spawn_id
 
-    dap_check_log_file
+    dap_check_log_file $expected_exception_count
 }
 
 # Search the event list EVENTS for an output event matching the regexp