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:

kep

Az is lehet, hogy több object állományt egy archivumba csomagolunk, ezáltal a logikailag összetartozó objecteket közelebb hozzhatjuk egymáshoz:

kep

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):

kep

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


Utolsó frissítés: 2020-10-21 09:51:52