Memory Allocation
Subsistemul de gestiune a memoriei din cadrul unui sistem de operare este folosit de toate celelalte subsisteme: planificator, I/O, sistemul de fișiere, gestiunea proceselor, networking. Memoria este o resursă importantă, de aceea sunt necesari algoritmi eficienți de utilizare și gestiune a acesteia.
Rolul subsistemului de gestiune a memoriei este de:
Nucleul sistemului de operare oferă un set de interfețe (apeluri de sistem) care permit alocarea/dealocarea de memorie, maparea unor regiuni de memorie virtuală peste fișiere, partajarea zonelor de memorie.
Din păcate, nivelul limitat de înțelegere a acestor interfețe și a acțiunilor ce se petrec în spate conduc la o serie de probleme foarte des întâlnite în aplicațiile software: memory leak-uri, accese nevalide, suprascrieri, buffer overflow, corupere de zone de memorie.
Este, în consecință, fundamentală cunoașterea contextului în care acționează subsistemul de gestiune a memoriei și înțelegerea interfeței pusă la dispoziție programatorului de către sistemul de operare.
Spațiul de adresă al unui proces, sau, mai bine spus, spațiul virtual de adresă al unui proces reprezintă zona de memorie virtuală utilizabilă de un proces. Fiecare proces are un spațiu de adresă propriu. Chiar în situațiile în care două procese partajează o zonă de memorie, spațiul virtual este distinct, dar se mapează peste aceeași zonă de memorie fizică.
În figura alăturată este prezentat un spațiu de adresă tipic pentru un proces. În sistemele de operare moderne, în spațiul virtual al fiecărui proces se mapează memoria nucleului, aceasta poate fi mapată fie la început, fie la sfârșitul spațiului de adresă. În continuare, ne vom referi numai la spațiul de adresă din user-space pentru un proces.
Cele 4 zone importante din spațiul de adresă al unui proces sunt zona de date, zona de cod, stiva și heap-ul. După cum se observă și din figură, stiva și heap-ul sunt zonele care pot crește. De fapt, aceste două zone sunt dinamice și au sens doar în contextul unui proces. De partea cealaltă, informațiile din zona de date și din zona de cod sunt descrise în executabil.
Segmentul de cod (denumit și text segment
) reprezintă instrucțiunile în limbaj mașină ale programului. Registrul de tip instruction pointer
(IP) va referi adrese din zona de cod. Se citește instrucțiunea indicată de către IP, se decodifică și se interpretează, după care se incrementează contorul programului și se trece la următoarea instrucțiune.
Zona de cod este, de obicei, o zonă read-only pentru ca procesul să nu poată modifica propriile instrucțiuni prin folosirea greșită a unui pointer. Zona de cod este partajată între toate procesele care rulează același program. Astfel, o singură copie a codului este mapată în spațiul de adresă virtual al tuturor proceselor.
Zonele de date conțin variabilele globale definite într-un program și variabilele de tipul read-only. În funcție de tipul de date există mai multe subtipuri de zone de date.
Zona .data
conține variabilele globale și statice inițializate la valori nenule ale unui program. De exemplu:
static int a = 3; char b = 'a';
Zona .bss
conține variabilele globale și statice neinițializate ale unui program. Înainte de execuția codului, acest segment este inițializat cu 0. De exemplu:
static int a; char b;
În general aceste variabile nu vor fi prealocate în executabil, ci în momentul creării procesului. Alocarea zonei .bss se face peste pagini fizice zero (zeroed frames).
Zona .rodata conține informație care poate fi doar citită, nu și modificată. Aici sunt stocate constantele:
const int a; const char *ptr;
și literalii:
"Hello, World!" "En Taro Adun!"
Stiva este o regiune dinamică în cadrul unui proces, fiind gestionată automat de compilator.
Stiva este folosită pentru a stoca “stack frame-uri”. Pentru fiecare apel de funcție se va crea un nou “stack frame”.
Un “stack frame” conține:
Pe marea majoritate a arhitecturilor moderne stiva crește în jos și heap-ul crește în sus. Stiva crește la fiecare apel de funcție și scade la fiecare revenire din funcție.
În figura de mai jos este prezentată o vedere conceptuală asupra stivei in momentul apelului unei funcții.
Heap-ul este zona de memorie dedicată alocării dinamice a memoriei. Heap-ul este folosit pentru alocarea de regiuni de memorie a căror dimensiune este determinată la runtime.
La fel ca și stiva, heap-ul este o regiune dinamică care își modifică dimensiunea. Spre deosebire de stivă, însă, heap-ul nu este gestionat de compilator. Este de datoria programatorului să știe câtă memorie trebuie să aloce și să rețină cât a alocat și când trebuie să dealoce. Problemele frecvente în majoritatea programelor țin de pierderea referințelor la zonele alocate (memory leaks) sau referirea de zone nealocate sau insuficient alocate (accese nevalide).
În limbaje precum Java, Lisp etc. unde nu există “pointer freedom”, eliberarea spațiului alocat se face automat prin intermediul unui garbage collector. Pe aceste sisteme se previne problema pierderii referințelor, dar încă rămâne activă problema referirii zonelor nealocate.
Alocarea memoriei este realizată static de compilator sau dinamic, în timpul execuției. Alocarea statică e realizată în segmentele de date pentru variabilele globale sau pentru literali.
În timpul execuției, variabilele se alocă pe stivă sau în heap. Alocarea pe stivă se realizează automat de compilator pentru variabilele locale unei funcții (mai puțin variabilele locale prefixate de identificatorul static).
Alocarea dinamică se realizează în heap. Alocarea dinamică are loc atunci când nu se știe, în momentul compilării, câtă memorie va fi necesară pentru o variabilă, o structură, un vector. Dacă se știe din momentul compilării cât spațiu va ocupa o variabilă, se recomandă alocarea ei statică, pentru a preveni erorile frecvent apărute în contextul alocării dinamice.
Pentru a fragmenta cât mai puțin spațiul de adrese al procesului, ca urmare a alocărilor și dealocărilor unor zone de dimensiuni variate, alocatorul de memorie va organiza segmentul de date alocate dinamic sub formă de heap, de unde și numele segmentului.
Dealocarea memoriei înseamnă eliberarea zonei de memorie (este marcată ca fiind liberă) alocate dinamic anterior.
Dacă se omite dealocarea unei zone de memorie, aceasta va rămâne alocată pe întreaga durata de rulare a procesului. Ori de câte ori nu mai este nevoie de o zonă de memorie, aceasta trebuie dealocată pentru eficiența utilizării spațiului de memorie.
Nu trebuie neapărat realizată dealocarea diverselor zone înainte de un apel exit sau înainte de încheierea programului pentru că acestea sunt automat eliberate de sistemul de operare.
Atenție! Pot apărea probleme și dacă se încearcă dealocarea aceleiași regiuni de memorie de două sau mai multe ori și se corup datele interne de management al zonelor alocate dintr-un heap.
În Linux, alocarea memoriei pentru procesele utilizator se realizează prin intermediul funcțiilor de bibliotecă malloc, calloc și realloc, iar dealocarea ei prin intermediul funcției free. Aceste funcții reprezintă apeluri de bibliotecă și rezolvă cererile de alocare și dealocare de memorie, pe cât posibil, în user space.
void *malloc(size_t size); void *calloc(size_t nmemb, size_t size); void *realloc(void *ptr, size_t size); void free(void *ptr);
Întotdeauna eliberați (free) memoria alocată. Memoria alocată de proces este eliberată automat la terminarea procesului, însă, de exemplu în cazul unui proces server care rulează foarte mult timp și nu eliberează memoria alocată acesta va ajunge să ocupe toată memoria disponibilă în sistem, având astfel consecințe nefaste.
Atenție! Nu eliberați de două ori aceeași zonă de memorie întrucât acest lucru va avea drept urmare coruperea tabelelor ținute de malloc ceea ce va avea din nou consecințe nefaste. Întrucât funcția free se întoarce imediat dacă primește ca parametru un pointer NULL
, este recomandat ca după un apel free, pointer-ul să fie resetat la NULL
.
În continuare, sunt prezentate câteva exemple de alocare a memoriei folosind malloc:
int n = atoi(argv[1]); char *str; /* usually malloc receives the size argument like: num_elements * size_of_element */ str = malloc((n + 1) * sizeof(char)); if (NULL == str) { perror("malloc"); exit(EXIT_FAILURE); } [...] free(str); str = NULL;
/* Creating an array of references to the arguments received by a program */ char **argv_no_exec; /* allocate space for the array */ argv_no_exec = malloc((argc - 1) * sizeof(char*)); if (NULL == argv_no_exec) { perror("malloc"); exit(EXIT_FAILURE); } /* set references to the program arguments */ for (i = 1; i < argc; i++) argv_no_exec[i-1] = argv[i]; [...] free(argv_no_exec); argv_no_exec = NULL;
Apelul realloc este folosit pentru modificarea spațiului de memorie alocat cu un apel malloc sau calloc:
int *p; p = malloc(n * sizeof(int)); if (NULL == p) { perror("malloc"); exit(EXIT_FAILURE); } [...] p = realloc(p, (n + extra) * sizeof(int)); [...] free(p); p = NULL;
Apelul calloc este folosit pentru alocarea de zone de memorie al căror conținut este nul (plin de valori de zero). Spre deosebire de malloc, apelul va primi două argumente: numărul de elemente și dimensiunea unui element.
list_t *list_v; /* list_t could be any C type ( except void ) */ list_v = calloc(n, sizeof(list_t)); if (NULL == list_v) { perror("calloc"); exit(EXIT_FAILURE); } [...] free(list_v); list_v = NULL;
Atenție Conform standardului C, este redundant (și considerat bad practice) să faceți cast la valoarea întoarsă de malloc.
int *p = (int *)malloc(10 * sizeof(int));
malloc întoarce void * care în C este automat convertit la tipul dorit. Mai mult, dacă se face cast, iar headerul stdlib.h
necesar pentru funcția malloc nu este inclus, nu se va genera eroare! Pe anumite arhitecturi, acest caz poate conduce la un comportament nedefinit. Spre deosebire de C, în C++ este nevoie de cast. Mai multe detalii despre această problemă: aici
Mai multe informații despre funcțiile de alocare găsiți în manualul bibliotecii standard C și în pagina de manual man malloc.
În Windows, un proces poate să-și creeze mai multe obiecte Heap
pe lângă Heap
-ul implicit al procesulului. Acest lucru este foarte util în momentul în care o aplicație alocă și dealocă foarte multe zone de memorie cu câteva dimensiuni fixe. Aplicația poate să-și creeze câte un Heap
pentru fiecare dimensiune și, în cadrul fiecărui Heap
, să aloce zone de aceeași dimensiune reducând astfel la maxim fragmentarea heap-ului.
Pentru crearea, respectiv distrugerea unui Heap
se vor folosi funcțiile HeapCreate și HeapDestroy:
HANDLE HeapCreate( DWORD flOptions, SIZE_T dwInitialSize, SIZE_T dwMaximumSize ); BOOL HeapDestroy( HANDLE hHeap );
Pentru a obține un descriptor al heap-ului implicit al procesului (în cazul în care nu dorim crearea altor heapuri) se va apela funcția GetProcessHeap. Pentru a obține descriptorii tuturor heap-urilor procesului se va apela GetProcessHeaps.
Există, de asemenea, funcții care enumeră toate blocurile alocate într-un heap, validează unul sau toate blocurile alocate într-un heap sau întorc dimensiunea unui bloc pe baza descriptorului de heap și a adresei blocului: HeapWalk, HeapValidate, HeapSize.
Pentru alocarea, dealocarea, redimensionarea unui bloc de memorie din Heap
, Windows pune la dispoziția programatorului funcțiile HeapAlloc, HeapFree, respectiv HeapReAlloc, cu signaturile de mai jos:
LPVOID HeapAlloc( HANDLE hHeap, DWORD dwFlags, SIZE_T dwBytes ); BOOL HeapFree( HANDLE hHeap, DWORD dwFlags, LPVOID lpMem ); LPVOID HeapReAlloc( HANDLE hHeap, DWORD dwFlags, LPVOID lpMem, SIZE_T dwBytes );
În continuare, este prezentat un exemplu de folosire al acestor funcții:
#include <windows.h> #include "utils.h" /* Example of matrix allocation */ int main(void) { HANDLE processHeap; DWORD **mat; DWORD i, j, m = 10, n = 10; processHeap = GetProcessHeap(); DIE (processHeap == NULL, "GetProcessHeap"); mat = HeapAlloc(processHeap, 0, m * sizeof(*mat)); DIE (mat == NULL, "HeapAlloc"); for (i = 0; i < n; i++) { mat[i] = HeapAlloc(processHeap, 0, n * sizeof(**mat)); if (mat[i] == NULL) { PrintLastError("HeapAlloc failed"); goto freeMem; /* free previously allocated memory */ } } /* do work */ freeMem: for (j = 0; j < i; j++) HeapFree(processHeap, 0, mat[j]); HeapFree(processHeap, 0, mat); return 0; }
Pe sistemele Windows se pot folosi și funcțiile bibliotecii standard C pentru gestiunea memoriei: malloc, realloc, calloc, free, dar apelurile de sistem specifice Windows oferă funcționalități suplimentare și nu implică legarea bibliotecii standard C în executabil.
Lucrul cu heap-ul este una dintre cauzele principale ale aparițiilor problemelor de programare. Lucrul cu pointerii, necesitatea folosirii unor apeluri de sistem/bibliotecă pentru alocare/dealocare, pot conduce la o serie de probleme care afectează (de multe ori fatal) funcționarea unui program.
Problemele cele mai des întâlnite în lucrul cu memoria sunt:
Ambele probleme și utilitarele care pot fi folosite pentru combaterea acestora vor fi prezentate în continuare.
De obicei, accesarea unei zone de memorie nevalide rezultă într-o eroare de pagină (page fault) și terminarea procesului (în Unix înseamnă trimiterea semnalului SIGSEGV → afișarea mesajului 'Segmentation fault'). Totuși, dacă eroarea apare la o adresă nevalidă, dar într-o pagină validă, hardware-ul și sistemul de operare nu vor putea sesiza acțiunea ca fiind nevalidă. Acest lucru este din cauza faptului că alocarea memoriei se face la nivel de pagină. Spre exemplu, pot exista situații în care să fie folosită doar jumătate din pagină. Deși cealaltă jumătate conține adrese nevalide, sistemul de operare nu va putea detecta accesele nevalide la acea zonă. Mai multe detalii în laboratorul de Memorie virtuală
Asemenea accese pot duce la coruperea heap-ului și la pierderea consistenței memoriei alocate. După cum se va vedea în continuare, există utilitare care ajută la detectarea acestor situații.
Un tip special de acces nevalid este buffer overflow. Acest tip de atac presupune referirea unor regiuni valide din spațiul de adresă al unui proces prin intermediul unei variabile care nu ar trebui să poată referenția aceste adrese. De obicei, un atac de tip buffer overflow rezultă în rularea de cod nesigur. Protejarea împotriva atacurilor de tip buffer overflow se realizează prin verificarea limitelor unui buffer/vector fie la compilare, fie la rulare.
O comandă foarte utilă atunci când se depanează programe complexe este backtrace
. Această comandă afișează toate apelurile de funcții în curs de execuție.
#include <stdio.h> #include <stdlib.h> static int fibonacci(int no) { if (1 == no || 2 == no) return 1; return fibonacci(no-1) + fibonacci(no-2); } int main(void) { short int numar, baza=10; char sir[1]; scanf("%s", sir); numar=strtol(sir, NULL, baza); printf("fibonacci(%d)=%d\n", numar, fibonacci(numar)); return 0; }
Pe exemplul de mai sus, vom demonstra utilitatea comenzii backtrace
:
so@spook$ gcc -Wall exemplul-7.c -g so@spook$ gdb a.out (gdb) break 8 Breakpoint 1 at 0x8048482: file exemplul-7.c, line 8. (gdb) run Starting program: /home/tavi/cursuri/so/lab/draft/intro/a.out 7 Breakpoint 1, fibonacci (no=2) at exemplul-7.c:8 8 return 1; (gdb) bt #0 fibonacci (no=2) at exemplul-7.c:8 #1 0x0804849d in fibonacci (no=3) at exemplul-7.c:9 #2 0x0804849d in fibonacci (no=4) at exemplul-7.c:9 #3 0x0804849d in fibonacci (no=5) at exemplul-7.c:9 #4 0x0804849d in fibonacci (no=6) at exemplul-7.c:9 #5 0x0804849d in fibonacci (no=7) at exemplul-7.c:9 #6 0x0804851c in main () at exemplul-7.c:20 #7 0x4003d280 in __libc_start_main () from /lib/libc.so.6 (gdb)
Se observă că la afișarea apelurilor de funcții se listează și parametrii cu care a fost apelată funcția. Acest lucru este posibil datorită faptului că atât variabilele locale, cât și parametrii acesteia sunt păstrați pe stivă până la ieșirea din funcție. (pentru detalii, revedeți secțiunea despre stivă)
Fiecare funcție are alocată pe stivă un frame, în care sunt plasate variabilele locale funcției, parametrii funcției și adresa de revenire din funcție. În momentul în care o funcție este apelată, se creează un nou frame prin alocarea de spațiu pe stivă de către funcția apelată. Astfel, dacă avem apeluri de funcții imbricate, atunci stiva va conține toate frame-urile tuturor funcțiilor apelate imbricat.
GDB dă posibilitatea utilizatorului să examineze frame-urile prezente în stivă. Astfel, utilizatorul poate alege oricare din frame-urile prezente folosind comanda frame
. După cum s-a observat, exemplul anterior are un bug ce se manifestă atunci când numărul introdus de la tastatură depășește dimensiunea buffer-ului alocat (static). Acest tip de eroare poartă denumirea de buffer overflow și este extrem de gravă. Cele mai multe atacuri de la distanță pe un sistem sunt cauzate de acest tip de erori. Din păcate, acest tip de eroare nu este ușor de detectat, pentru că în procesul de buffer overrun se pot suprascrie alte variabile, ceea ce duce la detectarea erorii nu imediat când s-a făcut suprascrierea, ci mai târziu, când se va folosi variabila afectatã.
so@spook$ gdb a.out (gdb) run Starting program: /home/tavi/cursuri/so/lab/draft/intro/a.out 10 Program received signal SIGSEGV, Segmentation fault. 0x08048497 in fibonacci (no=-299522) at exemplul-7.c:9 9 return fibonacci(no-1) + fibonacci(no-2); (gdb) bt -5 #299520 0x0804849d in fibonacci (no=-2) at exemplul-7.c:9 #299521 0x0804849d in fibonacci (no=-1) at exemplul-7.c:9 #299522 0x0804849d in fibonacci (no=0) at exemplul-7.c:9 #299523 0x0804851c in main () at exemplul-7.c:20 #299524 0x4003e280 in __libc_start_main () from /lib/libc.so.6
Din analiza de mai sus se observă că funcția fibonacci a fost apelată cu valoarea 0. Cum funcția nu testează ca parametrul să fie valid, se va apela recursiv de un număr suficient de ori pentru a cauza umplerea stivei programului. Se pune problema cum s-a apelat funcția cu valoarea 0, când trebuia apelată cu valoarea 10.
so@spook$ gdb a.out (gdb) run Starting program: /home/tavi/cursuri/so/lab/draft/intro/a.out 10 Program received signal SIGSEGV, Segmentation fault. 0x08048497 in fibonacci (no=-299515) at exemplul-7.c:9 9 return fibonacci(no-1) + fibonacci(no-2); (gdb) bt -2 #299516 0x0804851c in main () at exemplul-7.c:20 #299517 0x4003d280 in __libc_start_main () from /lib/libc.so.6 (gdb) fr 299516 #299516 0x0804851c in main () at exemplul-7.c:20 20 printf("fibonacci(%d)=%d\n", numar, fibonacci(numar)); (gdb) print numar $1 = 0 (gdb) print baza $2 = 48 (gdb)
Se observă că problema este cauzată de faptul că variabila baza a fost alterată. Pentru a determina când s-a întâmplat acest lucru, se poate folosi comanda watch
. Această comandă primește ca parametru o expresie și va opri execuția programului de fiecare dată când valoarea expresiei se schimbă.
(gdb) quit so@spook$ gdb a.out (gdb) break main Breakpoint 1 at 0x80484d6: file exemplul-7.c, line 15. (gdb) run Starting program: /home/tavi/cursuri/so/lab/draft/intro/a.out Breakpoint 1, main () at exemplul-7.c:15 15 short int numar, baza=10; (gdb) n 18 scanf("%s", sir); (gdb) watch baza Hardware watchpoint 2: baza (gdb) continue Continuing. 10 Hardware watchpoint 2: baza Old value = 10 New value = 48 0x40086b41 in _IO_vfscanf () from /lib/libc.so.6 (gdb) bt #0 0x40086b41 in _IO_vfscanf () from /lib/libc.so.6 #1 0x40087259 in scanf () from /lib/libc.so.6 #2 0x080484ed in main () at exemplul-7.c:18 #3 0x4003d280 in __libc_start_main () from /lib/libc.so.6 (gdb)
Din analiza de mai sus se observă că valoarea variabilei este modificată în funcția _IO_vfscanf
, care la rândul ei este apelată de către functia scanf
. Dacă se analizează apoi parametrii pasați functiei scanf
, se observă imediat cauza erorii.
Pentru mai multe informații despre GDB consultați documentația online (alternativ pagina info - info gdb
) sau folosiți comanda help
din cadrul GDB.
glibc permite verificarea consistenței heap-ului prin intermediul apelului mcheck definit în mcheck.h
. Apelul mcheck forțează malloc
să execute diverse verificări de consistență precum scrierea peste un bloc alocat cu malloc
.
Alternativ, se poate folosi opțiunea -lmcheck
la legarea programului fără a afecta sursa acestuia.
Varianta cea mai simplă este folosirea variabilei de mediu MALLOC_CHECK_
. Dacă un program va fi lansat în execuție cu variabila MALLOC_CHECK_
configurată, atunci vor fi afișate mesaje de eroare (eventual programul va fi terminat forțat - aborted).
În continuare, este prezentat un exemplu de cod cu probleme în alocarea și folosirea heap-ului:
#include <stdio.h> #include <stdlib.h> #include <string.h> int main(void) { int *v1; v1 = malloc(5 * sizeof(*v1)); if (NULL == v1) { perror("malloc"); exit (EXIT_FAILURE); } /* overflow */ v1[6] = 100; free(v1); /* write after free */ v1[6] = 100; /* reallocate v1 */ v1 = malloc(10 * sizeof(int)); if (NULL == v1) { perror("malloc"); exit (EXIT_FAILURE); } return 0; }
Mai jos se poate vedea cum programul este compilat și rulat. Mai întâi este rulat fără opțiuni de mcheck, după care se definește variabila de mediu MALLOC_CHECK_ la rularea programului. Se observă că deși se depășește spațiul alocat pentru vectorul v1
și se referă vectorul după eliberarea spațiului, o rulare simplă nu rezultă în afișarea nici unei erori.
Totuși, dacă definim variabila de mediu MALLOC_CHECK_
, se detectează cele două erori. De observat că o eroare este detectată doar în momentul unui nou apel de memorie interceptat de mcheck.
so@spook$ make cc -Wall -g mcheck_test.c -o mcheck_test so@spook$ ./mcheck_test so@spook$ MALLOC_CHECK_=1 ./mcheck_test malloc: using debugging hooks *** glibc detected *** ./mcheck_test: free(): invalid pointer: 0x0000000000601010 *** *** glibc detected *** ./mcheck_test: malloc: top chunk is corrupt: 0x0000000000601020 ***
mcheck nu este o soluție completă și nu detectează toate erorile ce pot apărea în lucrul cu memoria. Detectează, totuși, un număr important de erori și reprezintă o facilitate importantă a glibc.
O descriere completă găsiți în pagina asociată din manualul glibc.
Un leak de memorie apare în două situații:
Memory leak-urile au ca efect reducerea cantității de memorie existentă în sistem. Se poate ajunge, în situații extreme, la consumarea întregii memorii a sistemului și la imposibilitatea de funcționare a diverselor aplicații ale acestuia.
Ca și în cazul problemei accesului nevalid la memorie, utilitarul Valgrind este foarte util în detectarea leak-urilor de memorie ale unui program.
Valgrind reprezintă o suită de utilitare folosite pentru operații de debugging și profiling. Cel mai popular este Memcheck, un utilitar care permite detectarea de erori de lucru cu memoria (accese nevalide, memory leak-uri etc.). Alte utilitare din suita Valgrind sunt Cachegrind
, Callgrind
utile pentru profiling sau Helgrind
, util pentru depanarea programelor multithreaded.
În continuare, ne vom referi doar la utilitarul Memcheck de detectare a erorilor de lucru cu memoria. Mai precis, acest utilitar detectează următoarele tipuri de erori:
Valgrind nu necesită adaptarea codului unui program, ci folosește direct executabilul (binarul) asociat unui program. La o rulare obișnuită Valgrind va primi argumentul –tool
pentru a preciza utilitarul folosit și programul care va fi verificat de erori de lucru cu memoria.
În exemplul de rulare, de mai jos, se folosește programul prezentat la secțiunea ''mcheck'':
so@spook$ valgrind --tool=memcheck ./mcheck_test ==17870== Memcheck, a memory error detector. ==17870== Copyright (C) 2002-2007, and GNU GPL'd, by Julian Seward et al. ==17870== Using LibVEX rev 1804, a library for dynamic binary translation. ==17870== Copyright (C) 2004-2007, and GNU GPL'd, by OpenWorks LLP. ==17870== Using valgrind-3.3.0-Debian, a dynamic binary instrumentation framework. ==17870== Copyright (C) 2000-2007, and GNU GPL'd, by Julian Seward et al. ==17870== For more details, rerun with: -v ==17870== ==17870== Invalid write of size 4 ==17870== at 0x4005B1: main (mcheck_test.c:17) ==17870== Address 0x5184048 is 4 bytes after a block of size 20 alloc'd ==17870== at 0x4C21FAB: malloc (vg_replace_malloc.c:207) ==17870== by 0x400589: main (mcheck_test.c:10) ==17870== ==17870== Invalid write of size 4 ==17870== at 0x4005C8: main (mcheck_test.c:22) ==17870== Address 0x5184048 is 4 bytes after a block of size 20 free'd ==17870== at 0x4C21B2E: free (vg_replace_malloc.c:323) ==17870== by 0x4005BF: main (mcheck_test.c:19) ==17870== ==17870== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 8 from 1) ==17870== malloc/free: in use at exit: 40 bytes in 1 blocks. ==17870== malloc/free: 2 allocs, 1 frees, 60 bytes allocated. ==17870== For counts of detected errors, rerun with: -v ==17870== searching for pointers to 1 not-freed blocks. ==17870== checked 76,408 bytes. ==17870== ==17870== LEAK SUMMARY: ==17870== definitely lost: 40 bytes in 1 blocks. ==17870== possibly lost: 0 bytes in 0 blocks. ==17870== still reachable: 0 bytes in 0 blocks. ==17870== suppressed: 0 bytes in 0 blocks. ==17870== Rerun with --leak-check=full to see details of leaked memory.
S-a folosit utilitarul Memcheck pentru obținerea informațiilor de acces la memorie.
Se recomandă folosirea opțiunii -g
la compilarea programului pentru a include în executabil informații de depanare. În rularea de mai sus, Valgrind a identificat două erori: una apare la linia 17 de cod și este corelată cu linia 10 (malloc
), iar cealaltă apare la linia 22 și este corelată cu linia 19 (free
):
v1 = (int *) malloc (5 * sizeof(*v1)); if (NULL == v1) { perror ("malloc"); exit (EXIT_FAILURE); } /* overflow */ v1[6] = 100; free(v1); /* write after free */ v1[6] = 100;
Exemplul următor reprezintă un program cu o gamă variată de erori de alocare a memoriei:
#include <stdlib.h> #include <string.h> int main(void) { char buf[10]; char *p; /* no init */ strcat(buf, "al"); /* overflow */ buf[11] = 'a'; p = malloc(70); p[10] = 5; free(p); /* write after free */ p[1] = 'a'; p = malloc(10); /* memory leak */ p = malloc(10); /* underrun */ p--; *p = 'a'; return 0; }
În continuare, se prezintă comportamentul executabilului obținut la o rulare obișnuită și la o rulare sub Valgrind:
so@spook$ make cc -Wall -g valgrind_test.c -o valgrind_test so@spook$ ./valgrind_test so@spook$ valgrind --tool=memcheck ./valgrind_test ==18663== Memcheck, a memory error detector. ==18663== Copyright (C) 2002-2007, and GNU GPL'd, by Julian Seward et al. ==18663== Using LibVEX rev 1804, a library for dynamic binary translation. ==18663== Copyright (C) 2004-2007, and GNU GPL'd, by OpenWorks LLP. ==18663== Using valgrind-3.3.0-Debian, a dynamic binary instrumentation framework. ==18663== Copyright (C) 2000-2007, and GNU GPL'd, by Julian Seward et al. ==18663== For more details, rerun with: -v ==18663== ==18663== Conditional jump or move depends on uninitialised value(s) ==18663== at 0x40050D: main (valgrind_test.c:10) ==18663== ==18663== Invalid write of size 1 ==18663== at 0x400554: main (valgrind_test.c:20) ==18663== Address 0x5184031 is 1 bytes inside a block of size 70 free'd ==18663== at 0x4C21B2E: free (vg_replace_malloc.c:323) ==18663== by 0x40054B: main (valgrind_test.c:17) ==18663== ==18663== Invalid write of size 1 ==18663== at 0x40057C: main (valgrind_test.c:28) ==18663== Address 0x51840e7 is 1 bytes before a block of size 10 alloc'd ==18663== at 0x4C21FAB: malloc (vg_replace_malloc.c:207) ==18663== by 0x40056E: main (valgrind_test.c:24) ==18663== ==18663== ERROR SUMMARY: 6 errors from 3 contexts (suppressed: 8 from 1) ==18663== malloc/free: in use at exit: 20 bytes in 2 blocks. ==18663== malloc/free: 3 allocs, 1 frees, 90 bytes allocated. ==18663== For counts of detected errors, rerun with: -v ==18663== searching for pointers to 2 not-freed blocks. ==18663== checked 76,408 bytes. ==18663== ==18663== LEAK SUMMARY: ==18663== definitely lost: 20 bytes in 2 blocks. ==18663== possibly lost: 0 bytes in 0 blocks. ==18663== still reachable: 0 bytes in 0 blocks. ==18663== suppressed: 0 bytes in 0 blocks. ==18663== Rerun with --leak-check=full to see details of leaked memory.
Se poate observa că, la o rulare obișnuită, programul nu generează nici un fel de eroare. Totuși, la rularea cu Valgrind, apar erori în 3 contexte:
strcat
(linia 10) șirul nu a fost inițializatfree
(linia 20: p[1] = 'a
')
În plus, există leak-uri de memorie datorită noului apel malloc
care asociază o nouă valoare lui p
(linia 24).
Valgrind este un utilitar de bază în depanarea programelor. Este facil de folosit (nu este intrusiv, nu necesită modificarea surselor) și permite detectarea unui număr important de erori de programare apărute ca urmare a gestiunii defectuoase a memoriei.
Informații complete despre modul de utilizare a Valgrind și a utilitarelor asociate se găsesc în paginile de documentație Valgrind.
Un alt utilitar care poate fi folosit la depanarea erorilor de lucru cu memoria este mtrace. Acest utilitar ajută la identificarea leak-urilor de memorie ale unui program.
Utilitarul mtrace se folosește cu apelurile mtrace și muntrace implementate în biblioteca standard C:
void mtrace(void); void muntrace(void);
Utilitarul mtrace introduce handlere pentru apelurile de biblioteca pentru lucrul cu memoria (malloc
, realloc
, free
). Apelurile mtrace și muntrace activează, respectiv dezactivează monitorizarea apelurilor de bibliotecă de lucru cu memoria.
Jurnalizarea operațiilor efectuate se realizează în fișierul definit de variabila de mediu MALLOC_TRACE
. După ce apelurile au fost înregistrate în fișierul specificat, utilizatorul poate să folosească utilitarul mtrace
pentru analiza acestora.
În exemplul de mai jos este prezentată o situație în care se alocă memorie fără a fi eliberată:
#include <stdlib.h> #include <mcheck.h> int main(void) { /* start memcall monitoring */ mtrace(); malloc(10); malloc(20); malloc(30); /* stop memcall monitoring */ muntrace(); return 0; }
În secvența de comenzi ce urmează se compilează fișierul de mai sus, se stabilește fișierul de jurnalizare și se rulează comanda mtrace
pentru a detecta problemele din codul de mai sus.
so@spook$ gcc -Wall -g mtrace_test.c -o mtrace_test so@spook$ export MALLOC_TRACE=./mtrace.log so@spook$ ./mtrace_test so@spook$ cat mtrace.log = Start @ ./mtrace_test:[0x40054b] + 0x601460 0xa @ ./mtrace_test:[0x400555] + 0x601480 0x14 @ ./mtrace_test:[0x40055f] + 0x6014a0 0x1e = End so@spook$ mtrace mtrace_test mtrace.log Memory not freed: ----------------- Address Size Caller 0x0000000000601460 0xa at /home/razvan/school/so/labs/lab4/samples/mtrace.c:11 0x0000000000601480 0x14 at /home/razvan/school/so/labs/lab4/samples/mtrace.c:12 0x00000000006014a0 0x1e at /home/razvan/school/so/labs/lab4/samples/mtrace.c:15
Mai multe informații despre detectarea problemelor de alocare folosind mtrace găsiți în pagina asociată din manualul glibc.
Denumirea de “dublă dealocare” oferă o bună intuiție asupra cauzei: eliberarea de două ori a aceluiași spațiu de memorie. Dubla dealocare poate avea efecte negative deoarece afectează structurile interne folosite pentru a gestiona memoria ocupată.
În ultimele versiuni ale bibliotecii standard C, se detectează automat cazurile de dublă dealocare. Fie exemplul de mai jos:
#include <stdlib.h> int main(void) { char *p; p = malloc(10); free(p); free(p); return 0; }
Rularea executabilului obținut din programul de mai sus duce la afișarea unui mesaj specific al glibc de eliberare dublă a unei regiuni de memorie și terminare a programului:
so@spook$ make cc -Wall -g dfree.c -o dfree so@spook$ ./dfree *** glibc detected *** ./dfree: double free or corruption (fasttop): 0x0000000000601010 *** ======= Backtrace: ========= /lib/libc.so.6[0x2b675fdd502a] /lib/libc.so.6(cfree+0x8c)[0x2b675fdd8bbc] ./dfree[0x400510] /lib/libc.so.6(__libc_start_main+0xf4)[0x2b675fd7f1c4] ./dfree[0x400459]
Situațiile de dublă dealocare sunt, de asemenea, detectate de Valgrind.
Utilitarele prezentate mai sus nu sunt singurele folosite pentru detectarea problemelor apărute in lucrul cu memoria. Alte utilitare sunt:
În rezolvarea laboratorului folosiți arhiva de sarcini lab04-tasks.zip
Observații: Pentru a vă ajuta la implementarea exercițiilor din laborator, în directorul utils
din arhivă există un fișier utils.h
cu funcții utile.
1-adr_space
și deschideți sursa adr_space.c
.objdump -t adr_space | grep var
l
și g
din output-ul obținut? (Hint: man objdump, flag)readelf
(Hint: man readelf, -x)2-alloc
, compilați și rulați programul alloc
.valgrind
pentru a detecta eventualele probleme de lucru cu memoria și corectați-le.Segmentation Fault
3-gdb
și inspectați sursa. stdin
și să-l afișeze.(gdb) help
main
(gdb) frame main
buf
: (gdb) print buf
buf = NULL
(gdb) kill
main
: (gdb) break main
buf
înainte și după apelul funcției malloc
next
pentru a trece la instrucțiunea următoare, fără a urmări apelul de funcție.4-struct
.struct.c
conform comentariilor marcate cu TODO
.allocate_flowers
alocați memorie pentru no
elemente de tip flower_info
.free_flowers
eliberați memoria alocată în funcția allocate_flowers
.–tool=memcheck
pentru valgrind
.show_snapshot
iterați pe toată lungimea stivei și afișați adresa și valoarea de la adresa curentătake_snapshot
salvați în structura de date ce reține imaginea stivei câmpurile adresă și valoare.stack_elements
?f2
pune pe stivă un vector de 3 întregi. În ce ordine sunt puse elementele vectorului pe stivă?f2
?show_message
fără a o apela explicit. Astfel, după apelul funcției f2
, fluxul programului nu se va mai întoarce în funcția f1
, ci va executa show_message
. bp
/ebp
și sp
/esp
?6-trim
analizați programul trim.c
, compilați și rulați executabilul trim
.MALLOC_CHECK_=1 ./trim
7-endian
și inspectați sursa endian.c
w
afișați numărul n=0xDEADBEEF
n
ca la un vector de caractere8-counter
inc()
care întoarce de fiecare dată un întreg reprezentând numărul de apeluri până în momentul respectiv al funcției inc
.9-bad_stack
bad_stack.c
.main
, prima oară se afișează valoarea din str
, iar a doua oară nu?myfun()
. Găsiți minim 3 metode de a rezolva problema.malloc
și free
malloc-wrapper
.xmalloc.c
și xmalloc.h
.xmalloc.c
cu definiția funcției xmalloc
, și fișierul xmalloc.h
cu macrodefiniția xfree
după cum urmează:xmalloc
se alocă spațiu folosind HeapAlloc
; trebuie să verificați dacă alocarea are succes sau nuxfree
este un macro care primește ca argument pointer-ul de eliberat; se apelează HeapFree
și pointer-ul este resetat la NULL
xfree
care să realizeze aceleași operații?xmalloc
test.c
.tensor_alloc
, respectiv tensor_free
care alocă/dealocă un vector tridimensional (tensor).xmalloc
și xfree
implementate în cadrul exercițiului 1.TODO
.my_malloc.c
./test
LD_LIBRARY_PATH
(revedeți secțiunea de biblioteci partajate din laboratorul 1)