Jaetut kirjastot

Linux.fista
Versio hetkellä 9. huhtikuuta 2022 kello 04.52 – tehnyt Lahtis (keskustelu | muokkaukset) (typo)
(ero) ← Vanhempi versio | Nykyinen versio (ero) | Uudempi versio → (ero)
Siirry navigaatioon Siirry hakuun

Aliohjelmakirjastolla, tai lyhyemmin kirjastolla, tarkoitetaan kokoelmaa yleishyödyllisiä aliohjelmia tai funktioita, jotka on koottu yhteen. Perusajatuksena on, ettei jokaista kehitettävää uutta ohjelmaa varten tarvitsisi keksiä pyörää uudestaan, vaan ohjelman tekijä voi hyödyntää jo olemassa olevia aliohjelmia. Tällöin ohjelman tekijä voi nojautua olemassa olevaan alemman tason toiminnallisuuteen, kuten syöttö- ja tulostusrutiineihin, ja keskittyä kirjoittamaan omalle ohjelmalle olennaista koodia.

Jaettu kirjasto on aliohjelmakirjaston muunnelma, jossa kirjasto on talletettu erilliseen tiedostoon, jolloin sama kirjasto on jaettavissa monen ohjelman kesken.

Ei-jaetut kirjastot[muokkaa]

Aliohjelmakirjasto voi olla kahdessa eri muodossa, riippuen käyttötavasta: joko staattisena ei-jaettuna kirjastona tai jaettuna kirjastona. Ei-jaettu kirjasto koostuu joukosta, tyypillisesti C-kielisestä lähdekoodista käännetyistä objektikooditiedostoista, jotka on koottu yhteen arkistotiedostoon. Tällaisen tiedoston pääte on .a.

Kun ohjelma rakennetaan käyttäen ei-jaettua kirjastoa, ohjelman linkkausvaiheessa kerrotaan mistä arkistotiedostosta sen pitä etsiä kirjastossa olevia aliohjelmia. Linkkeri kopioi arkistotiedostosta tarvittavat osat, ja liittää ne ohjelman lopulliseen suoritustiedostoon.

Ei-jaetuissa kirjastoissa on haittapuolensa, jotka korostuvat sitä mukaa kuin kirjastojen koot ja lukumäärä kasvavat. Koska linkkeri liittää ohjelman rakennusvaiheessa ei-jaetusta kirjastosta tarvittavat aliohjelmat, on jokaisessa kirjastoa käyttävässä ohjelmassa aliohjelmista omat kopionsa. Tämä johtaa siihen, että ohjelmatiedostojen koot kasvavat, mutta ennen kaikkea se vaikuttaa haitallisesti tietokoneen keskusmuistin kulutukseen moniajojärjestelmässä, jossa on useampia ohjelmia ajossa samanaikaisesti. Tilannetta helpottaa hieman se, että älykäs linkkeri kopioi ainoastaan tarvittavat aliohjelmat, ei koko kirjastoa, mutta käytännössä säästö ei ole kovin suuri.

Jaetut kirjastot[muokkaa]

Edellä mainittua kirjastojen kopioitumisongelmaa ratkaisemaan on kehitetty niin sanotut jaetut kirjastot. Jaettuja kirjastoja käytettäessä ei kirjaston aliohjelmia liitetä osaksi suoritettavaa ohjelmatiedostoa, vaan ohjelmaan liitetään vain viittaus jaetun kirjaston sisältävään tiedostoon. Kun ohjelma suoritetaan, kirjasto ladataan muistiin.

Linuxin muistinhallinassa on ominaisuus, joka mahdollistaa keskusmuistisivujen jakamisen prosessien kesken. Prosesseilla on erilliset muistiavaruudet, jotka on suojattu toisiltaan, mutta on mahdollista järjestää asiat niin, että yksi fyysinen muistisivu näkyy useamman prosessin muistiavaruudessa. Tällä mekanismilla saadaan ratkaistuksi toinenkin ei-jaettujen kirjastojen ongelma, eli koodin kopioituminen keskusmuistissa.

Myös kirjastojen määrittelemä data on jaettu prosessien kesken. Kirjaston määrittelemät muuttujat eivät kuitenkaan ole prosesseille yhteisiä, eli kun yhdessä prosessissa muutetaan muuttujan arvoa, muutos ei saa näkyä toisessa prosessissa. Tämä on ratkaistu niin sanotulla Copy-on-write -mekanismilla, jossa prosessille joka muuttaa muuttujan arvoa, luodaan oma kopionsa muistisivusta, jossa muuttuja sijaitsee.

Huomaa, että mekanismi ei suoranaisesti liity Linuxin jaettuihin kirjastoihin, vaan on Linuxin ytimen ominaisuus jota hyödynnetään jaettujen kirjastojen käsittelyssä.

Jaettujen kirjastojen kehitys Linuxissa[muokkaa]

Alun perin Linuxissa ei ollut tukea jaetuille kirjastoille. Linuxin alkuvuosina käytettiin Unixin perintönä niin sanottua "a.out"-tiedostomuotoa suoritettaville ohjelmille. a.out-tiedostomuodolle suunniteltiin Linuxia varten mekanismi jaettujen kirjastojen käyttämiseksi, joka olikin käytössä jonkin aikaa. Tämän mekanismin suurin puute oli se, että kirjastot oli rakennettu ladattaviksi kiinteisiin osoitteisiin, joita ei pystytty enää muuttamaan latausvaiheessa. Tämän tyyppisiä jaettuja kirjastoja sanotaan staattisiksi jaetuiksi kirjastoiksi.

Latausosoitteiden joustamattomuuden takia samaan prosessiin ei voitu ladata päällekkäisiä muistiosoitteita käyttäviä kirjastoja. Päällekkäisyyksien välttämiseksi Linuxin jaetuista kirjastoista pidettiin keskitettyä rekisteriä, jossa niille allokoitiin jokaiselle omat käytettävissä olevat muistiavaruudet.

Nykyaikaiset jaetut kirjastot[muokkaa]

Linux siirtyi versiosta 1.2 käyttämään uudempaa ELF (Executable and Linkable Format) -tiedostomuotoa suoritettaville ohjelmille ja kirjastoille. Tässä formaatissa on otettu paremmin huomioon jaettujen kirjastojen asettamat vaatimukset. ELF-formaattissa on tuki ns. relokoinnille, jonka ansiosta kirjaston voi ladata alkaen mistä tahansa vapaana olevasta muistiosoitteesta. Tämä vaatii tosin jonkin verran ylimääräistä työtä sekä kirjaston latausvaiheessa että ohjelman ajon aikana.

Jaettujen kirjastojen versionumerot[muokkaa]

Käytäntönä on, että jaetun kirjaston versionumero koostuu kahdesta osasta, ns. major- ja minor-versionumerot. Major-numeroa muutetaan, kun kirjasto on olennaisesti erilainen kuin edellinen versio. Olennaisella muutoksella tarkoitetaan, että esimerkiksi funktion kutsutapa muuttuu, tai että kirjaston aliohjelmat toimivat eri tavalla kuin ennen. Minor-numero muuttuu tyypillisesti kun kirjastossa on korjattu virhe. (Toki kirjasto toimii silloinkin eri tavalla kuin sen edellinen versio, mutta tässä tapauksessa se on korjattu toimivaksi siten kuin se on dokumentoitu toimivan.)

Kun jaettu kirjasto asennetaan järjestelmään, kirjaston sisältävän tiedoston nimeksi annetaan nimi joka on muotoa libfoo.so.M.N. M on tässä edellä mainittu major-versionumero ja N vastaavasti minor-versionumero. Tiedoston pääte .so tulee sanoista shared object, eli jaettu objektitiedosto. Samaan hakemistoon luodaan symbolinen linkki, jonka nimi on libfoo.so.M, joka osoittaa kirjaston sisältämään tiedostoon. Esimerkki:

-rwxr-xr-x 1 root root 14616 Jul 24 19:34 libdl-2.13.so
lrwxrwxrwx 1 root root    13 Jul 24 19:36 libdl.so.2 -> libdl-2.13.so

Versioidut symbolit[muokkaa]

Edellä mainittu ei kuitenkaan ole koko totuus jaettujen kirjastojen versioinnista. Linuxin jaetut kirjastot voivat sisältää myös versioituja symboleja, joiden avulla sama kirjasto voi sisältää monta eri versiota samannimisestä aliohjelmasta. Vanhempi ohjelma, joka on rakennettu kirjaston vanhempaa versiota varten, löytää kirjastosta vanhemman version aliohjelmasta, kun taas uudempi ohjelma voi automaattisesti hyödyntää uutta versiota aliohjelmasta.

Jaetut kirjastot voivat olla käyttämättä versioituja symboleja, kirjaston tekijän valinnan mukaan. Yksi esimerkki kirjastosta joka käyttää niitä on standardikirjasto libc. Periaate on se, että kaikki kirjaston aliohjelmat, jotka säilyvät muuttumattomina versiosta toiseen, säilyttävät vanhan versionumeronsa. Kun kirjaston aliohjelmaa muutetaan siten, ettei se ole enää yhteensopiva vanhan version kanssa, kirjastoon lisätään uudella versionumerolla varustettu aliohjelma vanhan rinnalle.

Kun ohjelma linkataan libc:llä, kirjastosta otetaan lähes poikkeuksetta käyttöön uusin versio aliohjelmista. Kun suoritetaan vanhaa ohjelmaa, joka on rakennusvaiheessa linkattu vanhempaa libc:tä käyttäen, se on myös yhteensopiva uuden jaetun kirjaston kanssa, sillä uudesta kirjastosta löytyy myös vanhemmat versiot aliohjelmista. Uudella libc:llä linkattu ohjelmatiedosto saattaa myös toimia siirrettynä ympäristöön, jossa on vanhempi versio jaetusta kirjastosta. Tämä edellyttää kuitenkin, että ohjelma ei käytä aliohjelmaa, josta on uudempi versio kirjastossa jota vasten se on rakennettu. Nyrkkisääntönä on siis, että vanhempi ohjelma on yhteensopiva uudemman kirjaston kanssa, mutta uudempi ohjelma ei välttämättä toimi vanhemman kirjaston kanssa.

Tarkastellaan edellistä esimerkin valossa. libc-kirjasto määrittelee realpath-funktion:

char *realpath(const char *path, char *resolved_path);

GNU Libc -kirjaston versiossa 2.3 muutettiin funktion toimintaa siten, että resolved_path-parametrin arvoksi hyväksytään myös NULL, jolloin funktio itse varaa tarvittavan muistin ja palauttaa sen funktion paluuarvona.

readelf-ohjelmalla voi luetella kirjaston aliohjelmien nimet:

readelf -s /lib/libc.so.6

Tulosteesta löytyy kaksi realpath-funktiota:

1167: 000000000010e3c0    33 FUNC    GLOBAL DEFAULT   11 realpath@GLIBC_2.2.5
1168: 000000000003d210  1213 FUNC    GLOBAL DEFAULT   11 realpath@@GLIBC_2.3

Ohjelma, joka on tehty vanhemmalla libc:n versiolla kuin 2.3, sisältää viittauksen 2.2.5-versioon funktiosta, uudemmat ohjelmat käyttävät versiota 2.3.

Jaettua kirjastoa käyttävän ohjelman lataus[muokkaa]

Kun uusi ohjelma käynnistetään, käyttöjärjestelmän ydin lataa ohjelman keskusmuistiin. Tarkemmin sanottuna, latausta ei tehdä vielä tässä vaiheessa, vaan tehdään kuvaus (mmap) ohjelmatiedostosta prosessin muistiavaruuteen. Lataus massamuistilta keskumuistiin tapahtuu sitä mukaa kuin ohjelman suoritus etenee; jos ohjelman kontrolli siirtyy muistisivulle, joka on "tyhjä", eli jonka sisältöä ei ole ladattu massamuistilta, tapahtuu ns. page fault. Tällöin ytimen poikkeuskäsittelijä lataa sivun sisällön ohjelmatiedostosta ennen kuin suoritus voi jatkua.

Jos ohjelma käyttää ainakin yhtä jaettua kirjastoa, ohjelmaa ei vielä käynnistetä tässä vaiheessa. Sen sijaan käyttöjärjestelmän ydin lukee ELF-ohjelmatiedostosta dynaamisen linkkerin nimen. Dynaaminen linkkeri on jaetun kirjaston muodossa oleva ohjelma, jonka tehtävänä on ladata jaetut kirjastot muistiin. Dynaamisen linkkerin nimi on kerrottu ELF-tiedoston .interp-sektiossa (sanasta "interpreter", eli tulkki), ja se on sinne kirjoitettu ohjelman rakennusvaiheessa. Ydin lataa myös dynaamisen linkkerin prosessin muistiavaruuteen ja siirtää ohjelman suorituksen siihen. Dynaamisen linkkerin nimi riippuu käytettävästä prosessoriarkkitehtuurista, 64-bittisessä x86_64-koneessa se sijaitsee tiedostossa /lib64/ld-linux-x86-64.so.2.

Syy miksi näin tehdään on se, että ohjelma ei ole vielä ajokelpoinen ilman tarvitsemiaan jaettuja kirjastoja. Yksinkertaisimmankin C-kielellä kirjoitetun ohjelman suoritys tyssäisi heti ensimmäiseen printf-funktion kutsuun. Tämän ongelman olisi tietysti voinut ratkaista siten, että dynaamisen linkkerin toiminnallisuus olisi sisällytetty ytimeen ja ydin olisi ladannut sekä ohjelman että ohjelman tarvitsemat kirjastot muistiin ennen suorituksen alkua. Tätä ei kuitenkaan haluta tehdä, sillä ydin halutaan pitää mahdollisimman pienenä ja toiminnallisuus, jota ytimessä ei tarvita, siirretään pois sieltä.

Miten dynaaminen linkkeri löytää jaetut kirjastot?[muokkaa]

Dynaaminen linkkeri etsii dynaamisia jaettuja kirjastoja seuraavassa järjestyksessä:

  1. Jos ohjelman ELF-tiedosto sisältää DT_RPATH-sektion, se sisältää listan hakemistoja, joista dynaaminen linkkeri etsii jaettuja kirjastoja (tätä mekanismia ei enää suositella käytettäväksi).
  2. Jos ympäristömuuttuja LD_LIBRARY_PATH on määritelty, ja se sisältää kasoispisteillä erotettuja hakemistojen nimiä, kirjastoja etsitään näistä hakemistoista.
  3. Jos ohjelman ELF-tiedosto sisältää DT_RUNPATH-sektion, se sisältää listan hakemistoja, joista dynaaminen linkkeri etsii jaettuja kirjastoja.
  4. Kirjastoa etsitään /etc/ld.so.cache-tiedostosta. Tämä on normaali tapa, jolla dynaaminen linkkeri paikallistaa jaetut kirjastot.
  5. Viimeisenä oljenkortena dynaaminen linkkeri etsii kirjastoja hakemistoista /lib ja /usr/lib (ellei ohjelmaa ole linkattu -z nodeflib-optiolla).

LD_LIBRARY_PATH-muuttujaa ei turvallisuussyistä huomioida, jos suoritettava ohjelma on suid-binääri. Huomaa, että kirjastoa oletusarvoisesti ei etsitä

  • hakemistosta, jossa ohjelma on käynnistetty
  • hakemistosta, jossa ohjelmatiedosto sijaitsee

/etc/ld.so.cache-tiedosto on binääritiedosto, joka sisältää kuvauksia kirjaston nimestä kirjaston sisältävän tiedoston absoluuttiseen tiedostopolkuun. Kirjaston virallinen nimi on kerrottu soname-kentässä kirjaston sisällä olevassa .dynamic-sektiossa. Tämän nimen ei tarvitse olla sama kuin kirjaston sisältämän tiedoston nimi. Muissa tapauksissa kuin kohdassa 4 yllä olevassa hakujärjestyksessä käytetään kuitenkin tiedoston nimeä samannimisen kirjaston paikallistamiseen.

Aina kun järjestelmään on asennettu uusi jaettu kirjasto (joko kokonaan uusi, tai uusi versio olemassaolevasta kirjastosta), pitää suorittaa ldconfig-ohjelma. Tämä ohjelma päivittää yllä mainitun /etc/ld.so.cache-tiedoston ajan tasalle, ja huolehtii lisäksi siitä, että aiemmin mainitut symboliset linkit osoittavat viimeisimpään versioon kirjastosta. ldconfig-ohjelma lukee luettelon hakemistoista, joista kirjastoja etsitään, tiedostosta /etc/ld.so.conf.

Huom! Jos kirjastoja asennetaan jakelun ohjelmapaketeista, kaikesta tästä on tietenkin huolehdittu ja tapahtuu automaattisesti, eikä ldconfig-ohjelmaa ole tarvetta suorittaa "käsin".

Jaettujen kirjastojen lataaminen ohjelmakoodista[muokkaa]

Dynaamisen jaetun kirjaston pystyy myös lataamaan ohjelmasta käsin, ajon aikana. Tämä tapahtuu dlopen-funktiolla, joka palauttaa onnistuneen kutsun tuloksena kahvan jaettuun kirjastoon. Kahvaa annetaan dlsym-funktiolle, joka palauttaa osoitteen, johon haluttu symboli on ladattu muistiin. Lisätietoja dlopen:n manuaalisivulla.

Lisätietoja[muokkaa]

Ohjelman käyttämistä dynaamisista jaetuista kirjastoista saa tietoa komennolla ldd.

Esimerkki:

ldd /bin/ls
linux-vdso.so.1 =>  (0x00007fff5ddff000)
librt.so.1 => /lib64/librt.so.1 (0x00007f1e64e7c000)
libacl.so.1 => /lib64/libacl.so.1 (0x00007f1e64c73000)
libc.so.6 => /lib64/libc.so.6 (0x00007f1e648ed000)
libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f1e646d0000)
/lib64/ld-linux-x86-64.so.2 (0x00007f1e65085000)
libattr.so.1 => /lib64/libattr.so.1 (0x00007f1e644cb000)

ldd-komento myös ilmoittaa, jos jokin ohjelman tarvitsemista kirjastoista ei löydy.

Muita kiinnostavia ohjelmia, joilla voi tutkia kirjastoja, objektitiedostoja ja ohjelmia ovat readelf ja objdump.