3.4 main, startup, argumentumok és környezet

A natív program indulása két külön szerződés találkozása. Az ISO C azt határozza meg, milyen feltételekkel kapja meg a vezérlést a C program, és milyen adatokat használhat. A Linux, az ELF-formátum, a hívási ABI és a C futtatókörnyezete azt valósítja meg, hogyan jut el idáig a folyamat.

Az ELF-betöltésről szóló rész alapján már megkülönböztetjük a betöltőt és a dinamikus linkert. Most ezt kapcsoljuk össze a main függvénnyel. A deklarációk olvasásához elegendő a C szintaxis tömör áttekintése; a pointerek részletes modelljét később bontjuk ki.

3.4.1 Az ISO C hosted belépési szerződése

ISO C17 — hosted környezet. Hosted, vagyis teljes C futtatási környezetben a program indulásakor meghívott függvény neve main, visszatérési típusa int. Két szabványos definíciós alakja:

A második alakban a paraméterként szereplő char *argv[] típusa char **argv típusra igazodik. Ezért az int main(int argc, char **argv) is megfelelő alak. A paraméternevek szabadon választhatók.

Az implementáció további, dokumentált alakokat is támogathat. Ettől azonban például a void main(void) nem válik hordozható hosted C belépési ponttá. A main visszatérési értéke a program befejezési állapotának része, nem elhagyható díszítés.

3.4.1.1 Az argumentumok pontos feltételei

Az ISO C17 a következőket garantálja:

A sztring nullkarakterrel, '\0'-val lezárt karaktersorozat. Ez nem azonos a null pointerrel: az üres sztringnek van érvényes tárolója, amelynek első karaktere '\0'; az argv[argc] viszont nem sztringre mutat.

Az argumentumsztringek tartománya tehát:

0≤i<argc. 0 \leq i < argc.

Az argv[argc] csak lezáró jelzőérték, nem nyomtatható ki %s formátummal. Az argv[0] sem garantált abszolút útvonal, és nem hiteles bizonyíték a futó végrehajtható fájl azonosságára.

Linux/shell kapcsolódás. A shell még a program indítása előtt feldolgozza az idézőjeleket és az argumentumképzést. Az alpha "two words" szövegből szokásos shellhíváskor két argumentum lesz, nem három; az idézőjelek általában nem kerülnek bele az átadott sztringbe.

3.4.2 Tároló, módosíthatóság és élettartam

ISO C17. Az argc és argv paraméterek, valamint az argumentumtömb által mutatott sztringek módosíthatók. Az argumentumsztringek nem csupán a belépés pillanatában használhatók: a program befejezéséig rendelkezésre állnak.

Ez nem jelent korlátlanul növelhető buffert. Egy argumentum meglévő karakterének átírása és egy hosszabb sztring bemásolása különböző művelet; az utóbbi túllépheti a rendelkezésre álló tárolót.

Hozzáférés Tulajdonos és élettartam Változás, érvénytelenítés és hibakimenet
Az eredeti argv argumentumtömb és sztringjei A tárolót a futtatókörnyezet biztosítja; a program kölcsönzött hozzáférést kap A saját módosítás megváltoztathatja az adatot; a tárolóhoz való hozzáférés a program befejezéséig érvényes. free nem hívható rá
argv[argc] Lezáró null pointer, nincs mögötte argumentumsztring Dereferálása nem definiált viselkedés
A getenv eredménye A környezetkezelés által biztosított, kölcsönzött hozzáférés Későbbi környezetlekérdezés vagy platformspecifikus módosítás megváltoztathatja, illetve érvénytelenítheti; hiány esetén null pointer

A táblázat az eredeti argumentumtárolóról szól. Ha az argv paramétert más pointerértékre állítjuk, attól az újonnan hivatkozott tároló nem örökli az eredeti élettartam-garanciát.

3.4.3 Környezeti adatok lekérdezése

ISO C17. A környezet szabványos lekérdezési felülete a <stdlib.h> fejlécben deklarált getenv. Például a getenv("APP_MODE"):

A visszaadott sztringet a program nem módosíthatja, és nem szabad free-vel felszabadítania. Bár a függvény visszatérési típusa char *, a helyi változóhoz célszerű const char * típust használni. Ez kifejezi a hozzáférés szerződését.

Egy későbbi getenv hívás felülírhatja a korábban kapott sztringet. POSIX környezetben a környezetmódosító műveletekhez további érvényességi korlátok tartoznak. Tartósan megőrzendő értékről ezért saját másolat szükséges.

Linux-kiterjesztés. Egyes futtatókörnyezetek támogatják az int main(int argc, char *argv[], char *envp[]) alakot. Az envp ilyenkor tipikusan null pointerrel lezárt környezeti sztringtömb. Ez nem a két hordozható ISO C alak egyike; szabványos C kódhoz használjuk a getenv felületet.

Linuxon az induló argumentumokat és környezetet az indító folyamat adja át. A gyermek környezetének későbbi módosítása nem írja át a szülő shell környezetét. Mindkét adatforrás külső bemenet: validálást igényelhet, és titkokat is tartalmazhat.

3.4.4 A Linux CRT útja a main függvényig

A CRT, azaz C runtime startup a C futtatókörnyezet indulását biztosító kód. Nem azonos magával a teljes C standard könyvtárral, bár szorosan együttműködik vele.

Linux/ELF, szokásos glibc-alapú program. Az indulás fő útvonala:

  1. Egy sikeres programindításkor a kernel kialakítja az új folyamatképet, és előkészíti az induló adatokat.
  2. Dinamikusan linkelt programnál az ELF-ben megadott interpreter, vagyis a dinamikus linker kap először vezérlést. Elvégzi a szükséges betöltési és relokációs feladatokat.
  3. A vezérlés eljut a végrehajtható fájl ELF-belépési címére. A szokásos toolchain ezt a CRT _start belépési kódjára állítja.
  4. A startupkód és a libc együtt előkészíti a C környezetet, majd meghívja a main függvényt.
  5. A main visszatérésekor a futtatókörnyezet normál befejezést kezdeményez.
Réteg Belépési szerződés
ISO C17 hosted program A programindítás C-szintű függvénye a main
Linux ELF A belépési cím az ELF fejléc adata; nem szükségképpen a main címe
x86-64 System V ABI labor Az induló gépi állapot az ABI folyamatinicializálási szerződését követi
glibc CRT Tipikus belépési szimbólum a _start; gyakori közreműködő a __libc_start_main

A _start nem szokásos C függvényhívás címzettje: nincs mögötte olyan rendes hívó, amelyhez egyszerű C return művelettel visszatérhetne. A main viszont már a létrehozott C környezetben fut.

A pontos feladatmegosztás libc-, toolchain- és linkelésimód-függő. Statikus programnál nincs szükség ugyanarra a dinamikusinterpreter-útvonalra. A fordítódriver azért fontos, mert rendszerint automatikusan hozzáadja a megfelelő startup objektumokat és könyvtárakat.

3.4.5 Kidolgozott argumentum- és környezetvizsgáló példa

Az alábbi teljes program minimuma ISO C17 hosted környezet; maga a C kód nem igényel Linux-kiterjesztést.

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

int main(int argc, char *argv[])
{
    if (printf("argc=%d\n", argc) < 0) {
        return EXIT_FAILURE;
    }

    for (int i = 0; i < argc; ++i) {
        if (printf("argv[%d]=%s\n", i, argv[i]) < 0) {
            return EXIT_FAILURE;
        }
    }

    const char *mode = getenv("APP_MODE");

    if (printf("APP_MODE=%s\n",
               mode != NULL ? mode : "(unset)") < 0) {
        return EXIT_FAILURE;
    }

    if (fflush(stdout) == EOF) {
        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}

A választott külön laborprofil: Linux/x86-64, System V AMD64 ABI, LP64, glibc 2.39, GCC 14.2.0 vagy Clang 18.1.8. A parancsokban a fordítónevek ezeket a kiadásokat jelöljék. A példát itt nem kíséri fordítási vagy futtatási mérés.

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

clang -std=c17 -Wall -Wextra -Wpedantic -O0 -g \
    startup_demo.c -o startup_demo-clang

A szemléltető shellhívás: APP_MODE=verbose ./startup_demo alpha "two words". Ennek várt argumentumképe:

Elem Várt érték ebben a shellhívásban
argc 3
argv[0] ./startup_demo
argv[1] alpha
argv[2] two words
argv[3] null pointer
getenv("APP_MODE") tartalma verbose

Helyességi indoklás — bizonyításvázlat. A ciklus csak akkor használja az argv[i] elemet, ha 0≤i<argc0 \leq i < argc. Ezért nem adja át az argv[argc] null pointert a %s konverziónak. A környezeti értéket külön ellenőrzi; nincs újabb getenv vagy környezetmódosítás a lekérdezés és felhasználás között. A program nem szabadít fel kölcsönzött tárolót.

A végső fflush azért szerepel, mert egy korábbi sikeres printf után a pufferelt kiírás még meghiúsulhat. A program ezt hibastátusszal jelzi. Éles szolgáltatásban az argumentumok és környezeti értékek ilyen naplózása titokszivárgást okozhat.

3.4.6 Visszatérés és a shell által látott státusz

ISO C17. A main kezdeti hívásából való visszatérés normál programbefejezést jelent, a visszaadott értékkel végzett exit hatásának megfelelően. A részletes cleanup-szabályokat a leállásról szóló következő rész tárgyalja.

A return 0; és a return EXIT_SUCCESS; sikeres befejezést jelez. Az EXIT_FAILURE sikertelenséget jelez; numerikus értékét nem kell minden implementációban 1-nek feltételezni. C17-ben a main záró kapcsos zárójelének elérése is 0 visszaadásának felel meg. Ez nem általánosítható minden int visszatérési típusú függvényre.

Linux/POSIX. Normál befejezéskor a szülő számára hozzáférhető kilépési kód az átadott érték alsó nyolc bitje. Nemnegatív rr esetén:

s=rmod256. s = r \bmod 256.

Így return 256; esetén s=0s=0, amit a shell sikernek értelmezhet. Ezért nem helyes tetszőleges belső hibaszámot változtatás nélkül folyamatstátuszként használni.

Egy közvetlenül futtatott egyszerű parancs után a shell $? értéke mutatja annak státuszát. Jel miatti megszűnés más kategória; a Bash például rendszerint 128+N128+N értékkel jelzi az NN számú jel miatti befejezést. Ez shellkonvenció, nem a main visszatérési szabálya.

3.4.7 Hosted és freestanding környezet

ISO C17. Freestanding környezetben az induláskor meghívott függvény nevét és típusát az implementáció határozza meg. Nem követelmény a main, és nem feltételezhető a teljes hosted standard könyvtár rendelkezésre állása.

A freestanding program nem egyszerűen „statikusan linkelt program”. Egy statikus Linux CLI továbbra is lehet hosted C program. Hasonlóképpen a GCC vagy Clang -ffreestanding kapcsolója önmagában nem készít megfelelő linkerelrendezést, induló gépi állapotot vagy eszközinicializálást.

A döntő különbség a vállalt futtatási szerződés, nem a fájlkiterjesztés vagy az, hogy van-e dinamikus linker.

3.4.8 Gyakori hibák és javításuk

Az argv[1] feltétel nélküli használata helyett előbb argc > 1 szükséges. A getenv eredményének közvetlen dereferálása helyett nullpointer-ellenőrzés kell. Az argumentumok és környezeti értékek felszabadítása helyett a kölcsönzött hozzáférés szabályát kell követni.

A main és az ELF-belépési cím azonosságának feltételezése téves rétegkeverés. Ugyanilyen hiba a shell nyolcbites státuszát az ISO C teljes, int típusú visszatérési értékével azonosítani.

3.4.9 Feladatok

  1. Magyarázó: Mit szabad feltételezni argv[0]-ról, ha argc == 0, illetve ha argc == 1? Mit jelent az üres sztring?
  2. Kódolási: Módosítsd a példát úgy, hogy legfeljebb egy felhasználói argumentumot fogadjon el. Hiányzó vagy üres APP_MODE esetén használja a "standard" értéket.
  3. Diagnosztikai: Egy Linux-program ELF-belépési címe nem a main címe. A program return 256; utasítással fejeződik be, a shell mégis sikert mutat. Magyarázd meg mindkét jelenséget.

3.4.10 Megoldások

3.4.10.1 Argumentumhatárok

Ha argc == 0, akkor argv[0] az argv[argc] lezáró null pointer, nem sztring. Ha argc == 1, akkor argv[0] érvényes sztringre mutat, de lehet üres. Az üres sztring első karaktere '\0'.

Az ellenőrzési feltétel: %s csak az i < argc feltétellel elért argumentumelemhez használható.

3.4.10.2 Bemeneti korlát és alapérték

A következő C17-részlet első ellenőrzése a main elejére kerüljön; a környezetlekérdezés pedig váltsa fel az eredeti mode deklarációt:

if (argc > 2) {
    return EXIT_FAILURE;
}

const char *mode = getenv("APP_MODE");
if (mode == NULL || mode[0] == '\0') {
    mode = "standard";
}

A || rövidzáras kiértékelése miatt null pointer esetén nem történik mode[0] hozzáférés. A sztringliterál statikus tárolási idejű; nem módosítjuk és nem szabadítjuk fel.

Ellenőrzéskor külön eset legyen a hiányzó, üres és nem üres környezeti érték, valamint két felhasználói argumentum elutasítása.

3.4.10.3 A két külön belépési és befejezési réteg

Az ELF-belépési cím rendszerint a CRT _start kódjára mutat; a main később kap vezérlést. Az eltérés ezért önmagában nem hiba.

A normál Linux-kilépési kód pedig:

256mod256=0. 256 \bmod 256 = 0.

A shell így sikert láthat. Ellenőrzéskor külön kell vizsgálni az ELF-belépési pontot, a main elérését és a szülő által megfigyelt státuszt.

3.4.11 Összefoglaló

A main a hosted C program belépési szerződése, nem feltétlenül a folyamat első végrehajtott kódja. Az argumentumok és környezeti értékek kölcsönzött adatok, eltérő módosíthatósági és érvényességi szabályokkal. A visszatérési érték pedig több rétegen halad át: az ISO C befejezési szerződésén, az operációs rendszer státuszábrázolásán és végül a shell értelmezésén.