[RFC 3/3] gdb/debuginfod: support on-demand debuginfo downloading
Andrew Burgess <[email protected]>
| Newsgroups | gmane.comp.gdb.patches |
|---|---|
| Message-ID | <bfec66c89dedf0d67017cc62337580a543673900.1786715843.git.aburgess@redhat.com> |
From: Aaron Merey <[email protected]> At the beginning of a session, GDB may attempt to download debuginfo for all shared libraries associated with the process or core file being debugged. This can be a waste of time and storage space when much of the debuginfo ends up not being used during the session. To reduce the GDB's startup latency and to download only the debuginfo that is really needed, this patch adds on-demand downloading of debuginfo. The command 'set debuginfo enabled on' now causes GDB to attempt to download a .gdb_index for each shared library instead of its full debuginfo. Each corresponding separate debuginfo will be deferred until GDB needs to expand symtabs associated with the debuginfo's index. Because these indices are significantly smaller than their corresponding debuginfo, this generally reduces the total amount of data GDB downloads. Reductions of 80%-95% have been observed when debugging large GUI programs. (gdb) set debuginfod enabled on (gdb) start Downloading section .gdb_index for /lib64/libcurl.so.4 [...] 1826 client->server_mhandle = curl_multi_init (); (gdb) step Downloading separate debug info for /lib64/libcurl.so.4 Downloading separate debug info for [libcurl dwz] Downloading source file /usr/src/debug/curl-7.85.0-6.fc37.x86_64/build-full/lib/../../lib/multi.c curl_multi_init () at ../../lib/multi.c:457 457 { (gdb) Some of the key functions in this patch include dwarf2_has_separate_index which downloads the separate .gdb_index. If successful, a shared library objfile owns the index until its separate debug objfile is downloaded or found to be unavailable. read_full_dwarf_from_debuginfod downloads the full debuginfo and initializes the separate debug objfile. It is called by functions such as dw2_expand_symtabs_matching_symbol and dwarf2_gdb_index::expand_all_symtabs when symtab expansion is required. The deferred-download.exp test is a basic test for the core functionality. The deferred-expand-warning.exp test is testing for the following edge case: 1. When find_pc_sect_compunit_symtab matches a PC in a deferred objfile's index, it triggers download of full debuginfo and creation of a separate debug objfile. 2. An observer for this new objfile (e.g., an auto-loaded Python script) might trigger symtab expansion, for example, by searching for a symbol within the objfile. 4. When the loop in find_pc_sect_compunit_symtab continues to the new objfile, its symtabs may already be expanded, but the first loop in find_pc_sect_compunit_symtab (which searches expanded symtabs) never had a chance to see this expanded symtab because the objfile didn't exist then. 5. When the second loop finds a matching symtab, which is already expanded, which the first loop didn't find, GDB emits a warning with text like: "Internal error: pc ... in read in CU, but not in symtab". This warning is only emitted when warn_if_readin is true. To avoid this warning, we pass warn_if_readin as false when checking an objfile that was downloaded due to deferred debug download. The deferred-pending.exp test is testing for the following situation: 1. When on-demand downloading triggers a download, finish_new_objfile is called for the new separate debug objfile, which can then call breakpoint_re_set. 2. If there are deferred breakpoints then GDB will search for debug information relating to the breakpoint symbol. 3. If the objfile for which the new debug info was downloaded is still marked as having deferred debug information, and there is a deferred breakpoint within this objfile, then GDB will try to re-download the debug information for this objfile, which cases GDB to then add another new objfile, which calls finish_new_objfile, and thus GDB gets stuck in a loop. The solution is to clear the deferred debug status flag early in read_full_dwarf_from_debuginfod. Once read_full_dwarf_from_debuginfod is reached we have already decided to download the deferred debug information. After this the fact that this download was deferred is no longer relevant. The deferred-pending.exp test sets pending breakpoints on functions in two libraries with deferred downloads, then runs the inferior. If the deferred status is still set when finish_new_objfile is called GDB will enter the recursion loop, and eventually crash. The deferred-frame-cache.exp test is checking that GDB is able to to establish the current frame when a core file is loaded. In order to establish the current frame GDB calls lookup_selected_frame (frame.c), which may cause on-demand debuginfo download. If a new_objfile observer calls gdb.invalidate_cached_frames() during this download, the frame cache is cleared. The problem is that frame selection is done early in select_frame (frame.c), when selected_frame is set. The on-demand debug info downloading is triggered late within select_frame, by the call to find_compunit_symtab_for_pc. As a consequence, of clearing the frame cache, the selected_frame will be reset back to NULL. The fix in lookup_selected_frame handles this by retrying the frame lookup if selected_frame is nullptr after the initial attempt. This works because we assume the second lookup will not trigger the same observers (all debug should now be downloaded), and so, for the second call, the selected_frame will not be reset. Without this fix, get_selected_frame hits an assertion failure because selected_frame is NULL after a call to lookup_selected_frame. The test: 1. Builds a shared library that crashes via null pointer dereference. 2. Strips debuginfo for debuginfod to serve on-demand. 3. Runs the program outside GDB to generate a core file. 4. Starts GDB and loads a Python observer that invalidates frame cache from the new_objfile observer. This will trigger for on demand debuginfo load. 5. Loads the core file, triggering the frame cache invalidation. 6. Checks for assertions from GDB. The new deferred-observer-symbols.exp test is checking that when a deferred separate debug file is loaded, a Python observer, triggered from the new_objfile handler, is able to search for, and find, symbols within the newly added separate debug file. If GDB incorrectly manages the deferred debug state within the objfile then trying to access these symbols as soon as the new objfile is loaded can cause GDB to try and re-download the separate debug info file, in a recursive loop. This will eventually cause GDB to crash. The deferred-no-debuginfo.exp test is checking that GDB can handle the problem case where debuginfod serves a valid .gdb_index, but when asked for the full debug information, either gives back a file containing no debug information, or doesn't respond at all. In both cases, GDB should continue as if there is no debug infomration available, the test is ensuring that there are no crashes or other strange behaviours. The deferred-auto-load.exp test is checking that GDB will correctly auto-load from any .debug_gdb_scripts sections within separate debug info file that are downloaded from debuginfod. The test places a script within the main executable and within a shared library, the debug info is then split into a separate file, the separate debug file contains the .debug_gdb_scripts section. The separate debug files are then served to GDB from debuginfod. The deferred-dwz.exp test sets up a DWZ file with content pulled from two shared libraries. The libraries make use of deferred debug information downloading. The test ensures that the DWZ file is also downloaded correctly. --- gdb/dwarf2/index-cache.c | 22 +- gdb/dwarf2/index-cache.h | 11 + gdb/dwarf2/public.h | 12 + gdb/dwarf2/read-gdb-index.c | 185 +++++++++++++ gdb/dwarf2/read-gdb-index.h | 9 + gdb/dwarf2/read.c | 183 ++++++++++++- gdb/dwarf2/read.h | 9 + gdb/elfread.c | 3 +- gdb/frame.c | 52 ++-- gdb/objfile-flags.h | 4 + gdb/objfiles.h | 18 ++ gdb/symfile-debug.c | 11 + .../gdb.debuginfod/deferred-auto-load-lib.c | 26 ++ .../deferred-auto-load-py-script.h | 41 +++ .../gdb.debuginfod/deferred-auto-load.c | 27 ++ .../gdb.debuginfod/deferred-auto-load.exp | 223 ++++++++++++++++ .../gdb.debuginfod/deferred-download-lib1.c | 43 +++ .../gdb.debuginfod/deferred-download-lib2.c | 37 +++ .../gdb.debuginfod/deferred-download.c | 29 ++ .../gdb.debuginfod/deferred-download.exp | 158 +++++++++++ .../gdb.debuginfod/deferred-dwz-common.h | 41 +++ .../gdb.debuginfod/deferred-dwz-lib1.c | 24 ++ .../gdb.debuginfod/deferred-dwz-lib2.c | 24 ++ gdb/testsuite/gdb.debuginfod/deferred-dwz.c | 30 +++ gdb/testsuite/gdb.debuginfod/deferred-dwz.exp | 247 ++++++++++++++++++ .../deferred-expand-warning-lib.c | 36 +++ .../gdb.debuginfod/deferred-expand-warning.c | 25 ++ .../deferred-expand-warning.exp | 177 +++++++++++++ .../gdb.debuginfod/deferred-expand-warning.py | 61 +++++ .../gdb.debuginfod/deferred-frame-cache-lib.c | 25 ++ .../gdb.debuginfod/deferred-frame-cache.c | 28 ++ .../gdb.debuginfod/deferred-frame-cache.exp | 180 +++++++++++++ .../gdb.debuginfod/deferred-frame-cache.py | 68 +++++ .../deferred-no-debuginfo-lib.c | 22 ++ .../gdb.debuginfod/deferred-no-debuginfo.c | 25 ++ .../gdb.debuginfod/deferred-no-debuginfo.exp | 194 ++++++++++++++ .../deferred-observer-symbols.exp | 161 ++++++++++++ .../deferred-observer-symbols.py | 62 +++++ .../gdb.debuginfod/deferred-pending.exp | 172 ++++++++++++ .../deferred-select-frame-lib1.c | 26 ++ .../deferred-select-frame-lib2.c | 26 ++ .../gdb.debuginfod/deferred-select-frame.c | 61 +++++ .../gdb.debuginfod/deferred-select-frame.exp | 240 +++++++++++++++++ .../gdb.debuginfod/deferred-select-frame.py | 44 ++++ gdb/testsuite/lib/debuginfod-support.exp | 31 ++- gdb/testsuite/lib/gdb.exp | 8 +- 46 files changed, 3102 insertions(+), 39 deletions(-) create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-auto-load-lib.c create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-auto-load-py-script.h create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-auto-load.c create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-auto-load.exp create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-download-lib1.c create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-download-lib2.c create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-download.c create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-download.exp create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-dwz-common.h create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-dwz-lib1.c create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-dwz-lib2.c create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-dwz.c create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-dwz.exp create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-expand-warning-lib.c create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-expand-warning.c create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-expand-warning.exp create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-expand-warning.py create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-frame-cache-lib.c create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-frame-cache.c create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-frame-cache.exp create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-frame-cache.py create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-no-debuginfo-lib.c create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-no-debuginfo.c create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-no-debuginfo.exp create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-observer-symbols.exp create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-observer-symbols.py create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-pending.exp create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-select-frame-lib1.c create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-select-frame-lib2.c create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-select-frame.c create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-select-frame.exp create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-select-frame.py diff --git a/gdb/dwarf2/index-cache.c b/gdb/dwarf2/index-cache.c index 0e0aca3ac6d..709f99003be 100644 --- a/gdb/dwarf2/index-cache.c +++ b/gdb/dwarf2/index-cache.c @@ -217,14 +217,22 @@ index_cache::lookup_gdb_index (const bfd_build_id *build_id, /* Compute where we would expect a gdb index file for this build id to be. */ std::string filename = make_index_filename (build_id, INDEX4_SUFFIX); + return lookup_gdb_index (filename.c_str (), resource); +} + +/* See index-cache.h. */ + +gdb::array_view<const gdb_byte> +index_cache::lookup_gdb_index (const char *filename, + std::unique_ptr<index_cache_resource> *resource) +{ try { - index_cache_debug ("trying to read %s", - filename.c_str ()); + index_cache_debug ("trying to read %s", filename); /* Try to map that file. */ index_cache_resource_mmap *mmap_resource - = new index_cache_resource_mmap (filename.c_str ()); + = new index_cache_resource_mmap (filename); /* Yay, it worked! Hand the resource to the caller. */ resource->reset (mmap_resource); @@ -236,7 +244,7 @@ index_cache::lookup_gdb_index (const bfd_build_id *build_id, catch (const gdb_exception_error &except) { index_cache_debug ("couldn't read %s: %s", - filename.c_str (), except.what ()); + filename, except.what ()); } return {}; @@ -253,6 +261,12 @@ index_cache::lookup_gdb_index (const bfd_build_id *build_id, return {}; } +gdb::array_view<const gdb_byte> +index_cache::lookup_gdb_index_debuginfod + (const char *index_path, std::unique_ptr<index_cache_resource> *resource) +{ + return {}; +} #endif /* See dwarf-index-cache.h. */ diff --git a/gdb/dwarf2/index-cache.h b/gdb/dwarf2/index-cache.h index 4307bf97cf8..74ba3094444 100644 --- a/gdb/dwarf2/index-cache.h +++ b/gdb/dwarf2/index-cache.h @@ -93,6 +93,17 @@ class index_cache lookup_gdb_index (const bfd_build_id *build_id, index_cache_resource_up *resource); + /* Look for an index file located at INDEX_PATH. If found, return the + contents as an array_view and store the underlying resources (allocated + memory, mapped file, etc) in RESOURCE. The returned array_view is valid + as long as RESOURCE is not destroyed. + + If no matching index file is found, return an empty array view. This + function does not exit early if the index cache has not been enabled. */ + gdb::array_view<const gdb_byte> + lookup_gdb_index (const char *filename, + std::unique_ptr<index_cache_resource> *resource); + /* Return the number of cache hits. */ unsigned int n_hits () const { return m_n_hits; } diff --git a/gdb/dwarf2/public.h b/gdb/dwarf2/public.h index 2fb81a0749c..a439958d596 100644 --- a/gdb/dwarf2/public.h +++ b/gdb/dwarf2/public.h @@ -50,6 +50,11 @@ extern void dwarf2_build_frame_info (struct objfile *); void dwarf2_append_unwinders (struct gdbarch *gdbarch); +/* Query debuginfod for the .gdb_index associated with OBJFILE. + Used to defer separate debuginfo downloading until necessary. */ + +extern bool dwarf2_has_separate_index (struct objfile *); + #else /* DWARF_FORMAT_AVAILABLE */ static inline bool @@ -67,6 +72,13 @@ dwarf2_build_frame_info (struct objfile *) warning (_("No dwarf support available.")); } +static inline bool +dwarf2_has_separate_index (struct objfile *) +{ + warning (_("No dwarf support available.")); + return false; +} + #endif /* DWARF_FORMAT_AVAILABLE */ #endif /* GDB_DWARF2_PUBLIC_H */ diff --git a/gdb/dwarf2/read-gdb-index.c b/gdb/dwarf2/read-gdb-index.c index 324984c0de2..f261171166a 100644 --- a/gdb/dwarf2/read-gdb-index.c +++ b/gdb/dwarf2/read-gdb-index.c @@ -72,6 +72,186 @@ class cooked_gdb_index : public cooked_index int version; }; +/* A quick_symbol_functions implementation for objfiles with deferred + debuginfo downloading. This class encapsulates the + download-and-delegate pattern: each method checks whether the index + indicates a match, downloads the full debug info if so, then + delegates the query to the newly-created child objfile. + + By concentrating all deferred logic here, the normal (non-deferred) + code paths in dwarf2_gdb_index, cooked_index_functions, and + dwarf2_base_index_functions remain free of OBJF_DOWNLOAD_DEFERRED + checks. */ + +struct deferred_index_functions : public dwarf2_gdb_index +{ + bool has_symbols (struct objfile *) override + { return true; } + + bool has_unexpanded_symtabs (struct objfile *) override + { return true; } + + void expand_all_symtabs (struct objfile *objfile) override; + struct symtab *find_last_source_symtab (struct objfile *objfile) override; + + struct compunit_symtab *find_pc_sect_compunit_symtab + (struct objfile *objfile, bound_minimal_symbol msymbol, + CORE_ADDR pc, struct obj_section *section, + int warn_if_readin) override; + + iteration_status search + (struct objfile *objfile, + search_symtabs_file_matcher file_matcher, + const lookup_name_info *lookup_name, + search_symtabs_symbol_matcher symbol_matcher, + compunit_symtab_iteration_callback compunit_callback, + block_search_flags search_flags, + domain_search_flags domain, + search_symtabs_lang_matcher lang_matcher) override; + + void dump (struct objfile *objfile) override; + + /* No-ops — deferred index has no data for these. */ + void forget_cached_source_info (struct objfile *) override + { } + + void print_stats (struct objfile *, bool) override + { } + + void map_symbol_filenames (objfile *, symbol_filename_listener, + bool) override + { } + + struct symbol *find_symbol_by_address (struct objfile *, + CORE_ADDR) override + { return nullptr; } +}; + +/* See read-gdb-index.h. */ + +quick_symbol_functions_up +make_deferred_gdb_index_functions () +{ + return quick_symbol_functions_up (new deferred_index_functions); +} + +void +deferred_index_functions::expand_all_symtabs (struct objfile *objfile) +{ + struct objfile *child = read_full_dwarf_from_debuginfod (objfile, this); + if (child != nullptr) + child->expand_all_symtabs (); +} + +struct symtab * +deferred_index_functions::find_last_source_symtab (struct objfile *objfile) +{ + struct objfile *child = read_full_dwarf_from_debuginfod (objfile, this); + if (child != nullptr) + return child->find_last_source_symtab (); + return nullptr; +} + +struct compunit_symtab * +deferred_index_functions::find_pc_sect_compunit_symtab + (struct objfile *objfile, + bound_minimal_symbol msymbol, + CORE_ADDR pc, + struct obj_section *section, + int warn_if_readin) +{ + dwarf2_per_objfile *per_objfile = get_dwarf2_per_objfile (objfile); + dwarf2_per_bfd *per_bfd = per_objfile->per_bfd; + + if (per_bfd->index_table == nullptr) + return nullptr; + + CORE_ADDR baseaddr = objfile->text_section_offset (); + dwarf2_per_cu *data + = per_bfd->index_table->lookup ((unrelocated_addr) (pc - baseaddr)); + if (data == nullptr) + return nullptr; + + /* PC matches a symbol in the index but full debuginfo hasn't been + acquired yet. Download it and search the separate debug objfile + directly. Pass warn_if_readin=0 because the download triggers a + new_objfile observer that may legitimately expand symtabs in the + child before we query it. */ + struct objfile *child = read_full_dwarf_from_debuginfod (objfile, this); + if (child != nullptr) + return child->find_pc_sect_compunit_symtab (msymbol, pc, section, 0); + return nullptr; +} + +iteration_status +deferred_index_functions::search + (struct objfile *objfile, + search_symtabs_file_matcher file_matcher, + const lookup_name_info *lookup_name, + search_symtabs_symbol_matcher symbol_matcher, + compunit_symtab_iteration_callback compunit_callback, + block_search_flags search_flags, + domain_search_flags domain, + search_symtabs_lang_matcher lang_matcher) +{ + if (lookup_name == nullptr) + { + /* No name to filter by — we must download to answer the query. */ + struct objfile *child = read_full_dwarf_from_debuginfod (objfile, this); + if (child != nullptr) + return child->search (file_matcher, nullptr, nullptr, + compunit_callback, search_flags, domain, lang_matcher); + return iteration_status::keep_going; + } + + /* Check if the name exists in the index before downloading. */ + cooked_index *table = wait (objfile, true); + + lookup_name_info lookup_name_without_params + = lookup_name->make_ignore_params (); + bool completing = lookup_name->completion_mode (); + + /* Unique styles of language splitting. */ + static const enum language unique_styles[] = + { + language_c, + language_cplus, + language_d, + language_ada + }; + + for (enum language lang : unique_styles) + { + std::vector<std::string_view> name_vec + = lookup_name_without_params.split_name (lang); + std::vector<std::string> name_str_vec (name_vec.begin (), + name_vec.end ()); + + auto range = table->find (name_str_vec.back (), completing); + if (range.begin () != range.end ()) + { + /* Found a potential match in the index. Download and + delegate. */ + struct objfile *child = read_full_dwarf_from_debuginfod (objfile, this); + if (child != nullptr) + return child->search (file_matcher, lookup_name, + symbol_matcher, compunit_callback, + search_flags, domain, lang_matcher); + return iteration_status::keep_going; + } + } + + /* Name not found in the index — no download needed. */ + return iteration_status::keep_going; +} + +void +deferred_index_functions::dump (struct objfile *objfile) +{ + dwarf2_gdb_index::dump (objfile); + gdb_printf (" (deferred debuginfod download pending)\n"); +} + /* See above. */ void @@ -542,6 +722,11 @@ create_cus_from_gdb_index (dwarf2_per_bfd *per_bfd, gdb_assert (per_bfd->all_units.empty ()); per_bfd->all_units.reserve ((cu_list_elements + dwz_elements) / 2); + /* An index might be read before the debug_info section is available. + Create a placeholder section. */ + if (per_bfd->infos.empty ()) + per_bfd->infos.resize (1); + create_cus_from_gdb_index_list (per_bfd, cu_list, cu_list_elements, &per_bfd->infos[0], 0, units); diff --git a/gdb/dwarf2/read-gdb-index.h b/gdb/dwarf2/read-gdb-index.h index 56e112c6cbb..f03a8b97dd9 100644 --- a/gdb/dwarf2/read-gdb-index.h +++ b/gdb/dwarf2/read-gdb-index.h @@ -26,6 +26,7 @@ struct dwarf2_per_bfd; struct dwarf2_per_objfile; struct dwz_file; struct objfile; +struct quick_symbol_functions; /* .gdb_index doesn't distinguish between the various "other" symbols -- but the symbol search machinery really wants to. For example, @@ -58,4 +59,12 @@ bool dwarf2_read_gdb_index get_gdb_index_contents_ftype get_gdb_index_contents, get_gdb_index_contents_dwz_ftype get_gdb_index_contents_dwz); +/* Create a quick_symbol_functions for an objfile with deferred + debuginfo downloading. The returned object uses the .gdb_index + to gate downloads, only downloading full debug info when the + index indicates a match. */ + +extern std::unique_ptr<struct quick_symbol_functions> + make_deferred_gdb_index_functions (); + #endif /* GDB_DWARF2_READ_GDB_INDEX_H */ diff --git a/gdb/dwarf2/read.c b/gdb/dwarf2/read.c index 3557d88833a..8d434fc84aa 100644 --- a/gdb/dwarf2/read.c +++ b/gdb/dwarf2/read.c @@ -36,6 +36,7 @@ #include "dwarf2/cooked-index-worker.h" #include "dwarf2/cooked-indexer.h" #include "dwarf2/cu.h" +#include "dwarf2/frame.h" #include "dwarf2/index-cache.h" #include "dwarf2/leb.h" #include "dwarf2/line-header.h" @@ -98,6 +99,8 @@ #include "gdbsupport/unordered_set.h" #include "extract-store-integer.h" #include "cli/cli-style.h" +#include "inferior.h" +#include "debuginfod-support.h" /* See read.h. */ unsigned int dwarf_read_debug = 0; @@ -2113,6 +2116,65 @@ get_gdb_index_contents_from_cache_dwz (objfile *obj, dwz_file *dwz) return global_index_cache.lookup_gdb_index (build_id, &dwz->index_cache_res); } +/* Query debuginfod for the .gdb_index matching OBJFILE's build-id. Return the + contents if successful. */ + +static gdb::array_view<const gdb_byte> +get_gdb_index_contents_from_debuginfod (objfile *objfile, + dwarf2_per_bfd *per_bfd) +{ + const bfd_build_id *build_id = build_id_bfd_get (objfile->obfd.get ()); + if (build_id == nullptr) + return {}; + + gdb::unique_xmalloc_ptr<char> index_path; + scoped_fd fd = debuginfod_section_query (build_id->data, build_id->size, + bfd_get_filename + (objfile->obfd.get ()), + ".gdb_index", + &index_path); + if (fd.get () < 0) + return {}; + + return global_index_cache.lookup_gdb_index + (index_path.get (), &per_bfd->index_cache_res); +} + +/* Read the .gdb_index from DWZ, a dwz file, which we're looking at as + a consequence of loading OBJ. + + This function is passed to dwarf2_read_gdb_index only in the case + where we are performing deferred debug info download, and will only + be called if OBJ has an associated dwz file. + + In order to create a dwz_file for an objfile GDB checks two things: + + 1. The objfile must have some debug information, and + + 2. The objfile must have a link to the dwz file, + e.g. .gnu_debugaltlink. + + However, if an objfile satisfies (1) then we shouldn't need to use + deferred debug information downloading, and so this function should + never be called. + + As such, we don't actually try to fetch the .gdb_index for the dwz + file here, we just emit a warning, and return an empty array view. + Back in dwarf2_read_gdb_index the empty array view will trigger + another warning, and then GDB will proceed without any index for + OBJ. + + Right now I cannot imagine how we'd trigger this situation, but if + we ever see this then we can updated this code as appropriate. */ + +static gdb::array_view<const gdb_byte> +get_gdb_index_contents_from_debuginfod_dwz (objfile *obj, dwz_file *dwz) +{ + warning (_("ignoring unexpected dwz file for %ps"), + styled_string (file_name_style.style (), objfile_name (obj))); + return {}; +} + static void start_debug_info_reader (dwarf2_per_objfile *); /* See dwarf2/public.h. */ @@ -2122,11 +2184,13 @@ dwarf2_initialize_objfile (struct objfile *objfile, const struct dwarf2_debug_sections *names, bool can_copy) { - if (!dwarf2_has_info (objfile, names, can_copy)) + if (!dwarf2_has_info (objfile, names, can_copy) + && (objfile->flags & OBJF_DOWNLOAD_DEFERRED) == 0) return false; dwarf2_per_objfile *per_objfile = get_dwarf2_per_objfile (objfile); dwarf2_per_bfd *per_bfd = per_objfile->per_bfd; + bool separate_index = false; dwarf_read_debug_printf ("called"); @@ -2153,12 +2217,21 @@ dwarf2_initialize_objfile (struct objfile *objfile, dwarf_read_debug_printf ("found gdb index from file"); /* ... otherwise, try to find the index in the index cache. */ else if (dwarf2_read_gdb_index (per_objfile, - get_gdb_index_contents_from_cache, - get_gdb_index_contents_from_cache_dwz)) + get_gdb_index_contents_from_cache, + get_gdb_index_contents_from_cache_dwz)) { dwarf_read_debug_printf ("found gdb index from cache"); global_index_cache.hit (); } + /* Try to read just a separately downloaded gdb index. */ + else if ((objfile->flags & OBJF_DOWNLOAD_DEFERRED) != 0 + && dwarf2_read_gdb_index (per_objfile, + get_gdb_index_contents_from_debuginfod, + get_gdb_index_contents_from_debuginfod_dwz)) + { + dwarf_read_debug_printf ("found .gdb_index from debuginfod"); + separate_index = true; + } else { global_index_cache.miss (); @@ -2169,7 +2242,11 @@ dwarf2_initialize_objfile (struct objfile *objfile, { if (dwarf_synchronous) per_bfd->index_table->wait_completely (); - objfile->add_qf (per_bfd->index_table->make_quick_functions ()); + + if (separate_index) + objfile->add_qf (make_deferred_gdb_index_functions ()); + else + objfile->add_qf (per_bfd->index_table->make_quick_functions ()); } return true; @@ -2177,6 +2254,104 @@ dwarf2_initialize_objfile (struct objfile *objfile, /* See read.h. */ +objfile * +read_full_dwarf_from_debuginfod (struct objfile *objfile, + quick_symbol_functions *qf) +{ + gdb_assert ((objfile->flags & OBJF_DOWNLOAD_DEFERRED) != 0); + + /* However this function exits, we no longer want OBJFILE to be + marked as having deferred debug information. Of particular + interest is the case where we do load some separate debug + information and then add it into the program space. This can + trigger a call to breakpoint_re_set, which, if this object is + still marked as having deferred debug information, could result + in recursion, where we try to re-download the same debug + information. + + Clearing the deferred marker now is fine, nothing called from + here relies on this flag. */ + objfile->remove_deferred_status (qf); + + const struct bfd_build_id *build_id = build_id_bfd_get (objfile->obfd.get ()); + if (build_id == nullptr) + return nullptr; + + const char *filename = bfd_get_filename (objfile->obfd.get ()); + gdb::unique_xmalloc_ptr<char> symfile_path; + scoped_fd fd; + + fd = debuginfod_debuginfo_query (build_id->data, build_id->size, + filename, &symfile_path); + if (fd.get () < 0) + return nullptr; + + /* Separate debuginfo successfully retrieved from server. */ + gdb_bfd_ref_ptr debug_bfd = symfile_bfd_open (symfile_path.get ()); + if (debug_bfd == nullptr + || !build_id_verify (debug_bfd.get (), build_id->size, build_id->data)) + { + warning (_("File \"%s\" from debuginfod cannot be opened as bfd"), + filename); + return nullptr; + } + + /* This may trigger a dwz download. */ + symbol_file_add_separate (debug_bfd, symfile_path.get (), + current_inferior ()->symfile_flags, objfile); + return objfile->separate_debug_objfile; +} + +/* See public.h. */ + +bool +dwarf2_has_separate_index (struct objfile *objfile) +{ + /* An objfile already marked as OBJF_DOWNLOAD_DEFERRED will not get + here, as GDB will see such an objfile as having debug + information. See dwarf2_initialize_objfile for details. */ + gdb_assert ((objfile->flags & OBJF_DOWNLOAD_DEFERRED) == 0); + + /* There is no point deferring the download of the debug information + for the main objfile, we (almost) always end up needing the debug + information, so downloading the index first is just adding + additional work. */ + if ((objfile->flags & OBJF_MAINLINE) != 0) + return false; + + /* Objfiles marked OBJF_NOT_FILENAME include things like the vDSO + objfile, and JIT registered objfiles, for both of which, + downloading an index doesn't make much sense. */ + if ((objfile->flags & OBJF_NOT_FILENAME) != 0) + return false; + + const bfd_build_id *build_id = build_id_bfd_get (objfile->obfd.get ()); + + if (build_id == nullptr) + return false; + + gdb::unique_xmalloc_ptr<char> index_path; + scoped_fd fd = debuginfod_section_query (build_id->data, + build_id->size, + bfd_get_filename + (objfile->obfd.get ()), + ".gdb_index", + &index_path); + + if (fd.get () < 0) + return false; + + /* We found a separate .gdb_index file so a separate debuginfo file should + exist, but we don't want to download it until necessary. Associate the + index with this objfile and defer the debuginfo download until symtabs + referenced by the index need to be expanded. */ + objfile->flags |= OBJF_DOWNLOAD_DEFERRED; + dwarf2_initialize_objfile (objfile); + return true; +} + + + void dwarf2_find_base_address (struct die_info *die, struct dwarf2_cu *cu) { diff --git a/gdb/dwarf2/read.h b/gdb/dwarf2/read.h index 15dd2abf3a1..765ab54cfa1 100644 --- a/gdb/dwarf2/read.h +++ b/gdb/dwarf2/read.h @@ -1489,4 +1489,13 @@ extern struct dwarf2_section_info *get_debug_line_section extern bool is_ada_import_or_export (dwarf2_cu *cu, const char *name, const char *linkagename); +/* If OBJFILE contains information from a separately downloaded .gdb_index, + attempt to download the full debuginfo. QF is the deferred + quick_symbol_functions that triggered the download; it will be + removed from OBJFILE's qf list. Returns the separate debug + objfile on success, or nullptr on failure. */ + +extern objfile *read_full_dwarf_from_debuginfod (struct objfile *, + quick_symbol_functions *); + #endif /* GDB_DWARF2_READ_H */ diff --git a/gdb/elfread.c b/gdb/elfread.c index e3890ae0270..f51636090db 100644 --- a/gdb/elfread.c +++ b/gdb/elfread.c @@ -1222,7 +1222,8 @@ elf_symfile_read_dwarf2 (struct objfile *objfile, && objfile->separate_debug_objfile_backlink == NULL) { if (objfile->find_and_add_separate_symbol_file (symfile_flags)) - gdb_assert (objfile->separate_debug_objfile != nullptr); + gdb_assert (objfile->separate_debug_objfile != nullptr + || (objfile->flags & OBJF_DOWNLOAD_DEFERRED) != 0); else has_dwarf2 = false; } diff --git a/gdb/frame.c b/gdb/frame.c index 912404cd26a..077be739710 100644 --- a/gdb/frame.c +++ b/gdb/frame.c @@ -756,44 +756,42 @@ skip_tailcall_frames (const frame_info_ptr &initial_frame) frame. */ static void -compute_frame_id (const frame_info_ptr &fi) +compute_frame_id_1 (const frame_info_ptr &fi) { FRAME_SCOPED_DEBUG_START_END ("fi=%d", fi->level); gdb_assert (fi->this_id.p == frame_id_status::NOT_COMPUTED); - unsigned int entry_generation = get_frame_cache_generation (); + scoped_defer_reinit_frame_cache defer_reinit; - try - { - /* Mark this frame's id as "being computed. */ - fi->this_id.p = frame_id_status::COMPUTING; + auto restore_not_computed = make_scope_exit ([&] () { + fi->this_id.p = frame_id_status::NOT_COMPUTED; + }); - /* Find the unwinder. */ - if (fi->unwind == NULL) - frame_unwind_find_by_frame (fi, &fi->prologue_cache); + /* Mark this frame's id as "being computed. */ + fi->this_id.p = frame_id_status::COMPUTING; - /* Find THIS frame's ID. */ - /* Default to outermost if no ID is found. */ - fi->this_id.value = outer_frame_id; - fi->unwind->this_id (fi, &fi->prologue_cache, &fi->this_id.value); - gdb_assert (frame_id_p (fi->this_id.value)); + /* Find the unwinder. */ + if (fi->unwind == NULL) + frame_unwind_find_by_frame (fi, &fi->prologue_cache); - /* Mark this frame's id as "computed". */ - fi->this_id.p = frame_id_status::COMPUTED; + /* Find THIS frame's ID. Default to outermost if no ID is + found. */ + fi->this_id.value = outer_frame_id; + fi->unwind->this_id (fi, &fi->prologue_cache, &fi->this_id.value); + gdb_assert (frame_id_p (fi->this_id.value)); - frame_debug_printf (" -> %s", fi->this_id.value.to_string ().c_str ()); - } - catch (const gdb_exception &ex) - { - /* On error, revert the frame id status to not computed. If the frame - cache generation changed, the frame object doesn't exist anymore, so - don't touch it. */ - if (get_frame_cache_generation () == entry_generation) - fi->this_id.p = frame_id_status::NOT_COMPUTED; + /* Mark this frame's id as "computed". */ + fi->this_id.p = frame_id_status::COMPUTED; + restore_not_computed.release (); - throw; - } + frame_debug_printf (" -> %s", fi->this_id.value.to_string ().c_str ()); +} + +static void +compute_frame_id (const frame_info_ptr &fi) +{ + with_protected_frame_cache (compute_frame_id_1, fi); } /* Return a frame uniq ID that can be used to, later, re-find the diff --git a/gdb/objfile-flags.h b/gdb/objfile-flags.h index 3f362fc5fa5..f5338c45ece 100644 --- a/gdb/objfile-flags.h +++ b/gdb/objfile-flags.h @@ -56,6 +56,10 @@ enum objfile_flag : unsigned /* User requested that we do not read this objfile's symbolic information. */ OBJF_READNEVER = 1 << 6, + + /* A separate .gdb_index has been downloaded for this objfile. + Debuginfo for this objfile can be downloaded when required. */ + OBJF_DOWNLOAD_DEFERRED = 1 << 7, }; DEF_ENUM_FLAGS_TYPE (enum objfile_flag, objfile_flags); diff --git a/gdb/objfiles.h b/gdb/objfiles.h index 12e77d51b74..7ba30c04cfe 100644 --- a/gdb/objfiles.h +++ b/gdb/objfiles.h @@ -629,6 +629,24 @@ struct objfile : intrusive_list_node<objfile> domain_search_flags domain, bool *symbol_found_p); + /* Used to clear OBJF_DOWNLOAD_DEFERRED status when the debug objfile has + either been acquired or could not be found. QF is the + quick_symbol_functions entry to remove from the qf list (the + deferred index that triggered the download). */ + void remove_deferred_status (quick_symbol_functions *qf_to_remove) + { + flags &= ~OBJF_DOWNLOAD_DEFERRED; + + /* Remove the deferred quick_symbol_functions from the qf list. + If available the separate debug objfile's index will be used + instead, since that objfile actually contains the symbols and CUs + referenced in the index. */ + m_qf.remove_if ([&] (const quick_symbol_functions_up &qf_up) + { + return qf_up.get () == qf_to_remove; + }); + } + /* Return the relocation offset applied to SECTION. */ CORE_ADDR section_offset (bfd_section *section) const { diff --git a/gdb/symfile-debug.c b/gdb/symfile-debug.c index e009821f78c..4e425c1ae79 100644 --- a/gdb/symfile-debug.c +++ b/gdb/symfile-debug.c @@ -35,6 +35,7 @@ #include "filenames.h" #include "build-id.h" #include "debuginfod-support.h" +#include "dwarf2/public.h" /* We need to save a pointer to the real symbol functions. Plus, the debug versions are malloc'd because we have to NULL out the @@ -630,6 +631,16 @@ objfile::find_and_add_separate_symbol_file (symfile_add_flags symfile_flags) = simple_find_and_open_separate_symbol_file (this, find_separate_debug_file_by_debuglink, &warnings); + /* Attempt to download only a '.gdb_index' from the separate + debug info. As with the full debuginfo download below, only + attempt this once. */ + if (debug_bfd == nullptr && attempt == 0 + && dwarf2_has_separate_index (this)) + { + has_dwarf2 = true; + break; + } + /* Only try debuginfod on the first attempt. Sure, we could imagine an extension that somehow adds the required debug info to the debuginfod server but, at least for now, we don't support this diff --git a/gdb/testsuite/gdb.debuginfod/deferred-auto-load-lib.c b/gdb/testsuite/gdb.debuginfod/deferred-auto-load-lib.c new file mode 100644 index 00000000000..5b086cbe336 --- /dev/null +++ b/gdb/testsuite/gdb.debuginfod/deferred-auto-load-lib.c @@ -0,0 +1,26 @@ +/* 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 "deferred-auto-load-py-script.h" + +volatile int global_lib_var = 0; + +void +lib_func (void) +{ + global_lib_var = 42; /* lib_func breakpoint. */ +} diff --git a/gdb/testsuite/gdb.debuginfod/deferred-auto-load-py-script.h b/gdb/testsuite/gdb.debuginfod/deferred-auto-load-py-script.h new file mode 100644 index 00000000000..d6ed8eaeacd --- /dev/null +++ b/gdb/testsuite/gdb.debuginfod/deferred-auto-load-py-script.h @@ -0,0 +1,41 @@ +/* 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 "symcat.h" +#include "gdb/section-scripts.h" + +/* Ensure SCRIPT_SUFFIX is defined. */ +#ifndef SCRIPT_SUFFIX +#error missing SCRIPT_SUFFIX definition +#endif + +/* Create a Python script in a section. */ +#define DEFINE_GDB_SCRIPT_TEXT \ +asm( \ +".pushsection \".debug_gdb_scripts\", \"S\",%progbits\n" \ +".byte " XSTRING (SECTION_SCRIPT_ID_PYTHON_TEXT) "\n" \ +".ascii \"gdb.inlined-script." XSTRING(SCRIPT_SUFFIX) "\\n\"\n" \ +".ascii \"filename = gdb.current_objfile().filename\\n\"\n" \ +".ascii \"if not filename in global_auto_load_tracker:\\n\"\n" \ +".ascii \" global_auto_load_tracker[filename] = 0\\n\"\n" \ +".ascii \"global_auto_load_tracker[filename] += 1\\n\"\n" \ + ".ascii \"print('deferred-auto-load: script loaded: " XSTRING(SCRIPT_SUFFIX) "')\\n\"\n" \ +".byte 0\n" \ +".popsection\n" \ +); + +DEFINE_GDB_SCRIPT_TEXT diff --git a/gdb/testsuite/gdb.debuginfod/deferred-auto-load.c b/gdb/testsuite/gdb.debuginfod/deferred-auto-load.c new file mode 100644 index 00000000000..2c814d9c6ae --- /dev/null +++ b/gdb/testsuite/gdb.debuginfod/deferred-auto-load.c @@ -0,0 +1,27 @@ +/* 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 "deferred-auto-load-py-script.h" + +extern void lib_func (void); + +int +main () +{ + lib_func (); + return 0; +} diff --git a/gdb/testsuite/gdb.debuginfod/deferred-auto-load.exp b/gdb/testsuite/gdb.debuginfod/deferred-auto-load.exp new file mode 100644 index 00000000000..23468b0fc3e --- /dev/null +++ b/gdb/testsuite/gdb.debuginfod/deferred-auto-load.exp @@ -0,0 +1,223 @@ +# 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 that auto-loaded Python scripts from .debug_gdb_scripts +# sections in separate debug info files works correctly with on-demand +# debuginfo downloading. +# +# The test embeds a .debug_gdb_scripts section into the separate debug +# file of both the executable, and a shared library. When the +# deferred debuginfo is downloaded, GDB should auto-load and execute +# the embedded script. + +standard_testfile .c -lib.c -script.S + +load_lib debuginfod-support.exp + +require is_elf_target +require allow_debuginfod_tests +require allow_debuginfod_section_downloads +require allow_python_tests + +set lib_testfile "lib${testfile}.so" +set lib_srcfile $srcfile2 +set lib_binfile [standard_output_file $lib_testfile] + +# Build the shared library. +if { [build_executable "build $lib_testfile" $lib_binfile $lib_srcfile \ + [list debug build-id shlib \ + additional_flags=-I${srcdir}/../../include \ + additional_flags=-DSCRIPT_SUFFIX=lib]] != 0 } { + return +} + +# Build the main executable. +if { [build_executable "build executable" $binfile $srcfile \ + [list debug build-id shlib=$lib_binfile \ + additional_flags=-I${srcdir}/../../include \ + additional_flags=-DSCRIPT_SUFFIX=main]] != 0 } { + return +} + +# Start GDB, required for ensure_gdb_index call. +clean_restart + +# Add .gdb_index to the shared library. +if { [ensure_gdb_index $lib_binfile "" $lib_testfile] != 1 } { + untested "failed to add .gdb_index to $lib_testfile" + return +} + +# Add .gdb_index to the executable. +if { [ensure_gdb_index $binfile "" $testfile] != 1 } { + untested "failed to add .gdb_index to $testfile" + return +} + +# Strip debuginfo from shared library into a separate file. +if { [gdb_gnu_strip_debug $lib_binfile] != 0 } { + fail "strip $lib_testfile debuginfo" + return +} +set lib_debuginfo $lib_binfile.debug + +# Strip debuginfo from executable into a separate file. +if { [gdb_gnu_strip_debug $binfile] != 0 } { + fail "strip $testfile debuginfo" + return +} +set debuginfo $binfile.debug + +# Move debuginfo files into the directory that debuginfod will serve. +set debugdir [standard_output_file "debug"] +file mkdir $debugdir +file rename -force $lib_debuginfo $debugdir +file rename -force $debuginfo $debugdir + +# Helper proc to return the filename of the debuginfod cache file +# containing the full separate debuginfo for FILENAME. CACHE is the +# absolute directory name of the debuginfod client cache. The +# resulting filename is calculated based on the build-id of FILENAME, +# we don't check if the file actually exists in the cache. +proc debuginfod_cache_file { cache filename } { + set buildid [get_build_id $filename] + return [file join $cache $buildid debuginfo] +} + +# Test that the auto-loaded script from .debug_gdb_scripts in the +# separate debug files are executed when the deferred debuginfo is +# downloaded. +proc_with_prefix test_auto_load_script { cache } { + # Delete client cache so debuginfo downloads again. + file delete -force $cache + clean_restart + + gdb_test_no_output "set debuginfod enabled on" + gdb_test_no_output "set progress-bars enabled off" + + # Setup a global Python dictionary which will track scripts as + # they are auto-loaded. The keys of this dictionary are the + # objfile filenames from which scipts were loaded, and the values + # are the number of times the script was loaded. + gdb_test_no_output "python global_auto_load_tracker = {}" + + # Allow auto-loading from any file relating to this test. This + # will allow scripts to be loaded from the executable, the shared + # library, and from anything downloaded from debuginfod. We only + # expect to load scripts from the debuginfod client cache, but we + # want the other things in the safe path in case anything goes + # wrong, then we'll get to see GDB loading things from the wrong + # place. + # + # In real life it might be a bad idea to allow scripts downloaded + # from debuginfod to be auto-loaded and run, but for this test it + # should be fine as we only use a local debuginfod, and we control + # the script content. + gdb_test_no_output "set auto-load safe-path [standard_output_file {}]" \ + "set auto-load safe-path" + + gdb_load $::binfile + + if { ![runto_main] } { + return + } + + # At this point the .gdb_index should have been downloaded for the + # shared library, but its full debuginfo should still be deferred, + # so the auto-load script within the library should not have been + # loaded yet. + # + # However, the auto-load script for the main executable should + # have been loaded as the main executable's full debuginfo will + # have been downloaded by now. + gdb_test "info auto-load python-scripts" \ + "Yes\\s+gdb\\.inlined-script\\.main\\s*" \ + "check auto-loaded python scripts before download" + set cache_file [debuginfod_cache_file $cache $::binfile] + gdb_test_no_output "python assert(global_auto_load_tracker\[\"$cache_file\"\] == 1)" \ + "check executable script was loaded once before library has loaded" + gdb_test_no_output "python assert(len(global_auto_load_tracker.keys()) == 1)" \ + "script from just one source has been loaded" + + set lineno [gdb_get_line_number "lib_func breakpoint" $::srcfile2] + + # Set a breakpoint in the library to trigger the deferred + # debuginfo download. + set saw_script_loaded false + set saw_download false + set saw_breakpoint false + gdb_test_multiple "break lib_func" "break lib_func" { + -re "^break lib_func\r\n" { + exp_continue + } + + -re "^deferred-auto-load: script loaded: lib\r\n" { + set saw_script_loaded true + exp_continue + } + -re "^Downloading \[^\r\n\]*separate debug info\[^\r\n\]*\r\n" { + set saw_download true + exp_continue + } + -re "^Breakpoint $::decimal at $::hex\[^\r\n\]+\r\n" { + set saw_breakpoint true + exp_continue + } + -re "^$::gdb_prompt $" { + gdb_assert { $saw_download && $saw_script_loaded && $saw_breakpoint } \ + $gdb_test_name + } + -re "^\[^\r\n\]*\r\n" { + exp_continue + } + } + + # Verify the auto-loaded script appears in the list. + gdb_test "info auto-load python-scripts" \ + [multi_line \ + "Yes\\s+gdb\\.inlined-script\\.lib\\s*" \ + "Yes\\s+gdb\\.inlined-script\\.main\\s*"] \ + "scripts loaded after library debuginfo download" + + # Continue to the breakpoint. + gdb_test "continue" "Breakpoint $::decimal, lib_func.*" \ + "continue to lib_func" + + # We should now have loaded the auto-load script from both the + # main executable, and from the shared library. Check each script + # has been loaded exactly once. + set lib_cache_file [debuginfod_cache_file $cache $::lib_binfile] + gdb_test_no_output "python assert(global_auto_load_tracker\[\"$lib_cache_file\"\] == 1)" \ + "check library script was loaded once" + gdb_test_no_output "python assert(global_auto_load_tracker\[\"$cache_file\"\] == 1)" \ + "check executable script was loaded once" + gdb_test_no_output "python assert(len(global_auto_load_tracker.keys()) == 2)" \ + "scripts from two sources loaded" +} + +prepare_for_debuginfod cache db + +with_debuginfod_env $cache { + set url [start_debuginfod $db $debugdir] + if { $url == "" } { + unresolved "failed to start debuginfod server" + return + } + + setenv DEBUGINFOD_URLS $url + test_auto_load_script $cache +} + +stop_debuginfod diff --git a/gdb/testsuite/gdb.debuginfod/deferred-download-lib1.c b/gdb/testsuite/gdb.debuginfod/deferred-download-lib1.c new file mode 100644 index 00000000000..93e79c72f86 --- /dev/null +++ b/gdb/testsuite/gdb.debuginfod/deferred-download-lib1.c @@ -0,0 +1,43 @@ +/* This testcase is part of GDB, the GNU debugger. + + 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/>. */ + +#include <stdio.h> +#include <pthread.h> +#include <unistd.h> + +extern void libdeferred2_test (); +extern void *libdeferred2_thread_test (void *); + +static volatile int flag = 0; + +void +libdeferred1_test () +{ + pthread_t thr; + + printf ("In libdeferred1\n"); + libdeferred2_test (); + + pthread_create (&thr, NULL, libdeferred2_thread_test, (void *) &flag); + + /* Give the new thread a chance to actually enter libdeferred2_thread_test. */ + while (!flag) + ; + + printf ("Cancelling thread\n"); + pthread_cancel (thr); +} diff --git a/gdb/testsuite/gdb.debuginfod/deferred-download-lib2.c b/gdb/testsuite/gdb.debuginfod/deferred-download-lib2.c new file mode 100644 index 00000000000..97630e8f4ee --- /dev/null +++ b/gdb/testsuite/gdb.debuginfod/deferred-download-lib2.c @@ -0,0 +1,37 @@ +/* This testcase is part of GDB, the GNU debugger. + + 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/>. */ + +#include <stdio.h> +#include <pthread.h> + +void +libdeferred2_test () +{ + printf ("In libdeferred2\n"); +} + +void * +libdeferred2_thread_test (void *arg) +{ + int *flag = (int *) arg; + printf ("In thread test\n"); + + while (1) + *flag = 1; + + return NULL; +} diff --git a/gdb/testsuite/gdb.debuginfod/deferred-download.c b/gdb/testsuite/gdb.debuginfod/deferred-download.c new file mode 100644 index 00000000000..6fd34cddd60 --- /dev/null +++ b/gdb/testsuite/gdb.debuginfod/deferred-download.c @@ -0,0 +1,29 @@ +/* This testcase is part of GDB, the GNU debugger. + + 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/>. */ + +#include <stdio.h> + +extern void libdeferred1_test (); + +int +main () +{ + libdeferred1_test (); + printf ("in deferred exec\n"); + + return 0; +} diff --git a/gdb/testsuite/gdb.debuginfod/deferred-download.exp b/gdb/testsuite/gdb.debuginfod/deferred-download.exp new file mode 100644 index 00000000000..60dee8f28e5 --- /dev/null +++ b/gdb/testsuite/gdb.debuginfod/deferred-download.exp @@ -0,0 +1,158 @@ +# 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 debuginfod's ability to download .gdb-index section prior to +# downloading the full debug information file. + +standard_testfile .c -lib1.c -lib2.c + +load_lib debuginfod-support.exp + +require allow_debuginfod_tests +require allow_debuginfod_section_downloads + +# BINFILE calls a function from LIB_SL1. +set lib1_testfile "libdeferred1.so" +set lib1_srcfile $srcfile2 +set lib1_binfile [standard_output_file $lib1_testfile] + +# LIB1_BINFILE calls functions from LIB2_BINFILE. +set lib2_testfile "libdeferred2.so" +set lib2_srcfile $srcfile3 +set lib2_binfile [standard_output_file $lib2_testfile] + +# Build LIB1_BINFILE, LIB2_BINFILE, and BINFILE. +if { [build_executable "build $lib2_testfile" $lib2_binfile $lib2_srcfile \ + {debug build-id shlib}] != 0 } { + return +} + +if { [build_executable "build $lib1_testfile" $lib1_binfile $lib1_srcfile \ + [list debug build-id shlib_pthreads shlib=$lib2_binfile]] != 0 } { + return +} + +if { [build_executable "build executable" $binfile $srcfile \ + [list debug build-id shlib=$lib1_binfile shlib=$lib2_binfile]] != 0 } { + return +} + +# Start GDB now, we need GDB running in order to check the index types +# within the compiled executable and shared libraries. +clean_restart + +# Make sure libdeferred1 and libdeferred2 contain .gdb_index. +if { [ensure_gdb_index $lib1_binfile "" "libdeferred1"] != 1 } { + untested "failed to add .gdb_index to $lib1_testfile" + return +} + +if { [ensure_gdb_index $lib2_binfile "" "libdeferred2"] != 1 } { + untested "failed to add .gdb_index to $lib2_testfile" + return +} + +# Strip solib debuginfo into separate files. +if { [gdb_gnu_strip_debug $lib1_binfile ""] != 0} { + fail "strip $lib1_testfile debuginfo" + return +} + +if { [gdb_gnu_strip_debug $lib2_binfile ""] != 0} { + fail "strip $lib2_testfile debuginfo" + return +} + +# Move debuginfo files into directory that debuginfod will serve from. +set debugdir [standard_output_file "debug"] +set debuginfo_sl1 [standard_output_file $lib1_testfile.debug] +set debuginfo_sl2 [standard_output_file $lib2_testfile.debug] + +file mkdir $debugdir +file rename -force $debuginfo_sl1 $debugdir +file rename -force $debuginfo_sl2 $debugdir + +# Restart GDB and clear the debuginfod client cache. Then load BINFILE into +# GDB and start running it. Match output with pattern RES and use TESTNAME +# as the test name. +proc_with_prefix clean_restart_with_prompt { binfile testname } { + global cache + + # Delete client cache so debuginfo downloads again. + file delete -force $cache + clean_restart + + gdb_test_no_output "set debuginfod enabled on" \ + "clean_restart enable $testname" + gdb_test_no_output "set progress-bars enabled off" \ + "disable progress bars $testname" + gdb_load $binfile + + runto_main +} + +# Tests with no debuginfod server running. +proc_with_prefix no_url { } { + gdb_load $::binfile + if {![runto_main]} { + return + } + + # Check that no section is downloaded and no debuginfo is found. + gdb_test "info sharedlibrary" ".*Yes \\(\\*\\).*$::lib1_testfile.*" \ + "found no url lib1" + gdb_test "info sharedlibrary" ".*Yes \\(\\*\\).*$::lib2_testfile.*" \ + "found no url lib2" +} + +# Tests with a debuginfod server running. +proc_with_prefix local_url { } { + global debugdir db + + set url [start_debuginfod $db $debugdir] + if { $url == "" } { + unresolved "failed to start debuginfod server" + return + } + + # Point GDB to the server. + setenv DEBUGINFOD_URLS $url + + clean_restart_with_prompt $::binfile "index" + + # Download debuginfo when stepping into a function. + set res ".*separate debug info for [string_to_regexp $::lib1_binfile].*\"In libdeferred1\\\\n\".*" + gdb_test "step" $res "step" + + clean_restart_with_prompt $::binfile "break" + + # Download debuginfo when setting a breakpoint. + set res ".*separate debug info for [string_to_regexp $::lib2_binfile].*" + gdb_test "br libdeferred2_test" $res "break set" + + # Hit the breakpoint. + set res ".*Breakpoint 2, libdeferred2_test.*\"In libdeferred2\\\\n\".*" + gdb_test "c" $res "break continue" +} + +# Create CACHE and DB directories ready for debuginfod to use. +prepare_for_debuginfod cache db + +with_debuginfod_env $cache { + no_url + local_url +} + +stop_debuginfod diff --git a/gdb/testsuite/gdb.debuginfod/deferred-dwz-common.h b/gdb/testsuite/gdb.debuginfod/deferred-dwz-common.h new file mode 100644 index 00000000000..89ba734fa7a --- /dev/null +++ b/gdb/testsuite/gdb.debuginfod/deferred-dwz-common.h @@ -0,0 +1,41 @@ +/* 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/>. */ + +/* Common type shared between the two libraries. Having a shared type + that appears in both libraries gives the DWZ tool something to + extract into the common DWZ file. */ + +struct common_type +{ + int x; + int y; +}; + +/* A common function used in both libraries. Being defined in a + header file means its debug info will appear in both libraries' + DWARF, giving DWZ something to deduplicate. We must force this + function to be inlined though, as it is the abstact instance of + this function that will be deduplicate, not the concrete instance. + If we don't force the function inline, then all we get is a + non-inline instance within each library, and the DWARF for these + two instances will not be moved into the DWZ file. */ + +static inline int __attribute__((always_inline)) +common_add (struct common_type *ct) +{ + return ct->x + ct->y; +} diff --git a/gdb/testsuite/gdb.debuginfod/deferred-dwz-lib1.c b/gdb/testsuite/gdb.debuginfod/deferred-dwz-lib1.c new file mode 100644 index 00000000000..7ebc153a99b --- /dev/null +++ b/gdb/testsuite/gdb.debuginfod/deferred-dwz-lib1.c @@ -0,0 +1,24 @@ +/* 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 "deferred-dwz-common.h" + +int +lib1_func (struct common_type *ct) +{ + return common_add (ct) + 1; /* lib1_func breakpoint. */ +} diff --git a/gdb/testsuite/gdb.debuginfod/deferred-dwz-lib2.c b/gdb/testsuite/gdb.debuginfod/deferred-dwz-lib2.c new file mode 100644 index 00000000000..748476b33ba --- /dev/null +++ b/gdb/testsuite/gdb.debuginfod/deferred-dwz-lib2.c @@ -0,0 +1,24 @@ +/* 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 "deferred-dwz-common.h" + +int +lib2_func (struct common_type *ct) +{ + return common_add (ct) + 2; /* lib2_func breakpoint. */ +} diff --git a/gdb/testsuite/gdb.debuginfod/deferred-dwz.c b/gdb/testsuite/gdb.debuginfod/deferred-dwz.c new file mode 100644 index 00000000000..0d144fe937e --- /dev/null +++ b/gdb/testsuite/gdb.debuginfod/deferred-dwz.c @@ -0,0 +1,30 @@ +/* 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 "deferred-dwz-common.h" + +extern int lib1_func (struct common_type *ct); +extern int lib2_func (struct common_type *ct); + +int +main () +{ + struct common_type ct = { 10, 20 }; + int r1 = lib1_func (&ct); + int r2 = lib2_func (&ct); + return r1 + r2; +} diff --git a/gdb/testsuite/gdb.debuginfod/deferred-dwz.exp b/gdb/testsuite/gdb.debuginfod/deferred-dwz.exp new file mode 100644 index 00000000000..8b6198acb21 --- /dev/null +++ b/gdb/testsuite/gdb.debuginfod/deferred-dwz.exp @@ -0,0 +1,247 @@ +# 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 on-demand debuginfo downloading when the separate debug files use the +# DWZ tool to share common debug info. +# +# Two shared libraries are compiled with a common header that provides a +# shared type and an inline function. The debug info is stripped into +# separate files and then 'dwz -m' is used to extract common DWARF into a +# DWZ file. +# +# This test is checking that having a DWZ file in the mix doesn't cause +# problems for the deferred debug information downloading. + +standard_testfile .c -lib1.c -lib2.c + +load_lib debuginfod-support.exp + +require allow_debuginfod_tests +require allow_debuginfod_section_downloads +require {dwz_version_at_least 0.13} + +set lib1_testfile "libdeferred-dwz1.so" +set lib1_srcfile $srcfile2 +set lib1_binfile [standard_output_file $lib1_testfile] + +set lib2_testfile "libdeferred-dwz2.so" +set lib2_srcfile $srcfile3 +set lib2_binfile [standard_output_file $lib2_testfile] + +# Build the two shared libraries. +if { [build_executable "build $lib1_testfile" $lib1_binfile $lib1_srcfile \ + {debug build-id shlib}] != 0 } { + return +} + +if { [build_executable "build $lib2_testfile" $lib2_binfile $lib2_srcfile \ + {debug build-id shlib}] != 0 } { + return +} + +# Build the main executable. +if { [build_executable "build executable" $binfile $srcfile \ + [list debug build-id \ + shlib=$lib1_binfile shlib=$lib2_binfile]] != 0 } { + return +} + +# If we are running with a board that splits the debug information out and +# adds a .gnu_debuglink, then this test isn't going to work. We want to be +# in control of splitting out the debug information. +if {[section_get $lib1_binfile ".gnu_debuglink"] ne ""} { + unsupported "debug information has already been split out" + return +} + +# Start GDB, required for ensure_gdb_index calls. +clean_restart + +# Add .gdb_index to both libraries and the executable. +if { [ensure_gdb_index $lib1_binfile "" $lib1_testfile] != 1 } { + untested "failed to add .gdb_index to $lib1_testfile" + return +} + +if { [ensure_gdb_index $lib2_binfile "" $lib2_testfile] != 1 } { + untested "failed to add .gdb_index to $lib2_testfile" + return +} + +if { [ensure_gdb_index $binfile "" $testfile] != 1 } { + untested "failed to add .gdb_index to $testfile" + return +} + +# Strip debuginfo into separate files. Use no-debuglink so that debuginfod +# is the only way to find the debug info. +if { [gdb_gnu_strip_debug $lib1_binfile no-debuglink] != 0 } { + fail "strip $lib1_testfile debuginfo" + return +} + +if { [gdb_gnu_strip_debug $lib2_binfile no-debuglink] != 0 } { + fail "strip $lib2_testfile debuginfo" + return +} + +if { [gdb_gnu_strip_debug $binfile no-debuglink] != 0 } { + fail "strip $testfile debuginfo" + return +} + +# Move the .debug files into the debug directory. +set debugdir [standard_output_file "debug"] +file mkdir $debugdir +file rename -force $lib1_binfile.debug $debugdir +file rename -force $lib2_binfile.debug $debugdir +file rename -force $binfile.debug $debugdir + +# Run dwz -m on the separate debug files to extract common DWARF into a +# shared DWZ file. Use relative filenames so that the .gnu_debugaltlink in +# the .debug files contains a relative path; GDB will need debuginfod to +# find the DWZ file. +with_cwd $debugdir { + set status \ + [remote_exec build "dwz -m ./common.dwz \ + ./$lib1_testfile.debug \ + ./$lib2_testfile.debug"] + if {[lindex $status 0] != 0} { + unsupported "unable to run dwz tool" + return + } +} + +# Some versions of dwz exit with code 0 but print error output. +if {[lindex $status 1] ne ""} { + unsupported "unexpected output from dwz tool" + return +} + +# Verify the DWZ file was created. +if { ![file exists $debugdir/common.dwz] } { + unsupported "dwz did not produce common.dwz" + return +} + +# The build-id for the first shared library. When the full debug +# information for this library is downloaded, we will also download the DWZ +# file. This build-id will appear in the 'Downloading ...' line, and we use +# this to check the line is seen. +set lib1_buildid [get_build_id $lib1_binfile] + +# Test that on-demand debuginfo downloading works correctly when the +# separate debug files reference a DWZ common file. +proc_with_prefix test_deferred_dwz { cache } { + # Delete client cache so debuginfo downloads again. + file delete -force $cache + + clean_restart + + gdb_test_no_output "set debuginfod enabled on" + gdb_test_no_output "set progress-bars enabled off" + + gdb_load $::binfile + + if { ![runto_main] } { + return + } + + # At this point the .gdb_index should have been downloaded for both + # shared libraries, and the full debuginfo downloads should be deferred. + + # Set a breakpoint in lib1. This should trigger the full debuginfo + # download for lib1, which includes downloading the DWZ common file. + set saw_lib_debuginfo_download false + set saw_dwz_download false + set saw_breakpoint false + set saw_other_download false + gdb_test_multiple "break lib1_func" "" { + -re "^Downloading \[^\r\n\]*separate debug info for [string_to_regexp $::lib1_binfile]\\.\\.\\.\r\n" { + set saw_lib_debuginfo_download true + exp_continue + } + -re "^Downloading \[^\r\n\]*separate debug info for \[^\r\n\]*/$::lib1_buildid/debuginfo\\.\\.\\.\r\n" { + set saw_dwz_download true + exp_continue + } + -re "^Downloading \[^\r\n\]*\r\n" { + set saw_other_download true + exp_continue + } + -re "^Breakpoint $::decimal at $::hex: \[^\r\n\]+\r\n" { + set saw_breakpoint true + exp_continue + } + -re "^$::gdb_prompt " { + gdb_assert { $saw_lib_debuginfo_download && $saw_dwz_download \ + && $saw_breakpoint && !$saw_other_download } \ + $gdb_test_name + } + -re "^\[^\r\n\]*\r\n" { + exp_continue + } + } + + # Continue to the breakpoint. + gdb_continue_to_breakpoint "breakpoint in lib1" + + # Set a breakpoint in lib2. This should trigger the full debuginfo + # download for lib2. The DWZ common file should already be available + # from the earlier download. + set saw_lib_debuginfo_download false + set saw_breakpoint false + set saw_other_download false + gdb_test_multiple "break lib2_func" "" { + -re "^Downloading \[^\r\n\]*separate debug info for [string_to_regexp $::lib2_binfile]\\.\\.\\.\r\n" { + set saw_lib_debuginfo_download true + exp_continue + } + -re "^Downloading \[^\r\n\]*\r\n" { + set saw_other_download true + exp_continue + } + -re "^Breakpoint $::decimal at $::hex: \[^\r\n\]+\r\n" { + set saw_breakpoint true + exp_continue + } + -re "^$::gdb_prompt " { + gdb_assert { $saw_lib_debuginfo_download && $saw_breakpoint \ + && !$saw_other_download } $gdb_test_name + } + -re "^\[^\r\n\]*\r\n" { + exp_continue + } + } + + # Continue to the breakpoint. + gdb_continue_to_breakpoint "breakpoint in lib2" +} + +# Create CACHE and DB directories ready for debuginfod to use. +prepare_for_debuginfod cache db + +with_debuginfod_env $cache { + set url [start_debuginfod $db $debugdir] + if { $url == "" } { + unresolved "failed to start debuginfod server" + return + } + + setenv DEBUGINFOD_URLS $url + test_deferred_dwz $cache +} + +stop_debuginfod diff --git a/gdb/testsuite/gdb.debuginfod/deferred-expand-warning-lib.c b/gdb/testsuite/gdb.debuginfod/deferred-expand-warning-lib.c new file mode 100644 index 00000000000..f92f55136cf --- /dev/null +++ b/gdb/testsuite/gdb.debuginfod/deferred-expand-warning-lib.c @@ -0,0 +1,36 @@ +/* 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/>. */ + +extern void libdeferred_expand_warning_test (void); + +static volatile int global_var = 0; + +/* A function that the Python observer will look up to trigger symtab + expansion. */ + +void +libdeferred_expand_warning_func (void) +{ + ++global_var; +} + +void +libdeferred_expand_warning_test (void) +{ + ++global_var; /* Library entry point. */ + libdeferred_expand_warning_func (); +} diff --git a/gdb/testsuite/gdb.debuginfod/deferred-expand-warning.c b/gdb/testsuite/gdb.debuginfod/deferred-expand-warning.c new file mode 100644 index 00000000000..c7a988cfa94 --- /dev/null +++ b/gdb/testsuite/gdb.debuginfod/deferred-expand-warning.c @@ -0,0 +1,25 @@ +/* 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/>. */ + +extern void libdeferred_expand_warning_test (void); + +int +main (void) +{ + libdeferred_expand_warning_test (); + return 0; +} diff --git a/gdb/testsuite/gdb.debuginfod/deferred-expand-warning.exp b/gdb/testsuite/gdb.debuginfod/deferred-expand-warning.exp new file mode 100644 index 00000000000..b7607e181a6 --- /dev/null +++ b/gdb/testsuite/gdb.debuginfod/deferred-expand-warning.exp @@ -0,0 +1,177 @@ +# 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 that on-demand debuginfo downloading handles the case where a +# new_objfile observer triggers symtab expansion in the newly-downloaded +# separate debug objfile. +# +# The code in GDB that causes the full debug information to be +# downloaded has two steps. First we check already expanded symtabs, +# then if no result is found, we check the unexpanded symtabs, and +# potentially expand them. +# +# Full debug information download only happens during the second +# phase, when a match is found in the index. After finding a match in +# the index GDB will give a warning if the symtab is already expanded. +# +# But with deferred debug information download, this is OK, as a new +# objfile observer, triggered when the full debug information is +# downloaded, might force symtab expansion, so we suppress the warning +# in this case. +# +# This test replicates this setup and checks that the warning doesn't +# trigger. + +standard_testfile .c -lib.c + +load_lib debuginfod-support.exp + +require allow_debuginfod_tests +require allow_debuginfod_section_downloads + +set lib_testfile "lib${testfile}.so" +set lib_srcfile $srcfile2 +set lib_binfile [standard_output_file $lib_testfile] +set pyscript $srcdir/$subdir/$testfile.py + +# Build the shared library. +if { [build_executable "build $lib_testfile" $lib_testfile $lib_srcfile \ + {debug build-id shlib}] != 0 } { + return +} + +# Build the main executable. +if { [build_executable "build executable" $testfile $srcfile \ + [list debug build-id shlib=$lib_binfile]] != 0 } { + return +} + +# Start GDB, required for ensure_gdb_index call. +clean_restart + +# Add .gdb_index to the shared library. +if { [ensure_gdb_index $lib_binfile "" $lib_testfile] != 1 } { + untested "failed to add .gdb_index to $lib_testfile" + return +} + +# Strip debuginfo into a separate file. +if { [gdb_gnu_strip_debug $lib_binfile] != 0 } { + fail "strip $lib_testfile debuginfo" + return +} + +# Move debuginfo file into directory that debuginfod will serve. +set debugdir [standard_output_file "debug"] +set lib_debuginfo [standard_output_file $lib_testfile.debug] + +file mkdir $debugdir +file rename -force $lib_debuginfo $debugdir + +# Test that stepping into the library triggers debuginfo download and +# the observer runs without causing an "Internal error". +proc_with_prefix test_observer_symtab_expansion { } { + global debugdir db + + set url [start_debuginfod $db $debugdir] + if { $url == "" } { + unresolved "failed to start debuginfod server" + return + } + + # Point GDB to the server. + setenv DEBUGINFOD_URLS $url + + # Start GDB. + clean_restart $::testfile + + # Enable debuginfod. + gdb_test_no_output "set debuginfod enabled on" \ + "enable debuginfod" + gdb_test_no_output "set progress-bars enabled off" + + # Load the Python script that registers the new_objfile observer. + gdb_test "source $::pyscript" \ + "Python observer: registered new_objfile handler" \ + "load python script" + + # Run to main. This will download the .gdb_index for the shared + # library, but there should be no need, at this point, to download + # the full debug information. + if {![runto_main]} { + return + } + + set lineno [gdb_get_line_number "Library entry point" $::srcfile2] + verbose -log "APB: Line $lineno" + + # Step into the library function. This should trigger the + # download of the full debug info. Downloading the full debug + # information adds a new objfile, which triggers the new objfile + # observer from our Python script. The new objfile observer + # performs a symbol lookup within the debug information which was + # just downloaded. If this is not handled correctly by GDB then + # there is the possibility that an internal error could be + # triggered. + set saw_observer false + set saw_error false + set saw_download false + set saw_location_line false + set found_symbol false + gdb_test_multiple "step" "step into library" { + -re "^step\r\n" { + exp_continue + } + -re "^Downloading.*separate debug info for \[^\r\n\]+/$::lib_testfile\\.\\.\\.\r\n" { + set saw_download true + exp_continue + } + -re "^Python observer: triggering symtab expansion for \[^\r\n\]+\r\n" { + set saw_observer 1 + exp_continue + } + -re "^Python observer: found symbol \[^\r\n\]+\r\n" { + set found_symbol true + exp_continue + } + -re "^libdeferred_expand_warning_test \\(\\) at \[^\r\n\]+/$::srcfile2:$lineno\r\n" { + set saw_location_line true + # We don't have a specific pattern for the source code + # line which follows this one. That's not really relevant + # to this test. + exp_continue + } + -re "^\[^\r\n\]*Internal error\[^\r\n\]*\r\n" { + set saw_error 1 + exp_continue + } + -re "^$::gdb_prompt $" { + gdb_assert { !$saw_error && $saw_observer && $saw_download && $saw_location_line && $found_symbol } \ + $gdb_test_name + } + -re "^\[^\r\n\]*\r\n" { + exp_continue + } + } +} + +# Create CACHE and DB directories ready for debuginfod to use. +prepare_for_debuginfod cache db + +with_debuginfod_env $cache { + test_observer_symtab_expansion +} + +stop_debuginfod diff --git a/gdb/testsuite/gdb.debuginfod/deferred-expand-warning.py b/gdb/testsuite/gdb.debuginfod/deferred-expand-warning.py new file mode 100644 index 00000000000..eedccabc160 --- /dev/null +++ b/gdb/testsuite/gdb.debuginfod/deferred-expand-warning.py @@ -0,0 +1,61 @@ +# 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/>. + +# This script registers a new_objfile event handler that triggers +# symtab expansion by looking up a symbol. This is used to test +# that on-demand debuginfo downloading correctly handles the case +# where an observer expands symtabs in the newly-downloaded objfile. + +import gdb + + +def new_objfile_handler(event): + """Handler for new_objfile events. + + When a new objfile is loaded, check if it's the separate debug + objfile for our test library. If so, look up a symbol to trigger + symtab expansion. See the .exp file for why we do this. + """ + objfile = event.new_objfile + + # Only trigger for separate debug objfiles (those with an owner). + if objfile.owner is None: + return + + # Check if this is the separate debug objfile for our test library. + # The owner's filename should contain our library name. + owner_filename = objfile.owner.filename + if owner_filename is None: + return + + if "libdeferred-expand-warning" not in owner_filename: + return + + print( + "Python observer: triggering symtab expansion for {}".format(objfile.filename) + ) + + # Look up a global symbol from this objfile to trigger symtab + # expansion. + sym = gdb.lookup_global_symbol("libdeferred_expand_warning_func") + if sym is not None: + print("Python observer: found symbol {}".format(sym.name)) + else: + raise RuntimeError("No symbol found") + + +# Register the new objfile event handler. +gdb.events.new_objfile.connect(new_objfile_handler) +print("Python observer: registered new_objfile handler") diff --git a/gdb/testsuite/gdb.debuginfod/deferred-frame-cache-lib.c b/gdb/testsuite/gdb.debuginfod/deferred-frame-cache-lib.c new file mode 100644 index 00000000000..4d4671596cf --- /dev/null +++ b/gdb/testsuite/gdb.debuginfod/deferred-frame-cache-lib.c @@ -0,0 +1,25 @@ +/* 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/>. */ + +/* Library function that causes a crash (SIGSEGV). */ + +void +crash_in_lib (void) +{ + volatile int *p = (volatile int *) 0; + *p = 42; /* This will crash. */ +} diff --git a/gdb/testsuite/gdb.debuginfod/deferred-frame-cache.c b/gdb/testsuite/gdb.debuginfod/deferred-frame-cache.c new file mode 100644 index 00000000000..0331f65dd2d --- /dev/null +++ b/gdb/testsuite/gdb.debuginfod/deferred-frame-cache.c @@ -0,0 +1,28 @@ +/* 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/>. */ + +/* Test program that crashes in a shared library to trigger debuginfo + download during crash handling. */ + +extern void crash_in_lib (void); + +int +main (void) +{ + crash_in_lib (); + return 0; +} diff --git a/gdb/testsuite/gdb.debuginfod/deferred-frame-cache.exp b/gdb/testsuite/gdb.debuginfod/deferred-frame-cache.exp new file mode 100644 index 00000000000..e44f80b8c25 --- /dev/null +++ b/gdb/testsuite/gdb.debuginfod/deferred-frame-cache.exp @@ -0,0 +1,180 @@ +# 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 that frame cache invalidation during debuginfo download is handled. +# +# When on-demand debuginfo downloading triggers, a new objfile is added +# and new_objfile observers are notified. If an observer calls +# gdb.invalidate_cached_frames(), the frame cache is cleared. +# +# GDB's lookup_selected_frame function handles this by retrying the frame +# lookup if selected_frame is nullptr after the initial lookup attempt. +# This retry mechanism ensures GDB doesn't hit an assertion failure in +# get_selected_frame() when an observer clears the frame cache during +# debuginfo download. +# +# This test creates a program that crashes in a shared library with +# deferred debuginfo, then uses a Python new_objfile observer to +# invalidate the frame cache when the debuginfo is downloaded. This +# exercises the frame cache invalidation code path and verifies GDB +# handles it gracefully. + +load_lib debuginfod-support.exp + +require allow_debuginfod_tests +require allow_debuginfod_section_downloads + +standard_testfile .c -lib.c + +set lib_testfile "lib-${testfile}.so" +set lib_srcfile $srcfile2 +set lib_binfile [standard_output_file $lib_testfile] + +# Build the library and executable. +if { [build_executable "build $lib_testfile" $lib_binfile $lib_srcfile \ + {debug build-id shlib}] } { + return +} + +if { [build_executable "build executable" $binfile $srcfile \ + [list debug build-id shlib=$lib_binfile]] != 0 } { + return +} + +clean_restart + +# Add .gdb_index to the library. +if { [ensure_gdb_index $lib_binfile "" "$lib_testfile"] != 1 } { + untested "failed to add .gdb_index to $lib_testfile" + return +} + +# Strip debuginfo into a separate file. +if { [gdb_gnu_strip_debug $lib_binfile ""] != 0 } { + fail "strip $lib_testfile debuginfo" + return +} + +# Move debuginfo file into directory that debuginfod will serve. +set debugdir [standard_output_file "debug"] +set debuginfo_sl [standard_output_file $lib_testfile.debug] + +file mkdir $debugdir +file rename -force $debuginfo_sl $debugdir + +# Python script that invalidates the frame cache when the separate debug +# objfile is loaded. +set pyscript [gdb_remote_download host ${srcdir}/${subdir}/${testfile}.py] + +# Test that frame lookup handles cache invalidation when loading a core file. +# +# When loading a core file, GDB needs to establish the current frame +# immediately. This triggers debuginfo download during lookup_selected_frame, +# and if an observer invalidates the frame cache, the retry mechanism is +# needed to avoid an assertion failure. +proc_with_prefix test_frame_cache_core { } { + global cache + + # Delete client cache so debuginfo downloads again. + file delete -force $cache + + # Run the program outside of GDB to generate a core file. + set corefile [core_find $::binfile] + if {$corefile == ""} { + untested "could not generate core file" + return + } + + # Now start GDB fresh without the executable loaded yet. We want + # the Python script (and its new_objfile observer) in place before + # we start loading anything. + clean_restart + + gdb_test_no_output "set debuginfod enabled on" + gdb_test_no_output "set progress-bars enabled off" + + # Load the Python script BEFORE loading anything else. This + # ensures the observer is registered when the core file triggers + # debuginfo download. + gdb_test "source $::pyscript" "Python registered new_objfile handler" \ + "load python script" + + # Load the executable. + gdb_load $::binfile + + # Now load the core file. This will: + # 1. GDB needs to establish the current frame from the core. + # 2. This triggers lookup_selected_frame. + # 3. select_frame calls find_compunit_symtab_for_pc. + # 4. This triggers debuginfo download for the library. + # 5. The new_objfile handler invalidates the frame cache. + # 6. Without the fix, get_selected_frame() hits an assertion + set saw_invalidate false + set saw_error false + set saw_prog_terminated false + set saw_crash_frame false + set saw_debug_download false + gdb_test_multiple "core-file $corefile" "load core file" { + -re "^Python new_objfile handler: frame cache invalidated\r\n" { + set saw_invalidate true + exp_continue + } + -re "^\[^\r\n\]*Internal error\[^\r\n\]*\r\n" { + set saw_error true + exp_continue + } + -re "^\[^\r\n\]*Assertion\[^\r\n\]*failed\[^\r\n\]*\r\n" { + set saw_error true + exp_continue + } + -re "^#0\\s+\[^\r\n\]*crash_in_lib \\(\\) at \[^\r\n\]+\r\n" { + set saw_crash_frame true + exp_continue + } + -re "^Program terminated with signal SIG\[^\r\n\]+\r\n" { + set saw_prog_terminated true + exp_continue + } + -re "^Downloading \[^\r\n\]*separate debug info for \[^\r\n\]*/$::lib_testfile\\.\\.\\.\r\n" { + set saw_debug_download true + exp_continue + } + -re "^$::gdb_prompt $" { + gdb_assert { !$saw_error \ + && $saw_invalidate \ + && $saw_prog_terminated \ + && $saw_crash_frame } \ + $gdb_test_name + } + -re "^\[^\r\n\]*\r\n" { + exp_continue + } + } +} + +prepare_for_debuginfod cache db + +with_debuginfod_env $cache { + set url [start_debuginfod $db $debugdir] + if { $url == "" } { + unresolved "failed to start debuginfod server" + return + } + + setenv DEBUGINFOD_URLS $url + test_frame_cache_core +} + +stop_debuginfod diff --git a/gdb/testsuite/gdb.debuginfod/deferred-frame-cache.py b/gdb/testsuite/gdb.debuginfod/deferred-frame-cache.py new file mode 100644 index 00000000000..e58285e279f --- /dev/null +++ b/gdb/testsuite/gdb.debuginfod/deferred-frame-cache.py @@ -0,0 +1,68 @@ +# 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/>. + +# Python script that invalidates the frame cache when the separate +# debug objfile for lib-deferred-frame-cache.so is loaded. This +# simulates an observer that needs to reset frame-related state when +# new debug info arrives. + +import gdb + +# Guard to only invalidate once. +_already_invalidated = False + +def new_objfile_handler(event): + """Called when a new objfile is loaded. + + When the separate debug objfile for our library is loaded, + invalidate the frame cache. This happens during the crash + handling when GDB is trying to establish the current frame. + If lookup_selected_frame doesn't handle this correctly, GDB + will hit an assertion failure. + """ + global _already_invalidated + + if _already_invalidated: + return + + objfile = event.new_objfile + + # Only interested in the separate debug objfile (has an owner). + if objfile.owner is None: + return + + owner_filename = objfile.owner.filename + if owner_filename is None or "lib-deferred-frame-cache.so" not in owner_filename: + return + + _already_invalidated = True + + print("Python new_objfile handler: invalidating frame cache") + + # This calls reinit_frame_cache() which clears selected_frame. + # We're inside the crash handling call chain where GDB is trying + # to establish the current frame. The call sequence is: + # handle_inferior_event -> ... -> get_selected_frame() + # -> lookup_selected_frame() -> select_frame() + # -> find_compunit_symtab_for_pc() -> download -> here + # + # After this, selected_frame is nullptr. Without the fix in + # lookup_selected_frame, get_selected_frame would hit an assertion. + gdb.invalidate_cached_frames() + + print("Python new_objfile handler: frame cache invalidated") + +gdb.events.new_objfile.connect(new_objfile_handler) +print("Python registered new_objfile handler") diff --git a/gdb/testsuite/gdb.debuginfod/deferred-no-debuginfo-lib.c b/gdb/testsuite/gdb.debuginfod/deferred-no-debuginfo-lib.c new file mode 100644 index 00000000000..7d13adf7846 --- /dev/null +++ b/gdb/testsuite/gdb.debuginfod/deferred-no-debuginfo-lib.c @@ -0,0 +1,22 @@ +/* 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/>. */ + +int +lib_func (int x) +{ + return x + 1; +} diff --git a/gdb/testsuite/gdb.debuginfod/deferred-no-debuginfo.c b/gdb/testsuite/gdb.debuginfod/deferred-no-debuginfo.c new file mode 100644 index 00000000000..9d02f809557 --- /dev/null +++ b/gdb/testsuite/gdb.debuginfod/deferred-no-debuginfo.c @@ -0,0 +1,25 @@ +/* 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/>. */ + +extern int lib_func (int x); + +int +main (void) +{ + int res = lib_func (42); + return res; +} diff --git a/gdb/testsuite/gdb.debuginfod/deferred-no-debuginfo.exp b/gdb/testsuite/gdb.debuginfod/deferred-no-debuginfo.exp new file mode 100644 index 00000000000..bd209a9027d --- /dev/null +++ b/gdb/testsuite/gdb.debuginfod/deferred-no-debuginfo.exp @@ -0,0 +1,194 @@ +# 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 on-demand debuginfo downloading when debuginfod can serve a +# .gdb_index section but the corresponding full debuginfo has had its +# DWARF sections stripped. +# +# This tests the edge case where the .gdb_index download succeeds +# (so GDB defers the full debuginfo download) but the full debuginfo +# file doesn't contain any useful debug information. GDB should +# handle this gracefully without crashing or asserting. +# +# This is probably as close as we can easily get to a debuginfod +# server that serves a .gdb_index, but then fails to serve the full +# debug information. + +standard_testfile .c -lib.c + +load_lib debuginfod-support.exp + +require allow_debuginfod_tests +require allow_debuginfod_section_downloads + +set lib_testfile "lib${testfile}.so" +set lib_srcfile $srcfile2 +set lib_binfile [standard_output_file $lib_testfile] + +# Build the shared library and executable. +if { [build_executable "build $lib_testfile" $lib_binfile $lib_srcfile \ + {debug build-id shlib}] } { + return +} + +if { [build_executable "build executable" $binfile $srcfile \ + [list debug build-id shlib=$lib_binfile]] } { + return +} + +# Start GDB for the ensure_gdb_index call. +clean_restart + +# Add .gdb_index to the shared library. +if { [ensure_gdb_index $lib_binfile "" $lib_testfile] != 1 } { + untested "failed to add .gdb_index to $lib_testfile" + return +} + +# Strip debuginfo into a separate file. +if { [gdb_gnu_strip_debug $lib_binfile] != 0 } { + fail "strip $lib_testfile debuginfo" + return +} + +# Now strip all DWARF debug sections from the separate debug file, +# leaving only the .gdb_index. This simulates the case where +# debuginfod can serve the index but the full debuginfo is not +# actually available. +set debuginfo_file [standard_output_file $lib_testfile.debug] + +set dwarf_sections {info abbrev line str line_str aranges ranges rnglists \ + macro loc loclists frame addr str_offsets types } + +set objcopy_args {} +foreach section $dwarf_sections { + lappend objcopy_args --remove-section=.debug_${section} + lappend objcopy_args --remove-section=.zdebug_${section} + lappend objcopy_args --remove-section=.debug_${section}.dwo + lappend objcopy_args --remove-section=.zdebug_${section}.dwo +} + +set objcopy_program [gdb_find_objcopy] +set result [catch {exec $objcopy_program {*}$objcopy_args $debuginfo_file} output] +if { $result != 0 } { + fail "strip DWARF sections from $debuginfo_file: $output" + return +} + +# Verify .gdb_index is still present but .debug_info is gone. +set readelf_program [gdb_find_readelf] +set result [catch {exec $readelf_program -S $debuginfo_file} output] +if { $result != 0 } { + unresolved "readelf on $debuginfo_file: $output" + return +} + +if { [string first ".gdb_index" $output] == -1 } { + unresolved ".gdb_index missing from stripped debuginfo" + return +} + +# Final sanity check, make sure we cannot find .debug_info (or one of +# its variants) within DEBUGINFO_FILE. +foreach section { .debug_info .zdebug_info .debug_info.dwo .zdebug_info.dwo } { + if { [string first $section $output] != -1 } { + unresolved "$section still present in stripped debuginfo" + return + } +} + +# Move the debuginfo file into the directory that debuginfod will +# serve from. +set debugdir [standard_output_file "debug"] +file mkdir $debugdir +file rename -force $debuginfo_file $debugdir + +# Test that GDB handles the missing DWARF gracefully. When +# EARLY_SHUTDOWN is true terminate debuginfod after downloading the +# .gdb_index, but before triggering download of the full debug +# information. +proc_with_prefix test_no_debuginfo { early_shutdown } { + global cache + + # Delete client cache so debuginfo downloads again. + file delete -force $cache + clean_restart + + gdb_test_no_output "set debuginfod enabled on" + gdb_test_no_output "set progress-bars enabled off" + + gdb_load $::binfile + + if { ![runto_main] } { + return + } + + # At this point the .gdb_index should have been downloaded for the + # shared library, and the full debuginfo download should still be + # deferred. + + if { $early_shutdown } { + # Shutdown debuginfod. This proc doesn't actually wait for + # the shutdown to complete, so give the process a couple of + # seconds. This isn't perfect, but should be good enough to + # surface any bugs. + # + # We don't want to perform a blocking wait as doing so risks + # deadlocking the testsuite if debuginfod misbehaves. And, + # having closed the process, we can no longer 'expect' on its + # output to watch it shutdown. + # + # Even if we could be sure that debuginfod has shut down, then + # we cannot guarantee that a parallel test run isn't going to + # start another debuginfod server in its place. + # + # At the end of the day, we're just hoping that if this does + # trigger a bug someone will think to look into it. + stop_debuginfod + sleep 2 + } + + # Try to set a breakpoint on lib_func. This should trigger the + # full debuginfo download. The downloaded file won't contain + # DWARF, so GDB won't get debug info, but it should not crash. + gdb_breakpoint "lib_func" -message + + # Continue to the breakpoint to verify the inferior still works. + # Also, as we are missing debug information for the shared library + # containing this breakpoint, the breakpoint will be attributed to + # the .so file rather than the original .c source file. + gdb_continue_to_breakpoint "run to breakpoint" \ + "lib_func \\(\\) from \[^\r\n\]+/${::lib_testfile}" +} + +# Create CACHE and DB directories ready for debuginfod to use. +prepare_for_debuginfod cache db + +with_debuginfod_env $cache { + foreach_with_prefix early_shutdown { false true } { + set url [start_debuginfod $db $debugdir] + if { $url == "" } { + unresolved "failed to start debuginfod server" + return + } + + setenv DEBUGINFOD_URLS $url + + test_no_debuginfo $early_shutdown + + stop_debuginfod + } +} + diff --git a/gdb/testsuite/gdb.debuginfod/deferred-observer-symbols.exp b/gdb/testsuite/gdb.debuginfod/deferred-observer-symbols.exp new file mode 100644 index 00000000000..c6632c65b51 --- /dev/null +++ b/gdb/testsuite/gdb.debuginfod/deferred-observer-symbols.exp @@ -0,0 +1,161 @@ +# 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 that full symbol info is available to new_objfile observers. +# +# When on-demand debuginfo downloading completes, the new_objfile observer +# is notified about the separate debug objfile. This test verifies that +# observers can access full symbol and type information at this point. +# +# The test registers a Python observer that looks up a symbol's type +# information when the separate debug objfile is loaded. + +standard_testfile deferred-download.c \ + deferred-download-lib1.c \ + deferred-download-lib2.c + +load_lib debuginfod-support.exp + +require allow_debuginfod_tests +require allow_debuginfod_section_downloads + +# BINFILE calls a function from LIB1_BINFILE. +set lib1_testfile "libdeferred1.so" +set lib1_srcfile $srcfile2 +set lib1_binfile [standard_output_file $lib1_testfile] + +# LIB1_BINFILE calls functions from LIB2_BINFILE. +set lib2_testfile "libdeferred2.so" +set lib2_srcfile $srcfile3 +set lib2_binfile [standard_output_file $lib2_testfile] + +# Build LIB1_BINFILE, LIB2_BINFILE, and BINFILE. +if { [build_executable "build $lib2_testfile" $lib2_binfile $lib2_srcfile \ + {debug build-id shlib}] } { + return +} + +if { [build_executable "build $lib1_testfile" $lib1_binfile $lib1_srcfile \ + [list debug build-id shlib_pthreads shlib=$lib2_binfile]] } { + return +} + +if { [build_executable "build executable" $binfile $srcfile \ + [list debug build-id shlib=$lib1_binfile shlib=$lib2_binfile]] } { + return +} + +# Start GDB now, we need GDB running in order to check the index types +# within the compiled executable and shared libraries. +clean_restart + +# Add .gdb_index to the library. +if { [ensure_gdb_index $lib1_binfile "" "libdeferred1"] != 1 } { + untested "failed to add .gdb_index to $lib1_testfile" + return +} + +# Strip debuginfo into a separate file. +if { [gdb_gnu_strip_debug $lib1_binfile ""] != 0 } { + fail "strip $lib1_testfile debuginfo" + return +} + +# Move debuginfo file into a directory that debuginfod will serve. +set debugdir [standard_output_file "debug"] +set debuginfo_sl1 [standard_output_file $lib1_testfile.debug] + +file mkdir $debugdir +file rename -force $debuginfo_sl1 $debugdir + +# Python script that looks up type information from a new_objfile +# observer. +set pyscript [gdb_remote_download host ${srcdir}/${subdir}/${testfile}.py] + +# Test that symbol type info is available in the observer. +proc_with_prefix test_deferred_status { } { + global cache + + # Delete client cache so debuginfo downloads again. + file delete -force $cache + + clean_restart + + gdb_test_no_output "set debuginfod enabled on" + gdb_test_no_output "set progress-bars enabled off" + + # Load the Python script. + gdb_test "source $::pyscript" "Python observer registered" \ + "load python script" + + gdb_load $::binfile + + if { ![runto_main] } { + return + } + + # Step into the library to trigger debuginfo download. + # The observer should see full type info. + set saw_void_type false + set saw_int_type false + set saw_error false + set saw_lib_func false + set saw_dbg_dl false + gdb_test_multiple "step" "step into library" { + -re "^Python observer: found type void \\(\\)\r\n" { + set saw_void_type true + exp_continue + } + -re "^Python observer: found type volatile int\r\n" { + set saw_int_type true + exp_continue + } + -re "^Python observer: ERROR\[^\r\n\]+" { + set saw_error true + exp_continue + } + -re "^libdeferred1_test \\(\\) at \[^\r\n\]+\r\n" { + set saw_lib_func true + exp_continue + } + -re "^Downloading \[^\r\n\]*separate debug info for \[^\r\n\]+${::lib1_testfile}\\.\\.\\.\r\n" { + set saw_dbg_dl true + exp_continue + } + -re "^$::gdb_prompt $" { + gdb_assert { $saw_void_type && $saw_int_type && $saw_lib_func \ + && $saw_dbg_dl && !$saw_error } \ + $gdb_test_name + } + -re "^\[^\r\n\]*\r\n" { + exp_continue + } + } +} + +prepare_for_debuginfod cache db + +with_debuginfod_env $cache { + set url [start_debuginfod $db $debugdir] + if { $url == "" } { + unresolved "failed to start debuginfod server" + return + } + + setenv DEBUGINFOD_URLS $url + test_deferred_status +} + +stop_debuginfod diff --git a/gdb/testsuite/gdb.debuginfod/deferred-observer-symbols.py b/gdb/testsuite/gdb.debuginfod/deferred-observer-symbols.py new file mode 100644 index 00000000000..f45762e855d --- /dev/null +++ b/gdb/testsuite/gdb.debuginfod/deferred-observer-symbols.py @@ -0,0 +1,62 @@ +# 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/>. + +import gdb + + +# Look up a symbol NAME in OBJFILE and check we can get its type. If +# STATIC_P is true then NAME is a static symbol, otherwise it's a +# non-static global. +# +# Print a status message containing the symbol's type, or an error if +# the symbol cannot be found. +def lookup_symbol_in_objfile(objfile, name, static_p): + try: + if static_p: + sym = objfile.lookup_static_symbol(name) + else: + sym = objfile.lookup_global_symbol(name) + if sym is not None: + # Try to get the symbol's type - this needs full debug info. + sym_type = sym.type + if sym_type is not None: + print("Python observer: found type {}".format(sym_type)) + else: + print("Python observer: ERROR - symbol type is None") + else: + print("Python observer: ERROR - symbol not found") + except Exception as e: + print("Python observer: ERROR - exception: {}".format(e)) + + +# New objfile observer. +def new_objfile_handler(event): + objfile = event.new_objfile + + # Only interested in the separate debug objfile for libdeferred1. + if objfile.owner is None: + return + owner_filename = objfile.owner.filename + if owner_filename is None or "libdeferred1.so" not in owner_filename: + return + + print("Python observer: checking symbols in {}".format(objfile.filename)) + + lookup_symbol_in_objfile(objfile, "libdeferred1_test", False) + lookup_symbol_in_objfile(objfile, "flag", True) + + +gdb.events.new_objfile.connect(new_objfile_handler) +print("Python observer registered") diff --git a/gdb/testsuite/gdb.debuginfod/deferred-pending.exp b/gdb/testsuite/gdb.debuginfod/deferred-pending.exp new file mode 100644 index 00000000000..9d3dcd4fab2 --- /dev/null +++ b/gdb/testsuite/gdb.debuginfod/deferred-pending.exp @@ -0,0 +1,172 @@ +# 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 that pending breakpoints don't cause reentrant debuginfo downloads. +# +# When on-demand debuginfo downloading triggers a download, +# finish_new_objfile is called for the new separate debug objfile. At this +# point, the parent objfile still has OBJF_DOWNLOAD_DEFERRED set. If +# breakpoint_re_set() were called here, it could try to resolve other +# pending breakpoints, which might trigger symbol lookups that see +# OBJF_DOWNLOAD_DEFERRED still set and attempt to download again - causing +# reentrancy issues. +# +# This test sets pending breakpoints on functions in libraries with deferred +# downloads, then triggers the downloads to verify no crashes or assertions +# occur. + +# This test relies on 'run'. +require !use_gdb_stub + +# Reuse source from deferred-download.exp test. +standard_testfile deferred-download.c \ + deferred-download-lib1.c \ + deferred-download-lib2.c + +load_lib debuginfod-support.exp + +require allow_debuginfod_tests +require allow_debuginfod_section_downloads + +set lib1_testfile "libdeferred1.so" +set lib1_srcfile $srcfile2 +set lib1_binfile [standard_output_file $lib1_testfile] + +set lib2_testfile "libdeferred2.so" +set lib2_srcfile $srcfile3 +set lib2_binfile [standard_output_file $lib2_testfile] + +# Build LIB2_BINFILE, LIB1_BINFILE, and BINFILE. +if { [build_executable "build $lib2_testfile" $lib2_binfile $lib2_srcfile \ + {debug build-id shlib}] } { + return +} + +if { [build_executable "build $lib1_testfile" $lib1_binfile $lib1_srcfile \ + [list debug build-id shlib_pthreads shlib=$lib2_binfile]] } { + return +} + +if { [build_executable "build executable" $binfile $srcfile \ + [list debug build-id shlib=$lib1_binfile shlib=$lib2_binfile]] } { + return +} + +# Start GDB for the ensure_gdb_index calls. +clean_restart + +# Add .gdb_index to both libraries. +if { [ensure_gdb_index $lib1_binfile "" $lib1_testfile] != 1 } { + untested "failed to add .gdb_index to $lib1_testfile" + return +} + +if { [ensure_gdb_index $lib2_binfile "" $lib2_testfile] != 1 } { + untested "failed to add .gdb_index to $lib2_testfile" + return +} + +# Strip debuginfo into separate files. +if { [gdb_gnu_strip_debug $lib1_binfile] != 0 } { + fail "strip $lib1_testfile debuginfo" + return +} + +if { [gdb_gnu_strip_debug $lib2_binfile] != 0 } { + fail "strip $lib2_testfile debuginfo" + return +} + +# Move debuginfo files into directory that debuginfod will serve. +set debugdir [standard_output_file "debug"] +set lib1_debuginfo [standard_output_file $lib1_testfile.debug] +set lib2_debuginfo [standard_output_file $lib2_testfile.debug] + +file mkdir $debugdir +file rename -force $lib1_debuginfo $debugdir +file rename -force $lib2_debuginfo $debugdir + +# Test that setting pending breakpoints on functions in deferred libraries, +# then running to trigger the downloads, doesn't cause reentrancy issues. +proc_with_prefix test_pending_breakpoints { } { + global cache + + # Delete client cache so debuginfo downloads again. + file delete -force $cache + clean_restart + + gdb_test_no_output "set debuginfod enabled on" + gdb_test_no_output "set progress-bars enabled off" + + # Load the executable but don't run yet. + gdb_load $::binfile + + # Set pending breakpoints on functions in both libraries. + # These will be pending because the libraries aren't loaded yet. + gdb_breakpoint "libdeferred1_test" -allow-pending + gdb_breakpoint "libdeferred2_test" -allow-pending + + # Now run. This will: + # 1. Load the shared libraries. + # 2. Download .gdb_index for each, full debuginfo download will be + # deferred though. + # 3. Resolve pending breakpoints using the indices. + # 4. When the first breakpoint is hit, full debuginfo will be + # downloaded. + # + # If breakpoint_re_set() is incorrectly called during the download, + # it might try to re-resolve the other breakpoint, potentially causing + # reentrant download attempts. + # + # We check that we can run to the first breakpoint without crashing. + set saw_error false + gdb_test_multiple "run" "run to first breakpoint" { + -re "Internal error|Error in re-setting breakpoint" { + set saw_error true + exp_continue + } + -re -wrap "Breakpoint \[0-9\]+, libdeferred1_test \\(\\) at .*" { + gdb_assert { !$saw_error } $gdb_test_name + } + } + + # Continue to the second breakpoint to verify both libraries work. + set saw_error false + gdb_test_multiple "continue" "continue to second breakpoint" { + -re "Internal error|Error in re-setting breakpoint" { + set saw_error true + exp_continue + } + -re -wrap "Breakpoint \[0-9\]+, libdeferred2_test \\(\\) at .*" { + gdb_assert { !$saw_error } $gdb_test_name + } + } +} + +# Create CACHE and DB directories ready for debuginfod to use. +prepare_for_debuginfod cache db + +with_debuginfod_env $cache { + set url [start_debuginfod $db $debugdir] + if { $url == "" } { + unresolved "failed to start debuginfod server" + return + } + + setenv DEBUGINFOD_URLS $url + test_pending_breakpoints +} + +stop_debuginfod diff --git a/gdb/testsuite/gdb.debuginfod/deferred-select-frame-lib1.c b/gdb/testsuite/gdb.debuginfod/deferred-select-frame-lib1.c new file mode 100644 index 00000000000..b954375fe50 --- /dev/null +++ b/gdb/testsuite/gdb.debuginfod/deferred-select-frame-lib1.c @@ -0,0 +1,26 @@ +/* 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/>. */ + +/* ... */ + +typedef void (*callback_t) (void); + +void +library1_function (callback_t cb) +{ + cb (); +} diff --git a/gdb/testsuite/gdb.debuginfod/deferred-select-frame-lib2.c b/gdb/testsuite/gdb.debuginfod/deferred-select-frame-lib2.c new file mode 100644 index 00000000000..b742f2c4347 --- /dev/null +++ b/gdb/testsuite/gdb.debuginfod/deferred-select-frame-lib2.c @@ -0,0 +1,26 @@ +/* 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/>. */ + +/* ... */ + +typedef void (*callback_t) (void); + +void +library2_function (callback_t cb) +{ + cb (); +} diff --git a/gdb/testsuite/gdb.debuginfod/deferred-select-frame.c b/gdb/testsuite/gdb.debuginfod/deferred-select-frame.c new file mode 100644 index 00000000000..26a46ea27d8 --- /dev/null +++ b/gdb/testsuite/gdb.debuginfod/deferred-select-frame.c @@ -0,0 +1,61 @@ +/* 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/>. */ + +/* ... */ + +typedef void (*callback_t) (void); +extern void library1_function (callback_t cb); +extern void library2_function (callback_t cb); + +volatile int global_var; + +void +f0 (void) +{ + global_var = 42; /* Breakpoint in f0. */ +} + +void +f1 (void) +{ + f0 (); /* Breakpoint in f1. */ +} + +void +f3 (void) +{ + library2_function (f1); +} + +void +f4 (void) +{ + f3 (); +} + +void +f5 (void) +{ + library1_function (f4); +} + +int +main (void) +{ + f5 (); + return global_var - 42; +} diff --git a/gdb/testsuite/gdb.debuginfod/deferred-select-frame.exp b/gdb/testsuite/gdb.debuginfod/deferred-select-frame.exp new file mode 100644 index 00000000000..ea84b3a97c8 --- /dev/null +++ b/gdb/testsuite/gdb.debuginfod/deferred-select-frame.exp @@ -0,0 +1,240 @@ +# 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 that frame operations work correctly when deferred debuginfo +# download is triggered mid-operation. +# +# The call chain is: main -> library_function -> callback_in_main +# We stop at a breakpoint in callback_in_main. Debug info for the +# library is served via debuginfod in deferred mode. A Python +# new_objfile observer calls gdb.invalidate_cached_frames() when the +# library's separate debug objfile is loaded. + +load_lib debuginfod-support.exp + +require allow_debuginfod_tests +require allow_debuginfod_section_downloads + +standard_testfile .c -lib1.c -lib2.c + +# Build the first shared library. +set lib1_testfile "lib1-${testfile}.so" +set lib1_srcfile $srcfile2 +set lib1_binfile [standard_output_file $lib1_testfile] +if { [build_executable "build $lib1_testfile" $lib1_testfile $lib1_srcfile \ + {debug build-id shlib}] } { + return +} + +# Build the second shared library. +set lib2_testfile "lib2-${testfile}.so" +set lib2_srcfile $srcfile3 +set lib2_binfile [standard_output_file $lib2_testfile] +if { [build_executable "build $lib2_testfile" $lib2_testfile $lib2_srcfile \ + {debug build-id shlib}] } { + return +} + +if { [build_executable "build executable" $testfile $srcfile \ + [list debug build-id shlib=$lib1_binfile \ + shlib=$lib2_binfile]] != 0 } { + return +} + +clean_restart + +# Add .gdb_index to both libraries. +if { [ensure_gdb_index $lib1_binfile "" "$lib1_testfile"] != 1 } { + untested "failed to add .gdb_index to $lib1_testfile" + return +} + +if { [ensure_gdb_index $lib2_binfile "" "$lib2_testfile"] != 1 } { + untested "failed to add .gdb_index to $lib2_testfile" + return +} + +# Strip debuginfo into a separate file. +if { [gdb_gnu_strip_debug $lib1_binfile ""] != 0 } { + fail "strip $lib1_testfile debuginfo" + return +} + +if { [gdb_gnu_strip_debug $lib2_binfile ""] != 0 } { + fail "strip $lib2_testfile debuginfo" + return +} + +# Move debuginfo file into directory that debuginfod will serve. +set debugdir [standard_output_file "debug"] +set lib1_debuginfo [standard_output_file $lib1_testfile.debug] +set lib2_debuginfo [standard_output_file $lib2_testfile.debug] + +file mkdir $debugdir +file rename -force $lib1_debuginfo $debugdir +file rename -force $lib2_debuginfo $debugdir + +# Python script that invalidates the frame cache when the separate debug +# objfile is loaded. +set pyscript [gdb_remote_download host ${srcdir}/${subdir}/${testfile}.py] + +# Start GDB, enable debuginfod, load the Python observer, run to the +# breakpoint in callback_in_main. Returns true on success. +proc setup_for_test {} { + clean_restart + + gdb_test_no_output "set debuginfod enabled on" + gdb_test_no_output "set progress-bars enabled off" + + # Load the Python script BEFORE loading anything else so the + # observer is registered when the deferred download triggers. + gdb_test "source $::pyscript" "Python registered new_objfile handler" \ + "load python script" + + gdb_load $::binfile + + if {![runto_main]} { + return false + } + + gdb_breakpoint "f1" + gdb_continue_to_breakpoint "breakpoint in f1" + + return true +} + +# Setup a breakpoint in a function, then use Python to select the +# previous frame, which will require downloading the deferred debug +# information. This download will trigger the new objfile event +# handler, which will in turn invalidate the frame cache. +# +# Check that GDB handles this, and that the correct frame is selected. +proc test_select_frame { } { + if {![setup_for_test]} { + return + } + + # Navigate to the library frame via Python. The .older() call + # triggers the deferred download; the new_objfile observer calls + # gdb.invalidate_cached_frames() but GDB should still manage to + # select the expected frame. + set invalidate_count 0 + set saw_selected false + gdb_test_multiple \ + [multi_line_input \ + "python" \ + "f = gdb.selected_frame().older()" \ + "if f is None:" \ + " print('older frame is None after download')" \ + "else:" \ + " f.select()" \ + " print('selected frame: ' + gdb.selected_frame().name())" \ + "end"] \ + "select library frame" { + -re "^Python new_objfile handler: frame cache invalidated\r\n" { + incr invalidate_count + exp_continue + } + -re "^selected frame: library2_function\r\n" { + set saw_selected true + exp_continue + } + -re "^$::gdb_prompt $" { + gdb_assert { $invalidate_count == 1 && $saw_selected } \ + $gdb_test_name + } + -re "^\[^\r\n\]*\r\n" { + exp_continue + } + } + + # Verify GDB is still functional. + gdb_test "info frame" ".*library2_function.*" "info frame after select" + + # Verify we can get a full backtrace. The '.*' here matches + # against the deferred download for lib1 debug information. + gdb_test "bt" \ + [multi_line \ + "#0 ($::hex in )?f1 \\(\\) at \[^\r\n\]+" \ + "#1 ($::hex in )?library2_function \\(cb=$::hex <f1>\\) at \[^\r\n\]+" \ + "#2 ($::hex in )?f3 \\(\\) at \[^\r\n\]+" \ + ".*" \ + "#3 ($::hex in )?f4 \\(\\) at \[^\r\n\]+" \ + "#4 ($::hex in )?library1_function \\(cb=$::hex <f4>\\) at \[^\r\n\]+" \ + "#5 ($::hex in )?f5 \\(\\) at \[^\r\n\]+" \ + "#6 ($::hex in )?main \\(\\) at \[^\r\n\]+"] \ + "backtrace after select" +} + +# Test that backtrace handles cache invalidation when unwinding +# through a library frame with deferred debug info. +# +# The backtrace must unwind through library_function (frame #1), +# which triggers deferred download. The new_objfile observer +# invalidates the frame cache during the download. +proc test_backtrace { } { + if {![setup_for_test]} { + return + } + + set saw_invalidate false + set bt_line_count 0 + gdb_test_multiple "bt" "backtrace through deferred library" { + -re "^Python new_objfile handler: frame cache invalidated\r\n" { + set saw_invalidate true + exp_continue + } + + -re "^#$::decimal ($::hex in )?\[^\r\n\]+" { + incr bt_line_count + exp_continue + } + + -re "^$::gdb_prompt $" { + gdb_assert { $saw_invalidate } "frame cache was invalidated" + gdb_assert { $bt_line_count == 7 } "saw expected number of bt lines" + } + + -re "^\[^\r\n\]*\r\n" { + exp_continue + } + } +} + +# Setup directory for the debuginfod server to serve from, and create +# CACHE and DB directories ready for debuginfod to use. +prepare_for_debuginfod cache db + +with_debuginfod_env $cache { + save_vars { env(DEBUGINFOD_URLS) } { + foreach_with_prefix test { backtrace select_frame } { + ### select_frame backtrace + # Delete client cache so debuginfo downloads again. + file delete -force $cache + + set url [start_debuginfod $db $debugdir] + if { $url eq "" } { + unresolved "failed to start debuginfod server" + return + } + + setenv DEBUGINFOD_URLS $url + + test_${test} + + stop_debuginfod + } + } +} diff --git a/gdb/testsuite/gdb.debuginfod/deferred-select-frame.py b/gdb/testsuite/gdb.debuginfod/deferred-select-frame.py new file mode 100644 index 00000000000..911ddae40d9 --- /dev/null +++ b/gdb/testsuite/gdb.debuginfod/deferred-select-frame.py @@ -0,0 +1,44 @@ +# 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/>. + +# Python script that invalidates the frame cache when the separate +# debug objfile for lib-deferred-select-frame.so is loaded. +# +# This simulates an observer that resets frame-related state when new +# debug info arrives. The invalidation happens during select_frame's +# call to find_compunit_symtab_for_pc, after selected_frame has +# already been set. + +import gdb + +def new_objfile_handler(event): + objfile = event.new_objfile + + if objfile.owner is None: + return + + owner_filename = objfile.owner.filename + if owner_filename is None: + return + + if ("lib1-deferred-select-frame.so" not in owner_filename + and "lib2-deferred-select-frame.so" not in owner_filename): + return + + gdb.invalidate_cached_frames() + print("Python new_objfile handler: frame cache invalidated") + +gdb.events.new_objfile.connect(new_objfile_handler) +print("Python registered new_objfile handler") diff --git a/gdb/testsuite/lib/debuginfod-support.exp b/gdb/testsuite/lib/debuginfod-support.exp index 66b4098d4f9..e5be83cf3a8 100644 --- a/gdb/testsuite/lib/debuginfod-support.exp +++ b/gdb/testsuite/lib/debuginfod-support.exp @@ -115,6 +115,8 @@ proc with_debuginfod_env { cache body } { proc start_debuginfod { db debugdir } { global debuginfod_spawn_id spawn_id + set logfile [standard_output_file "server_log"] + # Find an unused port. set port 7999 set found false @@ -129,7 +131,8 @@ proc start_debuginfod { db debugdir } { set old_spawn_id $spawn_id } - spawn debuginfod -vvvv -d $db -p $port -F $debugdir + spawn sh -c "debuginfod -vvvv -d $db -p $port -F $debugdir 2>&1 \ + | tee $logfile" set debuginfod_spawn_id $spawn_id if { [info exists old_spawn_id] } { @@ -207,3 +210,29 @@ proc stop_debuginfod { } { unset debuginfod_spawn_id } } + +# Return true if gdb is configured to download ELF/DWARF sections from +# debuginfod servers. Otherwise return false. +gdb_caching_proc allow_debuginfod_section_downloads { } { + set cmd "maint set debuginfod download-sections on" + set msg "enable section downloads" + + gdb_exit + gdb_start + + set supported false + gdb_test_multiple $cmd $msg { + -re -wrap ".*not compiled into GDB.*" { + } + -re -wrap "^" { + set supported true + } + -re -wrap "" { + fail "$gdb_test_name (unexpected output)" + } + } + + gdb_exit + + return $supported +} diff --git a/gdb/testsuite/lib/gdb.exp b/gdb/testsuite/lib/gdb.exp index a40c87c6727..cc633f998b3 100644 --- a/gdb/testsuite/lib/gdb.exp +++ b/gdb/testsuite/lib/gdb.exp @@ -10985,10 +10985,14 @@ proc get_index_type { objfile { testname "" } } { # STYLE controls which style of index to add, if needed. The empty # string (the default) means .gdb_index; "-dwarf-5" means .debug_names. -proc ensure_gdb_index { binfile {style ""} } { +proc ensure_gdb_index { binfile {style ""} {testname_prefix ""} } { set testfile [file tail $binfile] - set test "check if index present" + + if { $testname_prefix != "" } { + set test "$testname_prefix $test" + } + set index_type [get_index_type $testfile $test] if { $index_type eq "gdb" || $index_type eq "dwarf5" } { -- 2.25.4