Debugmalloc, memóriakezelés

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

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.

1. A függvénykönyvtár használata

A könyvtár két fájlból áll, ezek a debugmalloc-impl.h és a debugmalloc.h. Mind a kettőt 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.

2. A függvénykönyvtár szolgáltatásai

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:

MEMORIATERULET: 0x55ce030c2a80, 100 bajt, kanari: ok
foglalas: example.c:30, malloc(100 * sizeof(char))
memoria eleje: 
    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 (0x55ce030c2a80). 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 például egy debugmalloc_naplofajl("memlog.txt") hívással lehet megadni.

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 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.

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. Egyebek

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.

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. Ha ez problémát jelent, akkor a debugmalloc.h fájl a debugmalloc-nomacro.h fájllal helyettesíthető. Ebben az esetben viszont az ellenőrzött memóriakezeléshez az alábbi négy makrót kell használni:

  • debugmalloc_malloc()
  • debugmalloc_free()
  • debugmalloc_realloc()
  • debugmalloc_calloc()

Vagyis lényegében ilyenkor a tesztelendő programot módosítani kell.

SDL

A debugmalloc a hibaüzeneteket a szabványos hibakimenetre (stderr) írja. A debugmalloc_naplofajl() 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.