Debugmalloc, memóriakezelés
Czirkos Zoltán, Szekeres Dániel · 2021.08.24.
A Debugmalloc egy varázs-malloc(), amely képes kilistázni a felszabadítatlan területeket, és ezzel megkönnyíti a hibakeresést. Bizonyos keretek között a túlindexelést is tudja ellenőrizni.
A Debugmalloc függvénykönyvtár egy okos malloc()
-free()
függvénypárost ad a használójának. A nagy házi
teszteléséhez hasznos, 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ásfájlban, és melyik sorban történtek.
- Képes megtalálni az olyan
free()
hívásokat, amelyek le nem foglalt memóriaterületre hivatkoznak. - Képes érzékelni és jelezni a tömbök túlindexelését (attól függően, hogy mekkora túlindexelésről volt szó).
- 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 könyvtár egyetlen egy fájlból áll, ez a debugmalloc.h. Ezt le kell tölteni, és a szokásos módon a projekthez hozzáadni.
A könyvtár makrókat definiál, amelyek a szokásos malloc()
és free()
függvényeket helyettesítik. A
debugmalloc.h fájlt az adott projekt minden forrásfájljába be kell szerkeszteni, vagyis a saját, tesztelt program minden
.c
fájljának elején szerepelnie kell az alábbi sornak. Fontos, hogy ez tényleg minden .c
fájlba
bekerüljön!
#include "debugmalloc.h"
A tesztelendő program kódjában ezen kívül semmilyen más módosítást nem kell végezni. 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.
Az új malloc()
a lefoglalt memóriát nem inicializálatlanul adja, hanem konstans számmal 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.
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 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:
0x56343a6e2d10, 100 bajt, kanari: ok example.c:30, malloc(100 * sizeof(char)) 0000 48 65 6c 6c 6f 2c 20 76 69 6c 61 67 21 00 4b 4b Hello, vilag!.KK 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
Az első sorban látható a pointer értéke, vagyis a memóriacím. A második sorban az, hogy az adott memóriaterület az
example.c fájl 30. 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 került – megfigyelhető a lezáró nulla is. A tartalom 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.
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 egy debugmalloc_log_file("memlog.txt")
hívással lehet megadni.
Az egyes foglalások méretét a Debugmalloc maximálja, alapbeállítás szerint 1 megabájtban. Ha ez kevés, akkor a beállítás
egy debugmalloc_max_block_size(s)
függvényhívással módosítható.
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.
A kö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.
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "debugmalloc.h"
int main(void) {
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;
}
Makrók
Mind a négy memóriakezelést végző függvény (malloc
, free
, realloc
, calloc
)
makróként jelenik meg. Ez zavaró lehet, mert emiatt sehol nem lehet ilyen nevű változó sem a programban. De ez amúgy is furcsa
és szokatlan lenne; ugyan elméletben szabad lenne, a gyakorlatban printf
-nek sem szoktunk változót elnevezni.
SDL
A debugmalloc a hibaüzeneteket a szabványos hibakimenetre (stderr) írja. A debugmalloc_log_file()
függvénnyel ezek
egy megadott nevű fájlba irányíthatóak át. Windows-on, SDL-es programoknál ezt a megoldást érdemes választani, vagy az SDL-es oldalon bemutatott freopen()
-es trükköt.
Belső felépítés
A függvénykönyvtár 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.
A 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
memóriaterület inicializálásán 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ő:
static void *debugmalloc_malloc_full(size_t size,
char const *func,
char const *expr,
char const *file,
unsigned line);
#define malloc(S) debugmalloc_malloc_full((S), "malloc", #S, __FILE__, __LINE__)
Ez első paraméterként megkapja S
-et, vagyis a lefoglalandó memóriaterület méretét. Harmadik paramétere az
S
kifejezés sztringgé alakítva (#S
). 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 negyedik és ötödik paraméter pedig a
fordító beépített makrói, amelyek a forrásfájl nevére és az adott sor számára helyettesítődnek be végül.