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.

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

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.

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:

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.

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

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.