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.
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 --versionA 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.
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 helloA 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.
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.sA 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.oA 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 helloA 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.
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.txtA 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.
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.txtAz -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.
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.
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?
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.
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;
}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.
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 megjelentLinux-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.
gcc -std=c17 -Wall -Wextra -Wpedantic -c missing.c -o missing.o
nm -u missing.o
gcc missing.o -o missingAz 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ő.
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.