2.5 Warningok, optimalizálási profilok és link-driver

Egy fordítóparancs nem pusztán kapcsolók gyűjteménye: egy buildlépés szerződését írja le. Meghatározza az elfogadott nyelvet, a keresett hibamintákat, az optimalizálási célt és azt, hogy milyen további eszközök kapják meg a feldolgozás eredményét. Ugyanaz a kapcsolólista nem megfelelő minden projekthez, fordítóverzióhoz és buildfázishoz.

A Fordítási fázisok és fordítási egységek rész alapján különítsük el a forrás feldolgozását, az objektumfájl előállítását és a végső linkelést. A Reprodukálható fordítási és diagnosztikai labor elvét követve a profilhoz mindig fordító-, target- és eszközverzió is tartozzon.

2.5.1 Diagnosztika: mit jelent egy warning?

ISO C17-szabály: a constraint a nyelvi leírásban kifejezetten előírt korlátozás. Például ha látható az int f(int); prototípus, akkor az f(1, 2) hívás megsérti az argumentumok számára vonatkozó követelményt.

Ha egy fordítási egység szintaktikai szabályt vagy constraintet sért, a megfelelő C-implementációnak legalább egy diagnosztikai üzenetet kell adnia. Ez nem jelent szükségképpen üzenetet minden hibáról, és nem írja elő általánosan a fordítás leállítását.

Ezzel szemben a warning a fordító diagnosztikai kategóriája. Jelölhet kötelezően diagnosztizálandó nyelvsértést, de figyelmeztethet teljesen szabályos, mégis gyanús kódra is. Egy használatlan lokális változó például önmagában nem teszi hibássá a C-programot.

GCC/Clang-kapcsolók: a következő nevek nem az ISO C részei. Pontos tartalmuk és alapértelmezésük verziófüggő.

Kapcsoló Rendeltetés Fontos korlát
-Wall Sok gyakori hibaminta diagnosztikáját bekapcsolja Neve ellenére nem minden warningot kapcsol be
-Wextra További, általában hasznos diagnosztikákat engedélyez Nem jelenti az összes fennmaradó warningot
-Wpedantic A kiválasztott ISO nyelvi módhoz kapcsolódó, többek között kiterjesztéseket érintő diagnosztikákat kér Nem teljes szabványossági bizonyítás
-Wconversion Értéket esetleg megváltoztató implicit konverziókra figyelmeztet Tudatos konverziók mellett is sok jelzést adhat
-Wshadow Más deklarációkat elfedő neveket jelez Pontos lefedettsége fordítónként eltérhet

A -std=c17 és a -Wpedantic együtt értelmezendő: az előbbi választja ki a nyelvi módot. A warningmentes build azonban sem a hordozhatóságot, sem a nem definiált viselkedés hiányát nem bizonyítja.

A -Wconversion és a -Wshadow különösen hasznos saját kódon, de örökölt projektben célszerű fokozatosan bevezetni őket. Egy explicit cast elnémíthat egy konverziós warningot, de nem teszi automatikusan helyessé a konverziót.

A -Werror a warningokat buildhibává emeli. Rögzített CI-eszközláncon ez erős minőségi kapu lehet; változó fordítóverzió mellett viszont egy új diagnosztika korábban elfogadott buildet is megállíthat. A GCC és Clang -pedantic-errors kapcsolója bizonyos, a szabvány által megkövetelt diagnosztikákat hibává emel; nem egyszerű szinonimája a -Werror kapcsolónak.

2.5.2 Optimalizálási szintek és debug információ

Az optimalizálási szint azt szabályozza, milyen átalakításokat mérlegeljen a fordító. Nem a program helyességi fokozata, és nem univerzális teljesítményskála.

Szint Jellemző cél Mire számítsunk?
-O0 Az optimalizálások nagy részének mellőzése Egyszerűbb forrásszintű követés; nem garantál szó szerinti forrás–utasítás megfelelést
-Og Hibakeresést támogató optimalizálás Hasznos kompromisszum lehet; GCC és Clang megvalósítása eltérhet
-O1 Mérsékelt optimalizálás Korlátozottabb fordítási költséggel végez átalakításokat
-O2 Általános teljesítményorientált profil Gyakori release-kiindulópont
-O3 További, agresszívebb optimalizálás Növelheti a kódméretet, és nem garantáltan gyorsabb
-Os Kódméretre hangolt optimalizálás Nem garantálja sem a legkisebb fájlt, sem a legjobb futásidőt

A pontos optimalizálási lépések fordító-, verzió- és targetfüggők. Különösen a -Og jelentését nem szabad két fordító között azonosnak tekinteni.

A debug információ olyan metaadat, amely a gépi program elemeit forrásfájlokhoz, sorokhoz, típusokhoz és változókhoz kapcsolja. A -g ennek előállítását kéri; nem kapcsolja ki az optimalizálást.

Indokolt kiinduló profilok:

Release-ben a -g incidenselemzéskor értékes. Az optimalizált változó ettől még eltűnhet, több változó osztozhat ugyanazon tárolón, és a debugger léptetése eltérhet a forrássorrendtől. A debug információ jelenléte nem állítja vissza az optimalizálás előtti programot.

A release-profilból nem következik automatikusan a -DNDEBUG: az külön döntés, mert megváltoztatja az assert használatának hatását. A -O3 választását pedig reprezentatív mérésnek kell indokolnia, nem a nagyobb számnak.

2.5.3 Kapcsolók buildfázis szerint

A compiler driver, például a gcc, vezérlőprogram: a bemenet és a kapcsolók alapján meghívja a szükséges fordítási eszközöket.

Kapcsoló Elsődleges szerep
-std=c17, -W... Nyelvi feldolgozás és diagnosztika
-Iinclude, -DNAME=value Headerkeresés és preprocessor-konfiguráció
-O2, -g Kódgenerálás és debug információ
-c Objektumfájl előállítása, végső linkelés nélkül
-Ldir Linkeléskor használt könyvtárkeresési útvonal
-lname Könyvtár kiválasztása a linker számára
-Wl,... Argumentumok továbbítása a linkernek

A -I és a -L tehát nem felcserélhető. Az előbbi deklarációkat tartalmazó headerek kereséséhez kell; az utóbbi a linkelhető könyvtárakhoz. A header megtalálása nem biztosítja a benne deklarált függvény implementációját.

Linux/GNU eszközláncon a -lwave szokásosan például libwave.so vagy libwave.a nevű fájl kiválasztásához vezet. A keresés részletei és a statikus vagy megosztott változat választása a linkelési beállításoktól függ.

A

-Wl,-Map,build/app.map

driverargumentum a linkernek két argumentumot továbbít: -Map és build/app.map. Itt a vesszők a driver által feldolgozott elválasztók.

A -L nem általános futásidejű könyvtárkeresési beállítás. A dinamikus betöltés külön kérdés; ezt a Statikus és megosztott könyvtárak rész bontja ki.

Fontos következmény: hagyományos, linkidőbeli optimalizálás nélküli buildben a már elkészült .o fájlok mellé tett -O2 nem optimalizálja újra a függvényeket. A release-profilhoz a forrásokat is újra kell fordítani.

Egyes kapcsolók több fázist érintenek. Például a támogatott sanitizerprofiloknál a megfelelő beállítás rendszerint fordításkor és a végső driveres linkeléskor is szükséges. Ezért a kapcsolókat jelentésük, nem pusztán nevük alapján kell szétválasztani.

2.5.4 Kidolgozott példa: külön fordítás és driveres linkelés

Laborprofil: ISO C17; Linux x86-64, System V ABI, LP64; GCC 14.2.0, GNU binutils 2.43.1 és glibc 2.40. Összehasonlító fordítóként Clang 19.1.7 használható ugyanazzal a rendszerkörnyezettel. Ezek kijelölt laborverziók, nem itt végzett fordítás eredményei. A parancsok feltételezik, hogy a gcc és az ar ezekre a rögzített eszközökre mutat.

A példa három fájlból áll. Az include/wave.h:

#ifndef BOOK_WAVE_H
#define BOOK_WAVE_H

double wave_sample(double radians);

#endif

Az src/wave.c:

#include "wave.h"
#include <math.h>

double wave_sample(double radians)
{
    return sin(radians);
}

Az src/main.c:

#include "wave.h"

int main(void)
{
    double sample = wave_sample(0.5);
    return sample > 0.0 ? 0 : 1;
}

A program értékeket ad át; nem kezel dinamikus erőforrást vagy kölcsönzött mutatót. A hívás véges, érvényes argumentumot ad a matematikai függvénynek. A várt sikerjelzés nulla visszatérési érték, nem itt megfigyelt futási eredmény.

A fájlok létrehozása után, tiszta buildkönyvtárból:

mkdir -p build/debug

gcc -std=c17 -Wall -Wextra -Wpedantic -Wconversion -Wshadow \
    -Og -g -Iinclude -c src/wave.c -o build/debug/wave.o

gcc -std=c17 -Wall -Wextra -Wpedantic -Wconversion -Wshadow \
    -Og -g -Iinclude -c src/main.c -o build/debug/main.o

ar rcs build/debug/libwave.a build/debug/wave.o

gcc -fuse-ld=bfd build/debug/main.o \
    -Lbuild/debug -lwave -lm \
    -Wl,-Map,build/debug/app.map -o build/debug/app

Az ar archívumot készít; ez nem a végső program linkelése. Az utolsó parancsban a gcc driver hívja meg a GNU BFD linkert.

Az argumentumok szerepe lépésenként:

  1. A main.o hivatkozik a wave_sample függvényre.
  2. A -Lbuild/debug -lwave elérhetővé teszi annak implementációját.
  3. A -lm a profil matematikai könyvtárát adja hozzá a sin feloldásához.

A -lm buildkövetelmény nem ISO C-szabály, hanem ennek a rendszerkörnyezetnek a sajátossága. A könyvtárakat a rájuk hivatkozó bemenetek után helyezzük; különösen statikus archívumoknál számít a sorrend.

Release-változathoz mindkét forrást újra kell fordítani -O2 -g beállítással, elkülönített build/release útvonalakra, majd az archívumot és a programot is újra előállítani. A debug- és release-objektumok keverése ne legyen véletlen buildállapot.

2.5.5 Miért ne közvetlenül az ld linkeljen?

A linker nem ugyanazokat az alapértelmezéseket ismeri, mint a nyelvi driver. A driver a telepített eszközlánc konfigurációja alapján választja ki többek között:

Linux/GNU környezetben a C-folyamat rendszerint nem közvetlenül a main függvénynél indul. Ezért a puszta ld main.o ... parancs akkor is hiányos lehet, ha minden saját függvény implementációja rendelkezésre áll.

A gcc és a g++ közötti különbség sem egyszerűen fájlkiterjesztés:

Vegyes programnál a C-forrást C-ként, a C++-forrást megfelelő C++ módban fordítsuk, a végső linkelést pedig rendszerint a C++ driver végezze. Clang esetén ennek megfelelő pár a clang és a clang++; a tényleges C++ könyvtár konfigurációfüggő.

2.5.6 Feladatok

2.5.6.1 Magyarázó feladat

Egy fejlesztő szerint a -O2 -g értelmetlen, mert az optimalizált program nem hibakereshető. Javítsd az állítást, és javasolj fejlesztői, illetve release-profilt.

2.5.6.2 Kódolási feladat

Az alábbi, C17-es fordítási egységet csak diagnosztikai kiindulópontként kezeld:

int accepted_count(double value)
{
    return value;
}

Módosítsd úgy, hogy csak a [0,100][0,100] tartományt fogadja el, törtrészt elhagyó konverzióval. Minden más bemenetre, NaN esetén is, adjon -1 értéket. Ellenőrző fordítás:

gcc -std=c17 -Wall -Wextra -Wpedantic -Wconversion \
    -c count.c -o count.o

2.5.6.3 Diagnosztikai feladat

A laborprogramot valaki közvetlenül így linkeli:

ld build/debug/main.o -Lbuild/debug -lwave -lm -o app

Miért hibás buildstratégia ez? Hogyan javítanád? Elegendő-e a javított linkparancshoz -O2 kapcsolót adni release készítéséhez?

2.5.7 Megoldások

2.5.7.1 A debug információ független döntés

Az optimalizált program hibakereshető, de a forrásszintű megfigyelések korlátozottabbak lehetnek. Indokolt fejlesztői kiindulópont a -Og -g, release-ben pedig a -O2 -g, mindkettőnél explicit -std=c17 és warningprofil.

Ellenőrzési feltétel: a debugger forrássorokat tudjon azonosítani; ne követeljük meg minden változó folyamatos megfigyelhetőségét.

2.5.7.2 Ellenőrzött konverzió

C17-es megoldás:

int accepted_count(double value)
{
    if (!(value >= 0.0 && value <= 100.0)) {
        return -1;
    }

    return (int)value;
}

A feltétel NaN esetén is elutasít, mert a tartományba tartozást igazoló összehasonlítások nem teljesülnek. Az elfogadott tartomány minden egész eredménye ábrázolható int típusban; a konverzió a törtrészt elhagyja.

Ellenőrzési feltétel: például 0.0 → 0, 12.75 → 12, 100.0 → 100; negatív, száznál nagyobb és NaN bemenetnél -1. A cast helyességét a tartományellenőrzés indokolja, nem a warning eltűnése.

2.5.7.3 A driver visszaállítása

A közvetlen ld-hívásból hiányozhatnak a startup- és runtime-összetevők, valamint szükséges keresési beállítások. A javítás a kidolgozott példa driveres linkparancsa.

Ellenőrzési feltétel: a linklépést a megfelelő nyelvi driver végezze. Release-hez minden érintett forrást újra kell fordítani a release-profillal; a meglévő debug-objektumok mellé írt -O2 önmagában nem elegendő.

2.5.8 Összefoglaló

A warningprofil hibakeresési eszköz, nem helyességi bizonyíték. Az optimalizálást és a debug információt külön döntésként kezeljük. A kapcsolókat buildfázis szerint rendezzük: -I a headerekhez, -L és -l a linkeléshez, -Wl, pedig a linkerargumentumok továbbításához tartozik. A végső linkelést általában a megfelelő compiler driver végezze, rögzített és dokumentált eszközlánccal.