1.5 Első megfigyelési ciklus a forrástól a futásig

A natív program megértéséhez nem elég látni, hogy „a fordító lefutott”. Külön kell megfigyelni a forrás feldolgozását, az objektumfájl létrejöttét, a linkelést és a folyamat futását. Ugyanaz a tünet — például hogy nincs kiírt üzenet — több különböző szakasz hibájából is származhat.

A szükséges C szintaxis tömör áttekintése alapján már olvasható egy rövid main függvény. Most ennek útját követjük végig, minden lépéshez megfigyelhető terméket vagy hibajelzést rendelve.

1.5.1 A megfigyelés feltételei

A példák minimumkövetelménye ISO C17, hosted környezetben. A választott laborprofil Linux, x86-64 System V ABI, LP64 adatmodell, GCC 12.2.0, második fordítóként Clang 14.0.6, glibc 2.36, GNU Binutils 2.40 és strace 6.1. Ebben a profilban egy C bájt nyolc bit. A shellparancsokat Bash alatt értelmezzük.

Ez választott, nem itt megmért konfiguráció. Disztribúciós javítások és fordítóbeállítások is módosíthatják a részleteket. A saját laborodhoz rögzítsd a teljes verziókimeneteket:

uname -srmo
gcc --version
gcc -dumpmachine
clang --version
getconf GNU_LIBC_VERSION
readelf --version
file --version
strace --version

A fejezetben az elvárt eredmény a program szerződéséből vagy a megadott eszközműveletből következik. Ténylegesen mért eredménynek csak a saját futtatásod rögzített kimenetét nevezd; az alábbi példák nem dokumentált tesztfuttatások.

1.5.2 A teljes program és első buildje

A hello.c teljes tartalma:

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    if (puts("hello") == EOF || fflush(stdout) == EOF) {
        (void)fputs("hello: stdout error\n", stderr);
        return EXIT_FAILURE;
    }

    if (fputs("hello: done\n", stderr) == EOF ||
        fflush(stderr) == EOF) {
        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}

ISO C17: a puts a szöveg után újsort ír a standard kimenetre. A fputs nem egészíti ki újsorral a szöveget. Mindkettő hibát jelezhet; a példában az EOF értékkel való összehasonlítás ezt vizsgálja.

A stdout és stderr könyvtári streamek, nem közvetlenül Linux file descriptorok. Életciklusukat itt a C futtatókörnyezet kezeli; a program nem zárja le őket kézzel. A kilépés utáni használatuk nem értelmezhető ebben a folyamatban.

Miért szerepel külön fflush? A sikeres könyvtári írás jelentheti azt is, hogy az adat csak egy felhasználói térben lévő pufferbe került. A kiürítés később hibázhat. A program még a sikeres visszatérés előtt ellenőrzi ezt. Ha maga a hibaüzenet kiírása is sikertelen, a hibastátusz marad az egyetlen jelzés.

Fordítás és linkelés egyetlen driverhívással:

gcc -std=c17 -Wall -Wextra -Wpedantic -O0 -g hello.c -o hello

A gcc itt fordítási lépéseket és linkelést is vezérel. A -O0 és -g megfigyeléshez választott beállítás; nem helyességi garancia. A warningkapcsolók hasznos ellenőrzéseket kérnek, de nem bizonyítják a hibamentességet.

1.5.3 A köztes termékek előállítása és vizsgálata

Ugyanezt az utat állítsuk meg három ponton:

gcc -std=c17 -E hello.c -o hello.i
gcc -std=c17 -Wall -Wextra -Wpedantic -O0 -g -S hello.c -o hello.s
gcc -std=c17 -Wall -Wextra -Wpedantic -O0 -g -c hello.c -o hello.o

head -n 20 hello.i
head -n 60 hello.s

A három parancs itt külön-külön az eredeti forrásból indul; nem az előző parancs kimenetét fogyasztja.

Megállási pont Termék Mit keress benne?
-E hello.i, előfeldolgozott szöveg Beillesztett headerdeklarációk, kibontott makrók, forráspozíció-jelölések
-S hello.s, assemblyszöveg Címkék, direktívák és gépi utasítások szöveges alakja
-c hello.o, objektumfájl Gépi kód, szimbólumok, relokációs és debugadatok

A .i fájl elején valószínűleg nem a main látszik, hanem a headerekből származó tartalom. Az assembly első sorai sem feltétlenül utasítások: az assemblernek szóló direktívák is szerepelnek benne.

Linux/ELF és GNU Binutils: az objektumfájlt most a platform eszközeivel vizsgáljuk:

file hello.o
readelf -h hello.o
nm hello.o
nm -u hello.o
objdump -dr hello.o

A file gyors típusazonosítást ad. A readelf -h az ELF fejlécét mutatja; ennél a profilnál relokálható objektumot, vagyis REL típust várunk.

Az nm szimbólumokat sorol fel. A main az objektumban definiált függvény; a szokásos megjelenítésben T jelölést kap. Az U olyan szimbólumot jelöl, amelyre az objektum hivatkozik, de amelyet nem itt definiáltunk. Ez önmagában nem hiba: a külső függvények feloldása a linkelési út része.

Az objdump -dr gépi kódot fordít vissza assemblyalakra, és megmutatja a relokációkat is. A relokáció leegyszerűsítve olyan információ, amellyel a linker egy címhez vagy szimbólumhoz kötött hivatkozást rendezhet. Az utasítások részletes értelmezése most nem cél.

A forrásbeli függvénynevek és az objektum külső hivatkozásai nem feltétlenül egyeznek egy az egyben: a fordító könyvtári műveleteket is átalakíthat. Például egy fputs hívás helyén más könyvtári hivatkozás jelenhet meg.

Végül linkeljünk az objektumból:

gcc hello.o -o hello
file hello
readelf -h hello

A végrehajtható ELF típusa lehet EXEC, illetve pozíciófüggetlen végrehajtható állomány, PIE esetén DYN. A DYN tehát nem jelenti automatikusan azt, hogy kizárólag megosztott könyvtárat vizsgálunk.

1.5.4 Standard kimenetek és kilépési státusz

A program két külön csatornát használ: a stdout a normál eredményé, a stderr a diagnosztikáé. Egy stderr-üzenet önmagában nem jelent sikertelen futást.

if ./hello >stdout.txt 2>stderr.txt; then status=0; else status=$?; fi
printf 'exit=%d\n' "$status"
cat stdout.txt
cat stderr.txt

A státuszt if ...; then status=0; else status=$?; fi alakban kérdezzük le. Az 1.3 laborban ajánlott set -euo pipefail beállítás mellett egy sima ./hello ... sor sikertelen futáskor az egész szkriptet leállítaná, és a status=$? sor sosem futna le; az if feltételében álló parancs hibája viszont nem váltja ki az errexit-et.

Elvárt eredmény sikeres I/O esetén:

Megfigyelés Elvárt érték
stdout.txt hello, majd újsor
stderr.txt hello: done, majd újsor
Bash kilépési státusz 0

ISO C17: az EXIT_SUCCESS sikeres, az EXIT_FAILURE sikertelen befejezést jelez a környezetnek. A választott Linux/glibc profilban ezek értéke rendre 0 és 1; a hordozható C programnak nem kell ezt a számpárt beégetnie.

A $? értékét közvetlenül a vizsgált parancs után kell elmenteni. Egy közbeiktatott cat után már annak státuszát olvasnád. Pipeline esetén pedig Bash alapbeállításban az utolsó parancs státuszát adja vissza, nem feltétlenül a vizsgált programét.

1.5.5 Könyvtári függvény vagy rendszerhívás?

A rendszerhívás (system call) a kernel szolgáltatásának belépési határa. A C könyvtári függvény ezzel szemben nyelvi könyvtári interfész: végezhet munkát kizárólag felhasználói térben, vagy kezdeményezhet rendszerhívásokat.

Linux, strace: az írásokat és a folyamatlezárást így figyelheted:

strace -o trace.txt -e trace=write,writev,exit_group \
    ./hello >stdout.txt 2>stderr.txt
cat trace.txt

Az -o külön fájlba teszi a trace-t, így az nem keveredik a program diagnosztikájával. A szűrés miatt ez nem a teljes rendszerhívás-történet.

A választott profilon tipikusan az 1-es és 2-es descriptorra irányuló írásokat, végül sikeres lezárást várunk. A pontos sorokat azonban a tényleges futtatásból kell leolvasni.

Nem puts rendszerhívást keresünk: ilyen Linux-rendszerhívás nincs. Több könyvtári írás összevonható egy kernelhívásba, egy nagyobb írás pedig több kernelhívást is igényelhet. A megfigyelt write sikere ráadásul nem bizonyítja, hogy az adat tartós háttértárra került.

1.5.6 A hiba helyének meghatározása

A diagnózis első kérdése: melyik szakasz nem teljesítette a szerződését?

Szakasz Példa Következmény
Fordítás Hiányzó pontosvessző Nem keletkezik használható új objektum
Linkelés Deklarált, de fel nem oldható függvény Nem keletkezik használható új végrehajtható állomány
Futás A kimenet írása hibázik A folyamat elindul, de hibastátusszal zárulhat

Egy külön forrásmásolat main törzsébe tett int unused = 0; a megadott warningprofilban várhatóan figyelmeztetést vált ki. Ez nem ugyanaz, mint egy fordítási hiba: -Werror nélkül a build ettől még sikerülhet.

Gyakori csapda a régi bináris futtatása sikertelen fordítás után. Egymáshoz kötött kézi próbánál használd az && operátort:

gcc -std=c17 -Wall -Wextra -Wpedantic hello.c -o hello && ./hello

Így a futtatás csak sikeres build után következik.

1.5.7 Feladatok

  1. Magyarázó: Miért nem bizonyít linkelési hibát az nm hello.o kimenetében egy U jelölés? Miért nem futtatható közvetlenül a hello.o?

  2. Kódolási: Bővítsd a programot úgy, hogy a stdout második sora ready legyen. Mindkét írást és a kiürítést ellenőrizd. Linuxon vizsgáld meg a futást /dev/full kimenettel is.

  3. Diagnosztikai: Az alábbi teljes, ISO C17 forrást mentsd missing.c néven. Jósolj eredményt külön az objektumképzésre és a linkelésre.

void missing(void);

int main(void)
{
    missing();
    return 0;
}

1.5.8 Megoldások

1.5.8.1 Külső hivatkozás az objektumban

Az U feloldandó külső hivatkozás. Akkor okoz linkelési hibát, ha az adott linkelés nem talál hozzá megfelelő definíciót. Az objektum még nem kész folyamatkép: hiányzik belőle a végrehajtható programhoz szükséges összerendezés és indítási infrastruktúra.

Ellenőrzési feltétel: a hello.o tartalmazhat U szimbólumokat, miközben a gcc hello.o -o hello sikeresen befejeződik.

1.5.8.2 Két sor és írási hiba

Az eredeti program első if blokkjának helyére kerülő C17-részlet:

if (puts("hello") == EOF ||
    puts("ready") == EOF ||
    fflush(stdout) == EOF) {
    (void)fputs("hello: stdout error\n", stderr);
    return EXIT_FAILURE;
}

A rövidzáras kiértékelés miatt az első észlelt hiba után hibakezelés következik. Fordítsd újra, majd:

if ./hello >/dev/full 2>error.txt; then status=0; else status=$?; fi
printf 'exit=%d\n' "$status"
cat error.txt
test "$status" -ne 0                  # váratlan siker: a negatív teszt bukjon
grep -q 'stdout error' error.txt      # a diagnosztika megjelent

Linux-specifikus előfeltétel: a /dev/full elérhető, és az átirányítás sikerül. Az eszköz az írást hibával utasítja el. Elvárt ellenőrzés: normál kimeneten két sor és nulla státusz; /dev/full esetén nem nulla státusz, írható stderr mellett hibaüzenet. A részlet set -euo pipefail mellett is végigfut; ha a program váratlanul sikeresen lépne ki, a test sor megbuktatja a szkriptet.

1.5.8.3 A deklaráció nem definíció

gcc -std=c17 -Wall -Wextra -Wpedantic -c missing.c -o missing.o
nm -u missing.o
gcc missing.o -o missing

Az objektumképzés elvártan sikerül: a deklaráció alapján a hívás ellenőrizhető. A linkelés viszont sikertelen, mert nincs missing-definíció.

Javításként ugyanahhoz a forráshoz hozzáadható ez a C17-részlet:

void missing(void)
{
}

Újrafordítás után a linkelésnek sikerülnie kell. A pontos diagnosztikai szöveg eszköz- és verziófüggő.

1.5.9 Rövid összefoglaló

A megfigyelési ciklus lánca: forrás, köztes termékek, objektum, linkelt állomány, folyamat, kimenetek és státusz. Az objektum szimbólumai, a kernel trace-e és a shell státusza különböző kérdésekre válaszolnak. Megbízható következtetéshez mindig nevezd meg a szakaszt, a vizsgált szerződést és azt, hogy elvárt vagy ténylegesen mért eredményről beszélsz.