Debugmalloc
Ez a függvénykönyvtár egy okos malloc()
-free()
függvénypárost ad a használójának. A szoftlab nagyházi teszteléséhez javaslom,
a memóriakezelési hibák megtalálásának könnyítésére.
A függvénykönyvtár a következő szolgáltatásokat nyújtja:
- Képes arra, hogy a program futásának a végén kilistázza a felszabadítatlan területeket. Vagyis meg lehet vele találni a memóriaszivárgásokat.
- Nyilvántartja, hogy az egyes memóriafoglalások hol, melyik forrás fájlban, és melyik sorban történtek.
- Képes megtalálni az olyan
free()
hívásokat, amelyek le nem foglalt memóriaterületre, vagyNULL
pointerre hivatkoznak. - Képes érzékelni és jelezni a tömbök túlindexelését (bizonyos keretek közt).
- Segít megtalálni az inicializálatlan változókat.
- A hibaüzeneteket a képernyőre, vagy egy megadott fájlba tudja írni.
(A belső megvalósítása használ olyan nyelvi elemeket, amelyek a dinamikus tömböket bemutató előadáson még nem szerepeltek. Ezeket azonban nem kell érteni a használathoz. A listás és a generikus algoritmusokat bemutató előadásokon egyébként azok az elemek is szerepelni fognak.)
1 A függvénykönyvtár használata
A könyvtár két fájlból áll, ezek a debugmalloc.c forrás fájl és a debugmalloc.h header fájl. Mind a kettőt le kell tölteni, és a megfelelő módon a projekthez hozzáadni.
A header fájl két makrót ad meg, amelyek a szokásos malloc()
és
free()
függvényeket helyettesítik. Ezt a header fájlt az adott
projekt minden forrás fájljába be kell szerkeszteni, vagyis a
saját, tesztelt program minden .c
fájljának elején szerepelnie kell ennek a sornak:
#include "debugmalloc.h"
A másik fájllal nem kell foglalkozni, csak a projektbe fel kell venni.
A tesztelendő program kódjában ezen kívül semmilyen más módosítást nem kell végezni a használathoz!
A memóriaterületek dinamikus foglalása a szokásos módon
kell történjen: a malloc(size_t)
hívás foglal, és a free(void *)
hívás szabadít fel egy területet. A calloc()
és realloc()
hívások
is működnek, de azok, mint az közismert, ellenjavaltak.
Fontos, hogy a fenti #include
sor minden .c
fájlba bekerüljön.
Ellenkező esetben nem fog helyesen működni a tesztelés!
2 A függvénykönyvtár szolgáltatásai
A malloc()
függvény a lefoglalt memóriát nem inicializálatlanul
adja, hanem véletlenszámokkal tölti ki. Ezzel könnyebbé válik az inicializálatlan
változók okozta hibák kiszűrése (legalábbis a dinamikusan foglalt elemek,
tömbök esetében), mert a program működése határozottan nemdeterminisztikussá válik.
A lefoglalt területekről bármikor lista kérhető a debugmalloc_dump()
függvényhívással. Az üzenetek alapértelmezés szerint a szabványos hibakimenetre kerülnek,
de fájlba is írhatóak. A nevét például egy
debugmalloc_naplofajl("memlog.txt")
hívással lehet megadni.
Az újraírt free()
függvény ellenőrzi a hibás felszabadításokat, és
ezeknél megszakítja a programot egy abort()
hívással. Így egy nyomkövetőben (debugger) látszik az is, hogy hogyan jutott a végrehajtás
a hibás részhez.
A programból kilépéskor, ha maradtak felszabadítatlan memóriaterületek, azokról lista készül. Egy felszabadítatlan tétel részletezése így néz ki:
MEMORIATERULET: 0x25bf090, kanari: ok foglalva itt: proba.c:27 meret megadasa: 100*sizeof(char) (100 bajt) memoria eleje: 0000 48 65 6c 6c 6f 2c 20 76 69 6c 61 67 21 00 02 75 Hello, vilag!..u 0010 33 ba ca 24 87 35 2d ad 78 cb 69 50 c0 fb 80 8d 3..$.5-.x.iP.... 0020 65 45 ef e4 61 28 39 14 78 a6 1f a0 eb c0 de 46 eE..a(9.x......F 0030 b7 74 79 07 df c1 6d 62 e0 28 3a 96 fb ab a6 a8 .ty...mb.(:.....
Az első sorban látható a pointer értéke (0x25bf090). A második sorban az, hogy az adott memóriaterület
a proba.c fájl 27. sorában lett lefoglalva. A mérete a 100*sizeof(char)
kifejezéssel lett megadva, amely 100 bájtra értékelődött ki a program futása közben.
Ezen kívül pedig látható a lefoglalt memóriaterület eleje. A lefoglalt területre itt
a Hello, vilag! szöveget másoltam – megfigyelhető a lezáró nulla is, és az
utána lévő, generált memóriaszemét. A memória tartalma segíthet beazonosítani, hogy az adott
memóriaterület mi célt szolgált a programban, és így következtetni arra, hogy mikor
és hol kellett volna felszabadítani azt.
Végezetül pedig, ha a program hibátlanul működik, akkor csak el kell távolítani a fenti
#include
sort a forrás fájlokból, és automatikusan újra a beépített,
szabványos memóriakezelés veszi át a debugmalloc helyét.
3 Tömbök túlindexelése – a kanárik
A függvénykönyvtár támogatja a túlindexeléses hibák megtalálását. Ezt úgy éri el, hogy minden egyes memóriafoglalásnál egy kicsit nagyobb területet kér az operációs rendszertől, mint amekkora a programban igényelve lett; előtte és utána 128 bájtot hagy rá. Ezeket a plusz területeket kanárinak hívják.
kanári 128 bájt | A malloc() hívásnál megadott méretű memóriaterület | kanári 128 bájt |
A plusz területeket egy megadott karakterrel, a K
betűvel
tölti ki foglaláskor. Ha a lefoglalt terület például egy tömb, és írásnál
túlindexelés történik, akkor az a K
betűket fogja
felülírni. A felszabadításnál, a free()
hívásakor
ellenőrzi, hogy a K
betűk megmaradtak-e. Ha nem, kiírja
a memóriatartalom elejét, és a kanárik teljes tartalmát.
Egy sérült kanári így néz ki:
0000 4b 4b 4b 4b 4b 4b 4b 4b 4b 4b 4b 4b 4b 4b 4b 4b KKKKKKKKKKKKKKKK 0010 4b 4b 4b 4b 4b 4b 4b 4b 4b 4b 4b 4b 4b 4b 4b 4b KKKKKKKKKKKKKKKK 0020 4b 4b 4b 4b 4b 4b 4b 4b 4b 4b 4b 4b 4b 4b 4b 4b KKKKKKKKKKKKKKKK 0030 4b 4b 4b 4b 4b 4b 4b 4b 4b 4b 4b 4b 4b 4b 4b 4b KKKKKKKKKKKKKKKK 0040 4b 4b 4b 4b 4b 4b 4b 4b 4b 4b 4b 4b 4b 4b 4b 4b KKKKKKKKKKKKKKKK 0050 4b 4b 4b 4b 4b 4b 4b 4b 4b 4b 4b 4b 4b 4b 4b 4b KKKKKKKKKKKKKKKK 0060 4b 4b 4b 4b 4b 4b 4b 4b 4b 4b 4b 4b 4b 4b 4b 4b KKKKKKKKKKKKKKKK 0070 4b 4b 4b 4b 78 56 34 12 4b 4b 4b 4b 4b 4b 4b 4b KKKKxV4.KKKKKKKK
Kanári madarakat még a 20. században is alkalmaztak bányákban, mivel érzékenyebbek bizonyos mérgező gázokra, mint az ember. Innen ered a programozásban is ez az elnevezés.
4 Példaprogram
#include <stdlib.h> #include <stdio.h> #include <string.h> #include "debugmalloc.h" int main() { int *adat; char *szoveg; /* itt minden rendben van. */ adat=(int *) calloc(8, sizeof(int)); adat[0]=0x12345678; printf("\n\nMost egy memoriaterulet van lefoglalva:\n"); debugmalloc_dump(); adat=(int *) realloc(adat, 16*sizeof(int)); printf("\n\nMost atmereteztem:\n"); debugmalloc_dump(); free(adat); printf("\n\nMost egy sem:\n"); debugmalloc_dump(); /* itt egy tulindexeles */ adat=(int *) malloc(32*sizeof(int)); adat[-3]=0x12345678; /* tulindexeles */ printf("\n\nEz uj memoria. Felszabaditom, de tulindexeltem, ezert szol:\n"); free(adat); /* ez pedig egy memoriaszivargas... */ szoveg=malloc(100*sizeof(char)); strcpy(szoveg, "Hello, vilag!"); /* ... amit a program vegere irt dumppal latunk majd. */ return 0; }
5 A függvénykönyvtár belső működése
A függvénykönyvtár malloc()
hívása végül a beépített, gyári
malloc()
függvényt hívja meg. A kanárik hozzáadásán, és
a véletlenszámokkal kitöltésen kívül a lefoglalt területek adatait egy
láncolt listában is rögzíti; így tudja ellenőrizni a felszabadítások
helyességét.
A belső, foglalást végző függvény fejléce, és az azt használó makró a következő:
void *debugmalloc_malloc_full(size_t size, char const *fv, char const *cel, char const *file, unsigned line); #define malloc(X) debugmalloc_malloc_full(X, "malloc", #X, __FILE__, __LINE__)
Ez első paraméterként megkapja X
-et, vagyis a lefoglalandó memóriaterület
méretét. Második paramétere az X
kifejezés sztringgé alakítva (#X
).
Ez teszi lehetővé azt, hogy a kifejezést magát is rögzítse a debugmalloc, ahogyan
az a forráskódban látható (pl. 100*sizeof(int)
). A harmadik és negyedik
paraméter pedig a fordító beépített makrói, amelyek a forrás fájl nevére és az
adott sor számára helyettesítődnek be végül.
Ennek a függvénynek a fejléce inkompatibilis a beépített malloc()
hívással.
Ha a tesztelt programban szükség van a prototípus kompatibilitására (például a malloc és
a free típushelyes függvénypointerére), akkor a fejlécfájl beillesztése előtt definiálni kell
a HASZNALOM_A_MALLOC_FREE_POINTERET
makrót. Ilyenkor a fenti függvény
utolsó három paramétere alapértelmezett értéket kap mindig, vagyis a forrás fájl
nevének, sorának, és a méretet megadó kifejezésnek a rögzítése, mint szolgáltatás,
nem elérhető.
A láncolt lista, amelyet a program épít, az egyszerűség kedvéért mindkét végén strázsás.
Érdekessége, hogy a strázsákat nem dinamikusan foglalja le a program. Ha minden
malloc()
-olt memóriaterületet felszabadít a tesztelt program, akkor egyáltalán nem marad
dinamikusan foglalt terület, hiszen a strázsák a globális változók területén vannak.
Vagyis a debugmalloc maga nem csinál memóriaszivárgást :).
Az inicializálatlan listát a program onnan ismeri meg, hogy a fejét megadó
pointer még NULL. Az első híváskor létrehozza azt; és ezen felül az atexit()
függvény hívásával arra kéri a futtató környezet, hogy a programból kilépéskor hívja meg
a felszabadítatlan területek listázását végző függvényt.
A könyvtár tartalmaz egy saját véletlenszám-generátort. Erre azért van szükség,
hogy ne a beépített rand()
függvényt használja. Mivel a rand()
függvénynek belső állapota van, az megzavarhatná a tesztelt program
működését.