
Предисловие
Официальная вики Gentoo пишет: «LLVM profiles are experimental. Most people do not want these.»
Эта статья — хроника моей реальной миграции на LLVM-тулчейн с заменой libstdc++ на libc++.
1. Что меняется и почему это может быть больно
libc++ вместо libstdc++
Ключевое изменение — ABI-несовместимость. libc++ оборачивает все свои символы в inline-namespace std::__1, libstdc++ этого не делает (у неё std::__cxx11 — лишь inline-namespace для части типов вроде std::string). Манглированные имена не совпадают, поэтому объектники и библиотеки, собранные с разными C++ stdlib, нельзя связывать друг с другом.
Следствие: недостаточно пересобрать LLVM. Нужен полный:
emerge -e @worldGCC после этого не может быть запасным C++ компилятором. C-код он всё ещё собирает, но C++ соберёт бинарники с libstdc++, которые не слинкуются с системными библиотеками на libc++.
default-lld
При использовании lld можно использовать дополнительные возможности линкера (--icf=all, -z pack-relative-relocs). Но при сборке GCC часть bootstrap-процесса игнорирует системный linker и вызывает GNU ld напрямую, поэтому эти флаги ломают сборку.
GCC остаётся в системе навсегда
Причин три:
- glibc не собирается clang на Gentoo — ebuild принудительно подставляет gcc через gcc-config. На практике это единственный такой пакет: при аудите моей системы из 768 установленных пакетов с CC=gcc собран только glibc.
- GCC — поставщик рантайм-библиотек: libstdc++.so.6 нужна бинарным пакетам (zen-bin, vesktop-bin и т.п.), libgcc_s.so.1 — всем Rust-бинарникам (об этом ниже).
- Полное удаление GCC невозможно без поломки системы — и не нужно: его наличие не мешает LLVM-тулчейну.
2. Подготовка
2.1. Предварительные условия — установка Clang на старом профиле
Это самый важный шаг, который нельзя пропустить. До смены профиля на LLVM у вас уже должны быть установлены и работать Clang, LLD и остальной LLVM-тулчейн.
Сейчас мы находимся на обычном профиле с GCC. Все установки выполняются до смены профиля.
Установить LLVM и Clang (собираются GCC):
emerge llvm-core/clang llvm-core/llvm llvm-core/lld \
llvm-runtimes/compiler-rt llvm-runtimes/libunwindБутстрапнуть Clang — пересобрать его самим собой через package.env:
# /etc/portage/env/compiler-clang
COMMON_FLAGS="-march=native -O2 -pipe"
CFLAGS="${COMMON_FLAGS}"
CXXFLAGS="${COMMON_FLAGS}"
CC="clang"
CXX="clang++"
LDFLAGS="-fuse-ld=lld -rtlib=compiler-rt -unwindlib=libunwind -Wl,--as-needed"
# /etc/portage/package.env
llvm-core/clang compiler-clang
llvm-core/llvm compiler-clang
llvm-core/lld compiler-clang
llvm-runtimes/compiler-rt compiler-clang
llvm-runtimes/libunwind compiler-clang
llvm-runtimes/libcxx compiler-clang
llvm-runtimes/libcxxabi compiler-clang
llvm-runtimes/compiler-rt-sanitizers compiler-clang
emerge llvm-core/clang llvm-core/llvm llvm-core/lld \
llvm-runtimes/compiler-rt llvm-runtimes/libunwind \
llvm-runtimes/libcxx llvm-runtimes/libcxxabi \
llvm-runtimes/compiler-rt-sanitizers
Теперь Clang установлен и проверен в работе на старом профиле. Следующие шаги делаются уже после этого.
2.2. Смена профиля
LLVM профиль включает libc++, compiler-rt, lld и llvm-libunwind через профильные настройки USE. Вручную дублировать их обычно не требуется.
(default/linux/amd64/23.0/llvm)
eselect profile set 19(default/linux/amd64/23.0/llvm/systemd)
eselect profile set 20env-update && source /etc/profile2.3. make.conf — замена тулчейна
В make.conf:
CC="clang"
CXX="clang++"
LD="ld.lld"
AR="llvm-ar"
NM="llvm-nm"
RANLIB="llvm-ranlib"
READELF="llvm-readelf"
STRIP="llvm-strip"
OBJCOPY="llvm-objcopy"
SIZE="llvm-size"
STRINGS="llvm-strings"
OBJDUMP="llvm-objdump"
ADDR2LINE="llvm-addr2line"Вообще, LLVM-профиль уже задаёт всё это сам. Явный блок в make.conf — это осталось ещё до того, как я перешёл на LLVM профиль, так что, добавлять особой нужды нет, вреда от него нет.
LDFLAGS:
LDFLAGS="
-fuse-ld=lld
-rtlib=compiler-rt
-unwindlib=libunwind
-Wl,--as-needed
-Wl,-O2
-Wl,--hash-style=gnu
"При желании можете добавить дополнительные оптимизационные флаги:
-Wl,-z,pack-relative-relocs
-Wl,--icf=all2.4. Что НЕ трогать в make.conf
- LLVM_TARGETS — Не изменяйте LLVM_TARGETS без необходимости. Ручное переопределение может привести к проблемам.
- USE с default-* флагами — профиль сам установит.
- -rtlib=compiler-rt -unwindlib=libunwind в LDFLAGS — профиль дублирует, но мешать не будут.
2.5. GCC с флагами lld
GCC при bootstrap лезет в /usr/x86_64-pc-linux-gnu/binutils-bin/.../ld (GNU ld), игнорируя системный lld. Флаги --icf= и -z pack-relative-relocs GNU ld не понимает.
Решение
cat > /etc/portage/env/gcc-clean-lf << 'EOF'
LDFLAGS="-Wl,--as-needed -Wl,-O2 -Wl,--hash-style=gnu"
EOF
echo 'sys-devel/gcc gcc-clean-lf' >> /etc/portage/package.envЭтот файл не удалять — он нужен будет при каждой пересборке GCC.
Опциональный вариант (у меня в системе именно он): собирать сам GCC компилятором clang — отдельный env-файл с CC="clang" CXX="clang++" и чистыми LDFLAGS. GCC 15 собирается clang'ом без проблем.
3. Пошаговая сборка
Фаза 1: LLVM toolchain без libc++
Сразу включить default-libcxx нельзя — cmake собран с libstdc++, упадёт с symbol lookup error.
Временно отключить default-libcxx
cat > /etc/portage/package.use/toolchain-bootstrap << 'EOF'
llvm-runtimes/clang-stdlib-config -default-libcxx
llvm-runtimes/clang-runtime -default-libcxx
llvm-core/clang-common -default-libcxx
EOFПересобрать тулчейн
emerge -1 --keep-going dev-build/cmake llvm-core/llvm llvm-core/clang \
llvm-core/lld llvm-runtimes/compiler-rt llvm-runtimes/libcxx \
llvm-runtimes/libcxxabi llvm-runtimes/libunwind sys-devel/gccФаза 2: полный ребилд на libc++
rm /etc/portage/package.use/toolchain-bootstrap
emerge -e --keep-going @world4. Верификация
clang --version
# clang version 22.1.8+libcxx → ключевое "+libcxx"
ld.lld --version
# LLD 22.1.8 (compatible with GNU linkers)
# любой пакет, написанный на C++:
readelf -d /usr/bin/btop | grep libc++
# libc++.so.1 — C++ пакеты пересобраны на libc++
Системный аудит — кто ещё ссылается на libstdc++ (ожидаем только /opt/* у бинарных пакетов и /usr/lib/gcc/* у самого GCC):
scanelf -qRn /usr /opt 2>/dev/null | grep -F 'libstdc++.so.6'Кто реально был собран GCC (ожидаем только glibc):
for f in /var/db/pkg/*/*/environment.bz2; do
bzcat "$f" 2>/dev/null | grep -qm1 '^declare -x CC="gcc"' && echo "$f"
doneЧем собрано ядро (если собирали его clang):
cat /proc/version
# Linux version ... (clang version 22.1.8+libcxx, LLD 22.1.8) ...В моей системе итог аудита: из ~4400 ELF-файлов 595 слинкованы с libc++.so.1, а libstdc++.so.6 осталась только у бинарных пакетов из /opt и у собственных библиотек GCC. Ни один файл не линкует обе stdlib сразу.
5. Известные исключения
- glibc — собирается только GCC, так зашито в ebuild.
- Бинарные пакеты (www-client/zen-bin, net-im/vesktop-bin и подобные) — собраны апстримом, навсегда останутся на libstdc++. Лечится только сборкой из исходников.
- Rust-бинарники (fish, niri, yazi, librsvg...) — линкуют libgcc_s.so.1 by design: target-spec x86_64-unknown-linux-gnu жёстко добавляет -lgcc_s. Так выглядят даже официальные сборки Rust на любой системе.
Бонус: sanitizer-рантаймы на libc++
Даже после полной миграции shared-рантаймы санитайзеров (libclang_rt.{asan,hwasan,ubsan_standalone}-*.so) продолжают линковать libstdc++ — это дефолт апстрима (SANITIZER_CXX_ABI=default — на Linux подставляется libstdc++). Исправляется пользовательской переменной MYCMAKEARGS для cmake-ebuild'ов:
cat > /etc/portage/env/sanitizers-libcxx << 'EOF'
MYCMAKEARGS="-DSANITIZER_CXX_ABI=libc++"
EOF
echo 'llvm-runtimes/compiler-rt-sanitizers sanitizers-libcxx' >> /etc/portage/package.env
emerge -1v llvm-runtimes/compiler-rt-sanitizersПроверка:
scanelf -n /usr/lib/clang/22/lib/linux/libclang_rt.asan-x86_64.so
# было: libstdc++.so.6, libgcc_s.so.1, ...
# стало: libc++abi.so.1, libgcc_s.so.1, ...
Итог
Переход с GCC/libstdc++ на LLVM/Clang + libc++ — это не просто замена компилятора в make.conf, а полноценная смена C++ окружения системы с другим ABI, runtime и особенностями сопровождения.
В результате получается полностью LLVM-ориентированный toolchain:
- Компилятор: clang
- Линкер: lld
- C++ stdlib: libc++
- C++ ABI: libc++abi
- Рантайм: compiler-rt
- Unwinder: llvm-libunwind
- Binutils: llvm-ar/llvm-nm/llvm-objcopy/...
Но стоит понимать: переход на LLVM не является чудотворным способом ускорить систему. Тут не будет заметного прироста FPS, меньшего потребления памяти или ускорения каких-либо системных приложений.
Основные преимущества находятся в другом:
- LLD значительно быстрее GNU ld.bfd, что особенно заметно при сборке больших проектов и частых пересборках Gentoo.
- LLVM toolchain становится более цельным: все основные компоненты используют одну экосистему вместо смешивания GCC/binutils/libgcc.
- Clang предоставляет удобный набор инструментов для разработки: хорошие диагностики, sanitizers, статический анализ и тесную интеграцию с LLVM-экосистемой.
- В некоторых задачах Clang может давать отличающиеся результаты по размеру или производительности бинарников, хотя это сильно зависит от конкретного проекта.
При этом есть и обратная сторона. LLVM-профиль остаётся экспериментальным и требует понимания того, как работает сборка. Использование libc++ ломает совместимость с привычным libstdc++ ABI, поэтому переход требует полной пересборки и дальнейшего внимательного сопровождения.
Для большинства пользователей стандартный профиль с GCC/libstdc++ остаётся самым простым и предсказуемым вариантом.
Также стоит отметить, что использование Clang не требует перехода на libc++. Можно оставить стандартный libstdc++ и просто собрать LLVM/Clang toolchain поверх существующей системы. То есть можно остановиться на пункте 2.1, или следовать по инструкции с Gentoo Wiki, согласно которой LLVM используется как компилятор, но C++ ABI остаётся совместимым с привычным окружением.
Переход на libc++ — это уже отдельный шаг, который меняет ABI и значительно усложняет сопровождение системы. Поэтому для большинства пользователей разумным вариантом будет использовать Clang + LLD + compiler-rt, сохранив libstdc++, а полноценный переход на libc++ рассматривать как эксперимент или осознанный выбор.
Комментарии
13:50
> В некоторых задачах Clang может давать отличающиеся результаты по размеру или производительности бинарников, хотя это сильно зависит от конкретного проекта.
По ходу чтения у меня и возник вопрос — а зачем всё это нужно, зачем внутрянку копашить, ради чего? Для настоящих гентушников это может и не проблема, небольшая, но есть риск всё поломать, испортить, то есть усилия и время окажутся потраченными зря. Возвращаясь в начало моего комментария, у меня они "жили" по- соседству: GGC разных версий собран, он довольно быстро собираеется, а вот LLVM/Clang были бинарниками, потому что сборка локальная прерывалась (и 21, и 22).
Когда был на кальке, не всё собиралось через LLVM, например, браузер Librewolf собирается через него по умолчанию, но с ошибкой, меня ему use-флаг, успешно сборка проходила через gcc, то есть иногда это можно задать для пакета через флаги.
И в конце вопрос — а сразу, при установке выбрать профиль с LLVM выбрать?
14:18