The code is unchanged from v1. This resends it with three corrections to the message and the supporting evidence that v1 lacked.
What changed since v1: - v1 omitted the Assisted-by: tag required by <https://gcc.gnu.org/ai-policy.html>. That was an oversight on my part, not a claim that the policy does not apply. It is present below. - v1's ChangeLog said "PR target/60939"; the bug is filed under libgcc. Corrected. (For a change in gcc/collect2.cc neither component is a good fit; if you would rather the bug were reclassified, say so and I will request it.) - v1 said "built and tested on powerpc-ibm-aix7.3.1.0" without naming a version. It was 13.3.0 with a rebuilt collect2. It has since been retested on trunk; numbers below. On AIX a C++ exception is not caught when the catching function lives in a translation unit other than the one holding main(), is reached only through a function pointer, and that pointer is published by a static constructor. The handler in its own frame is never found and std::terminate runs. collect2 leaves SCAN_DWEH out of the scan it performs before the first link: scanfilter this_filter = ld1_filter; #if HAVE_AS_REF if (!shared_obj) this_filter &= ~SCAN_DWEH; #endif The stated reason is that referencing every frame table would drag in the corresponding objects even when nothing else needs them. That cost is real for archive members. It is not for the object files named on the command line, which are linked unconditionally either way. Why the deferred scan loses the table. Every step here is documented by IBM. The linker discards by default. AIX 5L Version 5.3 Commands Reference, Volume 3, SC23-4890-04, ld command, "Garbage Collection": By default, the ld command performs garbage collection, deleting control sections (CSECTs) that are not referenced when generating the output file. A CSECT is an indivisible unit of coding or data. A CSECT references another CSECT if it contains a relocation entry (RLD) referring to a symbol contained in the other CSECT. A referenced CSECT causes all CSECTs it references to be referenced as well. In addition, a CSECT is referenced if it contains exported symbols, symbols specified with the -u flag, or the symbol designated as the entry point with the -e flag. That is the default, which is why the bug needs no unusual link flags, and why -bnogc, -r, -bkeepfile and -bgcbypass all make it disappear. .ref exists to prevent exactly that, and IBM's own example is this case. AIX 5L Version 5.3 Assembler Language Reference, SC23-4923-02, page 494, ".ref Pseudo-op": The .ref pseudo-op supports the creation of multiple RLD entries in the same location. This pseudo-op is used in the output of some compilers to ensure the linkage editor does not discard routines that are used but not referenced explicitly in the text or data sections. For example, in C++, constructors and destructors are used to construct and destroy class objects. Constructors and destructors are sometimes called only from the run-time environment without any explicit reference in the text section. But the .ref lives inside the function body's csect. ASM_OUTPUT_DWARF_TABLE_REF emits it from each function to its frame table. When nothing references that body during the first link -- because collect2 has not written the constructor table yet -- the body is discarded and the .ref goes with it. The frame table then has nothing pointing at it and is collected too: $ g++ -Wl,-debug -o t a.o b.o | grep 'frame table' 1 frame table found Constructors are already collected from the objects; frame tables are not. This removes the asymmetry for objects and leaves archives as they were. Visible in the process image, breaking on __register_frame_info_table_bases and dumping the table it is handed: without the patch: 0x10001040 <_GLOBAL__F_pFoo> 0x0 with the patch: 0x100010b0 <_GLOBAL__F__Z3foov> 0x10001060 <_GLOBAL__F_pFoo> 0x0 One entry against two, and the one that reappears is the frame table for foo(), the function holding the handler that was never found. Does this register a frame table twice? No, and it is measured rather than argued. Objects are now scanned before the first link and the first link's output is still scanned afterwards, so the same table can be seen twice; it is registered once, because add_to_list appends and then walks the list for an equal name, freeing the new node on a match. collect2's generated file, kept with -save-temps on patched trunk, holds exactly one entry per translation unit, on the default link and with -static-libgcc alike. The libgcc frame tables that appear in collect2's scan log are not in the table, since the archive path still has SCAN_DWEH stripped. Adjacent prior art, so it is not raised as a surprise: Tony Reix, 2018-06, "[PATCH,AIX] Fix -static-libgcc & -static-libgo issues for AIX" fixed frame tables being registered twice at runtime -- once by a shared library's initializer and once by the main program's -- and notes that this breaks assumptions in _Unwind_Find_FDE. That work is upstream; static_obj, static_libs and aixlazy_flag are all in collect2.cc today. The hazard there is cross-initializer, which per-run deduplication cannot see, and this patch does not approach it: nothing new is taken from libraries, and the objects it adds are registered by the same initializer as before. Testing. The new test, run through the testsuite with only collect2 swapped: 13.3.0 unpatched 4 expected passes, 4 unexpected failures patched 8 expected passes trunk unpatched 3 expected passes, 3 unexpected failures 17.0.0 patched 6 expected passes, 0 failures It fails in every language standard tested, so the standard is not involved. With the fix, collect2 reports two frame tables where it reported one. Full check-g++ on trunk, both arms on the same tree with only collect2 differing: 249505 and 249507 result lines, 16 differing. Three are this test going FAIL to PASS in gnu++98, gnu++20 and gnu++29, one is the timestamp, and the remaining twelve are two tests (g++.old-deja/g++.robertl/eb41.C and ebnull-cast.C) hitting "program timed out" in one arm. Those are load flakes, confirmed by re-running both tests in both arms afterwards: all pass. Every tally is identical in the two arms. The earlier full run on 13.3.0 was made on the suite without the new test, and showed zero differing lines out of 224411. Shared-object and static-archive links were also compared, and a link of one translation unit holding a single used function and sixteen unused ones, all -fexceptions: identical binary size, identical frame-table entry count, identical symbol count, and no dead function retained in either arm. So referencing the objects' frame tables does not inhibit the linker's dead-code elimination. Note that AIX emits one frame table per translation unit, not one per function, so the unit of anchoring is the object file, which is linked unconditionally anyway. A caution for anyone repeating those size comparisons: collect2 embeds its random temporary filename in the output, so two links with the same collect2 are not byte-identical, and unequal -B path or output filename lengths shift sizes as well. The affected block is byte-identical in releases/gcc-13, 14, 15 and master, and the patch applies cleanly to 13.3.0 and to master. This patch, its test cases and the analysis in this commit message were developed with LLM assistance, per <https://gcc.gnu.org/ai-policy.html>. Assisted-by: Claude Opus 5 PR libgcc/60939 gcc/ChangeLog: * collect2.cc (main): Use separate scan filters for object files and for libraries. Retain SCAN_DWEH when scanning objects; keep excluding it for libraries. gcc/testsuite/ChangeLog: * g++.dg/eh/pr60939.C: New test. * g++.dg/eh/pr60939-a.cc: New test. Signed-off-by: Bernard Ladenthin <[email protected]> --- diff --git a/gcc/collect2.cc b/gcc/collect2.cc index 84d5e1f20..7924e5f2e 100644 --- a/gcc/collect2.cc +++ b/gcc/collect2.cc @@ -1526,21 +1526,27 @@ main (int argc, char **argv) control whether we need a first pass link later on or not, and what will remain to be scanned there. */ - scanfilter this_filter = ld1_filter; + scanfilter obj_filter = ld1_filter; + scanfilter lib_filter = ld1_filter; #if HAVE_AS_REF if (!shared_obj) - this_filter &= ~SCAN_DWEH; + /* Referencing every frame table found in an archive would drag the whole + member in even when nothing else needs it. Objects named on the + command line are linked regardless, so nothing is saved by deferring + theirs to the first link -- and deferring loses the table whenever the + linker discards a csect that nothing references yet. */ + lib_filter &= ~SCAN_DWEH; #endif /* Scan object files. */ while (export_object_lst < object) - scan_prog_file (*export_object_lst++, PASS_OBJ, this_filter); + scan_prog_file (*export_object_lst++, PASS_OBJ, obj_filter); /* Scan libraries. */ for (; list; list = list->next) - scan_prog_file (list->name, PASS_FIRST, this_filter); + scan_prog_file (list->name, PASS_FIRST, lib_filter); - ld1_filter = ld1_filter & ~this_filter; + ld1_filter = ld1_filter & ~lib_filter; } if (exports.first) diff --git a/gcc/testsuite/g++.dg/eh/pr60939-a.cc b/gcc/testsuite/g++.dg/eh/pr60939-a.cc new file mode 100644 index 000000000..8c4c223ce --- /dev/null +++ b/gcc/testsuite/g++.dg/eh/pr60939-a.cc @@ -0,0 +1,30 @@ +// Companion to pr60939.C. Nothing here is referenced from main() except +// through the pointer the static constructor stores, which is what makes the +// unit a candidate for removal by a garbage-collecting linker. + +#include <stdexcept> +#include <cstdlib> + +extern "C" void (*pfoo) (void); +void (*pfoo) (void) = 0; + +static void +foo (void) +{ + try + { + throw std::range_error ("pr60939"); + } + catch (const std::range_error &) + { + return; + } + abort (); +} + +struct init +{ + init () { pfoo = foo; } +}; + +static init the_init; diff --git a/gcc/testsuite/g++.dg/eh/pr60939.C b/gcc/testsuite/g++.dg/eh/pr60939.C new file mode 100644 index 000000000..83115ee7e --- /dev/null +++ b/gcc/testsuite/g++.dg/eh/pr60939.C @@ -0,0 +1,24 @@ +// PR target/60939 - a C++ exception was not caught when the catching function +// lived in another translation unit, was reached only through a function +// pointer, and that pointer was published by a static constructor. +// +// On AIX the linker discards a csect nothing references during collect2's +// first link, taking the unit's DWARF frame table with it; collect2 then +// scanned that link's output and registered only the surviving table, so the +// unwinder found no handler and std::terminate ran. +// +// The test is not AIX-only on purpose: it is a valid program everywhere, and +// costs nothing to keep running on other targets. + +// { dg-do run } +// { dg-additional-sources "pr60939-a.cc" } +// { dg-options "-O2" } + +extern "C" void (*pfoo) (void); + +int +main () +{ + pfoo (); + return 0; +} -- 2.51.0
