diff --git a/gdb/Makefile.in b/gdb/Makefile.in index 91b164814d7..29b4efa58e9 100644 --- a/gdb/Makefile.in +++ b/gdb/Makefile.in @@ -399,6 +399,7 @@ SUBDIR_PYTHON_SRCS = \ python/py-color.c \ python/py-connection.c \ python/py-continueevent.c \ + python/py-corefile.c \ python/py-dap.c \ python/py-disasm.c \ python/py-event.c \ diff --git a/gdb/NEWS b/gdb/NEWS index 2c73776944f..ebd0e063227 100644 --- a/gdb/NEWS +++ b/gdb/NEWS @@ -83,6 +83,16 @@ qExecAndArgs ** The gdb.write() function now takes an additional, optional, 'style' argument, which can be used to style the output. + ** New gdb.Corefile class which represents a loaded core file. This + has an attribute Corefile.filename, the file name of the loaded + core file, and a method Corefile.is_valid(), which returns False + when a Corefile object becomes invalid (e.g. when the core file + is unloaded). + + ** New Inferior.corefile attribute. This read only attribute + contains the gdb.Corefile object if a core file is loaded into + the inferior, otherwise, this contains None. + *** Changes in GDB 17 * Debugging Linux programs that use x86-64 or x86-64 with 32-bit pointer diff --git a/gdb/corelow.c b/gdb/corelow.c index 29eafe8bdfd..2f202dc1fbf 100644 --- a/gdb/corelow.c +++ b/gdb/corelow.c @@ -53,6 +53,7 @@ #include "xml-tdesc.h" #include "memtag.h" #include "cli/cli-style.h" +#include "observable.h" #ifndef O_LARGEFILE #define O_LARGEFILE 0 @@ -678,6 +679,9 @@ core_target::clear_core () clear_solib (current_program_space); current_program_space->cbfd.reset (nullptr); + + /* Notify that the core file has changed. */ + gdb::observers::core_file_changed.notify (current_inferior ()); } } @@ -1278,6 +1282,9 @@ core_target_open (const char *arg, int from_tty) exception_print (gdb_stderr, except); } } + + /* Notify that the core file has changed. */ + gdb::observers::core_file_changed.notify (current_inferior ()); } void diff --git a/gdb/doc/python.texi b/gdb/doc/python.texi index 2f74a2311a6..c671d231345 100644 --- a/gdb/doc/python.texi +++ b/gdb/doc/python.texi @@ -234,6 +234,7 @@ optional arguments while skipping others. Example: * Disassembly In Python:: Instruction Disassembly In Python * Missing Debug Info In Python:: Handle missing debug info from Python. * Missing Objfiles In Python:: Handle objfiles from Python. +* Core Files In Python:: Python representation of core files. @end menu @node Basic Python @@ -3634,6 +3635,15 @@ necessary quoting for the shell; when a sequence is assigned, the quoting is applied by @value{GDBN}. @end defvar +@defvar Inferior.corefile +If a core file has been loaded into this inferior (@pxref{core-file +command}), then this contains a @code{gdb.Corefile} object that +represents the loaded core file (@pxref{Core Files In Python}). + +If no core file has been loaded into this inferior, then this +attribute contains @code{None}. +@end defvar + A @code{gdb.Inferior} object has the following methods: @defun Inferior.is_valid () @@ -8884,6 +8894,45 @@ handlers, all of the matching handlers are enabled. The @code{enabled} field of each matching handler is set to @code{True}. @end table +@node Core Files In Python +@subsubsection Core Files In Python +@cindex python, core files + +When a core file is loaded into an inferior (@pxref{Inferiors In +Python}) for examination (@pxref{core-file command}), information +about the core file is contained in a @code{gdb.Corefile} object. + +The @code{gdb.Corefile} for an inferior can be accessed using the +@code{Inferior.corefile} attribute. This will be @code{None} if +no core file is loaded. + +A @code{gdb.Corefile} object has the following attributes: + +@defvar Corefile.filename +This read only attribute contains a non-empty string, the file name of +the core file. Attempting to access this attribute on an invalid +@code{gdb.Corefile} object will raise a @code{RuntimeError} exception. +@end defvar + +A @code{gdb.Corefile} object has the following methods: + +@defun Corefile.is_valid () +Returns @code{True} if the @code{gdb.Corefile} object is valid, +@code{False} if not. A @code{gdb.Corefile} object will become invalid +when the core file is unloaded from the inferior using the +@kbd{core-file} command (@pxref{core-file command}), or if the +inferior in which the core file is loaded is deleted. All other +@code{gdb.Corefile} methods and attributes will throw an exception if +it is invalid at the time the method is called, or the attribute +accessed. +@end defun + +One may add arbitrary attributes to @code{gdb.Corefile} objects in the +usual Python way. This is useful if, for example, one needs to do +some extra record keeping associated with the corefile. +@xref{choosing attribute names}, for guidance on selecting a suitable +name for new attributes. + @node Python Auto-loading @subsection Python Auto-loading @cindex Python auto-loading diff --git a/gdb/observable.c b/gdb/observable.c index 1233a1943a6..8439f11c15d 100644 --- a/gdb/observable.c +++ b/gdb/observable.c @@ -76,6 +76,7 @@ DEFINE_OBSERVABLE (target_post_wait); DEFINE_OBSERVABLE (new_program_space); DEFINE_OBSERVABLE (free_program_space); DEFINE_OBSERVABLE (tui_enabled); +DEFINE_OBSERVABLE (core_file_changed); } /* namespace observers */ } /* namespace gdb */ diff --git a/gdb/observable.h b/gdb/observable.h index 4d913010c56..5f064cf1fc8 100644 --- a/gdb/observable.h +++ b/gdb/observable.h @@ -260,6 +260,12 @@ extern observable free_program_space; extern observable tui_enabled; +/* The core file loaded into the program space inferior INF has changed. + The process of changing has completed, i.e. when unloading, the unload + is now complete. When loading a new core file, the load is complete, + shared libraries have been loaded, registers and threads read in, etc. */ +extern observable core_file_changed; + } /* namespace observers */ } /* namespace gdb */ diff --git a/gdb/python/py-corefile.c b/gdb/python/py-corefile.c new file mode 100644 index 00000000000..bda93fa1325 --- /dev/null +++ b/gdb/python/py-corefile.c @@ -0,0 +1,291 @@ +/* Python interface to core files. + + Copyright (C) 2025 Free Software Foundation, Inc. + + This file is part of GDB. + + 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 . */ + +#include "python-internal.h" +#include "progspace.h" +#include "observable.h" +#include "inferior.h" + +/* A gdb.Corefile object. */ + +struct corefile_object +{ + PyObject_HEAD + + /* The inferior this core file is attached to. This will be set to NULL + when the inferior is deleted, or if a different core file is loaded + for the inferior. When this is NULL the gdb.Corefile object is + considered invalid.*/ + struct inferior *inferior; + + /* Dictionary holding user-added attributes. This is the __dict__ + attribute of the object. This is an owning reference. */ + PyObject *dict; +}; + +extern PyTypeObject corefile_object_type + CPYCHECKER_TYPE_OBJECT_FOR_TYPEDEF ("corefile_object"); + +/* Clear the inferior pointer in a Corefile object OBJ when an inferior is + deleted. */ + +struct inferior_corefile_deleter +{ + void operator() (corefile_object *obj) + { + if (!gdb_python_initialized) + return; + + gdbpy_enter enter_py; + + /* When OBJECT goes out of scope this will Py_DECREF on OBJ. */ + gdbpy_ref object (obj); + + /* Clearing the inferior pointer marks the gdb.Corefile as invalid. */ + object->inferior = nullptr; + } +}; + +/* Store a gdb.Corefile object in an inferior's registry. */ + +static const registry::key + cfpy_inferior_corefile_data_key; + +/* See python-internal.h. */ + +gdbpy_ref<> +gdbpy_core_file_from_inferior (inferior *inf) +{ + gdb_assert (inf != nullptr); + gdb_assert (inf->pspace != nullptr); + + program_space *pspace = inf->pspace; + + if (pspace->core_bfd () == nullptr) + return gdbpy_ref<>::new_reference (Py_None); + + PyObject *result = (PyObject *) cfpy_inferior_corefile_data_key.get (inf); + if (result == nullptr) + { + gdbpy_ref object + (PyObject_New (corefile_object, &corefile_object_type)); + if (object == nullptr) + return nullptr; + + /* Ensure the 'inferior' field is set to NULL. If the PyDict_New + call fails then the gdb.Corefile will be discarded and + cfpy_dealloc will be called, which requires that the 'inferior' be + set to NULL. */ + object->inferior = nullptr; + object->dict = PyDict_New (); + if (object->dict == nullptr) + return nullptr; + + /* Now that the gdb.Corefile has been successfully initialised and we + know that it is going to be passed back to the user, move it out + of the invalid state by setting the 'inferior' field to a non NULL + value. */ + object->inferior = inf; + cfpy_inferior_corefile_data_key.set (inf, object.get ()); + result = (PyObject *) object.release (); + } + + return gdbpy_ref<>::new_reference (result); +} + +/* Return true if OBJ is valid. */ + +static bool +cfpy_corefile_object_is_valid (const corefile_object *obj) +{ + if (obj->inferior == nullptr) + return false; + + gdb_assert (obj->inferior->pspace != nullptr); + + return obj->inferior->pspace->core_bfd () != nullptr; +} + +/* Require that COREFILE_OBJ be a valid core file. A valid core file + object has a valid program space, and the program space has a core file + loaded into it. */ +#define CFPY_REQUIRE_VALID(corefile_obj) \ + do { \ + if (!cfpy_corefile_object_is_valid (corefile_obj)) \ + { \ + PyErr_SetString (PyExc_RuntimeError, \ + _("Corefile no longer exists.")); \ + return nullptr; \ + } \ + } while (0) + +/* Read the gdb.Corefile.filename attribute. */ + +static PyObject * +cfpy_get_filename (PyObject *self, void *closure) +{ + corefile_object *obj = (corefile_object *) self; + + CFPY_REQUIRE_VALID (obj); + + /* If the program space's core file had been cleared, then this Corefile + object would have been invalidated. */ + bfd *abfd = obj->inferior->pspace->core_bfd (); + gdb_assert (abfd != nullptr); + + return host_string_to_python_string (bfd_get_filename (abfd)).release (); +} + +/* Implementation of gdb.Corefile.is_valid (self) -> Boolean. + Returns True if this core file object is associated with a program space + that still exists, an the program space still has a core file loaded. */ + +static PyObject * +cfpy_is_valid (PyObject *self, PyObject *args) +{ + corefile_object *obj = (corefile_object *) self; + + if (!cfpy_corefile_object_is_valid (obj)) + Py_RETURN_FALSE; + + Py_RETURN_TRUE; +} + +/* Callback from gdb::observers::core_file_changed. The core file in + PSPACE has been changed. */ + +static void +cfpy_corefile_changed (inferior *inf) +{ + cfpy_inferior_corefile_data_key.clear (inf); +} + +/* Called when a gdb.Corefile is destroyed. */ + +static void +cfpy_dealloc (PyObject *obj) +{ + corefile_object *corefile = (corefile_object *) obj; + + /* Every gdb.Corefile is cached in an inferior's registry. The only way + for a gdb.Corefile to be deallocated is to remove the object reference + from the registry (and dec its ref count), but before we do that, we + set the object's inferior pointer to NULL. */ + gdb_assert (corefile->inferior == nullptr); + + Py_XDECREF (corefile->dict); + + Py_TYPE (obj)->tp_free (obj); +} + +/* __repr__ implementation for gdb.Corefile. */ + +static PyObject * +cfpy_repr (PyObject *self) +{ + corefile_object *obj = (corefile_object *) self; + + if (!cfpy_corefile_object_is_valid (obj)) + return gdb_py_invalid_object_repr (self); + + program_space *pspace = obj->inferior->pspace; + gdb_assert (pspace != nullptr); + return PyUnicode_FromFormat ("<%s inferior=%d filename='%s'>", + Py_TYPE (self)->tp_name, + obj->inferior->num, + bfd_get_filename (pspace->core_bfd ())); +} + + + +static int +gdbpy_initialize_corefile () +{ + gdb::observers::core_file_changed.attach (cfpy_corefile_changed, + "py-corefile"); + + if (gdbpy_type_ready (&corefile_object_type) < 0) + return -1; + + return 0; +} + +GDBPY_INITIALIZE_FILE (gdbpy_initialize_corefile); + + + +static gdb_PyGetSetDef corefile_getset[] = +{ + { "__dict__", gdb_py_generic_dict, nullptr, + "The __dict__ for the gdb.Corefile.", &corefile_object_type }, + { "filename", cfpy_get_filename, nullptr, + "The filename of a valid Corefile object.", nullptr }, + { nullptr } +}; + +static PyMethodDef corefile_object_methods[] = +{ + { "is_valid", cfpy_is_valid, METH_NOARGS, + "is_valid () -> Boolean.\n\ +Return true if this Corefile is valid, false if not." }, + { nullptr } +}; + +PyTypeObject corefile_object_type = +{ + PyVarObject_HEAD_INIT (nullptr, 0) + "gdb.Corefile", /*tp_name*/ + sizeof (corefile_object), /*tp_basicsize*/ + 0, /*tp_itemsize*/ + cfpy_dealloc, /*tp_dealloc*/ + 0, /*tp_print*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_compare*/ + cfpy_repr, /*tp_repr*/ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ + 0, /*tp_hash */ + 0, /*tp_call*/ + 0, /*tp_str*/ + 0, /*tp_getattro*/ + 0, /*tp_setattro*/ + 0, /*tp_as_buffer*/ + Py_TPFLAGS_DEFAULT, /*tp_flags*/ + "GDB corefile object", /* tp_doc */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + corefile_object_methods, /* tp_methods */ + 0, /* tp_members */ + corefile_getset, /* tp_getset */ + 0, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + offsetof (corefile_object, dict), /* tp_dictoffset */ + 0, /* tp_init */ + 0, /* tp_alloc */ + 0, /* tp_new */ +}; diff --git a/gdb/python/py-inferior.c b/gdb/python/py-inferior.c index 2aa11d3160d..d926923915a 100644 --- a/gdb/python/py-inferior.c +++ b/gdb/python/py-inferior.c @@ -973,6 +973,22 @@ infpy_get_main_name (PyObject *self, void *closure) return host_string_to_python_string (name).release (); } +/* Implement the Inferior.corefile getter. Returns a gdb.Corefile + object, or None. */ + +static PyObject * +infpy_get_core_file (PyObject *self, void *closure) +{ + inferior_object *inf = (inferior_object *) self; + + INFPY_REQUIRE_VALID (inf); + + inferior *inferior = inf->inferior; + gdb_assert (inferior != nullptr); + + return gdbpy_core_file_from_inferior (inferior).release (); +} + static void infpy_dealloc (PyObject *obj) { @@ -1062,6 +1078,8 @@ static gdb_PyGetSetDef inferior_object_getset[] = { "progspace", infpy_get_progspace, NULL, "Program space of this inferior" }, { "main_name", infpy_get_main_name, nullptr, "Name of 'main' function, if known.", nullptr }, + { "corefile", infpy_get_core_file, nullptr, + "The corefile loaded in to this inferior, or None.", nullptr }, { NULL } }; diff --git a/gdb/python/python-internal.h b/gdb/python/python-internal.h index dbb2d7eb7e0..51ace132ddc 100644 --- a/gdb/python/python-internal.h +++ b/gdb/python/python-internal.h @@ -1127,6 +1127,14 @@ extern std::optional gdbpy_print_insn (struct gdbarch *gdbarch, CORE_ADDR address, disassemble_info *info); +/* Return the gdb.Corefile object representing the core file loaded into + the program space of INF, or None if there is no core file loaded. INF + must not be NULL. If an error occurs then NULL is returned, and a + suitable Python error will be set. */ + +extern gdbpy_ref<> gdbpy_core_file_from_inferior (inferior *inf); + + /* A wrapper for PyType_Ready that also automatically registers the type in the appropriate module. Returns 0 on success, -1 on error. If MOD is supplied, then the type is added to that module. If MOD diff --git a/gdb/testsuite/gdb.python/py-corefile.c b/gdb/testsuite/gdb.python/py-corefile.c new file mode 100644 index 00000000000..1334ff65143 --- /dev/null +++ b/gdb/testsuite/gdb.python/py-corefile.c @@ -0,0 +1,25 @@ +/* Copyright 2025 Free Software Foundation, Inc. + + This file is part of GDB. + + 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 . */ + +#include + +int +main (void) +{ + /* With correct ulimit, etc. this should cause a core dump. */ + abort (); +} diff --git a/gdb/testsuite/gdb.python/py-corefile.exp b/gdb/testsuite/gdb.python/py-corefile.exp new file mode 100644 index 00000000000..e9254bd9e78 --- /dev/null +++ b/gdb/testsuite/gdb.python/py-corefile.exp @@ -0,0 +1,178 @@ +# 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 . + +# This file is part of the GDB testsuite. It tests the core file +# support in Python. + +require isnative + +load_lib gdb-python.exp + +require allow_python_tests + +standard_testfile + +if {[build_executable "build executable" $testfile $srcfile] == -1} { + return +} + +set corefile [core_find $binfile] +if {$corefile == ""} { + unsupported "couldn't create or find corefile" + return +} + +# Create a copy of the corefile. +set other_corefile [standard_output_file ${testfile}-other.core] +remote_exec build "cp $corefile $other_corefile" + +clean_restart + +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" + +gdb_test "core-file $corefile" ".*" \ + "load core file" + +set file_re [string_to_regexp $corefile] +gdb_test "python print(inf.corefile)" "^" \ + "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" + +gdb_test "core-file" "^No core file now\\." "unload current core file" + +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)" "^" \ + "print an invalid gdb.Corefile object" + +gdb_test "python print(core1.filename)" \ + [multi_line \ + "Python Exception : 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" { + gdb_test "core-file $corefile" ".*" \ + "load core file" + + 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. +gdb_test "core $other_corefile" ".*" \ + "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" + + gdb_test "core-file" + + gdb_test "python print(core2.filename)" \ + [multi_line \ + "Python Exception : 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 : 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" +}