A natív build hibáinak értelmezéséhez két külön modellt kell használnunk. Az ISO C fordítási fázisai azt határozzák meg, hogyan kell a forrásprogramot értelmezni. A toolchain lépései ezzel szemben konkrét eszközöket, folyamatokat és fájlokat jelentenek: preprocessort, fordítót, assemblert és linkert. A két felosztás összefügg, de nem azonos.
Az Első megfigyelési ciklus a forrástól a futásig részben megismert megállítási pontokra építünk. Most azt vizsgáljuk meg, pontosan mi jut el az egyes pontokig, és milyen információ hiányzik még ott. Ez különösen fontos, amikor egy CI-buildben a „compilation failed” üzenet valójában headerkeresési, típusellenőrzési vagy linkelési problémát takar.
ISO C17: a fordítás nyolc fogalmi fázisból áll. Az implementációnak úgy kell viselkednie, mintha ezek a megfelelő sorrendben történnének; nem köteles nyolc külön programot vagy nyolc köztes fájlt létrehozni.
| Fázis | Fogalmi művelet | Következmény |
|---|---|---|
| 1. | A fizikai forrás karaktereinek leképezése a forráskarakter-készletre; a C17 szerinti trigráfok helyettesítése | A forrásfájl külső kódolása és a nyelvi karakterek nem azonos fogalmak |
| 2. | A közvetlenül egymást követő backslash és újsor törlése | Fizikai sorok logikai sorrá kapcsolódhatnak |
| 3. | Felbontás preprocessing tokenekre és whitespace-re; a kommentek helyettesítése egy-egy szóközzel | A komment nem egyszerűen „eltűnik” |
| 4. | Előfeldolgozási direktívák végrehajtása, makrók helyettesítése, include-ok feldolgozása | Kialakul az előfeldolgozás utáni fordítási egység |
| 5. | A karakter- és sztringliterálok karaktereinek, illetve escape-jeinek feldolgozása a végrehajtási karakterkészlet szerint | Például a \n escape nem két kiírandó karaktert
jelent |
| 6. | Szomszédos sztringliterálok összefűzése | A "net" "work" egyetlen sztringliterállá válik |
| 7. | A preprocessing tokenek nyelvi tokenekké alakítása; szintaktikai és szemantikai elemzés, fordítás | Itt érvényesülnek a C nyelvtani és típusozási szabályai |
| 8. | Külső objektum- és függvényhivatkozások feloldása, szükséges könyvtári összetevők hozzárendelése | Létrejön a végrehajtáshoz szükséges programkép |
Az include-dal bevont állományokon az első négy fázis rekurzívan végbemegy. A táblázat nem ír elő ELF-formátumot, assemblyfájlt vagy külön linkerfolyamatot: ezek implementációs megoldások.
A preprocessing token, vagyis előfeldolgozási token az előfeldolgozó által kezelt lexikai egység. Ilyen például az azonosító, az előfeldolgozási szám, a sztringliterál és az írásjel; meghatározott környezetben a headernév is külön kategória.
Az előfeldolgozási szám megengedőbb kategória, mint egy érvényes C
számkonstans. Például az 1e+ egyetlen preprocessing token
lehet, de nem érvényes lebegőpontos konstans. Ezért az előfeldolgozás
sikere még nem bizonyítja, hogy a program lexikailag és szemantikailag
helyes C-program.
A tokenizálás általában a következő tokenként felvehető leghosszabb
karaktersorozatot választja. Emiatt a whitespace helye számíthat: az
a ++ + b és az a + ++b nem ugyanazt a
tokensorozatot jelenti.
Az alábbi C17-részlet szemléltetésre szolgál, nem önálló program:
// A kovetkezo fizikai sor is komment lesz: \
int timeout = 30;
unsigned/**/int retries;
const char label[] = "net" "\n" "ok";A feldolgozás lépései:
int timeout = 30; így ugyanannak a logikai sornak a
része lesz, mint a // komment.timeout deklarációja tehát nem marad meg.unsigned/**/int alakból unsigned int
lesz, nem unsignedint."\n" escape-je újsorkaraktert jelöl; ez nem azonos a
forrásfájl sorösszekapcsolásával.sizeof label értéke 7 C bájt.A line splicing feltétele közvetlen szomszédság: ISO C17-ben a backslash és az újsor közé tett szóköz már nem része ennek a szabálynak. Egy fordítóbővítés esetleges engedékenységére nem építünk.
ISO C17: egy forrásfájl és az általa
#include direktívákkal bevont headerek, illetve
forrásfájlok együtt alkotják az előfeldolgozási fordítási
egységet (preprocessing translation unit). Az
előfeldolgozás után ezt fordítási egységnek
(translation unit, TU) nevezzük.
A fordítási egység tehát nem egyszerűen „egy .c fájl”.
Tartalmát a bevont headerek, a makródefiníciók és a feltételes fordítás
döntései is alakítják. Ugyanaz a .c fájl eltérő
előfeldolgozási konfigurációval eltérő TU-t eredményezhet.
A következő három fájl teljes, minimális C17-programot alkot. A függvény csak felső korlátot alkalmaz; a negatív értékeket változatlanul hagyja.
limit.h:
#define LIMIT_MAX 10
int limit_to_max(int value);limit.c:
#include "limit.h"
int limit_to_max(int value)
{
return value > LIMIT_MAX ? LIMIT_MAX : value;
}main.c:
#include "limit.h"
int main(void)
{
return limit_to_max(12) == LIMIT_MAX ? 0 : 1;
}Ebben a minimális példában minden TU egyszer tartalmazza a headert. Az ismételt bevonás elleni védelem és az include-higiénia a Headerek, deklarációk és include-higiénia témája.
A main.c előfeldolgozásának lényegi eredménye:
int limit_to_max(int value);
int main(void)
{
return limit_to_max(12) == 10 ? 0 : 1;
}Ez szemléltető, előfeldolgozott részlet: a tényleges
eszközkimenet sorjelöléseket is tartalmazhat. A makródefiníció már nincs
jelen, a használata helyén 10 áll.
Fontos, hogy a limit_to_max definíciója nincs
ebben a TU-ban. A main.c fordításához a deklaráció
biztosítja a hívás típusinformációját. A függvénytörzs másik fordítási
egységben van.
GCC/Clang laborprofil: ISO C17, Linux hosted környezet, x86-64 System V ABI, LP64. Kijelölt eszközkiadások: GCC 14.2.0 vagy Clang 19.1.7, glibc 2.40 és GNU binutils 2.43. A parancsok az ennek megfelelően kiválasztott eszközöket feltételezik; itt elvárt artefaktumokat mutatunk be, nem dokumentált fordítási vagy futtatási eredményeket.
A fájlok közös könyvtárából:
CC=gcc
"$CC" -std=c17 -Wall -Wextra -Wpedantic -E main.c -o main.i
"$CC" -std=c17 -Wall -Wextra -Wpedantic -O0 -S main.i -o main.s
"$CC" -c main.s -o main.o
"$CC" -std=c17 -Wall -Wextra -Wpedantic -O0 -c limit.c -o limit.o
"$CC" main.o limit.o -o demoA Clang-változathoz a CC=clang beállítással külön
munkakönyvtárban ismételhető meg a build. Az azonos nyelvi jelentés nem
követeli meg azonos assembly vagy azonos diagnosztikai szöveg
előállítását.
| Lépés | Artefaktum | Mit jelent? |
|---|---|---|
-E |
main.i |
Előfeldolgozott forrás szöveges megjelenítése |
-S |
main.s |
Célarchitektúrához tartozó assembly |
| Assembly fordítása | main.o |
Relokálható objektumfájl |
-c limit.c |
limit.o |
A másik TU fordításának és assemblálásának eredménye |
| Linkelés | demo |
A rendszer betöltője számára előkészített végrehajtható állomány |
A kapcsolók a compiler driver működését vezérlik. A driver nem maga az összes feldolgozó komponens: szükség szerint további eszközöket indít vagy integrált komponenseket használ.
A -c jelentése nem „csak a parserig menj”. C forrásból
objektumfájlt készít, de nem linkel teljes programot. Hasonlóképpen, a
-E kimenete sem a szabvány valamely belső állapotának
kötelező, karakterre pontos reprezentációja.
A parser, vagyis szintaktikai elemző azt ellenőrzi, hogyan alkotnak a tokenek deklarációkat, kifejezéseket és utasításokat. Ebből rendszerint absztrakt szintaxisfa, abstract syntax tree (AST) készül.
A szemantikai elemzés a nyelvi jelentést ellenőrzi: például létezik-e a használt deklaráció, megfelelőek-e az operandustípusok, és megengedett-e egy konverzió. A két feladat implementációban összefonódhat; nem szükséges két külön menetnek lennie.
Az IR, intermediate representation, köztes reprezentáció a fordító belső programleírása. Egy fordító többféle IR-t használhat, eltérő absztrakciós szinteken. Ezeken történhetnek optimalizálások, majd a kódgenerálás gépi utasításokat választ és regisztereket rendel az értékekhez. Az IR nem ISO C által előírt formátum.
Az assembler az assemblyből gépi kódot és kapcsolódó
objektumfájl-adatokat készít. A main.o tartalmazhat még
feloldatlan hivatkozást a limit_to_max függvényre: ettől az
objektumfájl nem hibás. A feloldás a linkelés feladata.
A külön fordítás (separate compilation)
során a fordítási egységeket önállóan dolgozzuk fel. Hagyományos, LTO
nélküli fordításban a main.c elemzésekor a fordító nem
látja a limit.c függvénytörzsét.
A közös header helyességi szerepe ezért alapvető: ugyanazt a deklarációt adja a hívónak és a megvalósításnak. Ha a megvalósítás is bevonja, a fordító helyben összevetheti a deklarációt a definícióval.
Bizonyításvázlat a helyi ellenőrzés korlátjáról: a
main.c TU-jában csak a deklarált függvénytípus érhető el.
Ebből a fordító ellenőrizheti a hívás argumentumait, de nem
következtethet egy nem látott definíció tényleges típusára. Ehhez
kereszt-egység információ kellene.
Elkülönített hibás C17-részletként képzeljük el,
hogy a limit.c nem vonja be a headert, és ezt
definiálja:
long limit_to_max(long value)
{
return value > 10L ? 10L : value;
}A hívó továbbra is int limit_to_max(int) típust ismer. A
két típus inkompatibilis; a teljes program viselkedése nem definiált. A
hagyományos linker ettől még összekapcsolhatja az azonos nevű
szimbólumokat. A sikeres link tehát nem bizonyítja a kereszt-egység
típushelyességet.
A linkidőbeli optimalizálás, link-time optimization (LTO), több információt tehet elérhetővé, és egyes eltéréseket diagnosztizálhat. Nem helyettesíti a helyes deklarációs szerződést.
| Jelenség | Elsődleges vizsgálati hely |
|---|---|
| Nem található a bevont header | Előfeldolgozás, include-keresési konfiguráció |
| Érvénytelen számkonstans | Lexikai feldolgozás, preprocessing token átalakítása |
| Hiányzó pontosvessző | Szintaktikai elemzés |
| Nem deklarált azonosító | Szemantikai elemzés |
| Módosított vagy rossz célarchitektúrájú assembly elutasítása | Assembler |
| A buildből kimaradt függvénydefiníció | Linkelés |
Ezek logikai kategóriák, nem kötelezően külön folyamatok. A hiba tényleges helyét a diagnosztika és az előállított utolsó sikeres artefaktum együtt segít azonosítani.
Gyakori tévedés, hogy minden headerben jelzett hiba előfeldolgozási hiba. Egy header hibás C-deklarációját az előfeldolgozó gond nélkül továbbíthatja; a parser vagy a szemantikai elemző később utasítja el.
Szintén hibás következtetés, hogy a sikeres -c teljes
programhelyességet bizonyít. Csak az adott fordítási egység
objektumfájlig történő feldolgozása sikerült. A hiányzó definíciók, az
inkompatibilis kereszt-egység deklarációk és a futásidejű hibák ettől
még fennmaradhatnak.
Magyarázd meg, miért nem deklarálódik a korábbi példában a
timeout, miközben az unsigned/**/int retries;
érvényes deklaráció marad. Miért 7 C bájt a label
mérete?
Módosítsd kizárólag a main.c fájlt úgy, hogy a
12 → 10 és a 7 → 7 esetet is ellenőrizze.
Siker esetén térjen vissza nullával. Fordítsd újra csak ezt a TU-t, majd
linkeld a változatlan limit.o mellé.
Három külön módosítást vizsgálj:
limit.h nincs az include-keresés számára elérhető
helyen.limit.o.long változat.Melyik esetben hol várható hiba? Melyikben nem kötelező diagnosztikára számítani?
A sorösszekapcsolás megelőzi a kommentfelismerést, ezért a
timeout deklarációja a // komment részévé
válik. A blokkkommentet viszont egy szóköz helyettesíti, így két külön
kulcsszó marad: unsigned és int.
A label tartalma három betű, egy újsorkarakter és két
további betű. Ehhez egy lezáró nullkarakter tartozik:
Az ellenőrzés feltétele a műveletek helyes sorrendje; nem az, hogy egy eszköz pontosan milyen whitespace-szel jeleníti meg az előfeldolgozott szöveget.
A teljes új main.c, az előző C17-laborprofilhoz:
#include "limit.h"
int main(void)
{
if (limit_to_max(12) != LIMIT_MAX) {
return 1;
}
if (limit_to_max(7) != 7) {
return 1;
}
return 0;
}"$CC" -std=c17 -Wall -Wextra -Wpedantic -O0 -c main.c -o main.o
"$CC" main.o limit.o -o demoA limit.o újrafelhasználásának feltétele, hogy sem a
megvalósítás, sem annak függőségei vagy fordítási profilja nem
változott. A program elvárt visszatérési értéke nulla; ez ellenőrzési
feltétel, nem közölt futtatási eredmény.
Az elérhetetlen header az előfeldolgozásnál okoz hibát. A kihagyott
limit.o esetén a fordítás sikerülhet, de a linkelés nem
találja a függvény definícióját.
Az inkompatibilis típusú megvalósítás külön-külön lefordulhat, és hagyományos linkeléssel össze is kapcsolható. A kereszt-egység eltérésre nem kötelező diagnosztikát kapni; a program ettől még hibás, viselkedése nem definiált. Javítása a közös deklaráció következetes használata, majd az érintett TU-k újrafordítása.
A szabvány fordítási fázisai a forrás értelmezését írják le; a toolchain lépései ennek gyakorlati megvalósítását. A fordítási egység az include-ok és az előfeldolgozási döntések eredménye, nem pusztán egy fájlnév. A parser szerkezetet, a szemantikai elemzés nyelvi helyességet vizsgál; a kódgenerálás és az assembler objektumfájlt állít elő, a linker pedig összekapcsolja az egységeket.
Hibakereséskor mindig két kérdést tegyünk fel: meddig jutott el a feldolgozás, és milyen információ állt ott rendelkezésre?