2.1 Fordítási fázisok és fordítási egységek

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.

2.1.1 A szabvány szerinti fordítási fázisok

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.

2.1.1.1 Preprocessing token és nyelvi token

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.

2.1.1.2 A sorrend látható következményei

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:

  1. A második fázis törli az első sor végén álló backslash–újsor párt.
  2. Az int timeout = 30; így ugyanannak a logikai sornak a része lesz, mint a // komment.
  3. A harmadik fázis ezt az egész kommentet szóközre cseréli. A timeout deklarációja tehát nem marad meg.
  4. Az unsigned/**/int alakból unsigned int lesz, nem unsignedint.
  5. A "\n" escape-je újsorkaraktert jelöl; ez nem azonos a forrásfájl sorösszekapcsolásával.
  6. A három szomszédos sztringliterál összefűződik. Az eredmény hat tartalmi karakter és egy lezáró nullkarakter, ezért 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.

2.1.2 Hogyan jön létre a fordítási egység?

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.

2.1.2.1 Kidolgozott, két fordítási egységes példa

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.

2.1.3 A gyakorlati toolchain lépései

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 demo

A 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.

2.1.3.1 Parser, szemantikai elemzés és IR

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.

2.1.4 Külön fordítás és hiányzó információ

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.

2.1.5 Hibahely-meghatározás és gyakori tévedések

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.

2.1.6 Feladatok

2.1.6.1 Tokenek és sorrend

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?

2.1.6.2 Két fordítási egység használata

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é.

2.1.6.3 Diagnosztikai szétválasztás

Három külön módosítást vizsgálj:

Melyik esetben hol várható hiba? Melyikben nem kötelező diagnosztikára számítani?

2.1.7 Megoldások

2.1.7.1 Tokenek és sorrend

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:

3+1+2+1=7. 3 + 1 + 2 + 1 = 7.

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.

2.1.7.2 Két fordítási egység használata

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 demo

A 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.

2.1.7.3 Diagnosztikai szétválasztás

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.

2.1.8 Összefoglaló

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?