38.5 Cross-compilation és multiarch build

Amikor a build gépen futó fordító nem a build gépén futtatható kódot állít elő, cross-compilationról (keresztfordításról) beszélünk. A DevOps gyakorlatban ez akkor kerül elő, amikor x86-64-es build agenten AArch64 szerverre, beágyazott targetre vagy multiarch konténerimage-hez készül csomag. A nehézség nem a compiler elindítása, hanem annak biztosítása, hogy a fordítás minden bemenete — fejléc, könyvtár, generált kód, teszt — a target rendszerhez tartozzon, ne a build géphez.

Az autotools terminológiájában a build a fordító gépe, a host a futtató rendszer, a target pedig a compiler által előállított kód rendszere; kanadai cross esetén a három eltér. A CMake viszont a build gépet nevezi hostnak (CMAKE_HOST_SYSTEM_NAME), a futtató rendszert pedig targetnek (CMAKE_SYSTEM_NAME). Ebben a fejezetben a félreértés elkerülésére build gépről és target rendszerről beszélünk; a CMake változóneveit változatlanul használjuk. Az előfeltételek között szerepel a CTest, negatív tesztek és sanitizer CI és az ABI- és optimalizálásdiagnosztikai labor; itt a cross build sajátosságaira építünk.

38.5.1 A build, a target és a toolchain

A toolchain itt a target kód előállításához szükséges eszközök együttese: a target compiler és binutils, a target libc fejlécei és könyvtárai, a sysroot, valamint opcionálisan a target debugger és emulátor. A toolchain konzisztenciája azt jelenti, hogy a compiler, a linker, a libc és a sysroot ugyanahhoz a target ABI-hoz és libc-verzióhoz tartozik. Ha például a compiler aarch64-linux-gnu triple-tel dolgozik, de a sysrootban x86-64-es glibc van, a linkelés vagy a futás fog elhasalni — gyakran félrevezető hibaüzenettel.

A target triple a compiler által használt, többé-kevésbé kanonikus azonosító, tipikusan arch-vendor-os-abi alakban, például aarch64-linux-gnu vagy x86_64-linux-gnu. A gnu a glibc családra utal, de a libc verzióját nem tartalmazza; a musl target triple külön, például aarch64-linux-musl. A triple tehát nem teljes ABI-szerződés: a libc verziója, a minimális kernelverzió, a CPU baseline (-march), a lebegőpontos ABI és a kivételkezelés modellje külön rögzítendő. A gcc -dumpmachine vagy a clang --target=... megmutatja, melyik triple-t használja a compiler; ezt érdemes a build logban rögzíteni.

A sysroot a target fájlrendszerének gyökere a build gépen. A compiler --sysroot hatására a target fejléceket és könyvtárakat ezen a gyökér alatt keresi. A compiler saját, beépített include könyvtárai ettől függetlenül a keresési sorrend részei; a pontos sorrendet a compiler dokumentációja és a -v kimenet mutatja. A sysroot nem azonos a CMAKE_FIND_ROOT_PATH-tal: előbbi a compilernek szól, utóbbi a CMake find_* parancsainak.

38.5.2 A CMake toolchain fájl és a find root policy

A CMake-ben a cross buildet akkor aktiváljuk, ha a CMAKE_SYSTEM_NAME értékét kézzel beállítjuk, és az eltér a build rendszertől. Ilyenkor a CMAKE_CROSSCOMPILING igaz lesz. Egy minimális AArch64 toolchain fájl:

# cmake/toolchains/aarch64-linux-gnu.cmake
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR aarch64)
set(CMAKE_C_COMPILER aarch64-linux-gnu-gcc)
set(CMAKE_CXX_COMPILER aarch64-linux-gnu-g++)
set(CMAKE_SYSROOT /usr/aarch64-linux-gnu)
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
set(CMAKE_CROSSCOMPILING_EMULATOR "qemu-aarch64;-L;${CMAKE_SYSROOT}" CACHE STRING "")

A CMAKE_FIND_ROOT_PATH_MODE_PROGRAM értéke azért NEVER, mert a pkg-config, protoc, kódgenerátorok és hasonló programok a build gépen futnak. A LIBRARY és INCLUDE ONLY, mert a target könyvtárakat és fejléceket a sysrootban kell megtalálni. A PACKAGE ONLY a legszigorúbb; BOTH csak indokolt esetben használható, mert a host package-et is megtalálhatja.

38.5.2.1 Levezetés (vázlat): hogyan keres a find_package cross alatt

  1. A find_package(ZLIB) a FindZLIB modult futtatja.
  2. A modul find_path(ZLIB_INCLUDE_DIR zlib.h) és find_library(ZLIB_LIBRARY z) hívást végez.
  3. A find_* a CMAKE_FIND_ROOT_PATH alatt keres, ha a megfelelő CMAKE_FIND_ROOT_PATH_MODE_* értéke ONLY; BOTH esetén a root és a build gép is szóba jön, a root előrébb.
  4. A CMAKE_SYSROOT a compiler --sysroot-ját adja, de a find_* nem használja automatikusan; ezért kell a CMAKE_FIND_ROOT_PATH.
  5. A PACKAGE mód ONLY a legszigorúbb; a host toolokat külön kell átadni.

38.5.3 Host eszköz és target könyvtár szétválasztása

A cross build egyik leggyakoribb hibája, hogy egy build gépen futó eszközt target compilerrel fordítunk le, vagy egy target könyvtárat a build gép könyvtárából linkelünk. Az előbbi bináris a build gépen nem fut, az utóbbi pedig rossz ABI-jú kódot ad. A megoldás a két szerep szétválasztása: a host tool natív buildben készül, a target library a target compilerrel. A host toolt IMPORTED targetként adjuk át a cross buildnek.

A következő példa egy minimális, de teljes multiarch projektet mutat be. A gen_config host tool a target arch-hoz generál egy config.hpp fejlécet; az answer target library és az app target executable a target compilerrel készül. A generált fejléc rögzíti a várt capability-t, a target kód pedig static_assert-tel ellenőrzi, hogy a target compiler tényleg azt a modellt állítja elő.

// tools/gen_config.cpp — a build gépen fut
#include <fstream>
#include <string>

int main(int argc, char** argv) {
    if (argc != 3) return 1;
    const std::string arch = argv[2];
    const int ld_digits = (arch == "aarch64") ? 113 : 64;
    std::ofstream out(argv[1]);
    out << "#pragma once\n"
        << "#define DEMO_TARGET_ARCH \"" << arch << "\"\n"
        << "#define DEMO_EXPECT_CHAR_UNSIGNED 1\n"
        << "#define DEMO_EXPECT_LONG_DOUBLE_BYTES 16\n"
        << "#define DEMO_EXPECT_LD_DIGITS " << ld_digits << "\n";
    return out ? 0 : 2;
}
// include/answer.hpp
#pragma once
#include <cstdint>

std::uint32_t answer() noexcept;
// src/answer.cpp
#include "answer.hpp"

std::uint32_t answer() noexcept { return 42u; }
// src/main.cpp
#include "answer.hpp"
#include "config.hpp"

#include <cstdint>
#include <cstdio>
#include <limits>

static_assert(DEMO_EXPECT_CHAR_UNSIGNED == (static_cast<char>(-1) > 0),
              "a target char signednesse eltér a rögzített capability-től");
static_assert(DEMO_EXPECT_LONG_DOUBLE_BYTES == sizeof(long double),
              "a target long double mérete eltér");
static_assert(DEMO_EXPECT_LD_DIGITS == std::numeric_limits<long double>::digits,
              "a target long double precisionje eltér");

int main() {
    std::printf("answer=%u\n", static_cast<unsigned>(answer()));
    std::printf("arch=%s\n", DEMO_TARGET_ARCH);
    std::printf("char unsigned=%s\n",
                (static_cast<char>(-1) > 0) ? "igen" : "nem");
    std::printf("sizeof(long double)=%zu, digits=%d\n",
                sizeof(long double),
                std::numeric_limits<long double>::digits);
    return 0;
}

A target oldali CMakeLists.txt target-alapú: a tulajdonságok a targethez kötődnek, nem globális változókhoz.

cmake_minimum_required(VERSION 3.20)
project(abi_demo LANGUAGES CXX)

set(DEMO_TARGET_ARCH "x86_64" CACHE STRING "Target arch")
set(DEMO_GEN_CONFIG "" CACHE FILEPATH "Host gen_config binary")

add_library(answer STATIC src/answer.cpp)
target_include_directories(answer PUBLIC include)
target_compile_features(answer PUBLIC cxx_std_20)

if(CMAKE_CROSSCOMPILING)
  add_executable(gen_config IMPORTED)
  set_target_properties(gen_config PROPERTIES
    IMPORTED_LOCATION "${DEMO_GEN_CONFIG}")
else()
  add_executable(gen_config tools/gen_config.cpp)
endif()

add_custom_command(
  OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/generated/config.hpp"
  COMMAND gen_config
          "${CMAKE_CURRENT_BINARY_DIR}/generated/config.hpp"
          "${DEMO_TARGET_ARCH}"
  DEPENDS gen_config
  VERBATIM)
add_custom_target(generate_config
  DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/generated/config.hpp")

add_executable(app src/main.cpp)
target_include_directories(app PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/generated")
target_link_libraries(app PRIVATE answer)
add_dependencies(app generate_config)

enable_testing()
add_test(NAME app_smoke COMMAND app)

A natív host tool külön, kis projekttel épül:

# tools/CMakeLists.txt
cmake_minimum_required(VERSION 3.20)
project(gen_config LANGUAGES CXX)

add_executable(gen_config gen_config.cpp)
target_compile_features(gen_config PRIVATE cxx_std_20)

38.5.3.1 A build

# Host tool natív buildje
cmake -S tools -B build-native -DCMAKE_BUILD_TYPE=Release
cmake --build build-native --parallel

# Target build AArch64-re
cmake -S . -B build-aarch64 \
  -DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/aarch64-linux-gnu.cmake \
  -DDEMO_GEN_CONFIG="$PWD/build-native/gen_config" \
  -DDEMO_TARGET_ARCH=aarch64 \
  -DCMAKE_BUILD_TYPE=Release
cmake --build build-aarch64 --parallel

# Ellenőrzés
file build-aarch64/app
readelf -h build-aarch64/app | grep Machine

A file várhatóan ELF 64-bit ... ARM aarch64 kimenetet ad, a readelf -h Machine: AArch64 sort. Ezek illusztratív, környezetfüggő ellenőrzések; a tényleges kimenet a binutils és a file verziójától függ.

38.5.4 x86-64 és AArch64: ABI és adatmodell

A két target Linuxon egyaránt LP64, de nem binárisan kompatibilis. A legfontosabb eltérések a char signednesse, a long double reprezentációja, a max_align_t és a struktúrák paddingja, valamint a bit-field layout. A sizeof egyezése nem jelent ABI-kompatibilitást: a long double mindkét targeten 16 byte lehet, de x86-64-en 80 bites extended, AArch64-en 128 bites quad. A bináris protokollhoz fix szélességű típusok, explicit endianness és static_assert kellenek.

Tulajdonság x86-64 System V Linux (GCC) AArch64 Linux (GCC)
adatmodell LP64 LP64
sizeof(void*) 8 8
sizeof(long) 8 8
sizeof(int) 4 4
char signedness signed unsigned
sizeof(long double) 16 16
long double formátum 80 bites extended 128 bites quad
alignof(std::max_align_t) 16 16
byte order little-endian little-endian

𝚜𝚒𝚣𝚎𝚘𝚏(𝚕𝚘𝚗𝚐 𝚍𝚘𝚞𝚋𝚕𝚎)=16,𝚍𝚒𝚐𝚒𝚝𝚜x86-64=64,𝚍𝚒𝚐𝚒𝚝𝚜AArch64=113. \texttt{sizeof}(\texttt{long double}) = 16, \qquad \texttt{digits}_{\text{x86-64}} = 64, \quad \texttt{digits}_{\text{AArch64}} = 113.

A táblázat jellemző Linux/GCC profilt mutat; a pontos értékeket a target ABI és a fordító verziója rögzíti, ezért a laborban static_assert-tel és sizeof méréssel kell ellenőrizni. A char signednesse miatt a char értékek bináris átvitele nem hordozható; használjunk std::uint8_t-t.

38.5.5 Feature probe cross környezetben

A check_cxx_source_compiles a target compilerrel fordít és linkel, ezért cross alatt is használható, ha a sysroot és a target libc elérhető. A check_cxx_source_runs és a try_run ezzel szemben futtatna; cross alatt vagy nem fut, vagy a CMAKE_CROSSCOMPILING_EMULATOR-t használja. Az emulátoron kapott eredmény nem azonos a natív targettel. Ezért a futásdetektálást rögzített capability-vel helyettesítjük: a target profil deklarálja a várt tulajdonságokat, a build pedig static_assert-tel ellenőrzi, hogy a target compiler tényleg azt a modellt állítja elő.

A fenti példában a gen_config a build gépen fut, de nem detektál: a profilból generálja a config.hpp-t. A target compiler a static_assert-tel ellenőrzi a char signednessét, a long double méretét és a precisionjét. Ha a target ABI eltér a rögzített capability-től, a build fordítási időben elhasal — ez sokkal jobb, mint egy futásidejű rejtélyes hiba.

38.5.6 Tesztelés: emuláció vagy natív target

A fordítási siker csak azt bizonyítja, hogy a target compiler elfogadta a forrást, és a linkelés a target könyvtárakkal megtörtént. A futási viselkedést a target libc, a kernel, a CPU feature-ök és a környezet határozza meg. A natív target runner (AArch64 CI agent) a legerősebb ellenőrzés; a qemu-user gyors, de a user-space viselkedést modellezi, a kernelét nem; a qemu-system teljes rendszert emulál, lassabb; a valós hardver a legjobb, de drága. A CTest cross alatt a CMAKE_CROSSCOMPILING_EMULATOR-ral futtatható:

ctest --test-dir build-aarch64 --output-on-failure

Ez azonban nem natív target teszt. A sanitizerek target runtime-ját a sysrootban elérhetővé kell tenni; a negatív tesztek és a sanitizer CI mintái a CTest, negatív tesztek és sanitizer CI (38.4) részben szerepelnek.

38.5.7 Tipikus hibák és javításuk

  1. A find_package a build gép könyvtárát találja meg. Tünet: linkeléskor file in wrong format, futáskor GLIBC_... not found. Javítás: CMAKE_SYSROOT, CMAKE_FIND_ROOT_PATH, és a LIBRARY/INCLUDE mód ONLY.
  2. A pkg-config a build gép .pc fájljait olvassa. Javítás: PKG_CONFIG_LIBDIR és PKG_CONFIG_SYSROOT_DIR beállítása, a PKG_CONFIG_PATH üresítése; a pontos viselkedés a pkg-config verziójától függ.
  3. A -march=native cross alatt. A build gép CPU-ját detektálja, nem a targetét. Javítás: explicit -march=armv8-a vagy a target CPU pontos típusa, -mcpu tuning.
  4. A try_run a build gépen fut, vagy hibás eredményt ad. Javítás: try_compile + static_assert, rögzített capability; a CMAKE_CROSSCOMPILING_EMULATOR csak tudatos, dokumentált esetben.
  5. A char signedness és a long double feltételezése. Javítás: fix szélességű típusok, static_assert, explicit konverzió.
  6. A host tool és a target library keveredése. Javítás: külön natív build, IMPORTED target.
  7. A CTest cross alatt nem futtat. Javítás: CMAKE_CROSSCOMPILING_EMULATOR vagy natív target runner.
  8. A CMAKE_SYSTEM_PROCESSOR és a compiler triple eltérése. Javítás: gcc -dumpmachine, clang --target=..., CMAKE_CXX_COMPILER_TARGET.

38.5.8 Feladatok

  1. Magyarázd el, mi a különbség a CMAKE_SYSROOT és a CMAKE_FIND_ROOT_PATH között, és miért kell a CMAKE_FIND_ROOT_PATH_MODE_PROGRAM értékét NEVER-re állítani!
  2. Írj egy AArch64 toolchain fájlt és egy target-alapú CMakeLists.txt-t, amely egy statikus könyvtárat és egy futtathatót épít! Ellenőrizd a bináris target architektúráját!
  3. A cross build sikeres, de a targeten a program Illegal instruction hibával elszáll. Mi a lehetséges ok, és hogyan javítod?
  4. Adott egy try_run-ra épülő feature probe, amely cross build alatt is lefut, és a build gép glibc verzióját detektálja. Alakítsd át úgy, hogy a target capability-t rögzítse, és fordítási időben ellenőrizze!

38.5.9 Megoldások

38.5.9.1 Első feladat

A CMAKE_SYSROOT a compiler --sysroot kapcsolóját adja; a CMAKE_FIND_ROOT_PATH a CMake find_* parancsainak keresési gyökere. A CMAKE_FIND_ROOT_PATH_MODE_PROGRAM azért NEVER, mert a pkg-config, protoc és más build eszközök a build gépen futnak; ha ONLY lenne, a CMake a target sysrootban keresné őket, és nem találná meg a futtatható host programot. Ellenőrzés: a find_program(PKG_CONFIG pkg-config) a build gép pkg-config-ját találja, míg a find_library(ZLIB_LIBRARY z) a sysroot target könyvtárát.

38.5.9.2 Második feladat

A toolchain fájl a CMAKE_SYSTEM_NAME, CMAKE_SYSTEM_PROCESSOR, CMAKE_C_COMPILER, CMAKE_CXX_COMPILER, CMAKE_SYSROOT, CMAKE_FIND_ROOT_PATH és a CMAKE_FIND_ROOT_PATH_MODE_* beállításaiból áll. A CMakeLists.txt a add_library(answer STATIC src/answer.cpp), target_include_directories(answer PUBLIC include), target_compile_features(answer PUBLIC cxx_std_20), add_executable(app src/main.cpp), target_link_libraries(app PRIVATE answer) hívásokat tartalmazza. Ellenőrzés: file build-aarch64/app ARM aarch64; readelf -h build-aarch64/app | grep Machine AArch64; a static_assert a generált config.hpp-val fordul.

38.5.9.3 Harmadik feladat

Az Illegal instruction jellemzően a target CPU által nem támogatott utasítás. Ok lehet a -march=native (a build gép CPU-ját detektálja), a túl új -march/-mcpu, vagy a target CPU baseline eltérése. Javítás: explicit -march=armv8-a vagy a target CPU pontos típusa, a -march=native eltávolítása, a -mcpu tuning. Ellenőrzés: objdump -d build-aarch64/app kimenetében a target CPU által nem támogatott utasítások; a target /proc/cpuinfo Features mezője; a build logban a tényleges -march érték.

38.5.9.4 Negyedik feladat

A try_run helyett try_compile + static_assert + generált capability header. A check_cxx_source_compiles a target compilerrel fordít; a try_run eltávolítandó. A gen_config a build gépen a profilból generálja a config.hpp-t; a target compiler a static_assert-tel ellenőrzi a char signednessét, a long double méretét és precisionjét. A CMAKE_CROSSCOMPILING_EMULATOR nem helyettesíti a natív target tesztet. Ellenőrzés: a CMakeCache.txt a target compilerre mutat; a build fail, ha a static_assert nem teljesül; a config.hpp a target arch-ot tartalmazza.

38.5.10 Összefoglaló

A cross build helyessége nem a compiler elindításán, hanem a teljes target környezet konzisztenciáján múlik: a target triple, a sysroot, a find root policy, a host tool és a target library szétválasztása, valamint a rögzített capability fordítási idejű ellenőrzése együtt adnak megbízható buildet. A fordítási siker nem bizonyítja a target futást; a natív target runner, a qemu és a valós hardver különböző erősségű ellenőrzést ad. A multiarch buildet targetenként izolált build könyvtárral és explicit toolchain fájllal érdemes vezetni.

38.5.11 Megjegyzés a forrásokhoz

A fejezet a CMake, a GCC/Clang, a binutils és a qemu viselkedését a laborban rögzített verziókhoz köti. Ezek a részletek verziófüggők; a konkrét beállítások helyességét a telepített eszközök dokumentációja és a labor mérése igazolja. A fejezet nem állítja, hogy a példák fordítását vagy futtatását elvégezte; a megadott kimenetek várt, illusztratív példák.