Linker
Mint látható, eddig a pontig minden C forrásnak "egyenes'" az útja: a header fájlokat nem számolva egy C forrásból egy object keletkezik. Eddig a pontig kivétel nélkül bármelyik helyes C forrás eljuttatható.
Futtatható program azonban csak olyan forrásból készíthető, amelyben van main()
függvény.
A linker feladata, hogy az object fájl(ok)ból -- amely csak a C forrásfájlban szereplő deklarációkat és definíciókat tartalmazza -- előállítsa a futtatható programot.
Első megközelítésben a linker olyan object fájlból tud futtatható programot csinálni, amiben van main()
függvény.
De miért kell ehhez linker? És mire jó a többi object (amiben nincs main
?)
Ahhoz, hogy ezt megértsük, meg kell ismerkednünk a modulok fogalmával.
Modulok¶
A korszerű programozási nyelvek lehetővé teszik programok mellett programrendszerek nyelvi szinten történő kezelését is. A programrendszer nem más, mint modulok hierarchiája. A modul olyan programozási egység, amely programelemek (konstans, típus, változó, függvény) együtteséből áll.
A modul két részre osztható:
- Interfészre, ami a modul más modulok (programok) által is látható és felhasználható közösségi része, és
- Implementációra, ami a modul privát, kívülről nem látható része.
A modulnak e két részre osztása biztosítja a felhasznált modulok stabilitását és a privát elemek védelmét.
A modulok természetes egységei a fordításnak, tehát minden modul külön fordítható. Fontos hangsúlyozni, hogy a külön fordítás nem független fordítást jelent. Következésképpen a modulok két formában léteznek:
- Forrásnyelvi forma.
- Lefordított forma.
C-ben alapvető egységként adódik az egy C forrás/object, mint legkisebb lehetséges modul.
.o
kiterjesztésű object fájl ekkor a modul lefordított formája lesz,.c
kiterjesztésű forrás a modul privát részének nyelvi formája,- a modul közösségi részét pedig a már említett
.h
kiterjesztésű header fájlok valósítják meg.
A header és forrás fájlok tehát általában párosan léteznek:
A .h
kiterjesztésű header fájl tartalmazza az összes olyan változó és függvény deklarációját, illetve konstans és típus definícióját, ami a modul interfészét, közösségi részét képezi.
Ha valaki használni akarja a modult, csak include-olnia kell ezt a header fájlt, és máris használhatja az ebben deklarált dolgokat.
A.c
kiterjesztésű forrás pedig tartalmazza a közösségi részben deklarált függvények definícióit, illetve a modul teljes privát részét, tehát az implementációt.
Ahhoz, hogy olyan programot (vagy másik modult) írjunk, ami a mi modulunkat használja, elegendő a modulunk header fájlját ismerni, azaz a programban include-olni.
Az adott program forrásába a megfelelő #include
helyére a preprocesszor bemásolja a mi header fájlunk tartalmát, így a program ismerni fogja az abban szereplő programelemeket, és object szintig lefordítható lesz.
Egy-egy programot vagy modult le tudunk fordítani object szintig, függetlenül attól, hogy milyen más modulokat használ.
A main()
nélküli objectekkel többet már nem nagyon tehetünk, maximum egy archiver segítségével össze tudjuk őket gyűjteni egy úgynevezett függvénykönyvtárban, ami valójában egy olyan fájl, ami objecteket tartalmaz.
A main()
függvénnyel rendelkező objectekből viszont programok készíthetők.
De egy-egy ilyen object önmagában eléggé hiányos lehet, hiszen a program a modul felhasznált függvényeinek csak a deklarációját ismeri, a függvény megvalósítása a modul object fájljában van. A linker feladata, hogy az ilyen objecteken átívelő hivatkozásokat feloldja, és egyetlen futtatható programot állítson elő sok .o
fájlból.
A sok .o
közül pontosan egynek tartalmaznia kell a main()
függvényt, hiszen az operációs rendszer majd ezt "hívja meg" a program indításakor.
Komplexebb programok fordítását a következő ábra szemlélteti:
Az is lehet, hogy több object állományt egy archivumba csomagolunk, ezáltal a logikailag összetartozó objecteket közelebb hozzhatjuk egymáshoz:
Amikor fordítunk, nem kell mindig minden forrást újrafordítani. A korábban már elkészült (és azóta nem változtatott források) object állományai felhasználhatóak a fordításhoz, elég, ha ezeket a végső linkelésnél szerkesztjük hozzá a futtatható programhoz. Ahhoz, hogy az újonnan fordítandó elemet fordítani tudjuk, elegendőek a header állományok, a linkelésnél szükséges a többi object (vagy az objectek archivált változata):
Látható tehát, hogy egy-egy modul több programhoz is felhasználható, gondoljunk csak például a math.h
-ra.
Az ebben deklarált függvények (pl.: sin()
) megvalósítása egy libm.a
függvénykönyvtárban van tárolva.
Az #include <math.h>
"csak" a függvények deklarációit tartalmazza, amellyel már fordítható a forráskód, de önmagában még nem készíthető belőle futtatható program.
A fordításkor megadott -lm
kapcsoló mondja meg a linkernek, hogy a m.a
fájlban is keresgéljen függvények után.
Meg kell jegyeznünk, hogy a modul kifejezés általában nem csupán egyetlen .o
fájlt, hanem egy vagy akár több függvénykönyvtárat is takarhat.
A függvénykönyvtárakból csak azok az eljárások kerülnek bele a programba, amiket valóban meg is hívunk.
Megvalósítás elrejtése¶
Programrendszer készítésekor az egyes modulokat célszerű úgy tervezni, hogy a header-ben ne legyenek láthatók azok a programelemek, amelyek csak a megvalósításhoz kellenek. Ennek két oka is van:
- Egyrészt ezzel biztosítani tudjuk, hogy a modult felhasználó csak azokhoz a programelemekhez tud hozzáférni, amit a műveletek szabályos használata megenged, ezzel védettséget biztosítunk a lokális elemek számára.
- Másrészt a megvalósításhoz használt elemek elrejtése az implementációs részbe azt eredményezi, hogy a megvalósítás módosítása, megváltoztatása esetén nem kell a programrendszer más modulját változtatni, pl. újrafordítani.
Nyilvánvalónak tűnik, hogy ha egy függvény deklarációja nem szerepel a modul header fájljában, akkor az a függvény más modulok által nem használható (közvetlenül nem hívható).
Ez viszont így nem igaz.
A linker nem tudja, honnan származik a deklaráció.
Ha valaki saját magának deklarálja a függvényt, a linker akkor is megtalálja azt a modul object fájljában.
Ha viszont egy függvényt a static
kulcsszóval deklarálunk, akkor azt a linker nem fogja látni, hiába kerül bele az object fájlba.
Vagyis, a headerben .h
nem szereplő, kizárólag a megvalósításhoz tartozó függvényeket a modul megvalósításában.c
ajánlott a static
kulcsszóval deklarálni, hogy valóban el legyenek rejtve a "külvilág" elől. (Emlékezzünk vissza az integrálos példára, ott láthattuk ezt a fajta deklarálást.)
A többféle programelem közül az adattípusok elrejtése okozhat még problémát.
Azoknak az eljárásoknak a deklarációjában ugyanis, amelyek a probléma megoldását adják -- tehát a headerben kell lenniük -- meg kell adni a paraméterek típusát, jóllehet a típus definícióját csak a modul .c
forrásában kellene megadni, mert ezek megadása már a megvalósításhoz tartozik.Adattípus megadásának elhalasztása, vagyis elrejtése a void*
pointer felhasználásával megoldható.
Az elrejtés technikája lehetővé teszi, hogy modul tervezésekor meg tudjuk adni az interfész végleges alakját, anélkül, hogy a megvalósítás bármely részletét is ismernénk. Ez különösen hasznos absztrakt adattípusok tervezése és megvalósítása esetén.
Optimalizálás¶
A linkelés fázisban is van mód optimalizálásra.
A külön fordított modulok hátránya, hogy bizonyos észszerű optimalizálásokat (mint például a function inlining nem lehet fordítási időben elvégezni, ha azok több modult érintenek.
A-flto
kapcsoló hatására a linker utólag lefuttatja ezeket az optimalizáló algoritmusokat.
A kapcsolót már fordításkor is meg kell adni, különben a fordító nem teszi bele a link-time optimalizáláshoz szükséges információt az object fájlokba.
Statikus és dinamikus linkelés¶
Statikus linkelés esetén (ami egyes rendszereken a default, de a -static
kapcsolóval kikényszeríthető) a linker minden szükséges függvényt belepakol a programba, hogy az önállóan futtatható legyen.
Egyes operációs rendszereken azonban lehetőség van dinamikus linkelésre is. Ekkor a könyvtári függvényeket az operációs rendszer rakja a programba a program betöltésekor.
A különböző objectekben lévő saját függvényeket ilyenkor is a linker teszi bele a programba, de a könyvtári függvényeket nem, ezáltal kisebb lesz a program. A dinamikus linkelés előnye, hogy ha egyszerre több program fut, ami ugyanazt a függvényt használja, a függvény kódja akkor is csak egyetlen példányban lesz meg a rendszer memóriájában, és a programok közösen használják ezt a kódot, osztoznak rajta