Egy magas szintű nyelvben gyakran elegendő úgy gondolkodni, hogy a változó értéket nevez meg, a futtatókörnyezet pedig gondoskodik annak tárolásáról és kezeléséről. C-ben ez a kép nem elég: külön kell választanunk az értéket, az azt tároló objektumot, az objektum típusát és a gépi megvalósítást. Ugyanakkor a másik véglet is félrevezető: a C-forrás nem processzorutasítások hordozható jelölésrendszere.
A helyes kiindulópont a nyelvi absztrakt gép (abstract machine): a program jelentését meghatározó szabályrendszer. Ezt kapcsoljuk össze a már ismert natív folyamatmodellel. A nyelvi állításokat itt az ISO C17, a konkrét rendszerpéldákat a Linux-labor szintjén kezeljük. A szabványos követelmény, az implementáció választása és egy futás megfigyelése különböző bizonyíték.
Célcsoport. A könyv olyan DevOps-, SRE- és rendszerüzemeltetési szakembereknek szól, akik a Linuxot és legalább egy szkript- vagy magas szintű nyelvet napi szinten használják, és natív (C, C++) komponenseket kell építeniük, hibát keresniük bennük, vagy felelősséggel üzemeltetniük őket. Nem kezdő programozási tankönyv: a vezérlési szerkezeteket és az alapvető algoritmusokat ismertnek tekinti.
Előismeretek. A következőket feltételezzük:
$?), környezeti változók, egyszerű
sh-szkript set -eu-val.man oldalak olvasása; alapvető
diagnosztikai eszközök (ps, strace vagy
ltrace legalább névről).Rövid szintfelmérő. Ha az alábbiak közül bármelyikre nem tudsz biztosan válaszolni, előbb az adott előismeretet érdemes pótolni, mert a későbbi elakadás nagy eséllyel ebből fakad, nem a C-ből vagy a C++-ból.
echo $? egy sikertelen grep
után, és miért?fork után?Tanulási útvonalak. A könyv négy részből áll. Az alapútvonal kötelező; a haladó útvonal választható, és az alapútvonalra épül.
| Útvonal | Fejezetek | Cél |
|---|---|---|
| C alapútvonal | 1–17 | C17 és Linux/POSIX rendszerprogramozás, a 17. fejezet záróprojektjével |
| C/C++ híd | 18–19 | a C23 és a C/C++ határ, átmenet a modern C++-ra |
| C++ alapútvonal | 20–39 | C++20 erőforráskezelés, generikus programozás, könyvtár, konkurencia, build; a 39. fejezet portjával |
| Haladó útvonal | 40–46 | C++23 elmélyítés, C++26 kitekintés, referencia-függelék |
Az alapútvonal kötelező laborjai, ebben a sorrendben: 1.3 (reprodukálható fordítási labor, ez a következő kötelező labor e fejezet után), 2.4, 5.5, 9.5, 14.6, 17.2–17.3 (C záróprojekt), 23.5, 25.5, 26.6, 28.6, 30.6, 35.5, 36.5, 37.6 és 39 (a C rendszer C++20 portja). A haladó útvonal laborjai: 40.1, 42.4 és 44.4. A laborok profilját és tesztelt forráscsomagjait az 1.3 alfejezet írja le.
Az önellenőrzéshez és a visszatéréshez a Hibaminták, döntési táblák és önellenőrző térkép (46.5) ad tünetből kiinduló térképet: ha egy későbbi fejezetben elakadsz, ott keresd meg a tünetet, és az a releváns fejezetre és előismeretre mutat.
A forrásprogram ember által szerkesztett szövegekből
áll. A fordítási egység (translation unit) nem
egyszerűen egy .c fájl: a preprocessor által feldolgozott
forrás, a beillesztett headerek tartalmával, a feltételes részek
kiválasztása és a makróhelyettesítések után.
A végrehajtható állomány a fordítási és linkelési folyamat egyik lehetséges eredménye. A folyamat ezzel szemben futásidejű rendszerpéldány: saját virtuális címtérrel, végrehajtási állapottal és OS-erőforrásokkal. Ugyanabból az állományból több folyamat indulhat; az állomány nem tartalmazza előre valamennyi futás konkrét állapotát.
A Linux-labor szokásos eszközlánca:
| Szereplő | Feladata | Fontos megkülönböztetés |
|---|---|---|
| Preprocessor | Include-ok, makrók és feltételes fordítás feldolgozása | Szövegszintű előkészítés, nem programfuttatás |
| Fordító (compiler) | Nyelvi elemzés, ellenőrzés, optimalizálás, célkód előállítása | A nyelvi jelentésből készít implementációt |
| Assembler | Assemblyből gépi kódot tartalmazó objektumfájl készítése | Integrálva is lehet a fordítóba |
| Linker | Objektumfájlok és könyvtárak összekapcsolása, szimbólumhivatkozások feloldása | Egy objektumfájl rendszerint még nem önálló program |
| Betöltő (loader) | A program futásának előkészítése | Linuxon a kernel és dinamikus programnál a dinamikus linker is részt vesz benne |
| Runtime | Indulási, leállási és egyéb futásidejű támogatás | Nem szükségképpen értelmező vagy virtuális gép |
| libc | A C könyvtári szolgáltatásainak és gyakran rendszerinterfészeknek a megvalósítása | Nem azonos sem a kernellel, sem a teljes runtime-mal |
Ezek logikai szerepek, nem feltétlenül külön programok. A
gcc vagy clang parancs rendszerint
fordítódriverként több lépést vezérel. Az objektumfájl
nevében szereplő „objektum” pedig nem azonos a nyelvi memóriamodell
objektumával.
A fordítási fázisok részleteit a Fordítási fázisok és fordítási egységek tárgyalja. Itt az a fontos, hogy az ISO C nem követel ELF-formátumot, Linux-folyamatot vagy külön assemblerprogramot.
ISO C17: az objektum
(object) a végrehajtási környezet olyan adattárolási
tartománya, amely értékeket tartalmazhat. Nem feltétlenül
objektumorientált osztálypéldány: egy int változó által
megnevezett tárolási tartomány is objektum.
Az érték az adat jelentése, például a negyvenkettő egész szám. A típus meghatározza többek között az értékkészletet, a megengedett műveleteket és az értékek tárolási értelmezését. Egy kifejezésnek is lehet értéke anélkül, hogy külön, címezhető objektum tartozna hozzá.
Az objektum címe pointerértékkel jelölhető, amikor a nyelv megengedi a címképzést. A pointer nem általánosan „egy egész számba írt fizikai cím”. A Linux felhasználói folyamatában megfigyelt szokásos cím virtuális cím; a fizikai leképezést az OS és a hardver kezeli.
| Nyelvi fogalom | Lehetséges gépi megvalósítás | Ami nem következik belőle |
|---|---|---|
| Lokális egész objektum | Memóriahely, regiszter vagy optimalizálással elhagyott tárolás | Nem szükségképpen kap stackhelyet |
| Objektum értéke | Bitekkel kódolt reprezentáció vagy előre kiszámított konstans | Nem szükségképpen történik memóriából olvasás |
| Objektum címe | A laborban rendszerint virtuális címként megvalósított pointer | Nem garantált, hogy futások között változatlan |
| Objektumméret | sizeof által megadott C bájtszám |
Nem azonos a hasznos értékbitek számával |
A könyvben C_byte jelöli a sizeof
egységét. Egy C bájt CHAR_BIT bitből áll; ezt a
<limits.h> adja meg. ISO C17 szerint:
A bájt ezért nem minden C-implementációban azonos a nyolcbites oktettel. A kijelölt x86-64 laborban nyolcbites bájtokkal dolgozunk, de ez a laborprofil tulajdonsága.
Ha x olyan objektum, amelyre a sizeof x
megengedett, reprezentációjának mérete bitekben:
Ez a teljes reprezentáció mérete. Nem állítja, hogy minden bit önálló értékinformációt hordoz: a típusnak lehetnek kitöltőbitjei is.
Az absztrakt gép nem egy háttérben futó szoftver. Azt írja le, hogyan értelmezzük a program műveleteit és azok megfigyelhető következményeit.
Az as-if elv előképe így foglalható össze: a
megvalósítás eltérhet a forrás lépésenkénti, naiv végrehajtásától,
amennyiben megőrzi a definiált programvégrehajtás szabvány által
megkövetelt megfigyelhető viselkedését. Az I/O és a
volatile hozzáférések például külön követelményekhez
kötöttek; ezek részletei később következnek.
Ebből nem következik, hogy minden forrásbeli változónak külön memóriahelye lesz, vagy minden aritmetikai kifejezéshez futásidejű utasítás tartozik. A debugger által mutatható lokális változók megőrzése sem általános nyelvi követelmény.
A nem definiált viselkedés (undefined behavior, UB) esetén a szabvány nem ír elő viselkedési követelményeket. Ez nem ugyanaz, mint egy dokumentálandó implementációs választás. Egy sikeres próbafutás önmagában egyik nyelvi előfeltétel teljesülését sem bizonyítja.
ISO C17: a hosted környezet a
szabvány hosted végrehajtásra előírt indulási és könyvtári
szolgáltatásait biztosítja. A szokásos Linux-parancssori C-program ilyen
környezetben fut; a program indulásakor meghívott függvény neve
main.
Freestanding környezetben az indulás módja és az induló függvény neve implementáció által meghatározott, a kötelező könyvtári készlet pedig szűkebb. Kernel, bootloader vagy mikrokontrolleres program gyakran ilyen környezethez tartozik. A „freestanding” azonban nem egyszerűen az „OS nélküli” szinonimája: a nyelvi és könyvtári szerződésről szól.
A C és a C++ önálló nyelvek. Hasonló szintaxisuk nem jelent azonos konverziós, inicializálási vagy élettartam-szabályokat. A könyv C-alapja C17, C++-alapja C++20. Egy C-program C++-fordítóval való elfogadtatása nem általános helyességi ellenőrzés; a különbségeket a C és C++: azonos szintaxis, eltérő szemantika rendszerezi.
Az alábbi teljes program minimumkövetelménye a könyvben C17 hosted
környezet. A GCC-buildparancsok profilja a központi
profiltábla lab-gcc profilja, a clang sor
a lab-clang profilhoz tartozik.
Buildjegyzőkönyv: a két GCC-parancs (-O0,
-O2) a lab-gcc profilban figyelmeztetés nélkül
fordított, a program mindkét esetben
value=42; size=4 C_byte; CHAR_BIT=8 kimenettel futott; a
Clang-sort nem ellenőriztük.
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int count = 21;
int total = count * 2;
if (printf("value=%d; size=%zu C_byte; CHAR_BIT=%d\n",
total, sizeof total, CHAR_BIT) < 0) {
return EXIT_FAILURE;
}
if (fflush(stdout) == EOF) {
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}Mentés model.c néven, majd külön laborlépésként
fordítás:
gcc -std=c17 -Wall -Wextra -Wpedantic -O0 -g model.c -o model-o0
gcc -std=c17 -Wall -Wextra -Wpedantic -O2 -g model.c -o model-o2
clang -std=c17 -Wall -Wextra -Wpedantic -O2 -g model.c -o model-clangA parancsokhoz a kijelölt verziójú eszközöknek már telepítve kell lenniük. A környezet rögzítését a Reprodukálható fordítási és diagnosztikai labor részletezi.
A count és total nyelvi objektumok. A
sizeof total a total típusának tárolási
méretét adja, nem a 42 leírásához szükséges minimális bitszámot. A
%zu a sizeof eredménytípusának megfelelő
formátum.
A stdout standard streamet a futtatási környezet
biztosítja; a program használja, de nem zárja le. A lezárás
érvénytelenítené ezt a használati lehetőséget. A példa a kiíráskor és a
puffer ürítésekor jelzett hibát sikertelen kilépési állapottal
kezeli.
Bizonyításvázlat az előre kiszámítás megengedhetőségére:
count inicializálás után 21.int típusban ábrázolható.Sikeres I/O mellett tehát a value mező elvárt értéke 42.
A méretmezők az implementációt jellemzik. A tényleges utasítássor és a
debuggerben látható változók köre csak vizsgálattal állapítható meg; az
-O0 sem garantál szó szerinti forrás–utasítás
megfelelést.
A „lokális változó a stacken van” hasznos kezdeti gépi kép lehet, de nem nyelvi garancia. Javítása: előbb az objektum típusát és élettartamát azonosítsuk, utána vizsgáljuk a megvalósítást.
A „lefordult, tehát helyes” következtetés szintén hibás. A fordító elfogadása nem bizonyítja minden lehetséges végrehajtás szabályosságát.
A „nálam négy bájt, tehát mindig négy bájt” általánosítás összekeveri a target tulajdonságát a nyelvi követelménnyel. A megfigyelés mellé mindig kell környezeti profil.
Miért nem azonos a model.c, a fordítási egység, a
model-o2 és az abból indított folyamat? Melyikből lehet
egyszerre több futó példány?
Egészítsd ki a programot a sizeof(char) és a
sizeof total * CHAR_BIT matematikai szorzatának kiírásával.
Melyik eredmény garantált, és melyik implementációfüggő? Tartsd meg az
I/O-hibakezelést.
Az optimalizált program debuggerében a count értékére
„optimized out” jelzés jelenik meg, miközben sikeres I/O mellett a
program 42-t ír ki. Ez önmagában fordítóhibát bizonyít? Mit
ellenőriznél?
A model.c forrásfájl; a fordítási egység a
preprocesszálás utáni nyelvi bemenet; a model-o2 linkelt
végrehajtható állomány; a folyamat annak futásidejű példánya. Ugyanabból
az állományból több folyamat indulhat.
Ellenőrzési feltétel: a magyarázat különítse el a szerkesztett szöveget, a fordítás bemenetét, a buildterméket és a futási állapotot.
A meglévő printf helyére illeszthető részlet, az eredeti
hibakezelő blokkal:
if (printf("char=%zu C_byte; total=%zu bits\n",
sizeof(char),
sizeof total * (size_t)CHAR_BIT) < 0) {
return EXIT_FAILURE;
}A szorzás eredménytípusa így size_t, tehát a
%zu megfelelő. Előfeltétel, hogy a matematikai szorzat
beleférjen ebbe a típusba; a kijelölt laborprofilban ez teljesül.
A char mérete garantáltan 1 C_byte. A második eredmény a
teljes objektumreprezentáció bitmérete, nem az értékbitek száma.
Ellenőrzési feltétel: a char mező 1, a
második mező pedig a külön megfigyelt méret és CHAR_BIT
szorzata legyen.
Nem bizonyít fordítóhibát. A fordító elhagyhatta a külön tárolást, a debuginformáció pedig nem feltétlenül teszi rekonstruálhatóvá a változót.
Ellenőrizni kell a forrás és a bináris összetartozását, a fordítási kapcsolókat, a fordítóverziót és szükség esetén a gépi kódot.
Ellenőrzési feltétel: a következtetés a megfigyelhető programviselkedésre támaszkodjon, ne egy feltételezett stackhely hiányára.
A C-program jelentését az absztrakt gép szabályai adják; a natív folyamat ezek egyik megvalósítása. Az objektum nem fizikai memóriahely, a C bájt nem szükségképpen oktett, a változó pedig nem garantált stackrekesz. A helyes rendszerszemlélet minden állításnál megkülönbözteti a nyelvi követelményt, az implementáció szerződését és a konkrét labor megfigyelését.