Кросс-компилиция с Clang – это просто
Кросс-компилиция с Clang — это просто.
Определимся с терминами:
Хост/host — система, в которой производится сборка
Целевая машина/target — система, в которой наше ПО должно работать
Нативная компиляция — архитектура целевой машина совпадает с архитектурой хоста
Кросс-компиляция — архитектуры хоста и целевой машины разные
Мотивация для кросс-компиляции:
Мощность. У нас есть большая и мощная система только на одной архитектуре, а собирать надо под другую.
Отсутствие компилятора — целевая машины не имеет компилятора, т.е. мы никак не можем там собирать. Например, у нас целевая машина — это микроконтроллер stm32.
Масштабируемость. Можно собирать на одной системе под множество разных.
Целевая платформа в принципе пока не имеет компилятора, и нам надо его собрать (самый редкий случай).
Почему Clang?
На этот вопрос нет одного ответа, возможно в вашем случае будет достаточно и gcc. Но если немного забежать вперед, то Clang позволяет получить самый последний стандарт на уровне компилятора, а линковаться при этом с древней std-либой, чего gcc в свою очередь не позволяет. Походу статьи разберем и другие преимущества/недостатки, но это было веской причиной для нас. А именно — баг в gcc 8.3.0, который исправили в следующих версиях, но изменения не бэкпортировали. Поэтому собрать проект gcc, поставляемым в Debian Buster не было возможности.
Здесь не будет кросс-компиляции под raspberry, но у нас схожая архитектура (am3352), поэтому можно рассматривать это и как гайд под rpi (коих миллион). Кросс-компилировать мы будем с x86_64 (debian: amd64) на arm v7a (debian: armhf).
Теория
Процесс кросс-компиляции
Посмотрим, как же у нас идет процесс кросс-компиляции (на пальцах):
Заголовки подставляются в исходники
Всё это скармливается кросс-компилятору
Кросс-компилятор отдает нам объектные файлы
Их мы скармливаем линкеру
Также линкеру передаем статические и динамические библиотеки, из которых он должен взять символы для связывания
Если всё прошло успешно, то получаем наш исполняемый файл (или библиотеку)
Ничего нового, всё, как всегда. Но это на нашем хосте. На целевую систему мы должны как-то доставить наше приложение, и динамические библиотеки, от которых оно зависит.
Деплой нам пока не интересно (самое вкусное — на десерт). Рассмотрим, что используется на 2–5 шагах. Т.е. какие инструменты нам будут нужны.
Когда речь идет о кросс-компиляции в контексте никсов, то обычно выбор между gcc и clang«ом. Они предполагают совершенно разный подход к кросс-компиляции.
Тулчейны
Компонент | GNU | LLVM |
Компилятор | GCC | Clang |
Ассемблер | GAS (Gnu ASsembler) | Встроенный в clang |
Компоновщик | Bfd/gold | lld |
Библиотека среды выполнения (RTL — runtime) | libgcc | compiler-rt |
Раскрутчик стека (unwinder) | libgcc_s | libunwind |
Стандартная библиотека С++ | libsupc++, libstdc++ | libc++abi, libc++ |
Стандартная библиотека C | glibc | - |
Утилиты | ar, objdump… | llvm-ar, llvm-objdump… |
По умолчанию, как в gcc, так и в clang предпочитаются всегда утилиты GNU, а не LLVM. Мы пойдем этим же путем, используем всю гнутую частью, а вот компилятор возьмем из LLVM (clang).
К сожалению, нельзя выбрать библиотеку среды выполнения, стандартную библиотеку C++ или раскрутчик стека во время выполнения (runtime). Это возможно только во время компиляции (compile-time). Также вы не можете линковаться на хосте с новой версией стдлибы, а запускать ваше ПО на целевой машине со старой стдлибой (теоретически можно конечно, но ноги вряд ли кто пришьет обратно), а вот наоборот как раз можно. Есть и другое ограничение, нельзя линковать стдлибу, glibc или рантайм статически. Если очень хочется, то можно. Но можно поймать UB даже не из своего кода, а из-за какой-нибудь сторонней библиотеки. Старт у нас случился с Debian buster gcc 8.3.0 и clang-11 из бэкпортов. Buster ушел в old stable. Теперь у нас bullseye. Итого имеем gcc 9.3.0 и тот же clang-11 (пока месть не будем забегать вперед, но можно взять и 13:)).
Полный комплект: Clang-11, libgcc, libgcc_s, libsupc++, libstdc++, glibc…
Надо отметить, что это не самый популярный набор, некоторые библиотеки (рецепты сборки) ожидают, что если вы взяли clang, то взяли и compiler-rt (поэтому что-то может и не собраться легко — например были сломаны рецепты m4 и b2).
Окружение
Создадим себе еще два очень важных условия. Готовые бинарники мы пакетируем (распростараняем как .deb пакеты), и они будут полностью совместимы с Debian Bullseye. Второе — наше окружение должно быть максимально простым и легковоспроизводимым. Учитывая первый пункт — мы сразу выкидываем всякие сторонние окружения; будем использовать тулчейн, компилятор и sysroot только из апстрима дебиана. Учитывая второй пункт, тоже смотрим на пакеты дебиана в апстриме, но не забудем еще и контейниризировать наше окружение — запихнем всё в докер.
Получается такой большой пирог. Но он всё еще недостаточно большой. Добавим еще один слой — возьмем conan для библиотек, слишком старых в апстриме или отсутствующих там. И еще один слой — часто хочется иметь самый последний cmake, поэтому его соберем из исходников (заодно немного сравним производительность), можно взять и из pip’а, но так получилось, что из коробки я не смог собрать под armhf, пришлось патчить баг в апстриме, теперь из pip’a всё собирается хорошо.
Компиляторы
С окружением разобрались, теперь посмотрим на gcc и clang, и почему нам так важен их разный подход.
Clang из коробки кросс-компилятор (ldd из коробки кросс-линкер, а вот с библиотеками llvm так не работает) и может нам всё собрать под любую платформу. Gcc же нет, он наоборот — под каждую платформу отдельный компилятор со всем тулчейном, т.е кросс-линкер тоже будет отдельный под каждую платформу.
Если совсем просто, то, например, gcc-arm-linux-gnueabihf
будет соответствовать clang -target arm-linux-gnueabihf
, под капотом всё тоже работает по-разному.
Есть один очень важный момент, всё это работает и должно работать из коробки только на самых популярных дистрибутивах, если вдруг где-то будет какой-то нестандартный путь, то вам придется самостоятельно расставлять все нужные флажки и подсказывать компилятору где и что искать. Debian является как раз тем дистрибутивом, на котором тестируют Clang, поэтому если здесь что-то не сработало, то можно смело писать багрепорт в llvm.
Чтобы запускать бинарники для целевой платформы, нам понадобится qemu.
Практика
Установим нужные пакеты
Внимание! Проделывайте все операции строго в изолированном окружении, т.к. вы можете легко оставить много ненужных артефактов или сломать свои пакеты, поэтому, крайней рекомендуется использовать контейнер или виртуальную машину.
Сначала соберем всё необходимое окружение для кросс-компилятора gcc. Пример первоначального докер образа можно взять здесь (https://github.com/qtdocker/base/blob/master/x86_64/bullseye/Dockerfile)
apt install -y build-essential \
pkg-config \
binutils \
gcc-arm-linux-gnueabihf \
crossbuild-essential-armhf
В принципе, если вы не хотите использовать llvm/clang, то можно остановиться, всё необходимое у нас уже есть.
К этому моменту у нас уже должен стоять пакет libstdc++-11-dev-armhf-cross
, но это std либа для gcc, Clang хочет другой пакет.
Поэтому:
dpkg --add-architecture armhf && apt update
apt install -y libstdc++-10-dev:armhf \
clang
Всё, окружение готово.
Сборка
Создадим main.cpp следующего содержания:
#include
using namespace std;
int main()
{
cout << "Hello World!" << endl;
return 0;
}
Теперь можем попробовать собрать hello world и посмотрим, что у нас получится.
clang++ --verbose -target arm-linux-gnueabihf main.cpp
Debian clang version 11.0.1-2
Target: arm-unknown-linux-gnueabihf
Thread model: posix
InstalledDir: /usr/bin
Found candidate GCC installation: /usr/bin/../lib/gcc-cross/arm-linux-gnueabihf/10
Found candidate GCC installation: /usr/bin/../lib/gcc/arm-linux-gnueabihf/10
Found candidate GCC installation: /usr/lib/gcc-cross/arm-linux-gnueabihf/10
Found candidate GCC installation: /usr/lib/gcc/arm-linux-gnueabihf/10
Selected GCC installation: /usr/bin/../lib/gcc/arm-linux-gnueabihf/10
Candidate multilib: .;@m32
Selected multilib: .;@m32
"/usr/lib/llvm-11/bin/clang" -cc1 -triple armv7-unknown-linux-gnueabihf ... -main-file-name main.cpp ... -target-cpu generic -target-abi aapcs-linux -mfloat-abi hard ... -x c++ main.cpp
clang -cc1 version 11.0.1 based upon LLVM 11.0.1 default target x86_64-pc-linux-gnu
ignoring nonexistent directory "/include"
ignoring duplicate directory "/usr/bin/../lib/gcc/arm-linux-gnueabihf/10/../../../../include/arm-linux-gnueabihf/c++/10"
\#include "..." search starts here:
\#include <...> search starts here:
/usr/bin/../lib/gcc/arm-linux-gnueabihf/10/../../../../include/c++/10
/usr/bin/../lib/gcc/arm-linux-gnueabihf/10/../../../../include/arm-linux-gnueabihf/c++/10
/usr/bin/../lib/gcc/arm-linux-gnueabihf/10/../../../../include/c++/10/backward
/usr/local/include
/usr/lib/llvm-11/lib/clang/11.0.1/include
/usr/include/arm-linux-gnueabihf
/usr/include <- стоит последним, т.к. имеет наименьший приоритет (host система)
End of search list.
...
Внимательно изучим получившийся бинарник.
readelf -h a.out
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: ARM <- наша целевая архитектура
Version: 0x1
Entry point address: 0x1066d
Start of program headers: 52 (bytes into file)
Start of section headers: 7604 (bytes into file)
Flags: 0x5000400, Version5 EABI, hard-float ABI
Если хотим его запустить, то необходимо поставить пакет ```qemu-user-static```
Мы получили рабочий hello world под нашу платформу. Но всё ли так просто? В общем случае, для hello world и популярного дистрибутива — оно очень просто. Но для более сложных случаев, всё может пойти совершенно не так, как нам хочется, даже если наше окружение правильно настроено.
Conan
Мы любим сложности, поэтому на достигнутом останавливаться не будем, попробуем запустить сначала conan:
apt update && apt install -y --no-install-recommends \
python3-pip \
python3-setuptools \
python3-dev \
python3-wheel
pip3 install conan
conan profile new default –detect
Создадим профиль по умолчанию, пускай будет
conan profile update settings.compiler.libcxx=libstdc++11 default
Вряд ли вам нужен старый abi
# как мы помним, у нас clang-11
conan profile update settings.compiler=clang default
conan profile update settings.compiler.version=11 default
Немного поправили профиль в соответствии с нашим компилятором. Так мы получим conan-профиль для архитектуры нашего хоста, но нам то нужно компилировать под другую архитектуру, поэтому создадим новый профиль следующего содержания:
target_host=arm-linux-gnueabihf
[settings]
os=Linux
arch=armv7hf
compiler=clang
compiler.version=11
compiler.libcxx=libstdc++11
build_type=Release
[options]
[build_requires]
[env]
CC=/usr/bin/clang
CXX=/usr/bin/clang++
CXXFLAGS="-target arm-linux-gnueabihf"
CFLAGS="-target arm-linux-gnueabihf"
CHOST=$target_host
AR=$target_host-ar
AS=$target_host-as
RANLIB=$target_host-ranlib
LD=$target_host-ld
STRIP=$target_host-strip
Который значит, что мы собираем под arm-linux-gnueabi-hf
, используя clang как компилятор С и C++, но всё остальное мы берем гнутое.
Target Triple
Что вообще значит тарабарщина arm-linux-gnueabi-hf
?
Попробуем разобраться. Называется это всё Target Triple (подробно тут https://clang.llvm.org/docs/CrossCompilation.html#general-cross-compilation-options-in-clang)
Он имеет общий вид
<архитектура><суб архитектура>-<производитель>-<ОС>-
архитектура = x86_64, i386, arm, thumb, mips…
суб архитектура = например на ARM: v5, v6m, v7a, v7m…
производитель = pc, apple, nvidia, ibm…
ОС = none (bare metal), linux, win32, darwin, cuda…
abi = eabi, gnu, android, macho, musleabi, elf…
Например
armv7a-linux-gnueabihf
Linux, hard-floating point Armv7-a
Linux, soft-floating point, Armv7-a
Любую опцию можно пропустить, и будет использовано значение по умолчанию. Аналогично будет, если вы подставили неизвестно значение.
Если вы помните, то в выводе команды clang++ --verbose -target arm-linux-gnueabihf main.cpp
Можно увидеть "/usr/lib/llvm-11/bin/clang" -cc1 -triple armv7-unknown-linux-gnueabihf
Т.е. мы явно не указали суб-архитектуру и clang подставил значение по умолчанию (v7), также мы не указали производителя, и clang подставил значение по умолчанию (unknown). Будьте внимательны, он может подставить и не то, что вы желаете.
Есть небольшой нюанс, если вы компилируете под bare-metal. По умолчанию clang ищет системный sysroot для всех ОС кроме bare-metal, поэтому он может не найти необходимые заголовки и библиотеки. Их придется передавать вручную.
CMake
Собрать его из исходников совсем просто, но не нужно в 99% случаев. К сожалению, иногда приходиться что-то править, поэтому в первый раз пришлось собирать из исходников и слать патчи в апстрим.
cd /tmp
git clone https://github.com/Kitware/CMake.git -b release
cd /tmp/CMake
./bootstrap
make
sudo make install
cd /tmp
rm -rf CMake
Забегая вперед, можно в одну команду conan install cmake/3.21.4@
или pip install cmake.
Небольшой benchmark
Как и обещал раньше, сравним производительность (https://github.com/qtdocker/base/pull/11), как раз используя conan
Проверим за сколько мы сумеем собрать cmake conan install cmake/3.21.4@ --build
|
---|
Итого кросс-компиляция дает примерно х10 относительно qemu. Все же она на 35% медленнее относительно нативной. Важно понимать, что на другом проекте может быть другое соотношение цифр между нативной и кросс, но порядок цифр будет примерно такой же на большой кодовой базе.
Теперь будем кросс-компилировать, используя CMake.
В первую очередь конечно обратимся к Clang#How-to-cross-compile
При корректном указании target, clang сам найдет нужные заголовки и системные библиотеки, пользователю не нужно думать об этом.
Обобщим всё сказанное и соберем маленький .deb пакет armhf под amd64. Будем использовать Clang, CMake и CPack. Если вы плохо знакомы с CPack или первый раз о нем слышите, то это интегрированный в CMake инструмент для создания пакетов .deb/.rpm/.msi… Вряд ли его нужно рассматривать отдельно, он — самый простой элемент цепочки.
Conan использовать не будем, т.к. есть много способов его интеграции в cmake, не хочется рассматривать их все. Главное — conan-пакеты собираются под нашу архитектуру.
CMake и CPack — компилируем и пакетируем
Если вы еще не перешли в контейнер, то сейчас самое время:
docker pull atom63/qt-docker:linux-bullseye-cross-x86_64-armv7a
docker run -it atom63/qt-docker:linux-bullseye-cross-x86_64-armv7a
Указанный контейнер собран по шагам в этой статье.
Выполним:
git clone https://github.com/Jihadist/cpack-systemd-demo -b no_systemd cpack
cd cpack
mkdir build
cd build
cmake -DCMAKE_BUILD_TYPE=Release \
-DCPACK_PACKAGING_INSTALL_PREFIX=/usr/local ..
Получим что-то крайне похожее на:
user@:~/cpack/build$ cmake -DCMAKE_BUILD_TYPE=Release \
-DCPACK_PACKAGING_INSTALL_PREFIX=/usr/local ..
-- The C compiler identification is Clang 11.0.1
-- The CXX compiler identification is Clang 11.0.1
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /usr/bin/clang - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/clang++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /home/user/cpack/build
Значит всё хорошо, идем дальше: cmake --build . --config Release
user@8163cbe8cf40:~/cpack/build$ cmake --build . --config Release
[ 50%] Building CXX object CMakeFiles/HelloWorld.dir/main.cc.o
[100%] Linking CXX executable HelloWorld
[100%] Built target HelloWorld
Если собралось, то идем дальше: cpack
user@:~/cpack/build$ cpack
CPack: Create package using DEB
CPack: Install projects
CPack: - Run preinstall target for: HelloWorld
CPack: - Install project: HelloWorld []
CPack: Create package
CPackDeb: - Generating dependency list
CPack: - package: /home/user/cpack/build/HelloWorld-0.1.deb generated.
user@:~/cpack/build$ dpkg -I HelloWorld-0.1.deb
new Debian package, version 2.0.
size 3992 bytes: control archive=352 bytes.
240 bytes, 10 lines control
59 bytes, 1 lines md5sums
Architecture: amd64 <- целевая архитектура пакета (не бинарника)
Depends: libc6 (>= 2.2.5), libgcc-s1 (>= 3.0), libstdc++6 (>= 4.8.1)
Description: HelloWorld built using CMake
Maintainer: MyCompany
Package: helloworld
Priority: optional
Section: devel
Version: 0.1
Installed-Size: 29
Обратим внимание на Architecture: amd64, таким образом мы собрали бинарник и пакет под архитектуру нашего хоста (amd64), но нам то надо под armhf.
Поэтому перейдем в другую директорию cd .. && mkdir build-armhf && cd build-armhf
Создадим там файлик toolchain-armhf.cmake:
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)
set(triple arm-linux-gnueabihf)
set(CMAKE_C_COMPILER clang)
set(CMAKE_C_COMPILER_TARGET ${triple})
set(CMAKE_CXX_COMPILER clang++)
set(CMAKE_CXX_COMPILER_TARGET ${triple})
Снова запустим cmake:
user@:~/cpack/build-armhf$ cmake -DCMAKE_BUILD_TYPE=Release \
-DCPACK_PACKAGING_INSTALL_PREFIX=/usr/local \
-DCMAKE_TOOLCHAIN_FILE=toolchain-armhf.cmake ..
...
-- Configuring done
-- Generating done
-- Build files have been written to: /home/user/cpack/build-armhf
user@:~/cpack/build-armhf$ cmake --build . --config Release
[ 50%] Building CXX object CMakeFiles/HelloWorld.dir/main.cc.o
[100%] Linking CXX executable HelloWorld
[100%] Built target HelloWorld
user@:~/cpack/build-armhf$ cpack
...
CPack: - package: /home/user/cpack/build-armhf/HelloWorld-0.1.deb generated.
user@:~/cpack/build-armhf$ dpkg -I HelloWorld-0.1.deb
new Debian package, version 2.0.
size 4100 bytes: control archive=349 bytes.
238 bytes, 10 lines control
59 bytes, 1 lines md5sums
Architecture: amd64 <- опять архитектура хоста)
Depends: libc6 (>= 2.4), libgcc-s1 (>= 3.5), libstdc++6 (>= 4.8.1)
...
user@:~/cpack/build-armhf$ readelf -h HelloWorld
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: ARM <- целевая архитектура
...
Да что ж это такое, собрали бинарник под armhf, а пакет — под amd64. Ничего, просто кое-что забыли. Необходимо указать CPack’у явно архитектуру пакета, если она отлична от архитектуры хоста (-DCPACK_DEBIAN_PACKAGE_ARCHITECTURE=armhf
или добавить ее в toolchain-armhf.cmake).
ser@:~/cpack/build-armhf$ cmake -DCMAKE_BUILD_TYPE=Release \
-DCPACK_PACKAGING_INSTALL_PREFIX=/usr/local \
-DCMAKE_TOOLCHAIN_FILE=toolchain-armhf.cmake \
-DCPACK_DEBIAN_PACKAGE_ARCHITECTURE=armhf ..
-- Configuring done
...
-- Build files have been written to: /home/user/cpack/build-armhf
user@:~/cpack/build-armhf$ cmake --build . --config Release
...
user@8163cbe8cf40:~/cpack/build-armhf$ cpack
CPack: Create package using DEB
...
CPack: - package: /home/user/cpack/build-armhf/HelloWorld-0.1.deb generated.
user@8163cbe8cf40:~/cpack/build-armhf$ dpkg -I HelloWorld-0.1.deb
new Debian package, version 2.0.
size 4102 bytes: control archive=350 bytes.
238 bytes, 10 lines control
59 bytes, 1 lines md5sums
Architecture: armhf <- целевая архитектура, ура!
...
Попробуем установить?
user@:~/cpack/build-armhf$ sudo apt install ./HelloWorld-0.1.deb
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
Note, selecting 'helloworld:armhf' instead of './HelloWorld-0.1.deb'
The following NEW packages will be installed:
helloworld:armhf
0 upgraded, 1 newly installed, 0 to remove and 1 not upgraded.
Need to get 0 B/4102 B of archives.
After this operation, 21.5 kB of additional disk space will be used.
Get:1 /home/user/cpack/build-armhf/HelloWorld-0.1.deb helloworld armhf 0.1 [4102 B]
debconf: delaying package configuration, since apt-utils is not installed
Selecting previously unselected package helloworld:armhf.
(Reading database ... 32705 files and directories currently installed.)
Preparing to unpack .../build-armhf/HelloWorld-0.1.deb ...
Unpacking helloworld:armhf (0.1) ...
Setting up helloworld:armhf (0.1) ...
```
Ну и запустим, не зря же мы столько времени потратили. Надеюсь qemu вы поставили, иначе не сработает (если не используете готовый контейнер).
user@:~/cpack/build-armhf$ cd ~
user@:~$ HelloWorld
Sun Nov 14 10:23:00 2021
Вуаля, мы собрали, установили и запустили armhf пакет под amd64. Дальше мы можем доставить этот пакет на наш armhf хост, и он будет там работать.
Заключение
Проще ли собирать clang’ом чем gcc? Думаю нет, но если у вас нет выбора, и нужен свежий компилятор — то это выход. Если подготовить всё окружение и использовать cmake, то кросс-компилировать не сильно сложнее чем нативно :)
P.S. Отдельные моменты взял у Peter Smith (Linaro Toolchain Working Group). Уж слишком хорошие доклады.
P.P. S. Пока писал статью, вышла другая очень интересная, рекомендую.