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.
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.
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.
lab-gcc
profil felállításaA 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 ninjaA 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.
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.
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.
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 localeA 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.
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:
-g hibakeresési információt kér;-O1 mérsékelt optimalizálást engedélyez;-fno-omit-frame-pointer segítheti a
veremvisszakövetést.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ő.
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-cppA -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.
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
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-overflowEhhez 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.
Magyarázó: Miért elégtelen laborazonosító a „Linux, GCC 13, konténerben” leírás? Nevezz meg öt hiányzó adatot.
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.
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?
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.
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.
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.
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.