NHF segédlet: Memóriakezelés, debugmalloc

Czirkos Zoltán, Szekeres Dániel · 2017.07.13.

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 határok között a túlindexelést is tudja ellenőrizni.

Ez a függvénykönyvtár egy okos malloc()-free() függvénypárost ad a használójának. A nagyhá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á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, vagy NULL 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"

Fontos, hogy a fenti #include sor minden .c fájlba bekerüljön. Ellenkező esetben nem fog helyesen működni a tesztelés!

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.

SDL-hez

A debugmalloc a hibaüzeneteket a szabványos hibakimenetre (stderr) írja. A debugmalloc_naplofajl("fajlnev.txt"); függvényhívással ezek egy megadott nevű fájlba irányíthatóak át. Windows-on, SDL-es programoknál ezt a megoldást érdemes választani (mert az SDL átirányítja a szabványos kimenetet), vagy az SDL-es oldalon bemutatott freopen()-es trükköt.

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(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;
}

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.