A C-objektum tárolási ideje nem a virtuális címterület egyik tartományának neve. Az „automatikus objektum” nyelvi fogalom; a „vermen elhelyezett lokális változó” egy gyakori megvalósítás leírása. Ez a különbség hibakereséskor döntő: attól, hogy egy cím továbbra is olvasható memórialapra mutat, az ott korábban létező objektum élettartama már véget érhetett.
A Scope, linkage, storage duration és lifetime részben bevezetett különbségekre építünk: a hatókör a név használhatóságáról, a nyelvi kapcsolás a deklarációk közötti azonosságról, a tárolási idő és az élettartam pedig az objektum létezéséről szól. Most ezeket kapcsoljuk össze egy Linux-folyamat megfigyelhető memóriatérképével, anélkül, hogy a térképet a C nyelv definíciójának tekintenénk.
ISO C17 — nyelvi modell. Az objektum olyan adattárolási terület, amelynek tartalma értékeket reprezentálhat. A C nem ír elő Linux-folyamatot, lapozást, vermet vagy egy „heap” nevű memóriatartományt. Egy pointer sem általánosan hordozható fizikai memóriacím.
Linux, x86-64 — megvalósítási modell. A fejezet laborprofiljában az objektumpointerek a folyamat virtuális címteréhez kapcsolódnak. A virtuális cím egy folyamat címfordítási környezetében értelmezett cím; a fizikai cím a hardver fizikai címterének egy helyét azonosítja. Ugyanaz a virtuális cím két különböző folyamatban rendszerint nem ugyanahhoz a fizikai tárolóhoz tartozik.
A címfordítást a hardver és a kernel lapokra bontva kezeli. Egyszerű, azonos méretű lapokat feltételező modellben, virtuális cím és lapméret mellett:
Itt a virtuális lapszám, a lapon belüli eltolás. Ha a lap jelen van, és a fordítás fizikai lapkeretre mutat, akkor a fizikai cím . A folyamat memóriatérképe azonban nem árulja el önmagában értékét.
Egy létező leképezéshez nem szükségképpen tartozik azonnal saját, rezidens fizikai lap. A demand paging, a megosztott lapok és a másolás íráskor (copy-on-write) tovább bontják a „lefoglalt memória” hétköznapi fogalmát.
A leképezés olvasási, írási és végrehajtási jogosultságokat is hordoz. Ezek nem helyettesítik a C szabályait: egy írható lapra történő írás is lehet nem definiált viselkedés, például ha az érintett objektum már nem él.
Az alábbi megfeleltetések Linux hosted, x86-64 System V ABI, LP64 profilban gyakoriak, de nem ISO C-elhelyezési garanciák.
| Nyelvi objektum vagy tároló | Gyakori megvalósítás | Fontos korlát |
|---|---|---|
| Automatikus lokális objektum | Verem, regiszter, esetleg külön tároló nélkül | Az optimalizáló megszüntetheti vagy átalakíthatja |
| Módosítható statikus objektum, nem nulla kezdőértékkel | Írható adatleképezés, gyakran .data |
A szekciónév nem nyelvi tulajdonság |
| Nullakezdőértékű statikus objektum | Gyakran .bss |
A fájlban rendszerint nem szerepel minden kezdeti nulla bájt |
| Statikus, nem módosítható adat | Gyakran .rodata vagy más csak olvasható tartomány |
A const önmagában nem lapvédelmi utasítás |
| Szálankénti tárolási idejű objektum | Thread-local storage, TLS | Nem azonos az adott szál automatikus objektumaival |
| Allokált tároló | [heap] vagy más, például anonim leképezés |
A malloc nem köteles a [heap] tartományból
dolgozni |
Az ELF-betöltés, PLT/GOT és dinamikus
szimbólumok részhez kapcsolódó fontos különbség: az ELF
szekciói és a folyamat
virtuálismemória-leképezései nem egy-egy arányban
felelnek meg egymásnak. A betöltés szegmensekhez kapcsolódik; több
szekció kerülhet azonos leképezésbe. A /proc térképében
ezért nem feltétlenül találunk .rodata vagy
.bss feliratú sort.
A „mapping” sem kizárólag a heapen kívüli területeket jelenti. A programkód, a verem, a megosztott könyvtárak és az allocator által használt tartományok egyaránt leképezésekből állnak.
Indoklásvázlat. Az ISO C17 az objektum nyelvi tulajdonságait és a program megfigyelhető viselkedését írja elő, nem minden objektum gépi tárolóhelyét. Ha egy lokális objektum értéke mindig egy konstans, a fordító helyettesítheti a felhasználásokat ezzel a konstanssal. Ha az érték csak számításokhoz kell, regiszterben tarthatja. Ha szükségtelen, elhagyhatja.
A cím megfigyelhetővé tétele korlátozza ezeket az átalakításokat, de abból sem következik általános nyelvi tételként, hogy az objektum „a stacken van”. A fordítónak a megfigyelhető viselkedést kell megőriznie.
Kijelölt, itt nem futtatott laborprofil: ISO C17,
GCC 14.2.0, glibc 2.40, GDB 15.2, Linux x86-64, System V ABI, LP64,
CHAR_BIT == 8. A tényleges reprodukcióban a kernel és az
eszközök pontos verzióját is rögzíteni kell. Az alábbiak laborutasítások
és értelmezési szempontok, nem dokumentált mérési eredmények.
A program többféle objektum címét és saját memóriatérképét írja ki. A
4096 itt az allokáció kért mérete C bájtban; nem a
lapméretre vonatkozó állítás.
/* address_map.c — ISO C17; a /proc elerese Linux-specifikus. */
#include <stdio.h>
#include <stdlib.h>
static int initialized = 7;
static int zeroed;
static const char label[] = "address probe";
static void frames(unsigned depth)
{
unsigned marker = depth;
printf("depth=%u marker=%p\n", depth, (void *)&marker);
if (depth != 0) {
frames(depth - 1);
}
printf("return marker=%p value=%u\n",
(void *)&marker, marker);
}
int main(void)
{
int automatic = 11;
void *owner = malloc(4096);
if (owner == NULL) {
fputs("allocation failed\n", stderr);
return EXIT_FAILURE;
}
FILE *maps = fopen("/proc/self/maps", "r");
if (maps == NULL) {
perror("fopen /proc/self/maps");
free(owner);
return EXIT_FAILURE;
}
printf("initialized=%p\nzeroed=%p\nlabel=%p\n",
(void *)&initialized, (void *)&zeroed,
(void *)label);
printf("automatic=%p\nowner-slot=%p\nallocation=%p\n",
(void *)&automatic, (void *)&owner, owner);
frames(2);
char line[512];
while (fgets(line, sizeof line, maps) != NULL) {
fputs(line, stdout);
}
int failed = ferror(maps) != 0;
if (fclose(maps) != 0) {
failed = 1;
}
free(owner);
if (fflush(stdout) == EOF || ferror(stdout)) {
failed = 1;
}
return failed ? EXIT_FAILURE : EXIT_SUCCESS;
}Fordítás a külön laborban:
gcc -std=c17 -Wall -Wextra -Wpedantic \
-O0 -g3 -fno-omit-frame-pointer -fPIE -pie \
address_map.c -o address-mapA %p formátumhoz void * argumentumot adunk.
A kiírás alakja implementáció által meghatározott; ebben a
Linux-profilban hexadecimális címalak várható.
Különösen fontos az owner-slot és az
allocation különbsége. Az előbbi magának az automatikus
pointerobjektumnak a címe, az utóbbi a benne tárolt pointerérték által
jelölt allokált terület kezdete. A pointer tárolási ideje nem azonos a
célobjektum tárolási idejével.
A tulajdonos itt programtervezési szerep: a C típusrendszere nem kényszeríti ki.
| Objektum vagy erőforrás | Tulajdonos, felelős | Kölcsönzött hozzáférés | Élettartamhatár, hibakimenet |
|---|---|---|---|
| Allokált 4096 C bájt | A main, az owner pointeren keresztül |
A cím kiírása; nincs tartós alias | free(owner); sikertelen kéréskor null pointer |
owner pointerobjektum, automatic |
A main blokkhoz kötött automatikus objektumok |
Címük átadása a printf hívásnak |
A blokk elhagyása |
Rekurzív marker objektumok |
Az egyes függvényaktivációk | Címük átadása a printf hívásnak |
Az adott hívás visszatérése |
initialized, zeroed,
label |
Statikus tárolási idejű objektumok | Csak címmegfigyelés | A program teljes végrehajtása alatt élnek |
Megnyitott FILE adatfolyam |
A main, a maps handle révén |
Könyvtári hívások a lezárásig | fclose; megnyitási, olvasási vagy lezárási hiba
lehetséges |
A kiírt cím nem hosszabbít meg élettartamot. A program a
felszabadítás után nem használja újra az owner régi
pointerértékét.
Linux — diagnosztikai interfész. A
/proc/self/maps sorai többek között a következő adatokat
tartalmazzák:
A tartomány fél-nyílt:
.
A b végcím már nem része a tartománynak. A
rw-p jelölés olvasható, írható, privát leképezést jelez; a
„privát” nem jelenti azt, hogy minden lap már saját fizikai példányban
létezik.
Illusztráció, nem mérés, a megadott x86-64 laborprofilhoz:
| Adat | Érték |
|---|---|
| Leképezés kezdete, | 0x0000555500004000 |
| Leképezés vége, | 0x0000555500006000 |
| Vizsgált virtuális cím, | 0x0000555500004120 |
Először ellenőrizzük:
Ez teljesül. A cím tartományon belüli eltolása:
A tartomány mérete:
Ezzel csak a leképezéshez tartozást állapítottuk meg. Nem bizonyítottuk az objektum méretét, élettartamát vagy a rajta végzett művelet nyelvi helyességét. Az intervallumvizsgálat itt Linux-címadatok számszerű elemzése; nem felhívás különálló C-objektumokra mutató pointerek relációs összehasonlítására.
A debuggerben az address-map betöltése után használható
a break frames, majd a run. Megálláskor a
print &marker, print &initialized és
info proc mappings kapcsolja össze az objektumcímeket a
leképezésekkel. Két további continue után a bt
a legbelső rekurzív hívásból mutathatja a hívási láncot.
A /proc/self/maps mindig az azt megnyitó folyamatra
vonatkozik. Ha egy shellben a cat /proc/self/maps parancsot
használjuk, a cat térképét látjuk, nem egy másik vizsgált
programét. Ezért olvassa a példaprogram saját maga a fájlt.
A veremkeret (stack frame) egy függvényaktivációhoz társított, megvalósításfüggő állapot. Tartalmazhat mentett regisztereket, lokális tárolót és a visszatéréshez szükséges adatokat; az ABI és a fordító döntései befolyásolják.
A frames(2) végrehajtása közben három függvényaktiváció
létezik egyszerre, saját marker objektummal. A hívás utáni
kiírás azt is megmutatja, hogy a külső aktiváció objektuma a belső hívás
alatt tovább él. Az egyszerű farokhívásos átalakítást ez megnehezíti, de
általánosan nem tiltja meg az optimalizálást.
Az x86-64 gyakori hívási megvalósításában a verem kisebb címek felé növekszik. Ebből nem következik, hogy tetszőleges lokális változók deklarációs sorrendje kiolvasható a címekből, vagy hogy két rekurziós szint címtávolsága minden buildben állandó. A részletes hívási szerződést a Hívási konvenció, stack frame és visszatérés tárgyalja.
Az allokált tárolónál más határt kell felismerni.
ISO C17 — könyvtári szerződés. A sikeres
malloc megfelelő tárolót biztosít; a free
megszünteti annak használati jogosultságát a C-program számára.
Linux és libc — megvalósítás. Az allocator
használhatja a program break változtatását, amelyhez Linuxon a
brk mechanizmus kapcsolódik, illetve kérhet külön
mmap leképezéseket. A választás allocator-, verzió-,
konfiguráció- és futástörténet-függő.
Ezért nincs általános megfeleltetés:
Egy free után a tároló az allocatornál maradhat későbbi
felhasználásra. A térkép változatlansága nem őrzi meg a felszabadított
objektum élettartamát. A részletes allokációs szerződés a malloc, calloc, free és allokációs szerződés
témája.
Az ASLR (address space layout randomization) a megfelelően konfigurált Linux-rendszerben több címtartomány elhelyezését változtathatja futásonként. A példát PIE-ként fordítjuk, hogy a végrehajtható program betöltési alapja is részt vehessen ebben. Nem minden cím változik szükségképpen minden indításkor.
A debugger befolyásolhatja a környezetet. GDB-ben a
show disable-randomization mutatja az erre vonatkozó
beállítást; a set disable-randomization off a debugger
által kezdeményezett kikapcsolást tiltja le. Ezt indítás előtt kell
beállítani, és a rendszerkorlátozások továbbra is számítanak.
Összehasonlításhoz készíthető optimalizált változat:
gcc -std=c17 -Wall -Wextra -Wpedantic \
-O2 -g3 -fPIE -pie \
address_map.c -o address-map-O2Az -O2 mellett egyes függvényhívások beágyazhatók, a
tárolóhelyek megváltozhatnak, és a debugger bizonyos értékekre
optimized out jelzést adhat. Ez nem bizonyítja, hogy a
forrásnyelvi objektumnak nem volt élettartama.
A mérés maga is alakítja a folyamatot: a stdio és a
fájlmegnyitás további allokációkat okozhat. Ezért ne rendeljünk minden
új térképsort automatikusan a saját malloc hívásunkhoz.
A legfontosabb hibajavítási szabály: a leképezés érvényességét és az objektum érvényességét külön vizsgáljuk. A segmentation fault hiánya nem helyességi bizonyíték.
Hasonlítsd össze az owner pointerobjektumot és az általa
jelölt allokált tárolót. Mi történik, ha a main elhagyja a
blokkot anélkül, hogy meghívná a free függvényt? Milyen
következtetést nem szabad levonni az owner-slot
címéből?
Egészítsd ki a frames függvényt egy
static unsigned shared; objektummal, és minden belépéskor
írasd ki a címét. Hasonlítsd össze a rekurzív hívások
shared és marker címeit.
Egy mérnök három megállapítást tesz:
malloc eredménye nincs a [heap] sorban,
ezért hibás.”free után a leképezés megmaradt, ezért a régi
pointer még használható.”Magyarázd meg mindhárom következtetés hibáját, és adj ellenőrzési lépést.
Az owner automatikus tárolási idejű pointerobjektum; a
célterület allokált tárolási idejű. Az automatikus pointer megszűnése
nem hívja meg a free függvényt. Ha az utolsó hozzáférési
lehetőség is elveszik, memóriafelszabadítási hiba keletkezik. A folyamat
végén történő OS-takarítás ettől külön mechanizmus.
Az owner-slot csak a pointerobjektum helyét mutatja, nem
az allokált területét. Ellenőrzési feltétel: külön azonosítsuk az
&owner és az owner kifejezés jelentését,
majd kövessük a felszabadítás minden vezérlési útját.
A korábbi C17-függvény törzsének elejéhez illeszthető részlet:
static unsigned shared;
printf("shared=%p\n", (void *)&shared);Minden aktiváció ugyanarra a statikus objektumra hivatkozik, miközben
saját automatikus marker objektuma van. A megadott
laborprofilban a shared címének egy futáson belül azonosnak
kell mutatkoznia. A marker címek között nem követelünk
meghatározott távolságot vagy sorrendet.
A címátadás kölcsönzött hozzáférés, nem tulajdonátadás; a részlet nem őriz meg pointert az automatikus objektum élettartamán túl.
A [heap] felirat nem foglalja magában az allocator
minden lehetséges tárolóját. Először az allokáció címét tartalmazó
leképezést kell megkeresni; az allocator döntési mechanizmusa külön
vizsgálatot igényel.
A debugger ASLR-beállítását ellenőrizni kell, majd külön, debugger nélkül is megismételni a megfigyelést. Azonos címek önmagukban nem írják le a teljes rendszer házirendjét.
A harmadik állítás nyelvi és OS-szintet kever. A free
lezárja az allokált objektum élettartamát akkor is, ha a lapok
megmaradnak. Az ellenőrzés alapja a felszabadítási út követése, nem egy
próbaolvasás: a hibás hozzáférésnek nincs garantált kimenete.
A C az objektumok élettartamát és használati szabályait határozza
meg; Linux a virtuális leképezéseket és lapjogosultságokat kezeli; a
fordító és az allocator köti össze e két nézőpontot. A stack, heap,
.data, .bss és .rodata hasznos
megvalósítási fogalmak, de nem helyettesítik a nyelvi szerződést. A
memóriatérkép helyet mutat, nem élettartamot; a debugger megfigyelést
ad, nem általános nyelvi garanciát.