3.2 Statikus és megosztott könyvtárak

A könyvtár nem pusztán újrahasznosítható forráskód: fordítási, linkelési és telepítési szerződés is. Statikus könyvtár használatakor a szükséges gépi kód rendszerint bekerül a fogyasztó binárisába. Megosztott könyvtárnál a bináris függőséget rögzít, amelyet futáskor a betöltési környezetnek kell teljesítenie. DevOps-szempontból ez meghatározza, mit kell újraépíteni egy biztonsági frissítés után, és miért indulhat el egy program a buildkonténerben, miközben a futtatóképben már nem.

A Szimbólumok, linkage és relocation részben megismert feloldatlan szimbólumhivatkozásokra építünk. Most ugyanahhoz az interfészhez készítünk libcount.a és libcount.so könyvtárat, majd különválasztjuk a linkeléskori és futásidejű keresést.

3.2.1 Két könyvtárforma, két függőségi modell

GNU/ELF-eszközlánc: a statikus könyvtár általában ar formátumú archívum, amely relokálható objektumfájlokat és szimbólumindexet tartalmaz. A linker alapesetben nem az egész archívumot másolja be: azokat az objektumtagokat emeli ki, amelyek szükségesek az addig felismert feloldatlan hivatkozásokhoz. A kiemelés egysége objektumfájl, nem egyetlen függvény; a későbbi optimalizálás és szekcióeldobás ettől külön mechanizmus.

A megosztott objektum (shared object) önálló ELF-bináris. Kódja külön fájlban marad, és a fogyasztó rendszerint futásidejű függőséget rögzít rá.

Állomány Szerep Kell a fogyasztó futásához?
count.h Fordításkori API-deklaráció Nem
libcount.a Objektumtagok linkeléskori forrása Maga az archívum nem
libcount.so A linker által keresett fejlesztői név Nem feltétlenül ez a név
libcount.so.1 A példában rögzített futásidejű könyvtárnév Igen, feloldható állományként

Fontos különbség: egyetlen könyvtár statikus felhasználása nem jelenti a teljes program statikus linkelését. A program más függőségei, például a libc, továbbra is lehetnek megosztott könyvtárak.

3.2.2 A libcount felépítése

A példák minimuma ISO C17. A választott, reprodukáláshoz rögzítendő mintaprofil Linux x86-64, System V ABI, LP64 adatmodell, GCC 14.2.0, GNU binutils 2.43 és glibc 2.40. Második fordítóként Clang 19.1.7 használható, ugyanazzal a célplatformmal és külön rögzített linkerrel. Ezek választott verziók, nem itt ellenőrzött futtatási környezetek; az alábbi eredmények elvárások, nem mért kimenetek.

A három forrásfájl ugyanabba a friss munkakönyvtárba kerüljön.

count.h:

#ifndef COUNT_H
#define COUNT_H

unsigned count_add(unsigned total, unsigned delta);

#endif

count.c:

#include "count.h"

unsigned count_add(unsigned total, unsigned delta)
{
    return total + delta;
}

main.c:

#include <stdio.h>
#include "count.h"

int main(void)
{
    if (printf("%u\n", count_add(2u, 3u)) < 0) {
        return 1;
    }
    return 0;
}

ISO C17: a függvény értékeket fogad és ad vissza; nincs erőforrás-tulajdonlás vagy kölcsönzött hozzáférés. Az unsigned összeadás az adott típus értéktartományának megfelelő moduláris összeadás. A függvény nem jelzi a matematikai összeg tartománytúllépését; ez az API szerződésének része, nem linkelési kérdés.

Először készítsünk objektumfájlokat és archívumot:

mkdir -p build/lib build/bin

gcc -std=c17 -Wall -Wextra -Wpedantic -O2 -fno-lto \
    -fPIC -c count.c -o build/count.pic.o

gcc -std=c17 -Wall -Wextra -Wpedantic -O2 -fno-lto \
    -fPIE -c main.c -o build/main.o

ar rcs build/lib/libcount.a build/count.pic.o

gcc -pie build/main.o build/lib/libcount.a \
    -o build/bin/count-static

Az ar rcs parancsban az r tagot illeszt be vagy cserél, a c jelzi az archívum létrehozásának szándékát, az s pedig szimbólumindexet készít.

A statikus archívumba itt szándékosan PIC objektum kerül, hogy ugyanazt az objektumot használhassuk a megosztott változathoz is. A PIC kód nem teszi dinamikussá a linkelést.

A megosztott változat:

gcc -shared build/count.pic.o \
    -Wl,-soname,libcount.so.1 \
    -o build/lib/libcount.so.1.0

ln -sfn libcount.so.1.0 build/lib/libcount.so.1
ln -sfn libcount.so.1 build/lib/libcount.so

gcc -pie build/main.o -Lbuild/lib -lcount \
    -Wl,--enable-new-dtags,-rpath,'$ORIGIN/../lib' \
    -o build/bin/count-shared

A -Lbuild/lib linkeléskori keresési könyvtárat ad meg. A -lcount a megadott GNU/ELF-profilban normál, dinamikus linkelési módban a megosztott változatot részesíti előnyben, ha az adott keresési helyen az archívum is jelen van. A statikus példában ezért írtuk ki közvetlenül a .a fájl nevét.

Sikeres betöltés és kimeneti írás esetén mindkét program várt kimenete 5, majd újsor.

3.2.3 Statikus linksorrend: mikor válik szükségessé egy archívumtag?

A hagyományos GNU linkerfeldolgozásban az archívum helye jelentéssel bír. Az alábbi sorrend szándékosan hibás:

# Szándékosan hibás linksorrend.
gcc -pie build/lib/libcount.a build/main.o \
    -o build/bin/count-wrong

Indoklás, lépésenként: tekintsük csak a count_add hivatkozását.

  1. Az archívum feldolgozásakor még nincs rá feloldatlan hivatkozás.
  2. Emiatt a linker nem emeli ki a count.pic.o tagot.
  3. A később feldolgozott main.o létrehozza a hivatkozást.
  4. A linker normál esetben nem tér vissza automatikusan a korábbi archívumhoz.

A várt következmény feloldatlan count_add szimbólum miatti linkelési hiba. A javítás: előbb a fogyasztó objektum, utána a függőség.

Ugyanez vonatkozik archívumok közötti függőségre. Ha libfront.a igényli libback.a függvényeit, tipikusan -lfront -lback a megfelelő sorrend. Ciklikus archívumfüggőségnél a GNU linker --start-group és --end-group kapcsolói ismételt keresést kérhetnek, de a ciklus gyakran modulhatár-problémát jelez.

Megosztott könyvtáraknál se feltételezzünk korlátlan sorrendfüggetlenséget: például a --as-needed beállítás befolyásolja, mely könyvtárakhoz marad meg futásidejű függőségi bejegyzés.

3.2.4 PIC, shared object és PIE

GCC/ELF-eszközlánc: a három kapcsolócsoport különböző feladatot végez.

Kapcsoló Fázis Rendeltetés
-fPIC Fordítás Megosztott objektumhoz alkalmas helyfüggetlen kód
-shared Linkelés Megosztott objektum előállítása
-fPIE, majd -pie Fordítás, majd linkelés Helyfüggetlen végrehajtható program előállítása

A position-independent code nem azt jelenti, hogy semmilyen relokáció nem szükséges. Azt jelenti, hogy a generált kód a célplatform megfelelő címzési mechanizmusaival támogatja az eltérő betöltési címet.

A PIE (position-independent executable) továbbra is program, nem általánosan használható könyvtár. A példában a count-static is PIE: a saját count kódját statikusan tartalmazza, miközben helyfüggetlen végrehajtható állomány.

ELF-ben a shared object és a PIE is lehet ET_DYN típusú. Ez az egy fejlécmező tehát önmagában nem elegendő a szerep eldöntéséhez. A betöltési mechanizmusokat az ELF-betöltés, PLT/GOT és dinamikus szimbólumok rész bontja ki.

3.2.4.1 SONAME és DT_NEEDED

A SONAME a megosztott könyvtárba írt logikai függőségi név. A példában:

A libcount.so fejlesztői szimbolikus link segíti a linkelést, de a futásidejű keresés nem erre a névre támaszkodik. A SONAME nem automatikus kompatibilitásvizsgálat: az azonos nevet viselő, hibásan inkompatibilis könyvtár is megtalálható és betölthető.

3.2.5 Futásidejű keresés és ellenőrzés

Linux/glibc: ha a függőség neve nem tartalmaz perjelet, normál, nem biztonsági végrehajtási módban a keresés fő sorrendje leegyszerűsítve:

  1. Az alkalmazható DT_RPATH bejegyzések, a megfelelő DT_RUNPATH hiányában.
  2. Az LD_LIBRARY_PATH.
  3. A függőséget igénylő objektum DT_RUNPATH bejegyzése.
  4. A loader gyorsítótára, rendszerint /etc/ld.so.cache.
  5. Az alapértelmezett, architektúrafüggő könyvtárak.

A részletes algoritmust befolyásolja a függőségi lánc, a glibc verziója, a hardverképesség-alapú könyvtárválasztás és egyes linkkapcsolók. Biztonsági végrehajtási módban, például bizonyos privilegizált programoknál, környezeti változók figyelmen kívül maradhatnak. Más libc betöltőjére ezt a sorrendet nem szabad automatikusan átvinni.

A régi RPATH és az újabb RUNPATH fontos eltérése, hogy a RUNPATH csak az adott objektum közvetlen függőségeinek keresésére vonatkozik. Ha a libcount egy további privát könyvtárat igényel, annak megtalálását nem garantálja a főprogram RUNPATH értéke. Az RPATH a függőségi láncon tovább is érvényesülhet, és az LD_LIBRARY_PATH-hoz viszonyított elsőbbsége is eltér.

A példában a --enable-new-dtags kifejezetten az új bejegyzést kéri. A $ORIGIN a bejegyzést tartalmazó bináris könyvtárát jelöli, ezért a build/bin alatti program a szomszédos build/lib könyvtárban kereshet. Az egyszeres idézőjel megakadályozza, hogy a shell idő előtt helyettesítse a dollárjeles kifejezést.

Ez áthelyezhető csomagelrendezést támogat, de csak megfelelő jogosultságokkal biztonságos: nem megbízható felhasználó által írható keresési könyvtár kódbetöltési kockázat.

3.2.5.1 readelf az ldd előtt

A külön laborban, a programok elindítása nélkül vizsgálhatók a metaadatok:

ar t build/lib/libcount.a
readelf -hW build/bin/count-shared
readelf -dW build/bin/count-shared
readelf -dW build/lib/libcount.so.1.0
readelf -dW build/bin/count-static

Az ellenőrzési feltételek:

Az ldd az aktuális környezetben feloldott függőségekhez hasznos, de ismeretlen, nem megbízható binárison ne használd: megvalósítástól és a bináris interpreterétől függően kódvégrehajtást is előidézhet. A readelf adatként értelmezi az állományt; ez előnyösebb, bár sérült bemenetekhez naprakész eszköz és elkülönített vizsgálókörnyezet indokolt.

A readelf -d sem teljes futásidejű bizonyíték: közvetlen metaadatokat mutat, nem a teljes feloldott függőségi fát, és nem látja előre az alkalmazás által később dinamikusan betöltött modulokat.

3.2.6 API, ABI és frissítési szerződés

Az API a forrásból látható interfész és viselkedési szerződés. Az ABI a már lefordított részek együttműködésének bináris szerződése: többek között szimbólumnevek, paraméterátadás, visszatérés és adatreprezentáció.

Változtatás Kompatibilitási következmény
Belső algoritmus cseréje változatlan szerződéssel Lehet API- és ABI-kompatibilis
Új függvény hozzáadása A régi fogyasztóknak általában kompatibilis
Meglévő szimbólum eltávolítása Régi fogyasztók működését megszakíthatja
unsigned helyett unsigned long paraméter A mintaprofilban eltérő típusszélesség; ABI-vizsgálat és rendszerint új ABI-verzió szükséges

Inkompatibilis ABI-váltáskor szokás új SONAME, például libcount.so.2 használata. Egy .so.1 nevű szimbolikus link átirányítása egy inkompatibilis .so.2 állományra nem javítás.

Statikus felhasználásnál a könyvtár frissítése után a fogyasztót újra kell linkelni. Kompatibilis megosztott könyvtárfrissítésnél ez elkerülhető lehet, de a már futó folyamat nem cseréli le automatikusan a betöltött kódját: a telepítési tervnek újraindítással is számolnia kell.

3.2.7 Gyakori hibák

A -L nem futásidejű keresési útvonal; a sikeres linkelés ezért nem bizonyítja a telepítés helyességét. A .a használata nem helyettesíti a teljes statikus linkelés külön döntését. A .so fájlnév pedig önmagában nem jelent megfelelő SONAME-ot vagy kompatibilis ABI-t.

A javítás közös mintája: a buildparancs mellett mindig vizsgáld a tényleges ELF-metaadatokat, majd külön ellenőrizd a futtatási környezetet.

3.2.8 Feladatok

  1. Magyarázó: hogyan lehet a count-static egyszerre PIE és részben dinamikusan linkelt program? Melyik állomány nem kell a telepítéséhez?
  2. Kódolási: adj hozzá unsigned count_zero(void) függvényt, amely nullát ad vissza. Készíts új megosztott könyvtárat változatlan SONAME-mal. Indokold a kompatibilitás irányát.
  3. Diagnosztikai: a linkelés sikeres, de a telepített program nem találja a libcount.so.1 fájlt. A csomag csak libcount.so és libcount.so.1.0 állományokat tartalmaz. Mit ellenőrzöl és javítasz?

3.2.9 Megoldások

3.2.9.1 Magyarázó feladat

A PIE a program címzési és betöltési tulajdonsága. A statikus libcount-felhasználás azt jelenti, hogy annak szükséges objektumtagja bekerült a programba. Ettől a libc még dinamikus függőség maradhat. A libcount.a nem kell futáskor.

Ellenőrzés: a count-static dinamikus szakaszában nincs libcount-hoz tartozó NEEDED.

3.2.9.2 Kódolási feladat

A header védelmén belüli deklaráció:

unsigned count_zero(void);

A count.c kiegészítése, az eredeti C17-profilban:

unsigned count_zero(void)
{
    return 0u;
}

Az előző fordítási és megosztott linkelési parancsok ismétlésével elkészíthető az új változat. Ellenőrizd a dinamikus szimbólumtáblát:

readelf --dyn-syms -W build/lib/libcount.so.1.0

A régi fogyasztó továbbra is megtalálja a változatlan count_add függvényt. Az új count_zero függvényt használó fogyasztó viszont nem működik a régi könyvtárral. A kompatibilitás tehát nem szimmetrikus; a csomagfüggőségnek szükség esetén minimumverziót is ki kell fejeznie.

3.2.9.3 Diagnosztikai feladat

Először a fogyasztó NEEDED és RUNPATH bejegyzéseit vizsgáljuk. Ezután ellenőrizzük, hogy a tényleges keresési könyvtárban létezik-e a kért libcount.so.1 név.

A mintacsomagból hiányzik a futásidejű szimbolikus link:

ln -sfn libcount.so.1.0 build/lib/libcount.so.1

Ez csak akkor megfelelő javítás, ha a célállomány valóban az elvárt ABI-t valósítja meg. Az ellenőrzési feltétel a teljes, elérhető névlánc és a helyes keresési útvonal, nem pusztán a .so kiterjesztésű fájl jelenléte.

3.2.10 Összefoglaló

A statikus archívum linkeléskori objektumforrás; a megosztott könyvtár futásidejű függőség. A linksorrend, a SONAME, a DT_NEEDED és a loader keresési útvonalai külön szerződéselemek. Megbízható kiadásnál ezeket együtt kell ellenőrizni az API/ABI-kompatibilitással és a telepített állományokkal.