Egy megosztott könyvtár függvényének meghívásakor a gépi kód nem feltétlenül tartalmazza a függvény végleges címét. A könyvtár betöltési címe futásonként változhat, és a névhez tartozó definíció kiválasztásában a dinamikus linker is részt vehet. A hívás működéséhez ezért össze kell kapcsolnunk az ELF-fájl szerkezetét, a virtuális címtér kialakítását és a futásidejű szimbólumfeloldást.
A Statikus és megosztott könyvtárak részben látott linkelési modellre építünk. Az alábbi ELF-, PLT/GOT- és védelmi mechanizmusok nem az ISO C részei: konkrét modellünk a Linux, az x86-64 System V ABI és a glibc dinamikus linkere.
Az ELF sectionje, vagyis szekciója a fájl
linkeléshez és elemzéshez használható logikai egysége. A section header
table leírja többek között a szekciók típusát, méretét, fájlon belüli
helyét és tulajdonságait. Ilyen egység a gépi kódot tartalmazó
.text vagy a szimbólumtábla.
A program header egy programfejléc-bejegyzés: a betöltéshez vagy futáshoz szükséges tartományt, illetve metaadatot ír le. A bejegyzések táblája a program header table. Különösen fontos típusai:
PT_LOAD: a virtuális címtérbe betöltendő szegmens;PT_INTERP: a programhoz használandó interpreter,
Linuxon rendszerint a dinamikus linker elérési útja;PT_DYNAMIC: a dinamikus linkelés metaadatainak
helye.A szekció és a szegmens nem egy-egy megfeleltetésben áll. Egy betölthető szegmens több, összeillő hozzáférési igényű szekciót is tartalmazhat. A betöltő számára a programfejlécek az irányadók; egy megfelelően elkészített futtatható ELF-nek a végrehajtáshoz nincs szüksége section header table-re.
Az alábbi táblázat az x86-64 Linux/GNU eszközlánc szokásos elrendezését szemlélteti, nem univerzális layoutot ír elő. A méretek ELF-beli bájtokban értendők; ebben a laborprofilban egy C bájt nyolc bit.
| Szekció | Tipikus tartalom | Tartalmi bájtok a fájlban | Tipikus futásidejű hozzáférés |
|---|---|---|---|
.text |
Függvények gépi kódja | Igen | Olvasás, végrehajtás |
.rodata |
Sztringliterálok, csak olvasható adatok | Igen | Olvasás |
.data |
Módosítható, fájlból inicializált adatok | Igen | Olvasás, írás |
.bss |
Nullázással inicializált tároló | Nincs megfelelő tartalmi terület | Olvasás, írás |
A .bss szokásosan SHT_NOBITS típusú: mérete
van, de tartalma nem foglal ugyanannyi helyet a fájlban. Egy
PT_LOAD szegmensnél p_filesz jelöli a fájlból
származó tartomány méretét, p_memsz pedig a memóriabeli
méretet. Feltétel:
A memóriabeli tartomány fájlból már nem fedezett végét nullákkal kell inicializálni. Ez magyarázza, hogyan lehet egy nagy nullázott adattartomány futásidejű memóriaigénye jelentős, miközben a bináris alig növekszik.
A hozzáférési jogokat nem a .text név varázsolja elő: a
betölthető szegmens jelzői és az operációs rendszer leképezései
határozzák meg őket. A lapméret és az igazítás miatt a szekcióhatár nem
feltétlenül lapvédelmi határ.
Linuxon az exec során a kernel értelmezi a végrehajtható
ELF programfejléceit. Dinamikusan linkelt programnál betölti az
interpretert is, majd annak adja át a vezérlést.
A dinamikus linker feldolgozza a függőségeket, betölti a szükséges
megosztott objektumokat, végrehajtja az előírt relokációkat és
megszervezi a szükséges inicializálásokat. Ezután a program belépési
pontjára kerül a vezérlés. Ez nem közvetlenül a C main; a
folytatást a main, startup, argumentumok és
környezet rész tárgyalja.
Pozíciófüggetlen ELF-objektumnál sok cím számítása szemléletesen:
ahol a betöltési eltolás (load bias), az objektumhoz tartozó ELF virtuális cím, pedig a tényleges futásidejű cím. Ez nem minden relokáció képlete: külső szimbólum esetén előbb a megfelelő definíciót is meg kell találni.
A Procedure Linkage Table, röviden PLT, függvényhívásokat közvetítő gépi kódú bejegyzéseket tartalmazhat. A Global Offset Table, vagyis GOT, futásidőben használt címeket tároló táblázat. A GOT nem kizárólag függvénycímekre szolgál.
Egy hagyományos, x86-64 ELF-es, késleltetett feloldású hívás útja:
Ez a lazy binding, a késleltetett feloldás. Csökkentheti az induláskori munkát, de az első hívás többletköltséget és későn jelentkező szimbólumhibát hozhat.
Az eager binding, vagy azonnali feloldás esetén a
függvényhívási relokációk is betöltéskor rendeződnek. A GNU linker
-z now kapcsolója ilyen követelményt rögzíthet az
objektumban.
A PLT/GOT nem kötelező, változatlan utasítássorozat. A fordító
-fno-plt beállítása, a linker optimalizálásai és a
célarchitektúra módosíthatják a hívás alakját. A lazy binding elsősorban
az erre alkalmas függvényhívási relokációkra vonatkozik: nem azt
jelenti, hogy minden címfeloldás elhalasztható.
Az ASLR az operációs rendszer
címelrendezés-véletlenítési mechanizmusa. A PIE
pozíciófüggetlen végrehajtható program: lehetővé teszi, hogy a főprogram
is változó címen helyezkedjen el. GNU eszközláncnál ehhez tipikusan
-fPIE fordítás és -pie linkelés tartozik.
A PIE és az ASLR nem ugyanaz. Az előbbi a bináris tulajdonsága, az
utóbbi futtatási környezeti mechanizmus. Az ELF ET_DYN
típusa önmagában sem bizonyítja, hogy az állomány PIE: megosztott
könyvtár is lehet ilyen.
A RELRO, vagyis relocation read-only, olyan területet jelöl ki, amelyet a loader a szükséges relokációk után csak olvashatóvá tehet.
| GNU linkelési beállítás | Szokásos következmény |
|---|---|
-z relro |
A PT_GNU_RELRO tartomány lezárható; a lazy bindinghoz
szükséges helyek írhatók maradhatnak |
-z now |
Az érintett objektum függvényhívási relokációinak azonnali feloldása |
-z relro -z now |
Szokásos GNU elrendezésben „full RELRO”, a hívási GOT-terület védelmével is |
A -z now önmagában nem tesz memóriát csak olvashatóvá. A
főprogram beállításaiból pedig nem szabad a folyamat összes megosztott
könyvtárának védelmét levezetni.
A teljes, gyakran eltávolítható .symtab és a dinamikus
linkeléshez használt .dynsym különböző szerepű. A dinamikus
linker nem minden forrásbeli függvénynevet lát: az exportálás, a
szimbólum láthatósága és a linkelési döntések korlátozzák a halmazt.
A GNU ELF szimbólumverziózás egy névhez
verzióazonosítót is társíthat. A vizsgálóeszközök például
probe_value@PROBE_1.0 jelöléssel mutathatnak verziózott
definíciót; a @@ az alapértelmezett verziót jelölheti. A
fogyasztó verziókövetelménye így különböző ABI-generációk
megkülönböztetésére használható. Ez nem C-típusellenőrzés, és nem
helyettesíti a helyes függvénydeklarációt.
A symbol interposition során egy névhez a keresési
szabályok alapján másik objektum definíciója is kiválasztható.
Linux/glibc alatt az LD_PRELOAD tipikus eszköz erre,
például instrumentációhoz.
Nem minden hívás téríthető el így. A rejtett láthatóság, a helyi kötés, egyes linkerbeállítások és a közvetlenre optimalizált hívások korlátozhatják az interpositiont. Biztonságos végrehajtási módban a loader a környezeti befolyásolás egy részét is korlátozza.
A választott reprodukciós célprofil: Linux x86-64, System V ABI, LP64, GCC 14.2.0, GNU binutils 2.43.1 és glibc 2.40. A példák C17-et használnak; a dinamikus betöltési példa külön jelölt platformkiterjesztést is igényel. A parancsok és az elvárt megfigyelések nem dokumentált futtatási eredmények.
A probe.h fájl:
#ifndef PROBE_H
#define PROBE_H
int probe_value(void);
#endifA probe.c könyvtári implementáció:
#include "probe.h"
int probe_value(void)
{
return 42;
}A direct.c közvetlen fogyasztó:
#include "probe.h"
int main(void)
{
return probe_value() == 42 ? 0 : 1;
}Fordítás és linkelés:
gcc -std=c17 -Wall -Wextra -Wpedantic -O0 -g \
-fPIC -shared probe.c \
-Wl,-soname,libprobe.so -Wl,-z,relro,-z,now \
-o libprobe.so
gcc -std=c17 -Wall -Wextra -Wpedantic -O0 -g \
-fPIE -fplt -pie direct.c -L. -lprobe \
-Wl,-rpath,'$ORIGIN' -Wl,-z,relro,-z,now \
-o directAz idézőjelezett $ORIGIN a loader számára marad meg:
ebben a beállításban a futtatható állomány könyvtárához viszonyított
keresést tesz lehetővé.
A vizsgálat lépései:
readelf -SW direct
readelf -lW direct
readelf -dW direct
readelf -rW direct
readelf --dyn-syms -W libprobe.so
readelf -V direct
objdump -d directA section–segment hozzárendelést, a NEEDED függőséget, a
relokációkat és a probe_value hívási útját kell
összekapcsolni. Konkrét címérték vagy PLT-utasítássorozat nincs
előírva.
dlopen és
dlsymA POSIX dlopen futás közben betöltött objektumhoz ad
handle-t; a dlsym a handle keresési tartományában keres
szimbólumot. A RTLD_NOW azonnali feloldást kér, a
RTLD_LOCAL pedig nem teszi általánosan globálissá az új
objektum szimbólumait.
A dynamic.c teljes platformpélda:
#include <dlfcn.h>
#include <stdio.h>
typedef int (*probe_fn)(void);
int main(void)
{
void *handle = dlopen("./libprobe.so", RTLD_NOW | RTLD_LOCAL);
if (handle == NULL) {
fputs("dlopen failed\n", stderr);
return 1;
}
(void)dlerror(); /* Korabbi hiba torlese. */
void *address = dlsym(handle, "probe_value");
const char *error = dlerror();
if (error != NULL || address == NULL) {
fputs(error != NULL ? error : "NULL symbol address", stderr);
fputc('\n', stderr);
(void)dlclose(handle);
return 1;
}
/* Linux/POSIX szerzodes; nem hordozhato ISO C konverzio. */
probe_fn function = (probe_fn)address;
int value = function();
if (dlclose(handle) != 0) {
return 1;
}
return value == 42 ? 0 : 1;
}gcc -std=c17 -Wall -Wextra -O0 -g \
-fPIE -pie dynamic.c -ldl \
-Wl,-z,relro,-z,now -o dynamicA dlsym eredményének függvénypointerré alakítását az ISO
C17 önmagában nem garantálja; itt a Linux/POSIX platform szerződésére
támaszkodunk. A -Wpedantic ezért szándékosan hiányzik ebből
a parancsból. A konverzió nem ellenőrzi a függvény típusát: az
int (void) szerződésnek ténylegesen egyeznie kell.
| Érték | Tulajdon és élettartam | Érvénytelenítés, hibakimenet |
|---|---|---|
handle |
A program felel a sikeres megnyitás lezárásáért | NULL jelzi a megnyitási hibát; lezárás után nem
használjuk |
address, function |
Kölcsönzött hozzáférés a betöltött kódhoz | dlclose után nem támaszkodunk az érvényességükre |
error |
A runtime által kezelt, kölcsönzött szöveg | A következő dlerror felülírhatja; nem szabad
free-vel felszabadítani |
A dlsym null eredménye önmagában nem általános
hibateszt; ezért használjuk a dlerror protokollját. A példa
ettől függetlenül elutasítja a null címet, mert meghívható függvényt
vár. A dlclose nem feltétlenül távolítja el azonnal a
könyvtárat, de erre nem építhető élettartam-garancia.
A section nem önálló memória-leképezés: a betöltési nézetet a
programfejlécekből kell kiolvasni. A .bss kis fájlköltsége
nem jelent nulla futásidejű memóriaigényt.
A sikeres linkelés nem garantálja, hogy futáskor minden könyvtár és megfelelő szimbólumverzió rendelkezésre áll. Lazy binding esetén egy hiányzó függvénydefiníció hibája akár az első érintett hívásig is késhet.
Végül a sikeres dlsym csak névfeloldást igazol. Nem
bizonyítja a függvénytípus, az ABI vagy az élettartam helyességét.
.text szekcióhoz külön PT_LOAD tartozzon?
Miért lehet a .bss memóriabeli mérete nagyobb a fájlbeli
tartalmánál?direct programból
lazy bindingot kérő változatot. Melyik linkeropciót változtatod meg, és
mit vizsgálsz meg?.bss tartalmát nullázás állítja
elő, nem azonos méretű fájlbeli bájtsorozat.
Ellenőrzés: a readelf -lW hozzárendelése
és a megfelelő p_memsz, p_filesz értékek
összevetése.-z now helyére
-z lazy kerül, például -Wl,-z,relro,-z,lazy
alakban. Ellenőrzés: a dinamikus jelzők és a hívási
relokációk vizsgálata. Az azonnali feloldási jelző hiánya még nem
bizonyít tényleges lazy működést: például a glibc
LD_BIND_NOW környezeti beállítása felülírhatja a
várakozást.Az ELF szekciói a linkelési, programfejlécei a betöltési nézetet adják. A dinamikus linker a leképezett objektumokhoz címeket és szimbólumdefiníciókat rendel; a PLT/GOT ennek egyik tipikus hívási mechanizmusa. A PIE, ASLR és RELRO különböző feladatokat lát el. Explicit betöltéskor pedig a névfeloldáson túl a függvénytípus és a betöltött kód élettartama is az alkalmazás szerződésének része.