1.3 Reprodukálható fordítási és diagnosztikai labor

A reprodukálható labor célja nem pusztán az, hogy „ugyanaz a program nálam is lefusson”. Azt kell rögzítenünk, melyik forrásból, milyen nyelvi módban, mely eszközökkel és milyen célrendszerre készült a vizsgált program. Egy fordítási figyelmeztetés, sanitizer-jelentés vagy eltérő eredmény csak ezzel a háttérrel értelmezhető.

A Szabványok, dialektusok és hordozhatósági szerződés alapján külön kezeljük az ISO nyelvi szabályokat és a Linuxon választott implementációt. Ebben a részben parancssori laboralapot készítünk; buildrendszerre vagy Linux-specifikus programozási API-ra még nincs szükség.

1.3.1 A laborprofil mint reprodukciós szerződés

Laborprofilnak nevezzük a forrás, a fordítási és futási környezet, valamint a vizsgálati beállítások rögzített együttesét. Legalább a következőket tartalmazza:

A megismételhető kísérlet és a bájtra azonos build nem ugyanaz. Az előbbihez az eredmények és körülmények összehasonlíthatósága kell; az utóbbihoz többek között az időbélyegek, beégetett útvonalak és függőségek további szabályozása is szükséges. Itt az első célt alapozzuk meg.

1.3.1.1 A könyv központi profiljai

A könyv minden buildpéldája pontosan egy, névvel azonosított profilhoz tartozik. A profilokat egyetlen helyen, az alábbi táblában rögzítjük; a fejezetek csak a nevükre hivatkoznak. Ha egy példa nem nevez meg profilt, a lab-gcc profil érvényes; a C23, C++23 és C++26 kitekintő példák alapértelmezése a lab-next.

Profil Státusz Mire szolgál
lab-gcc ellenőrzött: a könyvben közölt buildjegyzőkönyvek ebben készültek a C17 és C++20 alapprofil, minden teljes program és labor
lab-clang választott, a könyvben nem ellenőrzött második fordító ugyanarra a célra; diagnosztikai összevetés
lab-aarch64 választott, a könyvben nem ellenőrzött összehasonlító cél az ABI- és memóriamodell-fejezetekhez
lab-next választott, a könyvben nem ellenőrzött C23, C++23 és C++26 kitekintés újabb fordítóval

A lab-gcc profil pontos összetétele:

Összetevő Rögzített választás
Disztribúció Ubuntu 24.04.3 LTS
Cél x86-64 Linux, System V ABI, LP64, hosted user-space
C fordító GCC 13.3.0 (gcc-13 13.3.0-6ubuntu2~24.04.1)
C++ fordító G++ 13.3.0, ugyanabból a csomagból
C könyvtár glibc 2.39 (libc6 2.39-0ubuntu8.9)
C++ könyvtár libstdc++: a GCC 13 headerjei (_GLIBCXX_RELEASE = 13, __GLIBCXX__ = 20240904); futásidejű libstdc++.so.6 a libstdc++6 14.2.0-4ubuntu2~24.04.1 csomagból
Assembler és linker GNU binutils 2.42, GNU ld.bfd
Nyelvi mód -std=c17, illetve -std=c++20; GNU dialektus nélkül
Diagnosztika GDB 15.0.50 (Ubuntu csomag), strace 6.8
Build és teszt GNU Make 4.3; Python 3.12.3; CMake 4.4.4 és Ninja 1.13 (pip csomagból, nem disztribúciós csomag)
Kernel a jegyzőkönyvek idején Linux 7.0.0 (x86-64)

A C++ könyvtár sora szándékosan két adatot tartalmaz. A fordítás a GCC 13 libstdc++ headerjeivel történik, a programok viszont a disztribúció újabb, visszafelé kompatibilis libstdc++.so.6 futásidejű könyvtárával futnak. A kettő nem ugyanaz, és egy hibaelemzésnél mindkettő számít.

A többi profil a következő kiadásokat jelöli ki. Ezek választott profilspecifikációk, nem itt ellenőrzött telepítések; a könyv nem állít róluk fordítási vagy futási eredményt.

Profil Fordító Könyvtárak Cél
lab-clang Clang 18.1.8 glibc 2.39; libstdc++ a lab-gcc-ből, vagy libc++ 18 – a választást a buildnaplóban rögzíteni kell mint lab-gcc
lab-aarch64 GCC 13.3.0 natív AArch64 környezetben glibc 2.39, libstdc++ 13 AArch64 Linux, AAPCS64, LP64
lab-next GCC 14.2.0 vagy újabb; Clang 19.1.7 vagy újabb glibc 2.40 vagy újabb, a fordítóhoz tartozó standard könyvtár x86-64 Linux

Más kiadások is használhatók, de azok külön profilt alkotnak, saját névvel. A helyi jegyzékben a disztribúciós csomagverziót és eredetet is rögzíteni kell: azonos upstream verzió mögött eltérő javításkészlet állhat. A buildjegyzőkönyvekben a „GCC 13.3.0” megjelölés a lab-gcc profilt jelenti.

1.3.1.2 A lab-gcc profil felállítása

A lab-gcc profil egy Ubuntu 24.04 LTS (x86-64) rendszer disztribúciós csomagjaiból áll össze; külön fordítóépítés nem kell. Friss telepítésen vagy konténerben (például az ubuntu:24.04 image-ből indulva):

sudo apt-get update
sudo apt-get install -y build-essential gdb strace python3 python3-venv
# opcionális, a CMake-es fejezetekhez (38–39):
python3 -m venv ~/lab-venv && ~/lab-venv/bin/pip install cmake ninja

A build-essential a GCC/G++ 13-at, a GNU binutils-t, a make-et és a glibc fejlesztői fájljait hozza; az ASan és az UBSan futásidejű könyvtára (libasan8, libubsan1) a gcc-13 csomag függősége. A rögzített csomagverziók a központi profiltáblában szerepelnek; egy későbbi frissítés után a dpkg -l gcc-13 g++-13 libc6-dev binutils gdb kimenetét kell a laborjegyzőkönyvbe menteni, és eltérés esetén a profilt újra rögzíteni. Más disztribúció vagy macOS külön, nem ellenőrzött profil; ott a megfelelő csomagneveket és a sanitizer-támogatást külön kell igazolni.

A felállítás után az alábbi szkript egy lépésben ellenőrzi a C és C++ fordítást és linkelést, az ASan és az UBSan működését szimbolizált forráshellyel, valamint a gdb-t:

#!/usr/bin/env bash
# lab-check.sh — a lab-gcc profil működőképességének ellenőrzése
set -euo pipefail
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT
cd "$work"

cat > hello.c <<'C'
#include <stdio.h>
int main(void) { puts("c17 ok"); return 0; }
C
cat > hello.cpp <<'CPP'
#include <iostream>
#include <vector>
int main() { std::vector<int> v{1, 2, 3}; std::cout << "c++20 ok " << v.size() << '\n'; }
CPP
cat > heap_bug.c <<'C'
#include <stdlib.h>
int main(void) {
    int *p = malloc(4 * sizeof *p);
    volatile int *q = p;   /* volatile: az optimalizáló nem ejtheti ki */
    q[4] = 1;              /* szándékos heap-buffer-overflow */
    free(p);
    return 0;
}
C
cat > int_bug.c <<'C'
#include <limits.h>
#include <stdio.h>
int main(int argc, char **argv) {
    (void)argv;
    int x = INT_MAX - 1 + argc;   /* argc == 1: INT_MAX */
    printf("%d\n", x + 1);       /* szándékos signed overflow */
    return 0;
}
C

gcc -std=c17 -g -Wall -Wextra -Wpedantic -O2 hello.c -o hello_c && ./hello_c
g++ -std=c++20 -Wall -Wextra -Wpedantic -O2 hello.cpp -o hello_cpp && ./hello_cpp

# A várt hibát úgy kérdezzük le, hogy a set -e ne lépjen ki:
expect_failure() {      # minta fájl parancs...
    local pattern=$1 out=$2 status
    shift 2
    if "$@" 2> "$out"; then status=0; else status=$?; fi
    test "$status" -ne 0 || { echo "váratlan siker: $*" >&2; return 1; }
    grep -q "$pattern" "$out" || { echo "nincs '$pattern': $*" >&2; return 1; }
}

gcc -std=c17 -g -O1 -fsanitize=address -fno-omit-frame-pointer \
    heap_bug.c -o heap_bug
expect_failure 'heap-buffer-overflow' asan.txt ./heap_bug
grep -q 'heap_bug.c:5' asan.txt            # szimbolizált forráshely
echo "asan ok"

gcc -std=c17 -g -O1 -fsanitize=undefined -fno-sanitize-recover=undefined \
    int_bug.c -o int_bug
expect_failure 'signed integer overflow' ubsan.txt ./int_bug
echo "ubsan ok"

gdb --batch -ex 'break main' -ex run ./hello_c > gdb.txt 2>&1
grep -q 'Breakpoint 1, main' gdb.txt        # szimbólum és forráshely
echo "gdb ok"
echo "LAB-CHECK: OK"

Két részlet tanulságos. Az ASan csak a ténylegesen végrehajtott memória-hozzáférést látja. -O1 mellett a GCC egy free előtti, soha ki nem olvasott írást kiejthet, és akkor nincs mit jelezni; a próba ezért volatile hozzáférést használ. A kombinált -fsanitize=address,undefined buildben pedig az UBSan alapértelmezésben jelez, de továbbfut, és a program 0-val is kiléphet; a próbák ezért külön buildet és -fno-sanitize-recover=undefined-et használnak.

Buildjegyzőkönyv (lab-gcc profil). A szkript a profil gépén c17 ok, c++20 ok 3, asan ok, ubsan ok, gdb ok, LAB-CHECK: OK kimenettel, 0 kóddal futott. A fenti apt-get felállítást tiszta konténerben nem próbáltuk ki, mert a jegyzőkönyv gépén nem volt konténer-futtatókörnyezet; a csomaglista a jegyzőkönyv gépének dpkg -l kimenetéből származik.

1.3.1.3 Tesztelt forráscsomagok

A nagyobb laborokhoz a kiadás mellékleteként fejezetenként elkülönített forráscsomag tartozik: ch17-diag (17. fejezet: C mag, CLI, TCP-adapter, referencia-szerver), ch36-ffi (36.5: ABI- és FFI-labor), ch38-cmake (38.2: target-alapú CMake projekt) és ch39-port (39. fejezet: C++20 port, szolgáltatás, provenance). A csomagok fájljai bájtra azonosak a könyvben közölt kódblokkokkal, mert ugyanabból a forrásból generáljuk őket; a hosszú kódokat tehát nem kell a PDF-ből másolni. Minden csomag README.md fájlja rögzíti a profilt és a forrásfejezetek SHA-256 lenyomatát, a check.sh pedig a fejezetekben közölt build- és tesztparancsokat futtatja a lab-gcc profilban.

A konténer reprodukciós eszköz, nem önmagában bizonyíték. Az image tartalomazonosítója hasznosabb, mint egy változó címke; ugyanakkor a konténer továbbra is a gazdagép kernelét használja. A kernelverziót, CPU-környezetet és esetleges emulációt külön dokumentáljuk.

1.3.2 Az eszközök szerepe és azonosítása

A gcc és clang itt fordítómeghajtó (compiler driver): nemcsak fordítást, hanem linkelést is vezérelhet.

Eszköz Szerep Fontos korlát
gcc, clang C fordítása, linkelés vezérlése Az alapértelmezett nyelvi módra nem támaszkodunk
g++, clang++ C++ fordítása, C++ könyvtárak linkelése C++ program végső linkeléséhez ezeket használjuk
as, ld, readelf, nm, objdump Binutils-eszközök: assembly, linkelés, binárisvizsgálat Clang használhat beépített assemblert
gdb Folyamatállapot és forrás–gépi kód kapcsolat vizsgálata Optimalizálás mellett változók eltűnhetnek
strace Linux rendszerhívások megfigyelése Nem lát minden könyvtári vagy nyelvi műveletet

A PATH-ban található ld verziója még nem bizonyítja, hogy a fordító valóban azt használta. A driver -### kapcsolójával megvizsgálható a tervezett alparancssor; ez nem hajtja végre a fordítást.

1.3.2.1 Könyvtárstruktúra és naplózás

A következő parancsok Bash alatt, a tanuló külön laborjában használhatók. Feltételük, hogy a PATH már a választott eszközökre mutasson.

set -euo pipefail

export LC_ALL=C
export TZ=UTC

mkdir -p src negative/compile-fail negative/ub logs
mkdir -p build/{gcc,clang}/{debug,sanitize}

record() {
    printf '$' | tee -a logs/session.log
    printf ' %q' "$@" | tee -a logs/session.log
    printf '\n' | tee -a logs/session.log
    "$@" 2>&1 | tee -a logs/session.log
}

for tool in gcc g++ as ld gdb strace; do       # a lab-gcc profil kötelező eszközei
    record command -v "$tool"
    record "$tool" --version
done
for tool in clang clang++; do                   # lab-clang: csak ha telepítve van
    if command -v "$tool" >/dev/null; then
        record "$tool" --version
    else
        printf '# %s nem érhető el (lab-clang profil nélkül)\n' "$tool" \
            | tee -a logs/session.log
    fi
done

record uname -m
record uname -r
record gcc -dumpmachine
record getconf GNU_LIBC_VERSION
record g++ -print-file-name=libstdc++.so
record locale

A pipefail azért szükséges, hogy a tee sikeres naplózása ne fedje el a megelőző parancs hibáját. A set -e miatt a váratlanul sikertelen parancs megszakítja ezt a laborfolyamatot. A szándékosan hibás vagy opcionális lépéseket ezért if feltételébe tesszük: egy elvárt hiba státuszát if parancs; then status=0; else status=$?; fi alakban kérdezzük le, és utána ellenőrizzük, hogy valóban nem nulla. Ezt a mintát használja a könyv minden negatív futási próbája.

A getconf itt glibc-alapú, natív környezet azonosítására szolgál, nem keresztfordítási célkönyvtár vizsgálatára. A libstdc++.so útvonala pedig önmagában nem pontos verzióazonosító: a csomagjegyzéket és a tényleges fájl ellenőrző összegét is érdemes menteni.

A forrásokat UTF-8-ként tároljuk. Az LC_ALL=C a diagnosztika és rendezés kiszámíthatóságát szolgálja; nem állít be UTF-8 locale-t. Ha egy későbbi kísérlet Unicode-szövegkezelést vizsgál, telepített UTF-8 locale-t kell választani és külön rögzíteni. Az alábbi példák csak ASCII-karaktereket használnak.

1.3.3 Warning-, debug- és sanitizerprofil

GCC/Clang-beállítás: a C alapmód -std=c17, a C++ alapmód -std=c++20. Ezekhez rendeljük:

warnings=(-Wall -Wextra -Wpedantic)
debug=(-g -O1 -fno-omit-frame-pointer)

A -Wall neve ellenére nem kapcsol be minden figyelmeztetést. A -Wextra további csoportokat engedélyez, a -Wpedantic pedig a választott ISO módhoz kapcsolódó diagnosztikát kér. Egyik sem teljes szabványossági vagy helyességi bizonyítás.

A -Wconversion hasznos lehet például értéktartományt szűkítő implicit konverziók feltárására. Külön auditprofilként adjuk hozzá, és minden találatot értelmezzünk; a puszta cast nem bizonyítja a konverzió helyességét.

A debugprofilban:

Ettől még nem garantált minden forrásváltozó megfigyelhetősége vagy minden függvényben külön veremkeret. Optimalizálatlan összehasonlításhoz külön -O0 build készíthető.

1.3.4 Kidolgozott példa két nyelven, két fordítóval

A programok a típusméreteket C bájtban írják ki. ISO C/C++ szerint a sizeof egysége a karaktertárolás egysége; bitméretét a CHAR_BIT adja meg.

A src/probe.c teljes program, minimum C17 követelménnyel:

#include <limits.h>
#include <stdio.h>

int main(void)
{
    if (printf("CHAR_BIT=%d int=%zu long=%zu pointer=%zu\n",
               CHAR_BIT, sizeof(int), sizeof(long),
               sizeof(void *)) < 0) {
        return 1;
    }

    return fflush(stdout) == EOF;
}

A src/probe.cpp teljes program, a laborban C++20 móddal:

#include <climits>
#include <iostream>

int main()
{
    std::cout << "CHAR_BIT=" << CHAR_BIT
              << " int=" << sizeof(int)
              << " long=" << sizeof(long)
              << " pointer=" << sizeof(void*)
              << '\n' << std::flush;

    return std::cout ? 0 : 1;
}

A példák nem hoznak létre alkalmazásszintű borrowed pointert: a sizeof(void*) csak egy típus méretét kérdezi le.

A korábbi Bash-munkamenetben:

record gcc -std=c17 "${warnings[@]}" "${debug[@]}" \
    src/probe.c -o build/gcc/debug/probe-c

record g++ -std=c++20 "${warnings[@]}" "${debug[@]}" \
    src/probe.cpp -o build/gcc/debug/probe-cpp

record clang -std=c17 "${warnings[@]}" "${debug[@]}" \
    src/probe.c -o build/clang/debug/probe-c

record clang++ -std=c++20 -stdlib=libstdc++ \
    "${warnings[@]}" "${debug[@]}" \
    src/probe.cpp -o build/clang/debug/probe-cpp

record clang++ -std=c++20 -stdlib=libstdc++ -### \
    src/probe.cpp -o build/clang/debug/probe-cpp

A -stdlib=libstdc++ a könyvtárcsaládot választja ki, nem annak pontos kiadását. Több GCC-telepítés esetén a Clang toolchain-keresését is rögzíteni kell; erre szolgálhat a --gcc-toolchain kapcsoló. A futáskor betöltött könyvtárat a loader környezete is befolyásolja.

ABI-függő várt adatok, nem mért eredmények:

Mennyiség x86-64 System V LP64 AArch64 Linux LP64
CHAR_BIT 8 8
sizeof(int) 4 4
sizeof(long) 8 8
sizeof(void*) 8 8

Az egyező méretek nem jelentenek bináris kompatibilitást. Az AArch64-kísérlethez külön natív profil a legegyszerűbb. Keresztfordításnál a célkönyvtárakat és a futtatási környezetet is külön kell azonosítani; a uname -m önmagában csak a futó környezet egy adatát adja meg.

1.3.5 Elkülönített hibapélda és diagnosztika

A helyes források a src, a szándékosan hibás példák a negative könyvtárba kerülnek. A fordításkor elutasítandó esetek és a futás közbeni UB külön csoportot alkotnak.

Szándékosan hibás, C17-es tanítási példa, negative/ub/signed_overflow.c:

#include <limits.h>

int increment(int value)
{
    return value + 1;
}

int main(void)
{
    return increment(INT_MAX);
}

ISO C17-szabály: a signed összeadás eredményének reprezentálhatónak kell lennie a megfelelő típusban. Itt

INT_MAX+1>INT_MAX, \mathrm{INT\_MAX}+1>\mathrm{INT\_MAX},

ezért az összeadás nem definiált viselkedést okoz. Nincs garantált programkimenet.

Külön sanitizerbuild:

record gcc -std=c17 "${warnings[@]}" "${debug[@]}" \
    -fsanitize=address,undefined -fno-sanitize-recover=all \
    negative/ub/signed_overflow.c \
    -o build/gcc/sanitize/signed-overflow

record clang -std=c17 "${warnings[@]}" "${debug[@]}" \
    -fsanitize=address,undefined -fno-sanitize-recover=all \
    negative/ub/signed_overflow.c \
    -o build/clang/sanitize/signed-overflow

Ehhez a választott fordítóhoz tartozó sanitizer-runtime telepítése is szükséges. A kapcsolók ebben az egyutas fordításban a fordítási és linkelési lépéshez is eljutnak.

A későbbi kézi futtatáskor UBSan-diagnosztika várható, de annak pontos szövege és a kilépési státusz rögzítendő megfigyelés. Az ASan jelenléte nem jelenti, hogy ezt az aritmetikai hibát az ASan keresi. A sanitizer hibamentes futása pedig csak a ténylegesen bejárt végrehajtásokra ad korlátozott bizonyítékot.

1.3.6 Feladatok

  1. Magyarázó: Miért elégtelen laborazonosító a „Linux, GCC 13, konténerben” leírás? Nevezz meg öt hiányzó adatot.

  2. Kódolási: Bővítsd a C próbaprogramot a sizeof(short) és sizeof(long long) kiírásával. Fordítsd mindkét fordítóval, majd külön -Wconversion mellett.

  3. Diagnosztikai: Egy fordítási parancs tee használata mellett sikeresnek látszik, pedig a forrás szintaktikailag hibás. Hogyan különíted el a naplózási hibát a várt fordítási kudarctól? Mi legyen a negatív teszt sikerfeltétele?

1.3.7 Megoldások

1.3.7.1 Magyarázó feladat

Hiányozhat a teljes fordító- és csomagverzió, libc, linker, cél-ABI, nyelvi mód, kapcsolók, image-azonosító vagy forrásverzió. Az ellenőrzési feltétel: másik mérnöknek ne kelljen találgatnia ezeket a környezet összeállításakor.

1.3.7.2 Kódolási feladat

A printf formátumába két új %zu, argumentumlistájába sizeof(short) és sizeof(long long) kerül. Ezek eredménye size_t, ezért megfelelő a %zu. A választott két ABI-profilban rendre 2 és 8 C bájt várható. Ellenőrzés: mindkét fordítás sikeres, a diagnosztika áttekintett, az esetleges eltérés dokumentált.

1.3.7.3 Diagnosztikai feladat

Alapértelmezésben a pipeline státusza a végső parancsé lehet, így a sikeres tee elfedheti a fordító hibáját. Bash alatt a pipefail, illetve a PIPESTATUS közvetlen vizsgálata segít.

A negatív fordítási teszt sikere az elvárt okból bekövetkező elutasítás. Hiányzó header vagy elérhetetlen fordító nem megfelelő bizonyíték. Előbb ellenőrizni kell, hogy ugyanazzal a profillal a helyes alapminta lefordul.

1.3.8 Összefoglaló

A megbízható labor külön kezeli a forrást, a toolchaint, a cél-ABI-t és a futási környezetet. Explicit nyelvi módot, két fordítót, elkülönített buildkönyvtárakat és naplózott parancsokat használ. A warningok és sanitizerek értékes megfigyelési eszközök, de nem helyettesítik a program szerződésének ellenőrzését.