From 54fac927fb6f328d07a094f119ae4afe84bf491d Mon Sep 17 00:00:00 2001 From: Christopher Wellons Date: Sat, 5 Sep 2026 11:24:49 -0400 Subject: [PATCH 01/12] ld: Don't reference _pei386_runtime_relocator in relocatable links Since Binutils 2.41 (PR30343) the PE linker creates an early undefined reference to _pei386_runtime_relocator whenever the LTO plugin is loaded, which an LTO-enabled GCC does on every link. With -r that reference leaks into the output as a stray undefined symbol, breaking later -nostdlib links of the result. Skip it for relocatable output, where the final link makes its own reference. Co-Authored-By: Claude Fable 5.1 --- ...lto-no-relocator-ref-for-relocatable.patch | 48 +++++++++++++++++++ src/binutils/series | 1 + 2 files changed, 49 insertions(+) create mode 100644 src/binutils/pe-lto-no-relocator-ref-for-relocatable.patch diff --git a/src/binutils/pe-lto-no-relocator-ref-for-relocatable.patch b/src/binutils/pe-lto-no-relocator-ref-for-relocatable.patch new file mode 100644 index 0000000..3a02e62 --- /dev/null +++ b/src/binutils/pe-lto-no-relocator-ref-for-relocatable.patch @@ -0,0 +1,48 @@ +Since Binutils 2.41 (PR30343, commit defb881754d), whenever the LTO +plugin is loaded the PE linker unconditionally creates an undefined +reference to _pei386_runtime_relocator before the plugin's "all symbols +read" phase. Because it cannot yet know whether LTO output will need +runtime pseudo-relocations, it makes the reference anyway, marking the +symbol as referenced from a regular object so that an LTO-compiled +definition survives. In a final link this is harmless: the reference +resolves to pseudo-reloc.o in libmingw32.a, which the CRT startup drags +in regardless. + +In a relocatable link (-r) the reference has no definition to resolve +against and leaks into the output as a stray undefined symbol. Since GCC +passes the plugin to the linker for every link when built with LTO, this +affects even links with no LTO objects at all: + + $ cc -r -s -u ___chkstk_ms -o chkstk.o -lgcc + $ nm chkstk.o + 0000000000000000 T ___chkstk_ms + U _pei386_runtime_relocator + +An object linked with -r is never a final image, so no pseudo-relocation +will run from it. If it is later used in a final link, that link makes +its own reference. The early reference also buys nothing for LTO in a +relocatable link: the linker's plugin interface already reports every +symbol as visible from outside for relocatable output, so none resolve +as IR-only and GCC keeps them all. So skip the early reference for +relocatable output. + +--- a/ld/emultempl/pe.em ++++ b/ld/emultempl/pe.em +@@ -1457,6 +1457,7 @@ + { + #ifdef DLL_SUPPORT + if (link_info.lto_plugin_active ++ && !bfd_link_relocatable (&link_info) + && link_info.pei386_auto_import) + make_runtime_ref (); + #endif +--- a/ld/emultempl/pep.em ++++ b/ld/emultempl/pep.em +@@ -1465,6 +1465,7 @@ + { + #ifdef DLL_SUPPORT + if (link_info.lto_plugin_active ++ && !bfd_link_relocatable (&link_info) + && link_info.pei386_auto_import) /* -1=warn or 1=enable */ + make_runtime_ref (); + #endif diff --git a/src/binutils/series b/src/binutils/series index bd1a6f3..d743197 100644 --- a/src/binutils/series +++ b/src/binutils/series @@ -3,3 +3,4 @@ exclude-sysroot.patch windmc-bom.patch windmc-carry-severity-facility.patch windmc-per-facility-ids.patch +pe-lto-no-relocator-ref-for-relocatable.patch From 0b67059eea4a28990c898ca693b37eba3250f8d7 Mon Sep 17 00:00:00 2001 From: Christopher Wellons Date: Sat, 5 Sep 2026 14:09:01 -0400 Subject: [PATCH 02/12] Enable LTO, with fat LTO libstdc++, libgfortran, and libquadmath Build GCC with LTO support and compile the three runtime libraries that benefit most as fat LTO objects. They link as before without -flto, while an -flto link can inline into them and, more importantly, drop their unused functions, which --gc-sections cannot do on x86_64 PE because .pdata roots every function. libstdc++'s largest members (locale, format, and stream instantiations) are only partly used by any one program, so this is where the size win concentrates. libstdc++ is the only C++ target library, so CXXFLAGS_FOR_TARGET reaches it alone. libgfortran and libquadmath share CFLAGS_FOR_TARGET with libgcc, libgomp, and libatomic, which stay plain (libgcc feeds the -r tricks in this repository; the others synchronize threads and must not have their translation-unit boundaries dissolved), so those two are rebuilt after the main build with their own flags. The LTO plugin is also installed in lib/bfd-plugins so that plain ar, nm, and ranlib handle slim LTO objects without gcc-ar. Co-Authored-By: Claude Fable 5.1 --- Dockerfile | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index fcc2860..fc71618 100644 --- a/Dockerfile +++ b/Dockerfile @@ -301,7 +301,6 @@ RUN (cd /dl/gcc \ --disable-libstdcxx-verbose \ --disable-dependency-tracking \ --disable-nls \ - --disable-lto \ CFLAGS_FOR_TARGET="-O2" \ CXXFLAGS_FOR_TARGET="-O2" \ LDFLAGS_FOR_TARGET="-s" \ @@ -507,6 +506,15 @@ RUN if [ "$GCC_MULTILIB" = enable ]; then \ WORKDIR /gcc COPY src/gcc-final/ $PREFIX/src/gcc-final/ +# libstdc++, libgfortran, and libquadmath are built as fat LTO objects: +# they link normally without -flto, but an -flto link can inline into them +# and drop their unused functions, which --gc-sections cannot do on PE. +# libstdc++ is the only C++ target library, so CXXFLAGS_FOR_TARGET reaches +# it alone. +# libgfortran and libquadmath share CFLAGS_FOR_TARGET with libgcc, libgomp, +# and libatomic, which must stay plain, so they are rebuilt afterward with +# their own flags. The LTO plugin is also installed where ar, nm, and +# ranlib find it, so they can handle slim LTO objects without gcc-ar. RUN (cd /dl/gcc \ && QUILT_PATCHES=$PREFIX/src/gcc-final quilt push -a \ && rm -rf .pc) \ @@ -531,20 +539,23 @@ RUN (cd /dl/gcc \ --enable-version-specific-runtime-libs \ --disable-libstdcxx-verbose \ --disable-dependency-tracking \ - --disable-lto \ --disable-nls \ --disable-win32-registry \ $GCC_MANIFEST_FLAG \ --enable-mingw-wildcard \ CFLAGS_FOR_TARGET="-O2" \ - CXXFLAGS_FOR_TARGET="-O2" \ + CXXFLAGS_FOR_TARGET="-O2 -flto -ffat-lto-objects" \ LDFLAGS_FOR_TARGET="-s" \ CFLAGS="-O2" \ CXXFLAGS="-O2" \ LDFLAGS="-s" \ && make -j$(nproc) \ + && make clean-target-libquadmath clean-target-libgfortran \ + && make -j$(nproc) all-target-libquadmath all-target-libgfortran \ + CFLAGS_FOR_TARGET="-O2 -flto -ffat-lto-objects" \ && make install \ - && rm -f $PREFIX/bin/ld.bfd.exe \ + && cp $PREFIX/libexec/gcc/$ARCH/*/liblto_plugin.dll $PREFIX/lib/bfd-plugins/ \ + && rm -f $PREFIX/bin/ld.bfd.exe $PREFIX/bin/lto-dump.exe \ && $ARCH-gcc -DEXE=g++.exe -DCMD=c++ \ -Oz -fno-asynchronous-unwind-tables \ -Wl,--gc-sections -s -nostdlib \ From 6a19a189b3ec0adef57304bffc069f208d73ec2c Mon Sep 17 00:00:00 2001 From: Christopher Wellons Date: Sat, 5 Sep 2026 14:14:57 -0400 Subject: [PATCH 03/12] gcc: Never emit mangling compatibility aliases When a function's mangled name changed in some ABI version, GCC also emits its old name as a weak alias so that objects from older compilers still link. By default it does so for changes since ABI 18 (GCC 13), and -Wabi=N quietly implies -fabi-compat-version=N, which libstdc++ passes (-Wabi=19) and so ends up with a different set of aliases than user code. That asymmetry breaks LTO on PE: the aliases share a COMDAT group with their target, ld cannot reconcile a group with two members in one IR object and one in another, and any C++20 constrained function explicitly instantiated in libstdc++ and implicitly in an -flto user object fails with a multiple definition error. This toolchain is static and self-contained, so there are no older objects to stay compatible with. Emit no compatibility aliases unless -fabi-compat-version is given explicitly, and leave -Wabi= as a pure diagnostic. This goes in the bootstrap queue because the cross compiler is what compiles libstdc++. Co-Authored-By: Claude Fable 5.1 --- src/gcc/no-abi-compat-aliases.patch | 57 +++++++++++++++++++++++++++++ src/gcc/series | 1 + 2 files changed, 58 insertions(+) create mode 100644 src/gcc/no-abi-compat-aliases.patch diff --git a/src/gcc/no-abi-compat-aliases.patch b/src/gcc/no-abi-compat-aliases.patch new file mode 100644 index 0000000..d8e6081 --- /dev/null +++ b/src/gcc/no-abi-compat-aliases.patch @@ -0,0 +1,57 @@ +Never emit mangling compatibility aliases + +When a function's mangled name changed in some ABI version, GCC also +emits its old name as a weak alias so that objects compiled by older +compilers can still link. By default it does so for changes since ABI +18 (GCC 13), and -Wabi=N quietly implies -fabi-compat-version=N, which +libstdc++ uses (-Wabi=19) and so gets a different set of aliases than +user code. That asymmetry breaks LTO on PE: the aliases share a COMDAT +group with their target, ld cannot reconcile a group that has two +members in one IR object and one in another, and any C++20 constrained +function explicitly instantiated in libstdc++ and implicitly in an +-flto user object fails with a multiple definition error. + +This toolchain is static and self-contained, so there are no older +objects to stay compatible with. Emit no compatibility aliases unless +-fabi-compat-version is given explicitly, and leave -Wabi= as a pure +diagnostic. Applied before both compilers are built, since the +bootstrap cross compiler is what compiles libstdc++. + +--- a/gcc/c-family/c-opts.cc ++++ b/gcc/c-family/c-opts.cc +@@ -1110,8 +1110,6 @@ + const int latest_abi_version = 21; + /* Possibly different for non-default ABI fixes within a release. */ + const int default_abi_version = latest_abi_version; +- /* Generate compatibility aliases for ABI v18 (GCC 13) by default. */ +- const int abi_compat_default = 18; + + if (flag_abi_version > latest_abi_version) + warning (0, "%<-fabi-version=%d%> is not supported, using =%d", +@@ -1126,18 +1124,17 @@ + clamp (flag_abi_compat_version); + #undef clamp + +- /* Default -Wabi= or -fabi-compat-version= from each other. */ ++ /* Default -Wabi= from -fabi-compat-version=. Unless the latter is ++ given explicitly, never emit mangling compatibility aliases: they ++ only serve linking against objects from older compilers, and ++ -Wabi= is a diagnostic, not a request for them. */ + if (warn_abi_version == -1 && flag_abi_compat_version != -1) + warn_abi_version = flag_abi_compat_version; +- else if (flag_abi_compat_version == -1 && warn_abi_version != -1) +- flag_abi_compat_version = warn_abi_version; +- else if (warn_abi_version == -1 && flag_abi_compat_version == -1) ++ else if (flag_abi_compat_version == -1) + { +- warn_abi_version = 0; +- if (flag_abi_version == default_abi_version) +- flag_abi_compat_version = abi_compat_default; +- else +- flag_abi_compat_version = latest_abi_version; ++ if (warn_abi_version == -1) ++ warn_abi_version = 0; ++ flag_abi_compat_version = flag_abi_version; + } + + /* Allow warnings vs ABI versions beyond what we currently support. */ diff --git a/src/gcc/series b/src/gcc/series index 7598d77..4288b06 100644 --- a/src/gcc/series +++ b/src/gcc/series @@ -2,3 +2,4 @@ avx-misaligned.patch dynamic-load-tzdb.patch libgfortran-mapview.patch trap-terminate.patch +no-abi-compat-aliases.patch From 7571b9183c0a79faa8f7e595791fcd1bc32b68e8 Mon Sep 17 00:00:00 2001 From: Christopher Wellons Date: Sat, 5 Sep 2026 14:14:57 -0400 Subject: [PATCH 04/12] Build libstdc++ with only the new std::string ABI Disable the dual ABI, and patch libstdc++ to keep the new SSO string rather than the old gcc4-compatible COW string that configure imposes on single-ABI builds. The dual ABI exists for link compatibility with binaries built against GCC 4-era libstdc++, which a from-scratch static-only toolchain does not need, and it costs every program dead weight at archive-member granularity: both ABIs' locale facets and string instantiations, plus the facet shims to bridge them. The library nearly supports this configuration already. Beyond the configure default, the patch defines the new-ABI ios_base::failure in a C++11 translation unit, and extends the instantiate-once guards on ABI-agnostic locale instantiations to cover single-ABI builds. The Transactional Memory TS clones in cow-stdexcept.cc are written against COW string internals (_Rep, _M_dataplus) and so can only be compiled when std::basic_string is the COW string. This is invisible on x86_64, where _GLIBCXX_USE_WEAK_REF is false and the section is skipped entirely, but breaks the i686 build, where it is true. Compile the clones only when the string ABI matches. Their absence is legal by design: they are weakly referenced, and -fgnu-tm code falls back to serial-irrevocable execution. (-fgnu-tm with libstdc++ headers is broken in GCC 16.2 regardless, in both dual-ABI and single-ABI builds: new_allocator.h trips over __builtin_operator_new selection.) Now that libstdc++ is built as fat LTO objects, this also removes most of the library's deliberate ODR violations from what an -flto link can see: the punned __cow_string and __sso_string redefinitions and the old-ABI compile of the standard exception classes no longer exist, so -Wodr stops reporting them on every link. A trivial C++ program shrinks from 104K to 73K, iostream hello world from 1010K to 656K, and libstdc++.a from 8.1M to 6.0M. Exceptions, iostreams with error codes, locales, and std::filesystem verified working. Co-Authored-By: Claude Fable 5.1 --- Dockerfile | 1 + src/gcc-final/new-abi-only.patch | 245 +++++++++++++++++++++++++++++++ src/gcc-final/series | 1 + 3 files changed, 247 insertions(+) create mode 100644 src/gcc-final/new-abi-only.patch diff --git a/Dockerfile b/Dockerfile index fc71618..7790074 100644 --- a/Dockerfile +++ b/Dockerfile @@ -537,6 +537,7 @@ RUN (cd /dl/gcc \ --enable-threads=posix \ --enable-tls \ --enable-version-specific-runtime-libs \ + --disable-libstdcxx-dual-abi \ --disable-libstdcxx-verbose \ --disable-dependency-tracking \ --disable-nls \ diff --git a/src/gcc-final/new-abi-only.patch b/src/gcc-final/new-abi-only.patch new file mode 100644 index 0000000..bbf3c70 --- /dev/null +++ b/src/gcc-final/new-abi-only.patch @@ -0,0 +1,245 @@ +Keep the new std::string ABI when the dual ABI is disabled + +--disable-libstdcxx-dual-abi hardwires the surviving ABI to the old +gcc4-compatible COW std::string. For a from-scratch toolchain the +useful single-ABI configuration is the reverse: only the modern SSO +string. The library supports either direction: c++config.h neutralizes +the per-file ABI pins in single-ABI builds, and the Makefiles already +exclude the cow-*, cxx11-*, and shim facet objects. Four fixups +beyond the configure default: + +ios_base::failure derives from exception in the old ABI but from +system_error in the new one. Its new-ABI members normally come from +src/c++11/cxx11-ios_failure.cc, built only for the dual ABI, and they +cannot live in src/c++98/ios_failure.cc, compiled as C++98, where + declares the new failure against a layout-compatible stand-in +for system_error. Define them in the C++11 translation unit +src/c++11/ios_errcat.cc instead. + +locale-inst.cc guards ABI-agnostic instantiations (facet ids, num_get, +time_put, ctype, codecvt, __pad) with !_GLIBCXX_USE_CXX11_ABI so the +dual ABI instantiates them exactly once, in the old-ABI object. A +new-ABI-only build must instantiate them in its only compile. + +The Transactional Memory TS clones in cow-stdexcept.cc are written +against COW string internals (_Rep, public _M_dataplus), so they can +only be compiled when std::basic_string is the COW string. This is +invisible on x86_64, where _GLIBCXX_USE_WEAK_REF is false, but breaks +the i686 build. + +--- a/libstdc++-v3/configure ++++ b/libstdc++-v3/configure +@@ -51668,7 +51668,7 @@ + if test x"$enable_libstdcxx_dual_abi" != xyes; then + { $as_echo "$as_me:${as_lineno-$LINENO}: dual ABI is disabled" >&5 + $as_echo "$as_me: dual ABI is disabled" >&6;} +- default_libstdcxx_abi="gcc4-compatible" ++ default_libstdcxx_abi="new" + fi + + +--- a/libstdc++-v3/src/c++11/cow-stdexcept.cc ++++ b/libstdc++-v3/src/c++11/cow-stdexcept.cc +@@ -198,7 +198,7 @@ + // are not defined by us. Without such support, the exceptions will not be + // declared transaction-safe, so we just don't provide transactional clones + // in this case. +-#if _GLIBCXX_USE_WEAK_REF ++#if _GLIBCXX_USE_WEAK_REF && ! _GLIBCXX_USE_CXX11_ABI + #ifdef _GLIBCXX_USE_C99_STDINT + + #include +--- a/libstdc++-v3/src/c++11/ios_errcat.cc ++++ b/libstdc++-v3/src/c++11/ios_errcat.cc +@@ -28,6 +28,14 @@ + + #define _GLIBCXX_USE_CXX11_ABI 1 + #include ++#include ++ ++#ifdef _GLIBCXX_USE_NLS ++# include ++# define _(msgid) gettext (msgid) ++#else ++# define _(msgid) (msgid) ++#endif + + #if __has_cpp_attribute(clang::require_constant_initialization) + # define __constinit [[clang::require_constant_initialization]] +@@ -80,5 +88,39 @@ + iostream_category() noexcept + { return io_category_instance.cat; } + ++#if _GLIBCXX_USE_CXX11_ABI && ! _GLIBCXX_USE_DUAL_ABI ++ // In a single-ABI build with the new string ABI, these definitions ++ // cannot live in src/c++98/ios_failure.cc, which is compiled as C++98, ++ // nor in src/c++11/cxx11-ios_failure.cc, which is built only for the ++ // dual ABI. Class ios_base::failure derives from system_error. ++ ios_base::failure::failure(const string& __str) ++ : system_error(io_errc::stream, __str) { } ++ ++ ios_base::failure::failure(const string& __str, const error_code& __ec) ++ : system_error(__ec, __str) { } ++ ++ ios_base::failure::failure(const char* __str, const error_code& __ec) ++ : system_error(__ec, __str) { } ++ ++ ios_base::failure::~failure() ++ { } ++ ++ const char* ++ ios_base::failure::what() const throw() ++ { return runtime_error::what(); } ++ ++ void ++ __throw_ios_failure(const char* __s __attribute__((unused))) ++ { _GLIBCXX_THROW_OR_ABORT(ios::failure(_(__s))); } ++ ++ void ++ __throw_ios_failure(const char* __s __attribute__((unused)), ++ int __err __attribute__((unused))) ++ { ++ _GLIBCXX_THROW_OR_ABORT(ios::failure(_(__s), ++ __err ? error_code(__err, generic_category()) : io_errc::stream)); ++ } ++#endif ++ + _GLIBCXX_END_NAMESPACE_VERSION + } // namespace +--- a/libstdc++-v3/src/c++11/locale-inst-numeric.h ++++ b/libstdc++-v3/src/c++11/locale-inst-numeric.h +@@ -30,7 +30,7 @@ + + namespace std _GLIBCXX_VISIBILITY(default) + { +-#if ! _GLIBCXX_USE_CXX11_ABI ++#if ! _GLIBCXX_USE_CXX11_ABI || ! _GLIBCXX_USE_DUAL_ABI + // use_facet and has_facet instantiations + INSTANTIATE_FACET_ACCESSORS(num_get); + INSTANTIATE_FACET_ACCESSORS(num_put); +@@ -38,7 +38,7 @@ + + _GLIBCXX_BEGIN_NAMESPACE_LDBL + +-#if ! _GLIBCXX_USE_CXX11_ABI ++#if ! _GLIBCXX_USE_CXX11_ABI || ! _GLIBCXX_USE_DUAL_ABI + template class num_get >; + template class num_put >; + #endif +@@ -88,7 +88,7 @@ + unsigned long long&) const; + #endif + +-#if ! _GLIBCXX_USE_CXX11_ABI ++#if ! _GLIBCXX_USE_CXX11_ABI || ! _GLIBCXX_USE_DUAL_ABI + // num_put member function templates + template + ostreambuf_iterator +--- a/libstdc++-v3/src/c++11/locale-inst.cc ++++ b/libstdc++-v3/src/c++11/locale-inst.cc +@@ -52,7 +52,7 @@ + _GLIBCXX_BEGIN_NAMESPACE_VERSION + + // moneypunct, money_get, and money_put +-#if ! _GLIBCXX_USE_CXX11_ABI ++#if ! _GLIBCXX_USE_CXX11_ABI || ! _GLIBCXX_USE_DUAL_ABI + template struct __moneypunct_cache; + template struct __moneypunct_cache; + #endif +@@ -64,7 +64,7 @@ + _GLIBCXX_END_NAMESPACE_CXX11 + + // numpunct, numpunct_byname, num_get, and num_put +-#if ! _GLIBCXX_USE_CXX11_ABI ++#if ! _GLIBCXX_USE_CXX11_ABI || ! _GLIBCXX_USE_DUAL_ABI + template struct __numpunct_cache; + #endif + _GLIBCXX_BEGIN_NAMESPACE_CXX11 +@@ -73,7 +73,7 @@ + _GLIBCXX_END_NAMESPACE_CXX11 + + // time_get and time_put +-#if ! _GLIBCXX_USE_CXX11_ABI ++#if ! _GLIBCXX_USE_CXX11_ABI || ! _GLIBCXX_USE_DUAL_ABI + template class __timepunct; + template struct __timepunct_cache; + template class time_put >; +@@ -97,13 +97,13 @@ + ctype_byname::ctype_byname(const string& __s, size_t __refs) + : ctype_byname(__s.c_str(), __refs) { } + +-#if ! _GLIBCXX_USE_CXX11_ABI ++#if ! _GLIBCXX_USE_CXX11_ABI || ! _GLIBCXX_USE_DUAL_ABI + inline template class __ctype_abstract_base; + template class ctype_byname; + #endif + + // codecvt +-#if ! _GLIBCXX_USE_CXX11_ABI ++#if ! _GLIBCXX_USE_CXX11_ABI || ! _GLIBCXX_USE_DUAL_ABI + inline template class __codecvt_abstract_base; + template class codecvt_byname; + #else +@@ -118,7 +118,7 @@ + _GLIBCXX_END_NAMESPACE_CXX11 + + // use_facet and has_facet instantiations +-#if ! _GLIBCXX_USE_CXX11_ABI ++#if ! _GLIBCXX_USE_CXX11_ABI || ! _GLIBCXX_USE_DUAL_ABI + INSTANTIATE_FACET_ACCESSORS(ctype); + INSTANTIATE_FACET_ACCESSORS(codecvt); + #endif +@@ -127,14 +127,14 @@ + INSTANTIATE_FACET_ACCESSORS(moneypunct); + // No explicit instantiation of has_facet> for some reason. + INSTANTIATE_USE_FACET (moneypunct); +-#if ! _GLIBCXX_USE_CXX11_ABI ++#if ! _GLIBCXX_USE_CXX11_ABI || ! _GLIBCXX_USE_DUAL_ABI + INSTANTIATE_FACET_ACCESSORS(__timepunct); + INSTANTIATE_FACET_ACCESSORS(time_put); + #endif + INSTANTIATE_FACET_ACCESSORS(time_get); + INSTANTIATE_FACET_ACCESSORS(messages); + +-#if ! _GLIBCXX_USE_CXX11_ABI ++#if ! _GLIBCXX_USE_CXX11_ABI || ! _GLIBCXX_USE_DUAL_ABI + // locale functions. + template + C* +--- a/libstdc++-v3/src/c++98/ios_failure.cc ++++ b/libstdc++-v3/src/c++98/ios_failure.cc +@@ -45,6 +45,10 @@ + { + _GLIBCXX_BEGIN_NAMESPACE_VERSION + ++#if ! _GLIBCXX_USE_CXX11_ABI ++// For a single-ABI build with the new string ABI, ios::failure derives ++// from system_error and its members are defined in a C++11 translation ++// unit, src/c++11/ios_errcat.cc. + ios_base::failure::failure(const string& __str) throw() + : _M_msg(__str) { } + +@@ -54,6 +58,7 @@ + const char* + ios_base::failure::what() const throw() + { return _M_msg.c_str(); } ++#endif + + #if _GLIBCXX_USE_DUAL_ABI + // When the dual ABI is enabled __throw_ios_failure() is defined in +@@ -84,6 +89,7 @@ + + #else // ! _GLIBCXX_USE_DUAL_ABI + ++#if ! _GLIBCXX_USE_CXX11_ABI + void + __throw_ios_failure(const char* __s __attribute__((unused))) + { _GLIBCXX_THROW_OR_ABORT(ios::failure(_(__s))); } +@@ -91,6 +97,7 @@ + void + __throw_ios_failure(const char* str, int) + { __throw_ios_failure(str); } ++#endif + + #endif // _GLIBCXX_USE_DUAL_ABI + diff --git a/src/gcc-final/series b/src/gcc-final/series index 45893e4..81ddbe6 100644 --- a/src/gcc-final/series +++ b/src/gcc-final/series @@ -4,3 +4,4 @@ i686-dumpmachine.patch libgomp-gfortran-path.patch relocatable.patch static-complit-flex-array.patch +new-abi-only.patch From e1f2c74b3028df0b33e76861800e9118d1d074c8 Mon Sep 17 00:00:00 2001 From: Christopher Wellons Date: Sat, 5 Sep 2026 14:14:57 -0400 Subject: [PATCH 05/12] gcc: Use forward slashes in the self-located driver path The driver copies argv[0] verbatim into COLLECT_GCC, which lto-wrapper uses as the compiler for the LTRANS stage. For parallel LTO (-flto=N, -flto=auto, or a detected jobserver) lto-wrapper writes that path unquoted into a generated Makefile and runs make, whose shell then consumes the backslashes of a Windows path, leaving "C:Users...g++.exe" and a failed link. Since the driver now always replaces argv[0] with the GetModuleFileNameA result, every -flto=N link was affected. Convert the path to forward slashes, which GCC accepts everywhere. The i686-dumpmachine patch is refreshed for the shifted hunk. Co-Authored-By: Claude Fable 5.1 --- src/gcc-final/driver-self-locate.patch | 16 ++++++++++++---- src/gcc-final/i686-dumpmachine.patch | 2 +- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/gcc-final/driver-self-locate.patch b/src/gcc-final/driver-self-locate.patch index c91e169..ad27f19 100644 --- a/src/gcc-final/driver-self-locate.patch +++ b/src/gcc-final/driver-self-locate.patch @@ -7,7 +7,7 @@ in bin/ rather than a tooldir. With this patch the toolchain works without w64devkit bin/ in PATH. --- a/gcc/gcc.cc +++ b/gcc/gcc.cc -@@ -56,6 +56,11 @@ compilation is specified by a string called a "spec". */ +@@ -56,6 +56,11 @@ #include "opts-diagnostic.h" #include "auto-profile.h" /* for AUTO_PROFILE_VERSION. */ @@ -19,7 +19,7 @@ without w64devkit bin/ in PATH. #ifndef MATH_LIBRARY #define MATH_LIBRARY "m" #endif -@@ -5566,6 +5571,17 @@ process_command (unsigned int decoded_options_count, +@@ -5566,6 +5571,17 @@ "BINUTILS", PREFIX_PRIORITY_LAST, 0, 1); free (tooldir_prefix); @@ -37,7 +37,7 @@ without w64devkit bin/ in PATH. #if defined(TARGET_SYSTEM_ROOT_RELOCATABLE) && !defined(VMS) /* If the normal TARGET_SYSTEM_ROOT is inside of $exec_prefix, then consider it to relocate with the rest of the GCC installation -@@ -8307,6 +8323,14 @@ driver::main (int argc, char **argv) +@@ -8307,6 +8323,22 @@ bool early_exit; set_progname (argv[0]); @@ -47,7 +47,15 @@ without w64devkit bin/ in PATH. + static char self[260]; + unsigned selflen = GetModuleFileNameA (0, self, sizeof self); + if (selflen && selflen < sizeof self) -+ argv[0] = self; ++ { ++ /* Use forward slashes: this path becomes COLLECT_GCC, which ++ lto-wrapper writes unquoted into a Makefile for parallel LTO, ++ where a shell would consume backslashes. */ ++ for (char *p = self; *p; p++) ++ if (*p == '\\') ++ *p = '/'; ++ argv[0] = self; ++ } +#endif expand_at_files (&argc, &argv); decode_argv (argc, const_cast (argv)); diff --git a/src/gcc-final/i686-dumpmachine.patch b/src/gcc-final/i686-dumpmachine.patch index 3669a7e..5197d3a 100644 --- a/src/gcc-final/i686-dumpmachine.patch +++ b/src/gcc-final/i686-dumpmachine.patch @@ -7,7 +7,7 @@ compilers, and target-detecting build systems expect that prefix and invocation name. --- a/gcc/gcc.cc +++ b/gcc/gcc.cc -@@ -4249,6 +4249,13 @@ driver_handle_option (struct gcc_options *opts, +@@ -4254,6 +4254,13 @@ exit (0); case OPT_dumpmachine: From d882f4be62907d3803fcf81985f06975991a49af Mon Sep 17 00:00:00 2001 From: Christopher Wellons Date: Sat, 5 Sep 2026 14:14:57 -0400 Subject: [PATCH 06/12] gcc: Use the LTO linker plugin only when -flto is given at link time With full plugin support in the linker, the driver passes the LTO plugin on every link, and the plugin claims any input containing LTO bytecode. Since libstdc++, libgfortran, and libquadmath are now fat LTO objects, that recompiled the library members of every C++ program whether or not the user asked for LTO: links took 4x longer for tiny programs and 30-45x for typical ones, lto-wrapper nagged about serial compilation, and every LTO edge case reached every user. Only pass the plugin when -flto, -flto=N, or -fuse-linker-plugin is on the link line. Ordinary links use the libraries' regular code exactly as before, and -flto links get the full benefit, including dead-code elimination inside the libraries. The one behavior change is that a slim LTO object linked without -flto now fails with "plugin needed to handle lto object", which is what the GCC manual documents anyway. Co-Authored-By: Claude Fable 5.1 --- src/gcc-final/lto-plugin-only-with-flto.patch | 34 +++++++++++++++++++ src/gcc-final/series | 1 + 2 files changed, 35 insertions(+) create mode 100644 src/gcc-final/lto-plugin-only-with-flto.patch diff --git a/src/gcc-final/lto-plugin-only-with-flto.patch b/src/gcc-final/lto-plugin-only-with-flto.patch new file mode 100644 index 0000000..6f3d993 --- /dev/null +++ b/src/gcc-final/lto-plugin-only-with-flto.patch @@ -0,0 +1,34 @@ +Use the LTO linker plugin only when -flto is given at link time + +With full plugin support in the linker, the driver passes the LTO +plugin on every link, and the plugin claims any input containing LTO +bytecode. Since libstdc++, libgfortran, and libquadmath are now fat +LTO objects, that recompiled the library members of every C++ program, +whether or not the user asked for LTO: links took 4x longer for tiny +programs and 30-45x for typical ones, lto-wrapper nagged about serial +compilation, and every LTO edge case reached every user. + +Only pass the plugin when -flto, -flto=N, or -fuse-linker-plugin is on +the link line. Ordinary links use the libraries' regular code exactly +as before, and -flto links get the full benefit, including dead-code +elimination inside the libraries. The one behavior change is that a +slim LTO object linked without -flto now fails with "plugin needed to +handle lto object", which is what the GCC manual documents anyway. + +--- a/gcc/gcc.cc ++++ b/gcc/gcc.cc +@@ -1105,9 +1105,12 @@ + + /* The linker has some plugin support. */ + #if HAVE_LTO_PLUGIN > 0 +-/* The linker used has full plugin support, use LTO plugin by default. */ ++/* The linker used has full plugin support, but only use the LTO plugin ++ when LTO is requested at link time. The runtime libraries are fat ++ LTO objects, and the plugin would otherwise claim their IR on every ++ link, recompiling the library and slowing all links. */ + #if HAVE_LTO_PLUGIN == 2 +-#define PLUGIN_COND "!fno-use-linker-plugin:%{!fno-lto" ++#define PLUGIN_COND "!fno-use-linker-plugin:%{flto|flto=*|fuse-linker-plugin" + #define PLUGIN_COND_CLOSE "}" + #else + /* The linker used has limited plugin support, use LTO plugin with explicit diff --git a/src/gcc-final/series b/src/gcc-final/series index 81ddbe6..91a4127 100644 --- a/src/gcc-final/series +++ b/src/gcc-final/series @@ -5,3 +5,4 @@ libgomp-gfortran-path.patch relocatable.patch static-complit-flex-array.patch new-abi-only.patch +lto-plugin-only-with-flto.patch From 81ee7f7a1e908be71b3958dee194173d2cc95b3f Mon Sep 17 00:00:00 2001 From: Christopher Wellons Date: Sat, 5 Sep 2026 14:14:58 -0400 Subject: [PATCH 07/12] gcc: Keep libsupc++ and ios_init.cc out of LTO bytecode Two parts of libstdc++ cannot safely be link-time optimized, even though the rest of the library is built as fat LTO objects. libsupc++ is the C++ runtime proper. Compiler-generated code refers to some of its entry points in ways the LTO symbol resolution never sees: a must-not-throw landing pad calls __cxa_call_terminate, but that call is only materialized late, in pass_lower_resx, after whole-program analysis has already dropped the apparently unreferenced definition from eh_call.o. An -flto link of a program using or then fails with "defined in discarded section". Compile all of libsupc++ without LTO, as libgcc is. ios_init.cc defines cout, cin, and the other standard streams with deliberately unrelated types (character arrays sized and aligned like the stream classes) to control their construction. That is a textbook ODR violation that the compiler can only see across translation units with LTO, and -Wodr then reports every one of them on every -flto link that uses iostreams. Keep that one file plain as well. Co-Authored-By: Claude Fable 5.1 --- .../libstdcxx-no-lto-punned-types.patch | 97 +++++++++++++++++++ src/gcc-final/series | 1 + 2 files changed, 98 insertions(+) create mode 100644 src/gcc-final/libstdcxx-no-lto-punned-types.patch diff --git a/src/gcc-final/libstdcxx-no-lto-punned-types.patch b/src/gcc-final/libstdcxx-no-lto-punned-types.patch new file mode 100644 index 0000000..d8985af --- /dev/null +++ b/src/gcc-final/libstdcxx-no-lto-punned-types.patch @@ -0,0 +1,97 @@ +Keep libsupc++ and ios_init.cc out of LTO bytecode + +Two parts of libstdc++ cannot safely be link-time optimized, even +though the rest of the library is built as fat LTO objects. + +libsupc++ is the C++ runtime proper. Compiler-generated code refers to +some of its entry points in ways the LTO symbol resolution never sees: +a must-not-throw landing pad calls __cxa_call_terminate, but that call +is only materialized late, in pass_lower_resx, after whole-program +analysis has already dropped the apparently unreferenced definition +from eh_call.o. An -flto link of a program using or +then fails with "defined in discarded section". Compile all of +libsupc++ without LTO, as libgcc is. + +ios_init.cc defines cout, cin, and the other standard streams with +deliberately unrelated types (character arrays sized and aligned like +the stream classes) to control their construction. That is a textbook +ODR violation that the compiler can only see across translation units +with LTO, and -Wodr then reports every one of them on every -flto link +that uses iostreams. Keep that one file plain as well. + +--- a/libstdc++-v3/libsupc++/Makefile.am ++++ b/libstdc++-v3/libsupc++/Makefile.am +@@ -199,11 +199,17 @@ + # 2) Need to explicitly set LTCXXCOMPILE so that EXTRA_CXX_FLAGS is + # last. (That way, things like -O2 passed down from the toplevel can + # be overridden by --enable-debug.) ++# ++# Never compile the C++ runtime to LTO bytecode: compiler-generated code ++# refers to some of its entry points (e.g. __cxa_call_terminate from ++# must-not-throw landing pads) only after whole-program analysis, by ++# which time the linker plugin may have discarded the definition. + LTCXXCOMPILE = \ + $(LIBTOOL) --tag CXX --tag disable-shared \ + $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) \ + --mode=compile $(CXX) $(TOPLEVEL_INCLUDES) \ +- $(AM_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) $(EXTRA_CXX_FLAGS) ++ $(AM_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) $(EXTRA_CXX_FLAGS) \ ++ -fno-lto + + LTLDFLAGS = $(shell $(SHELL) $(top_srcdir)/../libtool-ldflags $(LDFLAGS)) + +--- a/libstdc++-v3/libsupc++/Makefile.in ++++ b/libstdc++-v3/libsupc++/Makefile.in +@@ -639,11 +639,17 @@ + # 2) Need to explicitly set LTCXXCOMPILE so that EXTRA_CXX_FLAGS is + # last. (That way, things like -O2 passed down from the toplevel can + # be overridden by --enable-debug.) ++# ++# Never compile the C++ runtime to LTO bytecode: compiler-generated code ++# refers to some of its entry points (e.g. __cxa_call_terminate from ++# must-not-throw landing pads) only after whole-program analysis, by ++# which time the linker plugin may have discarded the definition. + LTCXXCOMPILE = \ + $(LIBTOOL) --tag CXX --tag disable-shared \ + $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) \ + --mode=compile $(CXX) $(TOPLEVEL_INCLUDES) \ +- $(AM_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) $(EXTRA_CXX_FLAGS) ++ $(AM_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) $(EXTRA_CXX_FLAGS) \ ++ -fno-lto + + LTLDFLAGS = $(shell $(SHELL) $(top_srcdir)/../libtool-ldflags $(LDFLAGS)) + +--- a/libstdc++-v3/src/c++98/Makefile.am ++++ b/libstdc++-v3/src/c++98/Makefile.am +@@ -200,6 +200,14 @@ + parallel_settings.o: parallel_settings.cc + $(CXXCOMPILE) $(PARALLEL_FLAGS) -c $< + ++# ios_init.cc deliberately defines the standard streams with types ++# unrelated to their declarations, so keep it out of LTO bytecode, where ++# -Wodr would report them on every link. ++ios_init.lo: ios_init.cc ++ $(LTCXXCOMPILE) -fno-lto -c $< ++ios_init.o: ios_init.cc ++ $(CXXCOMPILE) -fno-lto -c $< ++ + # AM_CXXFLAGS needs to be in each subdirectory so that it can be + # modified in a per-library or per-sub-library way. Need to manually + # set this option because CONFIG_CXXFLAGS has to be after +--- a/libstdc++-v3/src/c++98/Makefile.in ++++ b/libstdc++-v3/src/c++98/Makefile.in +@@ -913,6 +913,14 @@ + parallel_settings.o: parallel_settings.cc + $(CXXCOMPILE) $(PARALLEL_FLAGS) -c $< + ++# ios_init.cc deliberately defines the standard streams with types ++# unrelated to their declarations, so keep it out of LTO bytecode, where ++# -Wodr would report them on every link. ++ios_init.lo: ios_init.cc ++ $(LTCXXCOMPILE) -fno-lto -c $< ++ios_init.o: ios_init.cc ++ $(CXXCOMPILE) -fno-lto -c $< ++ + # Tell versions [3.59,3.63) of GNU make to not export all variables. + # Otherwise a system limit (for SysV at least) may be exceeded. + .NOEXPORT: diff --git a/src/gcc-final/series b/src/gcc-final/series index 91a4127..4fbdba1 100644 --- a/src/gcc-final/series +++ b/src/gcc-final/series @@ -6,3 +6,4 @@ relocatable.patch static-complit-flex-array.patch new-abi-only.patch lto-plugin-only-with-flto.patch +libstdcxx-no-lto-punned-types.patch From 4beb12bd569bfde25e0dcb24665a12ac3c9b4534 Mon Sep 17 00:00:00 2001 From: Christopher Wellons Date: Sat, 5 Sep 2026 15:14:46 -0400 Subject: [PATCH 08/12] make: Look for the shell beside make before searching PATH On Windows, make finds its shell by searching PATH for sh.exe and falls back to cmd.exe, whose quoting rules break ordinary recipes. When make is invoked by path without w64devkit's bin/ on PATH, that search fails even though sh.exe sits right next to make.exe, and it does not help to set SHELL in the environment, which make ignores. Check make's own directory first, so that make works on its own like the rest of the toolchain. This is what GCC's lto-wrapper needs for parallel LTO in a PATH-free invocation. Verified with a native build: invoked with an empty PATH, the patched make runs a recipe through the adjacent sh.exe, and falls back to cmd.exe as before when sh.exe is removed. Co-Authored-By: Claude Fable 5.1 --- src/make/self-locate-shell.patch | 47 ++++++++++++++++++++++++++++++++ src/make/series | 1 + 2 files changed, 48 insertions(+) create mode 100644 src/make/self-locate-shell.patch diff --git a/src/make/self-locate-shell.patch b/src/make/self-locate-shell.patch new file mode 100644 index 0000000..08bbda7 --- /dev/null +++ b/src/make/self-locate-shell.patch @@ -0,0 +1,47 @@ +Look for the shell beside make before searching PATH + +On Windows, make finds its shell by searching PATH for sh.exe and falls +back to cmd.exe, whose quoting rules break ordinary recipes. When make +is invoked by path without w64devkit's bin/ on PATH, as GCC's +lto-wrapper now does for parallel LTO, the search fails even though +sh.exe sits right next to make.exe. Check make's own directory first, +so that make works on its own like the rest of the toolchain. + +--- a/src/main.c ++++ b/src/main.c +@@ -1056,8 +1056,34 @@ + char *p; + struct variable *v = lookup_variable (STRING_SIZE_TUPLE ("PATH")); + ++ /* Prefer a shell beside make itself, so that make works when ++ invoked by path without its directory on PATH. */ ++ { ++ PATH_VAR (exe_path); ++ DWORD n = GetModuleFileNameA (NULL, exe_path, GET_PATH_MAX); ++ if (token == NULL && n > 0 && n < GET_PATH_MAX) ++ { ++ char *base = exe_path + n; ++ while (base > exe_path && !ISDIRSEP (base[-1])) ++ base--; ++ if (base > exe_path ++ && strlen (search_token) < GET_PATH_MAX - (base - exe_path)) ++ { ++ strcpy (base, search_token); ++ if (_access (exe_path, 0) == 0) ++ { ++ default_shell = xstrdup (w32ify (exe_path, 0)); ++ sh_found = 1; ++ DB (DB_VERBOSE, ++ (_("find_and_set_shell() setting default_shell = %s\n"), ++ default_shell)); ++ } ++ } ++ } ++ } ++ + /* Search Path for shell */ +- if (v && v->value) ++ if (!sh_found && v && v->value) + { + char *ep; + diff --git a/src/make/series b/src/make/series index 5fd1c71..15f8355 100644 --- a/src/make/series +++ b/src/make/series @@ -1,2 +1,3 @@ force-slow.patch shell-space.patch +self-locate-shell.patch From 69cff98a5d09ca4906b34454d8966ae0b278a7bd Mon Sep 17 00:00:00 2001 From: Christopher Wellons Date: Sat, 5 Sep 2026 15:14:46 -0400 Subject: [PATCH 09/12] gcc: Look for make beside the driver for parallel LTO Parallel LTRANS (-flto=N, -flto=auto, or a detected jobserver) hands the partitions to make through a generated Makefile, and lto-wrapper finds make only through $MAKE or PATH. When the toolchain is invoked by path without its directory on PATH, which the self-locating driver otherwise supports, that search fails and lto-wrapper silently falls back to serial compilation, with a warning suggesting -flto=auto that the user may already be passing. After $MAKE and PATH, also try make in the driver's own directory, known from COLLECT_GCC, and run that one. Together with make locating its shell beside itself, an -flto=auto link now works in parallel with nothing on PATH; verified by pointing $MAKE at the kit's make from an empty PATH, which exercises the same invocation. Co-Authored-By: Claude Fable 5.1 --- .../lto-wrapper-make-beside-driver.patch | 106 ++++++++++++++++++ src/gcc-final/series | 1 + 2 files changed, 107 insertions(+) create mode 100644 src/gcc-final/lto-wrapper-make-beside-driver.patch diff --git a/src/gcc-final/lto-wrapper-make-beside-driver.patch b/src/gcc-final/lto-wrapper-make-beside-driver.patch new file mode 100644 index 0000000..0225f8e --- /dev/null +++ b/src/gcc-final/lto-wrapper-make-beside-driver.patch @@ -0,0 +1,106 @@ +lto-wrapper: Look for make beside the driver when it is not on PATH + +Parallel LTRANS (-flto=N, -flto=auto, or a detected jobserver) hands +the partitions to make through a generated Makefile, and lto-wrapper +finds make only through $MAKE or PATH. When the toolchain is invoked by +path without its directory on PATH, which the self-locating driver +otherwise supports, that search fails and lto-wrapper silently falls +back to serial compilation, with a warning suggesting -flto=auto that +the user may already be passing. + +After $MAKE and PATH, also try make in the driver's own directory, +known from COLLECT_GCC, and run that one. + +--- a/gcc/lto-wrapper.cc ++++ b/gcc/lto-wrapper.cc +@@ -1387,26 +1387,63 @@ + url.get ()); + } + +-/* Test that a make command is present and working, return true if so. */ ++#ifndef HOST_EXECUTABLE_SUFFIX ++#define HOST_EXECUTABLE_SUFFIX "" ++#endif + ++/* The make program to run for parallel LTRANS when $MAKE is unset. */ ++ ++static const char *make_program = "make"; ++ ++/* Return true if MAKE runs and reports a version. */ ++ + static bool +-make_exists (void) ++make_works (const char *make) + { +- const char *make = "make"; +- char **make_argv = buildargv (getenv ("MAKE")); +- if (make_argv) +- make = make_argv[0]; + const char *make_args[] = {make, "--version", NULL}; +- + int exit_status = 0; + int err = 0; + const char *errmsg +- = pex_one (PEX_SEARCH, make_args[0], const_cast (make_args), ++ = pex_one (PEX_SEARCH, make, const_cast (make_args), + "make", NULL, NULL, &exit_status, &err); +- freeargv (make_argv); + return errmsg == NULL && exit_status == 0 && err == 0; + } + ++/* Test that a make command is present and working, return true if so. ++ Honor $MAKE, then search PATH, then look beside the driver ++ COLLECT_GCC, so that a toolchain invoked by path works without its ++ directory on PATH. */ ++ ++static bool ++make_exists (const char *collect_gcc) ++{ ++ char **make_argv = buildargv (getenv ("MAKE")); ++ if (make_argv) ++ { ++ bool ok = make_works (make_argv[0]); ++ freeargv (make_argv); ++ return ok; ++ } ++ ++ if (make_works (make_program)) ++ return true; ++ ++ size_t dirlen = lbasename (collect_gcc) - collect_gcc; ++ if (dirlen) ++ { ++ char *dir = xstrndup (collect_gcc, dirlen); ++ char *make = concat (dir, "make" HOST_EXECUTABLE_SUFFIX, NULL); ++ free (dir); ++ if (make_works (make)) ++ { ++ make_program = make; ++ return true; ++ } ++ free (make); ++ } ++ return false; ++} ++ + /* Execute gcc. ARGC is the number of arguments. ARGV contains the arguments. */ + + static void +@@ -1710,7 +1747,7 @@ + } + + /* We need make working for a parallel execution. */ +- if (parallel && !make_exists ()) ++ if (parallel && !make_exists (collect_gcc)) + parallel = 0; + + if (!dumppfx) +@@ -2192,7 +2229,7 @@ + obstack_ptr_grow (&argv_obstack, make_argv[argc]); + } + else +- obstack_ptr_grow (&argv_obstack, "make"); ++ obstack_ptr_grow (&argv_obstack, make_program); + + obstack_ptr_grow (&argv_obstack, "-f"); + obstack_ptr_grow (&argv_obstack, makefile); diff --git a/src/gcc-final/series b/src/gcc-final/series index 4fbdba1..99b2147 100644 --- a/src/gcc-final/series +++ b/src/gcc-final/series @@ -7,3 +7,4 @@ static-complit-flex-array.patch new-abi-only.patch lto-plugin-only-with-flto.patch libstdcxx-no-lto-punned-types.patch +lto-wrapper-make-beside-driver.patch From 15cd6be60ec32a017c404f9985527db5e95d3f2c Mon Sep 17 00:00:00 2001 From: Christopher Wellons Date: Sat, 5 Sep 2026 16:22:39 -0400 Subject: [PATCH 10/12] ld: Match COFF COMDAT keys between real objects and LTO IR on i386 The linker deduplicates COMDAT sections by key. For a real COFF section the key is its COMDAT symbol name, which on i386 PE carries the user label prefix (__ZTISt9bad_alloc). For a symbol from an LTO IR object, ld creates a placeholder section named after the COMDAT group the compiler reported, which is the group name without any prefix (_ZTISt9bad_alloc). The two never match, so whenever a symbol is defined both by a regular object and by LTO bytecode, ld reports a multiple definition instead of keeping one copy. That is every -flto link on i686 once libstdc++ carries bytecode, since libsupc++ and the CRT are regular objects that define the same typeinfo and template instantiations, and it also breaks linking a plain object into an -flto program. x86_64 PE has no prefix and is unaffected. Strip the target's symbol leading char from keys derived from real objects so that they match the compiler's group names. Verified with native x86_64 and i686 builds of ld against both kits: -flto links, mixed plain and -flto links, and ordinary links all behave, and x86_64 is unchanged. Co-Authored-By: Claude Fable 5.1 --- .../coff-comdat-key-leading-char.patch | 36 +++++++++++++++++++ src/binutils/series | 1 + 2 files changed, 37 insertions(+) create mode 100644 src/binutils/coff-comdat-key-leading-char.patch diff --git a/src/binutils/coff-comdat-key-leading-char.patch b/src/binutils/coff-comdat-key-leading-char.patch new file mode 100644 index 0000000..1ac7b6e --- /dev/null +++ b/src/binutils/coff-comdat-key-leading-char.patch @@ -0,0 +1,36 @@ +Match COFF COMDAT keys between real objects and LTO IR on i386 + +The linker deduplicates COMDAT sections by key. For a real COFF +section the key is its COMDAT symbol name, which on i386 PE carries the +user label prefix (__ZTISt9bad_alloc). For a symbol from an LTO IR +object, ld creates a placeholder section named after the COMDAT group +the compiler reported, which is the group name without any prefix +(_ZTISt9bad_alloc). The two never match, so whenever a symbol is +defined both by a regular object and by LTO bytecode, ld reports a +multiple definition instead of keeping one copy. That is every -flto +link on i686 once libstdc++ carries bytecode, since libsupc++ and the +CRT are regular objects that define the same typeinfo and template +instantiations, and it also breaks linking a plain object into an -flto +program. x86_64 PE has no prefix and is unaffected. + +Strip the target's symbol leading char from keys derived from real +objects so that they match the compiler's group names. + +--- a/bfd/coffgen.c ++++ b/bfd/coffgen.c +@@ -2803,6 +2803,15 @@ + key = name; + } + ++ /* LTO IR plugin sections are keyed by the COMDAT group name as the ++ compiler knows it, without the symbol leading char that COMDAT ++ symbols in real objects carry on targets like i386 PE. Strip it ++ from keys derived from real objects so both kinds match. */ ++ if ((abfd->flags & BFD_PLUGIN) == 0 ++ && key[0] != '\0' ++ && key[0] == bfd_get_symbol_leading_char (abfd)) ++ key++; ++ + already_linked_list = bfd_section_already_linked_table_lookup (key); + if (!already_linked_list) + goto bad; diff --git a/src/binutils/series b/src/binutils/series index d743197..70df797 100644 --- a/src/binutils/series +++ b/src/binutils/series @@ -4,3 +4,4 @@ windmc-bom.patch windmc-carry-severity-facility.patch windmc-per-facility-ids.patch pe-lto-no-relocator-ref-for-relocatable.patch +coff-comdat-key-leading-char.patch From 4b156dcc497ee1a2963ca99ef2158c9ce0047746 Mon Sep 17 00:00:00 2001 From: Christopher Wellons Date: Sat, 5 Sep 2026 19:46:46 -0400 Subject: [PATCH 11/12] gcc: Stream REAL_CST significands in a host-independent layout LTO bytecode packs a REAL_VALUE_TYPE's significand as SIGSZ words of HOST_BITS_PER_LONG bits, which is the host's own layout: three 64-bit words on the Linux build host, five 32-bit words on Windows. The runtime libraries' bytecode is produced by the Linux-hosted cross compiler and consumed by the Windows-hosted compiler, so every floating-point constant in it arrived garbled: FLT_MAX read as zero, libgfortran printed every real as Infinity, and std::from_chars returned 0 for any floating input. Integers are unaffected because they stream as HOST_WIDE_INT, which is 64 bits on both hosts. Stream the significand's top 128 bits as four 32-bit words instead, most significant first, which covers every target format (binary128 needs 113), and the 16-byte decimal128 encoding in memory order for decimal values. Both hosts have at least 128 significand bits. The chunk arithmetic was checked to round-trip between the two layouts. This changes the bytecode format relative to upstream, so objects compiled by another GCC 16.2 build would misread constants here; the toolchain is self-contained and the version check never covered host differences anyway. Applied before both compilers are built. Co-Authored-By: Claude Fable 5.1 --- src/gcc/lto-portable-real-cst.patch | 97 +++++++++++++++++++++++++++++ src/gcc/series | 1 + 2 files changed, 98 insertions(+) create mode 100644 src/gcc/lto-portable-real-cst.patch diff --git a/src/gcc/lto-portable-real-cst.patch b/src/gcc/lto-portable-real-cst.patch new file mode 100644 index 0000000..4687c16 --- /dev/null +++ b/src/gcc/lto-portable-real-cst.patch @@ -0,0 +1,97 @@ +Stream REAL_CST significands in a host-independent layout + +LTO bytecode packs a REAL_VALUE_TYPE's significand as SIGSZ words of +HOST_BITS_PER_LONG bits, which is the host's own layout: three 64-bit +words on the Linux build host, five 32-bit words on Windows. The +runtime libraries' bytecode is produced by the Linux-hosted cross +compiler and consumed by the Windows-hosted compiler, so every +floating-point constant in it arrived garbled: FLT_MAX read as zero, +libgfortran printed every real as Infinity, and std::from_chars +returned 0 for any floating input. Integers are unaffected because +they stream as HOST_WIDE_INT, which is 64 bits on both hosts. + +Stream the significand's top 128 bits as four 32-bit words instead, +most significant first, which covers every target format (binary128 +needs 113), and the 16-byte decimal128 encoding in memory order for +decimal values. Both hosts have at least 128 significand bits. + +This changes the bytecode format relative to upstream, so objects +compiled by another GCC 16.2 build would misread constants here; the +toolchain is self-contained and the version check never covered host +differences anyway. Applied before both compilers are built. + +--- a/gcc/data-streamer.cc ++++ b/gcc/data-streamer.cc +@@ -122,6 +122,35 @@ + } + } + ++/* The significand of a REAL_VALUE_TYPE is an array of SIGSZ words of ++ HOST_BITS_PER_LONG bits, so its layout depends on the host. Stream ++ it in a host-independent form instead, so that bytecode produced by ++ a cross compiler on a host with a 64-bit long can be consumed by the ++ same compiler hosted on Windows, where long is 32 bits. ++ ++ A binary significand is aligned at the top of the array, and any ++ value in a target format occupies at most its top 128 bits, so stream ++ those as four 32-bit words, most significant first. A decimal value ++ keeps its decimal128 encoding in the first 16 bytes of the array, ++ which are streamed in memory order. Chunks never straddle a word ++ since HOST_BITS_PER_LONG is a multiple of 32. */ ++ ++#define SIG_STREAM_BITS 128 ++ ++static inline unsigned int ++sig_get32 (const REAL_VALUE_TYPE *r, unsigned bit) ++{ ++ return (unsigned int) (r->sig[bit / HOST_BITS_PER_LONG] ++ >> (bit % HOST_BITS_PER_LONG)); ++} ++ ++static inline void ++sig_set32 (REAL_VALUE_TYPE *r, unsigned bit, unsigned int v) ++{ ++ r->sig[bit / HOST_BITS_PER_LONG] ++ |= (unsigned long) v << (bit % HOST_BITS_PER_LONG); ++} ++ + /* Pack REAL_VALUE_TYPE R into BP. */ + + void +@@ -133,8 +162,16 @@ + bp_pack_value (bp, r->signalling, 1); + bp_pack_value (bp, r->canonical, 1); + bp_pack_value (bp, r->uexp, EXP_BITS); +- for (unsigned i = 0; i < SIGSZ; i++) +- bp_pack_value (bp, r->sig[i], HOST_BITS_PER_LONG); ++ if (r->decimal) ++ { ++ unsigned int w[SIG_STREAM_BITS / 32]; ++ memcpy (w, r->sig, sizeof w); ++ for (unsigned i = 0; i < SIG_STREAM_BITS / 32; i++) ++ bp_pack_value (bp, w[i], 32); ++ } ++ else ++ for (unsigned i = 0; i < SIG_STREAM_BITS / 32; i++) ++ bp_pack_value (bp, sig_get32 (r, SIGNIFICAND_BITS - 32 * (i + 1)), 32); + } + + /* Unpack REAL_VALUE_TYPE R from BP. */ +@@ -151,6 +188,15 @@ + r->signalling = (unsigned) bp_unpack_value (bp, 1); + r->canonical = (unsigned) bp_unpack_value (bp, 1); + r->uexp = (unsigned) bp_unpack_value (bp, EXP_BITS); +- for (unsigned i = 0; i < SIGSZ; i++) +- r->sig[i] = (unsigned long) bp_unpack_value (bp, HOST_BITS_PER_LONG); ++ if (r->decimal) ++ { ++ unsigned int w[SIG_STREAM_BITS / 32]; ++ for (unsigned i = 0; i < SIG_STREAM_BITS / 32; i++) ++ w[i] = (unsigned int) bp_unpack_value (bp, 32); ++ memcpy (r->sig, w, sizeof w); ++ } ++ else ++ for (unsigned i = 0; i < SIG_STREAM_BITS / 32; i++) ++ sig_set32 (r, SIGNIFICAND_BITS - 32 * (i + 1), ++ (unsigned int) bp_unpack_value (bp, 32)); + } diff --git a/src/gcc/series b/src/gcc/series index 4288b06..fd591b6 100644 --- a/src/gcc/series +++ b/src/gcc/series @@ -3,3 +3,4 @@ dynamic-load-tzdb.patch libgfortran-mapview.patch trap-terminate.patch no-abi-compat-aliases.patch +lto-portable-real-cst.patch From 9d333ff1c36d07e19291b80e67ce01722e67828f Mon Sep 17 00:00:00 2001 From: Christopher Wellons Date: Sat, 5 Sep 2026 20:04:10 -0400 Subject: [PATCH 12/12] ld: Don't export library symbols from DLLs built with LTO By default a DLL exports every global symbol, minus those from the standard libraries, startup objects, and (here) any archive under the sysroot, which ld filters by the symbol's archive of origin, and minus weak symbols, which is what COMDAT template instantiations are. LTO defeated all of that: a DLL using std::string exported 704 symbols instead of 2, with every one of them pinned against dead-code elimination. The exports came from the placeholder bfds that stand in for claimed IR objects. They stay in the input list after LTO, still listing every definition the plugin reported as a strong global, and they have no archive to be excluded by. Skip them in the export loop; the definitions that survive LTO are in the output objects the plugin added, and only those count. That alone would still export what the compiler keeps global, since ld tells the plugin that in a DLL link every symbol may be visible from outside. Add an emulation hook consulted during symbol resolution, which the PE emulations answer with the same auto-export decision, using the IR object's original archive member and treating weak or COMDAT definitions as unexportable. A library symbol is then reported as not visible, so the compiler localizes and prunes it as it would in an executable, and its export name is remembered so that auto-export also rejects the ones that survive as globals because regular objects reference them. Symbols listed in a .def file remain exported, and dllexport attributes are unaffected since the compiler honors them regardless of resolution. Verified with a native x64 build of ld on both the x64 and -m32 multilib: export-all LTO DLLs export exactly what non-LTO ones do and come out half the size, dllexport and .def exports are kept, the DLLs work when called, and executables are unchanged. Co-Authored-By: Claude Fable 5.1 --- .../pe-lto-no-export-library-symbols.patch | 295 ++++++++++++++++++ src/binutils/series | 1 + 2 files changed, 296 insertions(+) create mode 100644 src/binutils/pe-lto-no-export-library-symbols.patch diff --git a/src/binutils/pe-lto-no-export-library-symbols.patch b/src/binutils/pe-lto-no-export-library-symbols.patch new file mode 100644 index 0000000..cdbd042 --- /dev/null +++ b/src/binutils/pe-lto-no-export-library-symbols.patch @@ -0,0 +1,295 @@ +Don't export library symbols from DLLs built with LTO + +By default a DLL exports every global symbol, minus those from the +standard libraries, startup objects, and (here) any archive under the +sysroot, which ld filters by the symbol's archive of origin, and minus +weak symbols, which is what COMDAT template instantiations are. LTO +defeated all of that: a DLL using std::string exported 704 symbols +instead of 2, with every one of them pinned against dead-code +elimination. + +The exports came from the placeholder bfds that stand in for claimed +IR objects. They stay in the input list after LTO, still listing every +definition the plugin reported as a strong global, and they have no +archive to be excluded by. Skip them in the export loop; the definitions +that survive LTO are in the output objects the plugin added, and only +those count. + +That alone would still export what the compiler keeps global, since ld +tells the plugin that in a DLL link every symbol may be visible from +outside. Add an emulation hook consulted during symbol resolution, which +the PE emulations answer with the same auto-export decision, using the +IR object's original archive member and treating weak or COMDAT +definitions as unexportable. A library symbol is then reported as not +visible, so the compiler localizes and prunes it as it would in an +executable (the test DLL halves in size), and its export name is +remembered so that auto-export also rejects the ones that survive as +globals because regular objects reference them. Symbols listed in a +.def file remain exported, and dllexport attributes are unaffected since +the compiler honors them regardless of resolution. + +--- a/ld/emultempl/emulation.em ++++ b/ld/emultempl/emulation.em +@@ -37,6 +37,7 @@ + ${LDEMUL_ACQUIRE_STRINGS_FOR_CTF-NULL}, + ${LDEMUL_NEW_DYNSYM_FOR_CTF-NULL}, + ${LDEMUL_PRINT_SYMBOL-NULL}, +- ${LDEMUL_FIND_START_SYMBOL-NULL} ++ ${LDEMUL_FIND_START_SYMBOL-NULL}, ++ ${LDEMUL_LTO_SYMBOL_VISIBLE-NULL} + }; + EOF +--- a/ld/emultempl/pe.em ++++ b/ld/emultempl/pe.em +@@ -1463,6 +1463,17 @@ + #endif + } + ++static bool ++gld${EMULATION_NAME}_lto_symbol_visible (bfd *abfd, const char *name, ++ bool weak) ++{ ++#ifdef DLL_SUPPORT ++ return pe_dll_lto_symbol_visible (abfd, name, weak); ++#else ++ return true; ++#endif ++} ++ + static void + gld${EMULATION_NAME}_after_open (void) + { +@@ -2572,6 +2583,7 @@ + + LDEMUL_AFTER_PARSE=gld${EMULATION_NAME}_after_parse + LDEMUL_BEFORE_PLUGIN_ALL_SYMBOLS_READ=gld${EMULATION_NAME}_before_plugin_all_symbols_read ++LDEMUL_LTO_SYMBOL_VISIBLE=gld${EMULATION_NAME}_lto_symbol_visible + LDEMUL_AFTER_OPEN=gld${EMULATION_NAME}_after_open + LDEMUL_BEFORE_ALLOCATION=gld${EMULATION_NAME}_before_allocation + LDEMUL_FINISH=gld${EMULATION_NAME}_finish +--- a/ld/emultempl/pep.em ++++ b/ld/emultempl/pep.em +@@ -1471,6 +1471,17 @@ + #endif + } + ++static bool ++gld${EMULATION_NAME}_lto_symbol_visible (bfd *abfd, const char *name, ++ bool weak) ++{ ++#ifdef DLL_SUPPORT ++ return pep_dll_lto_symbol_visible (abfd, name, weak); ++#else ++ return true; ++#endif ++} ++ + static void + gld${EMULATION_NAME}_after_open (void) + { +@@ -2364,6 +2375,7 @@ + + LDEMUL_AFTER_PARSE=gld${EMULATION_NAME}_after_parse + LDEMUL_BEFORE_PLUGIN_ALL_SYMBOLS_READ=gld${EMULATION_NAME}_before_plugin_all_symbols_read ++LDEMUL_LTO_SYMBOL_VISIBLE=gld${EMULATION_NAME}_lto_symbol_visible + LDEMUL_AFTER_OPEN=gld${EMULATION_NAME}_after_open + LDEMUL_BEFORE_ALLOCATION=gld${EMULATION_NAME}_before_allocation + LDEMUL_FINISH=gld${EMULATION_NAME}_finish +--- a/ld/ldemul.c ++++ b/ld/ldemul.c +@@ -74,6 +74,14 @@ + ld_emulation->before_plugin_all_symbols_read (); + } + ++bool ++ldemul_lto_symbol_visible (bfd *abfd, const char *name, bool weak) ++{ ++ if (ld_emulation->lto_symbol_visible) ++ return ld_emulation->lto_symbol_visible (abfd, name, weak); ++ return true; ++} ++ + void + ldemul_after_open (void) + { +--- a/ld/ldemul.h ++++ b/ld/ldemul.h +@@ -34,6 +34,8 @@ + (void); + extern void ldemul_before_plugin_all_symbols_read + (void); ++extern bool ldemul_lto_symbol_visible ++ (bfd *, const char *, bool); + extern void ldemul_after_open + (void); + extern void ldemul_after_check_relocs +@@ -265,6 +267,14 @@ + struct bfd_link_hash_entry * (*find_alt_start_symbol) + (struct bfd_sym_chain *entry); + ++ /* Called during LTO symbol resolution for a symbol defined in IR ++ file ABFD (the archive member when it came from one). WEAK says ++ whether the definition is weak or in a COMDAT group. Return false ++ if the symbol could never be visible from outside the output, e.g. ++ because it would not be exported from a DLL, so that the compiler ++ may localize it. */ ++ bool (*lto_symbol_visible) (bfd *abfd, const char *name, bool weak); ++ + } ld_emulation_xfer_type; + + typedef enum { +--- a/ld/pe-dll.c ++++ b/ld/pe-dll.c +@@ -41,6 +41,7 @@ + #include "coff/internal.h" + #include "../bfd/libcoff.h" + #include "deffile.h" ++#include "hashtab.h" + + #ifdef pe_use_plus + +@@ -612,6 +613,50 @@ + return (startswith (n, "__imp_")); + } + ++/* Names of symbols defined in LTO bytecode that auto-export would have ++ rejected had they come from a regular object, typically members of ++ the standard libraries. Recorded at symbol resolution, when their ++ origin is still known, and consulted when exporting the LTO output, ++ where it is not. */ ++static htab_t lto_unexported; ++ ++static int auto_export (bfd *, def_file *, const char *); ++ ++/* Called during LTO symbol resolution for NAME, defined in IR file ++ ABFD, weak or in a COMDAT group if WEAK. Return whether the symbol ++ could be exported from this DLL. */ ++ ++bool ++pe_dll_lto_symbol_visible (bfd *abfd, const char *name, bool weak) ++{ ++ if (!bfd_link_dll (&link_info)) ++ return true; ++ ++ if (pe_def_file == NULL) ++ pe_def_file = def_file_empty (); ++ ++ /* Export names, in a .def file and in auto_export below, lack the ++ leading underscore that symbols carry on underscored targets. */ ++ if (pe_details->underscored && name[0] == '_') ++ name++; ++ for (int i = 0; i < pe_def_file->num_exports; i++) ++ if (strcmp (pe_def_file->exports[i].name, name) == 0) ++ return true; ++ ++ /* Auto-export skips weak symbols, which is what COMDAT template ++ instantiations are in regular objects; after LTO the prevailing ++ copies would come out strong and be exported instead. */ ++ if (!weak && auto_export (abfd, pe_def_file, name)) ++ return true; ++ ++ if (lto_unexported == NULL) ++ lto_unexported = htab_create_alloc (1024, htab_hash_string, ++ htab_eq_string, NULL, ++ xcalloc, free); ++ *htab_find_slot (lto_unexported, name, INSERT) = (void *) name; ++ return false; ++} ++ + /* abfd is a bfd containing n (or NULL) + It can be used for contextual checks. */ + +@@ -623,6 +668,12 @@ + const autofilter_entry_type *afptr; + const char * libname = NULL; + ++ /* Symbols in LTO output have lost their origin; use what symbol ++ resolution recorded about them. */ ++ if (abfd && abfd->lto_output && lto_unexported ++ && htab_find (lto_unexported, n) != NULL) ++ return 0; ++ + if (abfd && abfd->my_archive) + libname = lbasename (bfd_get_filename (abfd->my_archive)); + +@@ -818,6 +869,12 @@ + asymbol **symbols; + int nsyms; + ++ /* An LTO IR placeholder still lists every symbol the plugin ++ reported, but the definitions that survive LTO are in the ++ output objects the plugin added, and only those count. */ ++ if ((b->flags & BFD_PLUGIN) != 0) ++ continue; ++ + if (!bfd_generic_link_read_symbols (b)) + { + fatal (_("%P: %pB: could not read symbols: %E\n"), b); +--- a/ld/pe-dll.h ++++ b/ld/pe-dll.h +@@ -46,6 +46,8 @@ + (const char *); + extern void pe_dll_add_excludes + (const char *, const exclude_type); ++extern bool pe_dll_lto_symbol_visible ++ (bfd *, const char *, bool); + extern void pe_dll_generate_def_file + (const char *); + extern void pe_dll_generate_implib +--- a/ld/pep-dll.c ++++ b/ld/pep-dll.c +@@ -53,6 +53,7 @@ + #define pe_exe_fill_sections pep_exe_fill_sections + #define pe_dll_generate_implib pep_dll_generate_implib + #define pe_dll_add_excludes pep_dll_add_excludes ++#define pe_dll_lto_symbol_visible pep_dll_lto_symbol_visible + #define pe_bfd_is_dll pep_bfd_is_dll + #define pe_output_file_set_long_section_names \ + pep_output_file_set_long_section_names +--- a/ld/pep-dll.h ++++ b/ld/pep-dll.h +@@ -45,6 +45,7 @@ + + extern void pep_dll_id_target (const char *); + extern void pep_dll_add_excludes (const char *, const exclude_type); ++extern bool pep_dll_lto_symbol_visible (bfd *, const char *, bool); + extern void pep_dll_generate_def_file (const char *); + extern void pep_dll_generate_implib (def_file *, const char *, struct bfd_link_info *); + extern void pep_process_import_defs (bfd *, struct bfd_link_info *); +--- a/ld/plugin.c ++++ b/ld/plugin.c +@@ -30,6 +30,7 @@ + #include "ldexp.h" + #include "ldlang.h" + #include "ldfile.h" ++#include "ldemul.h" + #include "plugin-api.h" + #include "../bfd/plugin.h" + #include "plugin.h" +@@ -880,6 +881,17 @@ + + if (res == LDPR_PREVAILING_DEF_IRONLY) + { ++ /* Let the emulation rule out visibility from outside, e.g. a ++ symbol that would never be exported from a DLL. Ask for every ++ prevailing IR definition, since the emulation may also use the ++ answer to filter exports after LTO. */ ++ bool visible ++ = ldemul_lto_symbol_visible (input->ibfd ? input->ibfd ++ : input->abfd, ++ blhe->root.string, ++ (syms[n].def == LDPK_WEAKDEF ++ || syms[n].comdat_key != NULL)); ++ + /* We need to know if the sym is referenced from non-IR files. Or + even potentially-referenced, perhaps in a future final link if + this is a partial one, perhaps dynamically at load-time if the +@@ -891,7 +903,7 @@ + res = LDPR_PREVAILING_DEF; + else if (wrap_status == wrapped) + res = LDPR_RESOLVED_IR; +- else if (is_visible_from_outside (&syms[n], blhe)) ++ else if (visible && is_visible_from_outside (&syms[n], blhe)) + res = def_ironly_exp; + } + diff --git a/src/binutils/series b/src/binutils/series index 70df797..0c39eb4 100644 --- a/src/binutils/series +++ b/src/binutils/series @@ -5,3 +5,4 @@ windmc-carry-severity-facility.patch windmc-per-facility-ids.patch pe-lto-no-relocator-ref-for-relocatable.patch coff-comdat-key-leading-char.patch +pe-lto-no-export-library-symbols.patch