binutils-gdb/gdb/testsuite/gdb.python/py-corefile.exp
Andrew Burgess e492fb22b7 gdb: backport DAP core file support
Back-port the following upstream commits to enable core file support
within GDB's DAP protocol implementation:

  * 0a481bb9a6 gdb/dap: add support for opening core files
  * 4ecaac39c2 gdb/python: new events.corefile_changed event
  * ecac42af73 Always propagate exceptions in DAP
  * fc8e5a565b gdb: make structured core file mappings processing global
  * f69c1d03c4 gdb/python: add Corefile.mapped_files method
  * 7862554bcf gdb/python: introduce gdb.Corefile API

The commit messages for each individual commit follow:

--- 0a481bb9a6:

gdb/dap: add support for opening core files

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>

---  4ecaac39c2:

gdb/python: new events.corefile_changed event

Add a new Python event registry, events.corefile_changed.  This event
is emitted each time the corefile within an inferior changes.

The event object has a single 'inferior' attribute which is the
gdb.Inferior object for which the core file changed.  The user can
then inspect Inferior.corefile to see details about the new core file,
or this will be None if the core file was removed from the inferior.

I've updated the existing test to cover this new event.

The new test covers both the corefile_changed event, but also monitors
the exited event.  This ties into the work done in the previous
commit where we use whether the inferior has exited or not as a guard
for whether core_target::exit_core_file_inferior should be called.
Unloading a core file should result in a single corefile_changed event
and a single exited event.

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

--- ecac42af73:

Always propagate exceptions in DAP

This changes the DAP exec_and_log function to always transform an
exception into a DAPException and propagate it.

As the bug points out, we haven't always wrapped calls when
appropriate.  I think it's better to cause the request to fail by
default; if any spot truly needs to ignore errors, that is readily
done at the point of call.

Bug: https://sourceware.org/bugzilla/show_bug.cgi?id=33346

--- fc8e5a565b:

gdb: make structured core file mappings processing global

In corelow.c, within core_target::build_file_mappings, we have code
that wraps around a call to gdbarch_read_core_file_mappings and
provides more structure to the results.

Specifically, gdbarch_read_core_file_mappings calls a callback once
for every region of every mapped file.  The wrapper code groups all of
the mappings for one file into an instance of 'struct mapped_file',
this allows all of the mapped regions to be associated with the
build-id and filename of a file.

In the next commit I plan to make this information available via the
Python API, and so I need to allow access to this structured wrapping
outside of corelow.c.

This commit renames 'struct mapped_file' to 'struct core_mapped_file'
and moves the struct into gdbcore.h.  Then a new global function
gdb_read_core_file_mappings is created into which I move the code to
build the structured data.

Then corelow.c is updated to call gdb_read_core_file_mappings.

This commit does not extend the Python API, that is for the next
commit.

There should be no user visible changes after this commit.

Bug: https://sourceware.org/bugzilla/show_bug.cgi?id=32844

Approved-By: Tom Tromey <tom@tromey.com>

--- f69c1d03c4:

gdb/python: add Corefile.mapped_files method

Add a new Corefile.mapped_files method which returns a list of
gdb.CorefileMappedFile objects.

Each gdb.CorefileMappedFile object represents a file that was mapped
into the process when the core file was created.

A gdb.CorefileMappedFile has attributes:

  + filename  -- A string, the name of the mapped file.
  + build_id -- A string or None, the build-id of the mapped file if
                GDB could find it (None if not).
  + is_main_executable -- A boolean, True if this mapping is the main
                          executable.
  + regions -- A list containing the regions of this file that were
               mapped into the process.

The 'regions' list is a list of gdb.CorefileMappedFileRegion objects,
each of these objects has the following attributes:

  + start -- the start address within the inferior.
  + end -- the end address within the inferior.
  + file_offset -- the offset within the mapped file for this mapping.

There are docs and tests.

Bug: https://sourceware.org/bugzilla/show_bug.cgi?id=32844

Approved-By: Tom Tromey <tom@tromey.com>

--- 7862554bcf:

gdb/python: introduce gdb.Corefile API

This commit starts adding some core file related features to the
Python API.

In this initial commit I've tried to keep the changes as small as
possible for easy review.

There's a new Python class gdb.Corefile, which represents a loaded
core file.  This API doesn't allow the user to create their own
gdb.Corefile objects, a core file must be loaded using the 'core-file'
command, then a gdb.Corefile object can be obtained by querying the
inferior in which the core file was loaded.

There's a new attribute gdb.Inferior.corefile, this is None when no
core file is loaded, or contains a gdb.Corefile object if a core file
has been loaded.

Currently, the gdb.Corefile object has one attribute, and one method,
these are:

  gdb.Corefile.filename -- the file name of the loaded core file.

  gdb.Corefile.is_valid() -- indicates if a gdb.Corefile object is
  valid or not.  See notes below.

A gdb.Corefile object is only valid while the corresponding core file
is loaded into an inferior.  Unloading the core file, or loading a
different one will cause a gdb.Corefile object to become invalid.  For
example:

  (gdb) core-file /tmp/core.54313
  ... snip ...
  (gdb) python core=gdb.selected_inferior().corefile
  (gdb) python print(core)
  <gdb.Corefile inferior=1 filename='/tmp/core.54313'>
  (gdb) python print(core.is_valid())
  True
  (gdb) core-file
  No core file now.
  (gdb) python print(core)
  <gdb.Corefile (invalid)>
  (gdb) python print(core.is_valid())
  False
  (gdb)

In order to track changes to the core file, there is a new observable
'core_file_changed', which accounts for the changes in corelow.c,
observable,c, and observable.h.  Currently, this observable is not
visible as a Python event.

I chose to access the core file via the inferior even though the core
file BFD object is actually stored within the program_space.  As such,
it might seem that the natural choice would be to add the attribute as
gdb.Progspace.corefile.

For background reading on my choice, please see:

  https://inbox.sourceware.org/gdb-patches/577f2c47793acb501c2611c0e6c7ea379f774830.1668789658.git.aburgess@redhat.com

This patch was never merged, it is still on my backlog, but the
observation in that work is that some targets are not really
shareable.  For example, the core_target (corelow.c) stores
information about the loaded core file within the target instance.  As
such, each target instance represents a single loaded core file.

Except that the BFD part of the core file is stored in the
program_space, which is a little weird.

During review, Tom made the observation, that maybe we should
investigate moving the core file BFD into the core_target.  I'm
inclined to agree with this as a direction of travel.

All this leaves us with two observations:

  1. Currently, loading a core file into an inferior, then using
     'add-inferior' will try to share the core_target between
     inferiors.  This is broken, and can trigger GDB crashes.  The
     obvious fix, without reworking core_target, is just to prevent
     this sharing, making core_target per-inferior.

  2. Having the core file information split between the core_target
     instance, and the BFD stored in the program_space is a little
     weird, and is really just historical.  Planning for a future
     where the BFD is also stored in the core_target might be wise.

So, if we imagine that the BFD is (one day) moved into the
core_target, and that the core_target really becomes non-shareable,
then it is, I think, clearer that the corefile attribute should live
on the gdb.Inferior object, not the gdb.Progspace object.

There's testing for all the functionality added in this commit.

Bug: https://sourceware.org/bugzilla/show_bug.cgi?id=32844

Reviewed-By: Eli Zaretskii <eliz@gnu.org>
Approved-By: Tom Tromey <tom@tromey.com>
2026-06-22 15:41:28 +01:00

365 lines
12 KiB
Text

# Copyright (C) 2025 Free Software Foundation, Inc.
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# This file is part of the GDB testsuite. It tests the core file
# support in Python.
require isnative
require {!is_remote host}
load_lib gdb-python.exp
require allow_python_tests
standard_testfile
if {[build_executable "build executable" $testfile $srcfile] == -1} {
return
}
set remote_python_file \
[gdb_remote_download host ${srcdir}/${subdir}/${testfile}.py]
set corefile [core_find $binfile]
if {$corefile == ""} {
unsupported "couldn't create or find corefile"
return
}
# Helper proc to run the 'core-file' command. Takes optional arguments:
#
# -corefile FILENAME : Load FILENAME as the new core file. If this
# argument is not given then the current core
# file will be unloaded.
#
# -inferior NUM : The inferior in which the corefile is being changed.
# This is used to match the corefile_changed events
# that will be emitted.
#
# -prefix STRING : A test prefix, to make test names unique.
#
# -replacement : There's already a core file loaded when this command
# is being run.
proc core_file_cmd { args } {
parse_some_args {
{corefile ""}
{inferior 1}
{prefix ""}
{replacement}
}
if { $prefix eq "" } {
if { $corefile eq "" } {
set prefix "unload corefile"
} else {
set prefix "load corefile"
}
}
with_test_prefix $prefix {
gdb_test "events corefile_changed check" \
"^No corefile_changed event has been seen\\." \
"no corefile event has been seen"
gdb_test "events exited check" \
"^No exited event has been seen\\." \
"no exited event has been seen"
if { $corefile eq "" } {
gdb_test "core-file" "^No core file now\\." "unload current core file"
gdb_test "events corefile_changed check" \
"Event 1/1, Inferior $inferior, Corefile None" \
"expected corefile event has been seen"
gdb_test "events exited check" \
"Event 1/1, Inferior $inferior, Exit Code None" \
"expected exited event has been seen"
} else {
gdb_test "core-file $corefile" ".*" "load core file"
if { $replacement } {
gdb_test "events corefile_changed check" \
[multi_line \
"Event 1/2, Inferior $inferior, Corefile None" \
"Event 2/2, Inferior $inferior, Corefile [string_to_regexp $corefile]"] \
"expected corefile event has been seen"
gdb_test "events exited check" \
"Event 1/1, Inferior $inferior, Exit Code None" \
"expected exited event has been seen"
} else {
gdb_test "events corefile_changed check" \
"Event 1/1, Inferior $inferior, Corefile [string_to_regexp $corefile]" \
"expected corefile event has been seen"
gdb_test "events exited check" \
"^No exited event has been seen\\." \
"no exited event was emitted"
}
}
}
gdb_test_no_output -nopass "events corefile_changed reset"
gdb_test_no_output -nopass "events exited reset"
}
# A helper proc runs clean_restart passing through ARGS, and then loads the
# test's Python script.
proc clean_restart_and_load_py_script { args } {
clean_restart {*}$args
# Load the Python script into GDB.
gdb_test "source $::remote_python_file" "^Success" \
"source python script"
}
# Create a copy of the corefile.
set other_corefile [standard_output_file ${testfile}-other.core]
remote_exec build "cp $corefile $other_corefile"
clean_restart_and_load_py_script
gdb_test_no_output "python inf = gdb.selected_inferior()" \
"capture current inferior"
gdb_test "python print(inf.corefile)" "^None" \
"Inferior.corefile is None before loading a core file"
core_file_cmd -corefile $corefile
set file_re [string_to_regexp $corefile]
gdb_test "python print(inf.corefile)" "^<gdb\\.Corefile inferior=1 filename='$file_re'>" \
"Inferior.corefile is a valid object after loading a core file"
gdb_test_no_output "python core1=inf.corefile" "capture gdb.Corefile object"
gdb_test "python print(core1.__dict__)" "^\\{\\}" \
"print Corefile.__dict__ when empty"
gdb_test_no_output "python core1._my_attribute = \"Hello\"" \
"write new attribute into Corefile object"
gdb_test "python print(core1._my_attribute)" "^Hello" \
"immediately read new attribute"
gdb_test "python print(core1.__dict__)" "^\\{'_my_attribute': 'Hello'\\}" \
"print Corefile.__dict__ after adding an attribute"
gdb_test "python print(core1.filename)" "^$file_re" \
"Corefile.filename attribute works as expected"
gdb_test "python print(core1.is_valid())" "^True" \
"Corefile.is_valid() is True while corefile is loaded"
core_file_cmd
gdb_test "python print(core1.is_valid())" "^False" \
"Corefile.is_valid() is False after corefile is unloaded"
gdb_test "python print(core1.__dict__)" "^\\{'_my_attribute': 'Hello'\\}" \
"print Corefile.__dict__ with attribute when invalid"
gdb_test "python print(core1)" "^<gdb\\.Corefile \\(invalid\\)>" \
"print an invalid gdb.Corefile object"
gdb_test "python print(core1.filename)" \
[multi_line \
"Python Exception <class 'RuntimeError'>: Corefile no longer exists\\." \
"Error occurred in Python: Corefile no longer exists\\."] \
"error when reading filename from invalid Corefile"
gdb_test "python print(inf.corefile)" "^None" \
"Inferior.corefile is None again after corefile unload"
gdb_test "python print(core1._my_attribute)" "^Hello" \
"read new attribute from invalid core file"
# Create a second inferior.
gdb_test "add-inferior"
gdb_test "inferior 2"
with_test_prefix "in second inferior" {
core_file_cmd -corefile $corefile -inferior 2
gdb_test "python print(inf.corefile)" "^None" \
"first inferior still has no core file"
gdb_test_no_output "python core2=gdb.selected_inferior().corefile" \
"capture gdb.Corefile object"
# The _my_attribute was added to CORE1, not CORE2. Check it
# doesn't somehow appear on CORE2.
gdb_test "python print(core2._my_attribute)" \
"AttributeError.*: 'gdb\\.Corefile' object has no attribute '_my_attribute'" \
"try to read attribute that doesn't exist"
gdb_test "python print(core2.filename)" "^$file_re" \
"Corefile.filename attribute works as expected"
gdb_test "inferior 1"
}
# Read the name of the core file from the second program space while
# the current program space is the first one.
gdb_test "python print(core2.filename)" "^$file_re" \
"Corefile.filename attribute works from different progspace"
# Load the other corefile into the first inferior.
core_file_cmd -corefile $other_corefile \
-prefix "load other corefile into inferior 1"
# Delete the second inferior. We need to switch to the second
# inferior and unload its corefile before we can do that. Then,
# switch back to the first inferior, delete the second, and try to
# read the filename of the core file from the (now deleted) second
# inferior. We should get an error about the gdb.Corefile being
# invalid.
with_test_prefix "remove second inferior" {
gdb_test "inferior 2"
gdb_test "python print(inf.corefile.filename)" \
"^[string_to_regexp $other_corefile]" \
"read inferior 1 corefile when in inferior 2"
gdb_test_no_output "python core1=inf.corefile" \
"capture inferior 1 gdb.Corefile while in inferior 2"
# This is a new CORE1 object, check that _my_attribute is gone.
gdb_test "python print(core1._my_attribute)" \
"AttributeError.*: 'gdb\\.Corefile' object has no attribute '_my_attribute'" \
"try to read attribute that doesn't exist"
core_file_cmd -inferior 2
gdb_test "python print(core2.filename)" \
[multi_line \
"Python Exception <class 'RuntimeError'>: Corefile no longer exists\\." \
"Error occurred in Python: Corefile no longer exists\\."] \
"error when reading filename from invalid Corefile"
gdb_test "inferior 1"
gdb_test "remove-inferiors 2"
gdb_test "python print(core2.is_valid())" "^False" \
"Corefile.is_valid() is False after corefile is unloaded, and Progspace is deleted"
gdb_test "python print(core2.filename)" \
[multi_line \
"Python Exception <class 'RuntimeError'>: Corefile no longer exists\\." \
"Error occurred in Python: Corefile no longer exists\\."] \
"error when reading filename of an invalid Corefile, from deleted program space"
gdb_test "python print(core1.is_valid())" "^True" \
"check inferior 1 core file is still valid"
}
# Test the Corefile.mapped_files() API. The Python script that is
# sourced here implements 'info proc mappings' in Python using the
# mapped_files API. The output from the built-in command, and the
# Python command should be identical.
with_test_prefix "test mapped files data" {
clean_restart_and_load_py_script
# Load the core file.
core_file_cmd -corefile $corefile
# Two files to write the output to.
set out_1 [standard_output_file ${gdb_test_file_name}-out-1.txt]
set out_2 [standard_output_file ${gdb_test_file_name}-out-2.txt]
# Run the built-in command, then the new Python command, capture
# the output.
gdb_test "pipe info proc mappings | tee $out_1" ".*" \
"capture built-in mappings output"
gdb_test "pipe info proc py-mappings | tee $out_2" ".*" \
"capture Python based mappings data"
# Check the output is identical.
gdb_test "shell diff -s $out_1 $out_2" \
"Files \[^\r\n\]+-out-1.txt and \[^\r\n\]+-out-2.txt are identical" \
"diff input and output one"
# Check build-ids within the core file mapping data.
gdb_test "check-build-ids" "^PASS"
# Check the is_main_executable flag in the mapping data.
gdb_test "check-main-executable" "^PASS"
# Check that the mapped files "list" is actually an immutable
# tuple.
gdb_test_no_output "python core = gdb.selected_inferior().corefile"
gdb_test_no_output "python mapped_files = core.mapped_files()"
gdb_test "python print(type(mapped_files))" \
"^<class 'tuple'>"
gdb_test "python mapped_files\[0\] = None" \
"'tuple' object does not support item assignment"
gdb_test "python print(mapped_files\[0\] is None)" "^False"
# And same for the list of regions for a mapped file.
gdb_test_no_output "python regions = mapped_files\[0\].regions"
gdb_test "python print(type(regions))" \
"^<class 'tuple'>"
gdb_test "python regions\[0\] = None" \
"'tuple' object does not support item assignment"
}
# Load a core file. GDB should figure out which file is being debugged.
# Then use 'start' to run this executable, this will replace the core file
# target. At least on Linux, this replacement is done without calling
# target_detach. This test checks that the expected core file changed and
# inferior exited events are still seen.
with_test_prefix "start from corefile" {
if { [gdb_protocol_is_native] } {
clean_restart_and_load_py_script
# Load the core file.
core_file_cmd -corefile $corefile
# Check GDB figured out the executable.
gdb_test "info inferiors 1" \
"\[^\r\n\]+[string_to_regexp $binfile]\\s*" \
"check executable was detected correctly"
gdb_test "start" \
"Temporary breakpoint $::decimal, main \\(\\).*" \
gdb_test "events corefile_changed check" \
"Event 1/1, Inferior 1, Corefile None" \
"expected corefile event has been seen"
gdb_test "events exited check" \
"Event 1/1, Inferior 1, Exit Code None" \
"expected exited event has been seen"
gdb_test_no_output -nopass "events corefile_changed reset"
gdb_test_no_output -nopass "events exited reset"
}
}
# Load a core file, then load a different core file to replace it.
# Check that the events that are emitted are as expected.
with_test_prefix "load one core file over another" {
clean_restart_and_load_py_script
# Load the core file.
core_file_cmd -corefile $corefile \
-prefix "load first corefile"
core_file_cmd -corefile $other_corefile \
-prefix "load second corefile" \
-replacement
}