binutils-gdb/gdb/testsuite/gdb.python/py-corefile.py
Andrew Burgess 37cb3cd8f8 gdb/linux: handle missing NT_FILE note when opening core files
This patch originated from this mailing list discussion:

  https://inbox.sourceware.org/gdb-patches/b9b5bf03c59b58e02ca27b522338c6103d5ae49f.camel@gnu.org

The user has some core files which lack an NT_FILE note.  They
wondered why GDB was still unable to find the shared libraries based
on their build-id.

The reason right now is that GDB only records the build-id information
for mappings based on the entries in the NT_FILE note.  With the
entries in this note we build several lookup tables; a filename to
build-id table, a soname (extracted from the file if it is a shared
library) to build-id table, and an address range to build-id table.

When a shared library is being loaded we perform a lookup using two
pieces of information; the shared library's filename, and an address
that we know is within the shared library.  If either of these give a
build-id, then we can use that build-id to ensure GDB loads the
shared library that matches the core file.

If the NT_FILE note is missing then none of the lookup tables are
created, and so the shared library build-id lookup fails, meaning that
all GDB can do is look for the shared library by name on the local
file system.  This often results in the wrong library version being
loaded, or the library not being found at all.

However, Linux core files also have the segment table.  This table
gives address ranges.  The segment table doesn't tell us what file was
mapped in, or the offset within the file that was mapped in.  But if
we go back to the three lookup tables, we can use the segment table to
build the address to build-id lookup table, and that would be enough
to allow GDB to find the build-id for a shared library in most cases.

So, here's what this patch does: linux_read_core_file_mappings (in
linux-tdep.c) is updated to first parse the NT_FILE note as it
currently does.  But after this we also walk the segment table (BFD
actually converts these into sections with the LOAD flag set), and if
a segment has a build-id, and doesn't correspond to an entry found in
the NT_FILE note, we create an anonymous mapping.  An anonymous
mapping is just like a mapping from the NT_FILE note, but without a
filename and file offset.  This mapping is passed through the callback
just like the traditional, non-anonymous, mappings.

Then in corelow.c various functions are updated in order to handle
anonymous mappings.

Back in linux-tdep.c, function linux_core_info_proc_mappings gets a
small update to handle anonymous mappings.

The corefile-buildid.exp test is updated to remove the NT_FILE notes
and rerun the tests.  This should make no difference as all this test
is checking is that GDB is able to find and load the shared libraries
and executable based on their build-ids; this is something we can do
fine now without the NT_FILE note.

I have also had to update the Python core file API documentation after
this commit.  Previously we claimed that CorefileMappedFile.filename
would never be empty, but this is now possible.  Luckily, this API has
not yet been in a released version of GDB, so this minor tweak isn't
going to break any existing user code.  I did consider having
CorefileMappedFile.filename be a non-empty string or None, but I
couldn't see much value in this, so I just documented that the string
could be empty, and what this means.

The py-corefile.exp test needed a minor update to filter out anonymous
mappings (those without a filename), this matches the behaviour of the
builtin 'info proc mappings' command.

Reviewed-By: Keith Seitz <keiths@redhat.com>
2026-05-01 19:56:56 +01:00

320 lines
9.5 KiB
Python

# Copyright (C) 2025-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/>.
import pathlib
import gdb
class Mapping:
def __init__(self, mapping, region):
self._mapping = mapping
self._region = region
@property
def start(self):
return self._region.start
@property
def end(self):
return self._region.end
@property
def offset(self):
return self._region.file_offset
@property
def filename(self):
return self._mapping.filename
def info_proc_mappings():
ptr_size = gdb.lookup_type("void").pointer().sizeof
print("Mapped address spaces:")
print("")
if ptr_size == 4:
format_str = "%-10s %-10s %-10s %-10s %s "
else:
format_str = "%-18s %-18s %-18s %-18s %s "
print(format_str % ("Start Addr", "End Addr", "Size", "Offset", "File"))
core = gdb.selected_inferior().corefile
mappings = core.mapped_files()
result = []
for m in mappings:
# Ignore anonymous mappings.
if m.filename == "":
continue
for r in m.regions:
result.append(Mapping(m, r))
result.sort(key=lambda x: x.start)
for r in result:
sz = r.end - r.start
if ptr_size == 4:
t = (
"0x%08x" % r.start,
"0x%08x" % r.end,
"0x%-8x" % sz,
"0x%-8x" % r.offset,
"%s" % r.filename,
)
else:
t = (
"0x%016x" % r.start,
"0x%016x" % r.end,
"0x%-16x" % sz,
"0x%-16x" % r.offset,
"%s" % r.filename,
)
print(format_str % t)
class InfoProcPyMappings(gdb.Command):
def __init__(self):
gdb.Command.__init__(self, "info proc py-mappings", gdb.COMMAND_DATA)
def invoke(self, args, from_tty):
info_proc_mappings()
InfoProcPyMappings()
# Assume that a core file is currently loaded.
#
# Look through all the objfiles for the current inferior, and record
# any that have a build-id.
#
# Then look through the core file mapped files. Look for entries that
# correspond with the loaded objfiles. For these matching entries,
# capture the build-id extracted from the core file.
#
# Finally, print a table with the build-id from the objfile, the
# build-id from the core file, and the file name.
#
# This is then processed from the test script to check the build-ids
# match.
class ShowBuildIds(gdb.Command):
def __init__(self):
gdb.Command.__init__(self, "show-build-ids", gdb.COMMAND_DATA)
def invoke(self, args, from_tty):
inf = gdb.selected_inferior()
objfiles = inf.progspace.objfiles()
path_to_build_id = {}
# Initial length based on column headings.
longest_build_id = 18
for o in objfiles:
if not o.is_file or o.build_id is None or o.owner is not None:
continue
p = pathlib.Path(o.filename).resolve()
b = o.build_id
path_to_build_id[p] = {"objfile": b, "corefile": "missing"}
if len(b) > longest_build_id:
longest_build_id = len(b)
core_mapped_files = inf.corefile.mapped_files()
for m in core_mapped_files:
p = pathlib.Path(m.filename).resolve()
b = m.build_id
if b is not None and len(b) > longest_build_id:
longest_build_id = len(b)
if p in path_to_build_id:
path_to_build_id[p]["corefile"] = b
format_str = (
"%-" + str(longest_build_id) + "s %-" + str(longest_build_id) + "s %s"
)
def make_title(string, length=0):
if length > 0:
padding_len = length - len(string)
else:
padding_len = 0
padding = " " * padding_len
style = gdb.Style("title")
return style.apply(string) + padding
print(
"%s %s %s"
% (
make_title("Objfile Build-Id", longest_build_id),
make_title("Core File Build-Id", longest_build_id),
make_title("File Name"),
)
)
for p, b in path_to_build_id.items():
print(format_str % (b["objfile"], b["corefile"], p))
ShowBuildIds()
class CheckMainExec(gdb.Command):
def __init__(self):
gdb.Command.__init__(self, "check-main-executable", gdb.COMMAND_DATA)
def invoke(self, args, from_tty):
inf = gdb.selected_inferior()
pspace = inf.progspace
exec_filename = pathlib.Path(pspace.executable_filename).resolve()
count = 0
core_mapped_files = inf.corefile.mapped_files()
for m in core_mapped_files:
if not m.is_main_executable:
continue
p = pathlib.Path(m.filename).resolve()
count += 1
assert exec_filename == p, "main exec filename mismatch"
assert count == 1, "invalid main executable count"
print("PASS")
CheckMainExec()
# An 'events' prefix command.
class events_cmd(gdb.Command):
"""Information about recent Python events."""
def __init__(self):
gdb.Command.__init__(self, "events", gdb.COMMAND_USER, prefix=True)
# An 'events corefile_changed' sub-command.
class events_corefile_changed_cmd(gdb.Command):
"""Check recent corefile_changed events.
Requires a single argument either 'check' or 'reset'. With
'check', print details of every recent corefile_changed event.
With 'reset' clear the list of recent corefile_changed events."""
def __init__(self):
gdb.Command.__init__(self, "events corefile_changed", gdb.COMMAND_USER)
self._events = []
gdb.events.corefile_changed.connect(lambda e: self._corefile_changed_handler(e))
def _corefile_changed_handler(self, event):
assert isinstance(event, gdb.CorefileChangedEvent)
inf = event.inferior
assert isinstance(inf, gdb.Inferior)
corefile = inf.corefile
if corefile is not None:
assert corefile.is_valid()
corefile = corefile.filename
obj = {"inferior": inf.num, "corefile": corefile}
self._events.append(obj)
def invoke(self, args, from_tty):
if args == "check":
if len(self._events) == 0:
print("No corefile_changed event has been seen.")
else:
total = len(self._events)
for idx, obj in enumerate(self._events, start=1):
inf_num = obj["inferior"]
corefile = obj["corefile"]
if corefile is None:
msg = "None"
else:
msg = corefile
print(
"Event {}/{}, Inferior {}, Corefile {}".format(
idx, total, inf_num, msg
)
)
elif args == "reset":
self._events = []
else:
raise gdb.GdbError("Unknown command args: {}".format(args))
# An 'events exited' sub-command.
class events_exited_cmd(gdb.Command):
"""Check recent exited events.
Requires a single argument either 'check' or 'reset'. With
'check', print details of every recent exited event. With 'reset'
clear the list of recent exited events."""
def __init__(self):
gdb.Command.__init__(self, "events exited", gdb.COMMAND_USER)
self._events = []
gdb.events.exited.connect(lambda e: self._exited_handler(e))
def _exited_handler(self, event):
assert isinstance(event, gdb.ExitedEvent)
inf = event.inferior
assert isinstance(inf, gdb.Inferior)
if hasattr(event, "exit_code"):
assert isinstance(event.exit_code, int)
exit_code = event.exit_code
else:
exit_code = None
obj = {"inferior": inf.num, "exit_code": exit_code}
self._events.append(obj)
def invoke(self, args, from_tty):
if args == "check":
if len(self._events) == 0:
print("No exited event has been seen.")
else:
total = len(self._events)
for idx, obj in enumerate(self._events, start=1):
inf_num = obj["inferior"]
exit_code = obj["exit_code"]
if exit_code is None:
msg = "None"
else:
msg = exit_code
print(
"Event {}/{}, Inferior {}, Exit Code {}".format(
idx, total, inf_num, msg
)
)
elif args == "reset":
self._events = []
else:
raise gdb.GdbError("Unknown command args: {}".format(args))
events_cmd()
events_corefile_changed_cmd()
events_exited_cmd()
print("Success")