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.
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.
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.
find_package(ZLIB) a FindZLIB modult
futtatja.find_path(ZLIB_INCLUDE_DIR zlib.h) és
find_library(ZLIB_LIBRARY z) hívást végez.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.CMAKE_SYSROOT a compiler --sysroot-ját
adja, de a find_* nem használja automatikusan; ezért kell a
CMAKE_FIND_ROOT_PATH.PACKAGE mód ONLY a legszigorúbb; a host
toolokat külön kell átadni.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)# 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 MachineA 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.
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 |
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.
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.
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-failureEz 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.
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.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.-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.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.char signedness és a long double
feltételezése. Javítás: fix szélességű típusok,
static_assert, explicit konverzió.IMPORTED target.CTest cross alatt nem futtat.
Javítás: CMAKE_CROSSCOMPILING_EMULATOR vagy natív target
runner.CMAKE_SYSTEM_PROCESSOR és a compiler triple
eltérése. Javítás: gcc -dumpmachine,
clang --target=...,
CMAKE_CXX_COMPILER_TARGET.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!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!Illegal instruction hibával elszáll. Mi a lehetséges ok, és
hogyan javítod?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!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.
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.
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.
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.
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.
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.