Laboratoare:Gestiunea memoriei

= Gestiunea memoriei =

Subsistemul de gestiune a memoriei din cadrul unui sistem de operare este folosit de toate celelalte subsisteme: scheduling, I/O, filesystem, gestiunea proceselor, networking. Memoria este o resursă importantă și sunt necesari algoritmi eficienți de utilizare și gestiune a acesteia.

Rolul subsistemului de gestiune a memoriei este de a ține evidența zonelor de memorie fizică ocupate sau libere, de a oferi proceselor sau celorlalte subsisteme acces la memorie și de a mapa paginile de memorie virtuală ale unui proces (pages) peste paginile fizice (frames).

Nucleul sistemului de operare oferă un set de interfețe (apeluri de sistem) care permit alocarea/dezalocarea 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 invalide, 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 de sistemul de operare programatorului.

= Spațiul de adresă al unui proces =



Spațiul de adrese 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 începutul fie la sfârșitul spațiului de adresă. (Note). Î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.

Zona de cod
Zona/segmentul de cod (denumit și 'text segment') reprezintă instrucțiunile programului. Registrul de tip 'instruction pointer' va referi adrese din zona de cod. Se citește instrucțiunea indicată, 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.

Zone de date
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 sub-tipuri de zone de date.

.data
Zona .data conține variabilele globale inițializate la valori nenule ale unui program. De exemplu:

.bss
Zona .bss conține variabilele globale neinițializate sau inițializate la zero ale unui program. De exemplu:

În general acestea nu vor fi prealocate în executabil ci în momentul creării precesului. Alocare zonei .bss se face peste pagini fizice zero (zeroed frames).

.rodata
Zona .rodata conține informație care poate fi doar citită, nu și modificată. Aici sunt stocate constantele:

și literalii:

Stiva
Stiva este o regiune dinamica în cadrul unui proces. Stiva este folosită pentru a reține "stack frame-urile" (link) în cazul apelurilor de funcții și pentru a stoca variabilele locale. Pe marea majoritate a arhitecturilor moderne stiva crește în jos și heap-ul crește în sus. Stiva este gestionată automat de compilator. La fiecare revenire din funcție stiva este golită.

În figura de mai jos este prezentată o vedere conceptuală asupra stivei in momentul apelului unei funcţii.



Heap-ul
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 se află doar la runtime.

La fel ca și stiva, heap-ul este o regiune dinamică și 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ă dezaloce. Problemele frecvente în majoritatea programelor țin de pierderea referințelor la zonele alocate (memory leaks) sau referirea de zone nealocate sau insuficient alocate (accese invalide).

La limbaje precum Java, Lisp, etc. unde nu există "pointer freedom", eliberarea spațiului alocat se face automat prin intermediul unui garbage collector (link). Pe aceste sisteme se previne problema pierderii referințelor, dar încă rămâne activă problema referirii zonelor nealocate.

= Alocarea memoriei =

Alocarea memoriei este realizată static de compilator sau dinamic, în timpul execuției. Alocarea statică este realizată în segmentele de date pentru variabilele locale 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 functii (mai putin 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 cat spațiu va ocupa o varibilă, se recomandă alocarea ei statică, pentru a preveni erorile frecvente 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 dezalocă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.

Alocarea memoriei în Linux
În Linux alocarea memoriei pentru procesele utilizator se realizează prin intermediul funcțiilor de bibliotecă malloc, calloc și realloc iar dezalocarea ei prin intermediul funcției free. Aceste funcții reprezintă apeluri de bibliotecă și rezolvă cererile de alocare și dezalocare de memorie pe cât posibil în user space. Așadar, se țin niște tabele care specifică zonele de memorie alocate în heap. Dacă există zone libere pe heap, un apel malloc care cere o zonă de memorie care poate fi încadrată într-o zonă liberă din heap va fi satisfăcut imediat marcând în tabel zona respectivă ca fiind alocată și întorcând programului apelant un pointer spre ea. Dacă în schimb se cere o zonă care nu încape în nicio zonă liberă din heap, malloc va încerca extinderea heap-ului prin apelul de sistem brk sau mmap.

Întotdeauna eliberați (free</tt>) memoria alocată. Memoria alocată de proces este eliberată automat la terminarea procesului însă în cazul unui proces server, de exemplu, care rulează foarte mult timp și nu eliberează memoria alocată acesta va ajunge să ocupe toată memoria disponibilă în sistem cauzând astfel consecințe nefaste. Atenție să nu eliberați de două ori aceeași zonă de memorie întrucât acest lucru va avea drept urmare coruperea tabelelor ținute de malloc</tt> ceea ce va duce din nou la consecințe nefaste. Întrucât funcția free</tt> se întoarce imediat dacă primește ca parametru un pointer NULL</tt>, este recomandat ca după un apel free</tt>, pointer-ul să fie resetat la NULL</tt>.

Câteva exemple de alocare a memoriei sunt prezentate în continuare:

Apelul realloc</tt> este folosit pentru modificarea spatiului de memorie alocat dupa un apel malloc</tt>:

Apelul calloc</tt> este folosit pentru alocarea de zone de memorie al căror conținut este nul (plin de valori de zero). Spre deosebire de malloc</tt>, apelul va primi două argumente: numărul de elemente și dimensiunea unui element.

Mai multe informații găsiți în manualul bibliotecii standard C și în pagina de manual man malloc.

Alocarea memoriei în Windows
În Windows un proces poate să-și creeze mai multe obiecte Heap</tt> pe lângă Heap</tt>-ul cu care este creat procesul. Acest lucru este foarte util în momentul în care o aplicație alocă și dezalocă foarte multe zone de memorie cu câteva dimensiuni fixe. Aplicația poate să-și creeze câte un Heap</tt> pentru fiecare dimensiune și, în cadrul fiecărui Heap</tt>, să aloce zone de aceeași dimensiune reducând astfel la maxim fragmentarea heapului.

Pentru crearea, respectiv distrugerea, unui Heap</tt> se vor folosi funcțiile HeapCreate</tt> și <tt>HeapDestroy</tt>:

Pentru a obține un descriptor al heapului cu care a fost creat procesul (în cazul în care nu dorim crearea altor heapuri) se va apela funcția <tt>GetProcessHeap</tt>. Pentru a obține descriptorii tuturor heapurilor procesului se va apela <tt>GetProcessHeaps</tt>.

Exista, 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, HeapSize, HeapValidate.

Pentru alocarea, dezalocarea, redimensionarea unui bloc de memorie din <tt>Heap</tt>, Windows pune la dispoziția programatorului funcțiile <tt>HeapAlloc</tt>, <tt>HeapFree</tt>, respectiv <tt>HeapReAlloc</tt>, cu signaturile de mai jos:

Câteva exemple de folosire a acestor funcții sunt prezentate în continuare:

Pe sistemele Windows se pot folosi și funcțiile bibliotecii standard C pentru gestiunea memoriei: <tt>malloc</tt>, <tt>realloc</tt>, <tt>calloc</tt>, <tt>free</tt>, dar apelurile de sistem specifice Windows oferă funcționalități suplimentare și nu implică legarea bibliotecii standard C în executabil.

= Dezalocarea memoriei =

Pentru dezalocarea memoriei, se folosesc funcțiile <tt>free</tt>, respectiv <tt>HeapFree</tt>. Funcțiile primesc ca argument un pointer la un spațiu de memorie alocat anterior cu o funcție de alocare.

Dacă se omite dezalocarea 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 dezalocată pentru eficiența utilizării spațiului de memorie.

Nu trebuie neapărat realizată dezalocarea diverselor zone înainte de un apel <tt>exit</tt> sau <tt>ExitProcess</tt> sau înainte de încheierea programului pentru că acestea sunt automat eliberate de sistemul de operare.

Probleme pot apărea și dacă se încearcă dezalocarea aceleiași regiuni de memorie de două sau mai multe ori și se corup listele

= Probleme de lucru cu memoria =

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/dezalocare, 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:
 * accesul invalid la memorie
 * leak-urile de memorie

Accesul invalid la memorie prespune accesarea unor zone care nu au fost alocate sau au fost eliberate. Leak-urile de memorie sunt situațiile în care se pierde referința la o zonă alocată anterior. Acea zonă va rămâne ocupată până la încheierea procesului. Ambele probleme și utilitarele care pot fi folosite pentru combaterea acestora vor fi prezentate în continuare.

Acces invalid
De obicei, accesarea unei zone de memorie invalide 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ă invalidă dar într-o pagină validă, hardware-ul și sistemul de operare nu vor putea sesiza acțiunea ca fiind invalidă. Acest lucru se datorează faptului că alocarea memoriei se face la nivel de pagină. Pot exista situații în care să fie folosită doar jumătate din pagină. Deși cealaltă jumătate conține adrese invalide, sistemul de operare nu va putea detecta accesele invalide la acea zonă.

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 invalid 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. Protecția la accese de tip buffer overflow se realizează prin verificarea limitelor unui buffer/vector fie la compilare, fie la rulare.

GDB - Detectarea zonei de acces invalid de tip page fault
Pe lângă facilități de bază precum urmărirea unei variabile sau configurarea de puncte de oprire (breakpoints), GDB pune la dispoziția utilizatorilor și comenzi avansate, utile în anumite cazuri. Comanda <tt>disassamble</tt> poate fi folosită pentru a afisa codul mașină generat de compilator. Comanda <tt>info reg</tt> afișează conținutul registrelor. Aceste comenzi sunt folosite rar, atunci când utilizatorul încearcă să depaneze codul generat de compilator, sau când are părți din program scrise direct în asamblare.

O comandă foarte utilă atunci când se depanează programe complexe este <tt>backtrace</tt>. Această comandă afișează toate apelurile de funcții în curs de execuție.

Exemplu: fibonacci_gdb_test.c

Pentru exemplul de mai sus, vom demonstra utilitatea comenzii <tt>backtrace</tt>:

[tavi@dhcp-48 intro]$ gcc -Wall exemplul-7.c -g [tavi@dhcp-48 intro]$ 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 (gdb)
 * 1) 0 fibonacci (no=2) at exemplul-7.c:8
 * 2) 1 0x0804849d in fibonacci (no=3) at exemplul-7.c:9
 * 3) 2 0x0804849d in fibonacci (no=4) at exemplul-7.c:9
 * 4) 3 0x0804849d in fibonacci (no=5) at exemplul-7.c:9
 * 5) 4 0x0804849d in fibonacci (no=6) at exemplul-7.c:9
 * 6) 5 0x0804849d in fibonacci (no=7) at exemplul-7.c:9
 * 7) 6 0x0804851c in main  at exemplul-7.c:20
 * 8) 7 0x4003d280 in __libc_start_main  from /lib/libc.so.6

Se observă că la afișarea apelurilor de funcții se afișează ș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.

Fiecare funcție are alocată pe stivă un frame, în care sunt plasate variabilele locale funcției, parametrii pasați funcției și adresa de revenire din functie. Î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 <tt>frame</tt>. 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ã.

[tavi@tropaila intro]$ 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
 * 1) 299520 0x0804849d in fibonacci (no=-2) at exemplul-7.c:9
 * 2) 299521 0x0804849d in fibonacci (no=-1) at exemplul-7.c:9
 * 3) 299522 0x0804849d in fibonacci (no=0) at exemplul-7.c:9
 * 4) 299523 0x0804851c in main at exemplul-7.c:20
 * 5) 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.

[tavi@dhcp-48 intro]$ 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 (gdb) fr 299516 20             printf("fibonacci(%d)=%d\n", numar, fibonacci(numar)); (gdb) print numar $1 = 0 (gdb) print baza $2 = 48 (gdb)
 * 1) 299516 0x0804851c in main at exemplul-7.c:20
 * 2) 299517 0x4003d280 in __libc_start_main from /lib/libc.so.6
 * 1) 299516 0x0804851c in main at exemplul-7.c:20

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 <tt>watch</tt>. 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 [tavi@dhcp-48 intro]$ 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 (gdb)
 * 1) 0 0x40086b41 in _IO_vfscanf  from /lib/libc.so.6
 * 2) 1 0x40087259 in scanf  from /lib/libc.so.6
 * 3) 2 0x080484ed in main  at exemplul-7.c:18
 * 4) 3 0x4003d280 in __libc_start_main  from /lib/libc.so.6

Din analiza de mai sus se observă că valoarea variabilei este modificată în funcția <tt>_IO_vfscanf</tt>, care la rândul ei este apelată de către functia <tt>scanf</tt>. Dacă se analizează apoi parametrii pasați functiei <tt>scanf</tt> se observã imediat cauza erorii.

Pentru mai multe informații despre GDB consultați manualul online (alternativ pagina info - <tt>info gdb</tt>) sau folosiți comanda <tt>help</tt> din cadrul GDB.

mcheck - verificarea consistenței heap-ului
glibc permite verificarea consistenței heap-ului prin intermediul apelului mcheck definit în <tt>mcheck.h</tt>. Apelul mcheck forțează <tt>malloc</tt> să execute diverse verificări de consistență precum scrierea peste un bloc alocat cu <tt>malloc</tt>.

Alternativ, se poate folosi opțiunea <tt>-lmcheck</tt> 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 <tt>MALLOC_CHECK_</tt> configurată, atunci vor fi afișate mesaje de eroare (eventual programul va fi terminat forțat - aborted).

Mai jos se găsește un exemplu de cod cu probleme în alocarea și folosirea heap-ului:

Exemplu: mcheck_test.c

Mai jos 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 <tt>v1</tt> ș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 <tt>MALLOC_CHECK_</tt>, se detectează cele două erori. De observat că o eroare este detectată doar în momentul unui nou apel de memorie interceptat de mcheck.

razvan@valhalla:~/school/2007-2008/so/labs/lab4/samples/mcheck$ make cc -Wall -g   mcheck_test.c   -o mcheck_test razvan@valhalla:~/school/2007-2008/so/labs/lab4/samples/mcheck$ ./mcheck_test razvan@valhalla:~/school/2007-2008/so/labs/lab4/samples/mcheck$ 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.

Leak-uri de memorie
Un leak de memorie apare în două situații:
 * un program omite să elibereze o zonă de memorie
 * un program pierde referința la o zonă de memorie dealocată și, drept consecință, nu o poate elibera

Memory leak-urile au ca efect reducerea cantității de memorie existentă în sistem. Se poate ajunge, în situațiile 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 invalid la memorie, utilitarul Valgrind este foarte util în detectarea leak-urilor de memorie ale unui program.

mtrace
Un 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:

Utilitarul mtrace introduce handlere pentru apelurile de biblioteca de lucru cu memoria (<tt>malloc</tt>, <tt>realloc</tt>, <tt>free</tt>). Apelurile <tt>mtrace</tt> și <tt>muntrace</tt> activează, respectiv dezactivează monitorizarea apelurilor de bibliotecă de lucru cu memoria.

Jurnalizarea operațiilor efectuate se realizează în fișierul definit de variabile de mediu MALLOC_TRACE. După ce apelurile au fost înregistrate în fișierul specificat, utilizatorul poate să folosească utilitarul <tt>mtrace</tt> pentru analiza acestora.

În exemplul de mai jos este prezentată o situație în care se alocă memorie fără a fi eliberată:

Exemplu: mtrace_test.c

În secvența de comenzi de mai jos se compilează fișierul de mai sus, se stabilește fișierul de jurnalizare și se rulează comanda <tt>mtrace</tt> pentru a detecta problemele din codul de mai sus.

$ gcc -Wall -g mtrace_test.c -o mtrace_test $ export MALLOC_TRACE=./mtrace.log $ ./mtrace_test $ cat mtrace.log = Start @ ./mtrace_test:[0x40054b] + 0x601460 0xa @ ./mtrace_test:[0x400555] + 0x601480 0x14 @ ./mtrace_test:[0x40055f] + 0x6014a0 0x1e = End $ mtrace mtrace_test mtrace.log Memory not freed: - 		  Address     Size     Caller 0x0000000000601460     0xa  at /home/razvan/school/2007-2008/so/labs/lab4/samples/mtrace.c:11 0x0000000000601480    0x14  at /home/razvan/school/2007-2008/so/labs/lab4/samples/mtrace.c:12 0x00000000006014a0    0x1e  at /home/razvan/school/2007-2008/so/labs/lab4/samples/mtrace.c:15 Mai multe informații despre detectarea problemelor de alocare folosind mtrace gasiti în pagina asociată din manualul glibc.

Dublă dezalocare
Denumirea "dublă dezalocare" oferă o bună intuiție asupra cauzei: eliberarea de două ori a aceluiași spațiu de memorie. Dubla dezalocare 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ă dezalocare. Fie exemplul de mai jos:

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 terminarea programului:

razvan@valhalla:~/school/2007-2008/so/labs/lab4/samples/dfree$ make cc -Wall -g   dfree.c   -o dfree razvan@valhalla:~/school/2007-2008/so/labs/lab4/samples/dfree$ ./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ții de dezalocare sunt, de asemenea, detectate de Valgrind.

Valgrind
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 invalide, 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:
 * folosirea de memorie neinițializată
 * citirea/scrierea din/în memorie după ce regiunea respectivă a fost eliberată
 * citirea/scrierea dincolo de sfârșitul zonei alocate
 * citirea/scrierea pe stivă în zone necorespunzătoare
 * memory leak-uri
 * folosirea necorespunzătore de apeluri malloc/new și free/delete

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 <tt>--tool</tt> 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:

razvan@valhalla:~/school/2007-2008/so/labs/lab4/samples/mcheck$ 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 <tt>memcheck</tt> pentru obținerea informațiilor de acces la memorie.

Se recomandă folosirea opțiunii <tt>-g</tt> la compilarea programului pentru a prezenta î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 (<tt>malloc</tt>), iar cealaltă apare la linia 22 și este corelată cu linia 19 (<tt>free</tt>):

Exemplul următor reprezintă un program cu o gamă variată de erori de alocare a memoriei:

Exemplu: valgrind_test.c

În continuare, se prezintă comportamentul executabilului obținut la o rulare obișnuită și la o rulare sub Valgrind:

razvan@valhalla:~/school/2007-2008/so/labs/lab4/samples/valgrind$ make cc -Wall -g   valgrind_test.c   -o valgrind_test razvan@valhalla:~/school/2007-2008/so/labs/lab4/samples/valgrind$ ./valgrind_test razvan@valhalla:~/school/2007-2008/so/labs/lab4/samples/valgrind$ 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 Valgrind apar erori în 3 contexte:
 * 1) la apelul <tt>strcat</tt> (linia 10) șirul nu a fost inițializat
 * 2) se scrie în memorie după <tt>free</tt> (linia 20: <tt>p[1] = 'a'</tt>)
 * 3) underrun (linia 28)

În plus există leak-uri de memorie datorită noului apel <tt>malloc</tt> care asociază o nouă valoare lui <tt>p</tt> (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 moduol de utilizare a Valgrind și a utilitarelor asociate se găsesc în paginile de documentație Valgrind.

Alte utilitare pentru depanarea problemelor de lucru cu memoria
Utilitarele prezentate mai sus nu sunt singurele folosite pentru detectarea problemelor aparute in lucrul cu memoria. Alte utilitare sunt:


 * dmalloc
 * mpatrol
 * DUMA
 * Electric Fence

= Exerciții =

Prezentare
Pentru a urmări mai ușor noțiunile expuse la începutul laboratorului urmăriți această prezentare. odp|pdf

Quiz
Pentru autoevaluare raspundeți la întrebările din acest quiz.

Exerciții pre-laborator
Folosiți [[media:lab4-pre.zip|arhiva de pre-sarcini]] a laboratorului.

Linux
Folosiți directorul <tt>lin/</tt> din [[media:lab4-pre.zip|arhiva de pre-sarcini]] a laboratorului.


 * 1) Intrați în subdirectorul <tt>my_malloc/</tt>.
 * 2) * În fișierul <tt>my_malloc_test.c</tt>, completați funcția <tt>main</tt> alocați spațiu pentru <tt>N_ELEM</tt> elemente întregi în pointerul <tt>elem_p</tt>.
 * 3) * Completați două poziții din vectorul obținut (indicat de <tt>elem_p</tt>): poziția <tt>ARRAY_POS_1</tt> și <tt>ARRAY_POS_2</tt>.
 * 4) * La completare folosiți două metode diferite (dereferențiere ca pointer și referire sub formă de vector).
 * 5) * Eliberați spațiul ocupat de vector.
 * 6) * Folosiți fișierul Makefile din director pentru compilarea programului.
 * 7) * Rulați programul astfel obținut.
 * 8) * Testați folosind Valgrind că programul nu conține erori de lucru cu memoria.
 * 9) Rămâneți în subdirectorul <tt>my_malloc/</tt>.
 * 10) * Configurați macroul <tt>ARRAY_POS_2</tt> pentru a fi egal cu <tt>N_ELEM</tt>.
 * 11) * Folosiți fișierul Makefile din director pentru compilarea programului.
 * 12) * Rulați programul astfel obținut.
 * 13) * Rulați programul obținut în Valgrind. Care este cauza apariției erorii?
 * 14) Intrați în directorul <tt>my_realloc/</tt>.
 * 15) * Lucrați peste fișierul <tt>my_realloc_test.c</tt>.
 * 16) * În funcția <tt>main</tt> alocați spațiu de <tt>N_ELEM_PHASE1</tt> elemente întregi în pointer-ul <tt>elem_p</tt>.
 * 17) * Modificați spațiul ocupat la <tt>N_ELEM_PHASE2</tt>.
 * 18) * Omiteți (intenționat) eliberarea spațiului ocupat.
 * 19) * Folosiți fișierul Makefile din director pentru compilarea programului.
 * 20) * Rulați programul astfel obținut.
 * 21) * Rulați programul obținut în Valgrind. Care este cauza apariției erorii?
 * 22) * De ce, totuși, nu este nevoie de eliberare spațiului ocupat?
 * 23) Intrați în directorul <tt>inv_free/</tt>.
 * 24) * Investigați fișierul <tt>inv_free.c</tt>.
 * 25) * Folosiți fișierul Makefile din director pentru compilarea programului.
 * 26) * Rulați programul astfel obținut.
 * 27) * Care este cauza apariției erorii?

Windows
Folosiți directorul <tt>win/</tt> din [[media:lab4-pre.zip|arhiva de pre-sarcini]] a laboratorului.


 * 1) Intrați în subdirectorul <tt>my_heap_alloc/</tt>.
 * 2) * În fișierul <tt>my_ha_test.c</tt>, completați funcția <tt>main</tt> alocați spațiu pentru <tt>N_ELEM</tt> elemente întregi în pointerul <tt>elem_p</tt> (folosiți <tt>HeapAlloc</tt>).
 * 3) * Completați două poziții din vectorul obținut (indicat de <tt>elem_p</tt>): poziția <tt>ARRAY_POS_1</tt> și <tt>ARRAY_POS_2</tt>.
 * 4) * La completare folosiți două metode diferite (dereferențiere ca pointer și referire sub formă de vector).
 * 5) * Eliberați spațiul ocupat de vector. (folosiți <tt>HeapFree</tt>)
 * 6) * Folosiți fișierul Makefile din director pentru compilarea programului.
 * 7) * Rulați programul astfel obținut.
 * 8) Rămâneți în subdirectorul <tt>my_heap_alloc/</tt>.
 * 9) * Configurați macroul <tt>ARRAY_POS_2</tt> pentru a fi egal cu <tt>N_ELEM</tt>.
 * 10) * Folosiți fișierul Makefile din director pentru compilarea programului.
 * 11) * Rulați programul astfel obținut.
 * 12) Intrați în directorul <tt>my_heap_realloc</tt>.
 * 13) * Lucrați peste fișierul <tt>my_hra_test.c</tt>.
 * 14) * În funcția <tt>main</tt> alocați spațiu de <tt>N_ELEM_PHASE1</tt> elemente întregi în pointer-ul <tt>elem_p</tt>.
 * 15) * Modificați spațiul ocupat la <tt>N_ELEM_PHASE2</tt>. (folosiți <tt>HeapReAlloc</tt>).
 * 16) * Omiteți (intenționat) eliberarea spațiului ocupat.
 * 17) * Folosiți fișierul Makefile din director pentru compilarea programului.
 * 18) * Rulați programul astfel obținut.
 * 19) * De ce, totuși, nu este nevoie de eliberare spațiului ocupat?

Exerciții de laborator
Folosiți arhiva de sarcini a laboratorului.

Linux
Folosiți directorul <tt>lin/</tt> din arhiva de sarcini a laboratorului.


 * 1) (1 punct) Intrați în directorul <tt>01-alloc/</tt>.
 * 2) * Rezolvaţi, în fișierul <tt>alloc.c</tt>, toate problemele marcate cu <tt>TODO</tt> astfel:
 * 3) ** Alocaţi memorie pentru un vector care să stocheze <tt>no</tt> şiruri de caractere.
 * 4) ** Alocaţi memorie astfel încât fiecare şir să stocheze <tt>crt_len+1</tt> caractere.Nu uitaţi să verificaţi rezultatul întors de <tt>malloc</tt>.
 * 5) ** Afişati vectorul de şiruri de caractere, fiecare şir pe o linie nouă.
 * 6) ** Eliberaţi memoria alocată.
 * Hints:
 * 1) *** Citiţi secţiunea Alocarea memoriei în Linux din laborator.
 * 2) *** Dezalocati mai intai memoria alocata pentru fiecare sir de caractere, iar apoi memoria alocata pentru vectorul de siruri de caractere.
 * 3) (1 punct) Intrați în directorul <tt>02-struct/</tt>.
 * 4) * Rezolvaţi, in fisierul <tt>struct.c </tt>, toate problemele marcate cu <tt>TODO </tt> astfel:
 * 5) ** În funcţia <tt>allocate_flowers</tt> alocaţi memorie pentru no elemente de tip <tt>flower_info</tt>.
 * 6) ** În funcţia <tt>free_flowers</tt> eliberaţi memoria alocată in funcţia <tt>allocate_flowers</tt>.
 * 7) ** Folosiţi Valgrind pentru a descoperi eventualele probleme de lucru cu memoria şi corectaţi-le.
 * Hints:
 * 1) ***Folosiţi opţiunea --tool=memcheck pentru valgrind.
 * 2) ***Citiţi secţiunea Valgrind din laborator.
 * 3) (2.5 puncte) Intrați în directorul <tt>03-stack/</tt>.
 * 4) * Inspectati fisierul <tt>stack_param.c.</tt>
 * 5) ** Compilati si rulati programul.
 * 6) ** Ce observaţi?
 * Hints:
 * 1) ** Citiţi secţiunea Stiva din laborator.
 * 2) * Inspectaţi programul din fişierul <tt>README</tt> şi incercati sa descrieţi conţinutul stivei la momentul indicat în program.
 * 3) * Folosiţi programul ajutator <tt>stack.c</tt> dupa ce rezolvati problemele indicate de TODO, astfel:
 * 4) ** în funcţia <tt>show_snapshot</tt> iteraţi pe toata lungimea stivei şi afişaţi adresa şi valoarea de la adresa curentă
 * 5) ** în funcţia <tt>take_snapshot</tt> salvaţi in structura de date ce retine imaginea stivei campurile adresă şi valoare.
 * 6) * Rulaţi executabilul <tt>stack </tt> şi identificaţi adresele si valorile asociate cu variabilele din program.
 * Hints:
 * 1) ** Ciţiti comentariile din codul sursa <tt>stack.c</tt> pentru a întelege cum se salvează imaginea stivei.
 * 2) ** Citiţi secţiunea Stiva din laborator.
 * 3) (1 punct) Intraţi in directorul <tt>04-overflow</tt>
 * 4) * Completaţi problemele marcate de <tt>TODO</tt> la fel ca la exerciţiul precedent.
 * 5) * De data aceasta functia <tt>f2</tt> pune pe stivă un vector de 3 întregi. În ce ordine sunt puse elementele vectorului pe stivă?
 * 6) * Care este adresa de revenire din funcţia <tt>f2?</tt>.
 * 7) * Folosindu-vă de vectorul v fortaţi execuţia funcţiei <tt>show_message</tt> fară a o apela explicit. Astfel dupa apelul funcţiei f2 fluxul programului nu se va mai întoarce în funcţia f1 ci va executa <tt>show_message</tt>.
 * 8) (0.5 puncte) Intraţi in directorul <tt>05-trim</tt>.
 * 9) * Analizaţi programul trim.c şi rulaţi executabilul trim.
 * 10) * Folositi mcheck pentru a detecta problema si corectati-o.
 * Hints:
 * 1) ** Rulati programul folosind MALLOC_CHECK_=1 ./trim
 * 2) ** Citiţi secţiunea mcheck din laborator.

Windows
Folosiți directorul <tt>win/</tt> din arhiva de sarcini a laboratorului.


 * 1) (1 punct) Intrați în directorul <tt>01-util/</tt>.
 * 2) * Inspectați cele două fișiere existente: <tt>util.c</tt> și <tt>util.h</tt>.
 * 3) * Completați fișierul <tt>util.c</tt> cu definiția funcțiilor <tt>xmalloc</tt>, și a macrodefiniției <tt>xfree</tt> după cum urmează:
 * 4) ** În cazul <tt>xmalloc</tt> se alocă spațiu folosind <tt>HeapAlloc</tt>; dacă alocarea eșuează, programul se încheie cu <tt>abort</tt>.
 * 5) ** <tt>xfree</tt> este un macro care primește ca argument pointer-ul de eliberat; se apelează <tt>HeapFree</tt> și pointer-ul este resetat la <tt>NULL</tt>
 * 6) ** De ce este mai dificil să se realizeze o funcție <tt>xfree</tt> care să realizeze aceleași operații?
 * 7) (1 punct) Intrați în directorul <tt>02-xtest/</tt>.
 * 8) * Inspectați fișierul <tt>x_test.c</tt>.
 * 9) * Completați fișierul <tt>Makefile</tt> cu informațiile necesare pentru a compila fișierele <tt>01-util/util.c</tt> și <tt>x_test.c</tt> și pentru a le lega în executabilul <tt>x_test</tt>.
 * 10) * Modulul obiect asociat lui <tt>util.c</tt> trebuie să se găsească tot în directorul <tt>01-util/</tt>.
 * Hints:
 * 1) ** Citiţi secţiunea Alocarea memoriei în Windows din laborator.
 * 2) ** Folosiți variabila Makefile standard <tt>CFLAGS</tt> și opțiunea de preprocesare <tt>/I</tt>.
 * 3) ** Target-urile și cerințele dintr-o regulă Makefile pot fi precizate sub formă de căi în sistemul de fișiere.
 * 4) * Rulați executabilul obținut.
 * 5) (2 puncte) Intrați în directorul <tt>03-tensor/</tt>.
 * 6) * Analizați fișierul <tt>tensor.c</tt>.
 * 7) * Implementați funcțiile <tt>tensor_alloc</tt>, respectiv <tt>tensor_free</tt> care alocă/dezalocă un vector tridimensional (tensor).
 * Hints:
 * 1) ** Citiţi secţiunea Alocarea memoriei în Windows din laborator.
 * 2) ** Folosiţi funcţiile <tt>xmalloc</tt> si <tt>xfree</tt> definite la punctul 1.
 * 3) (1 punct) Intraţi în directorul <tt>04-bad_stack/</tt>
 * 4) * Analizaţi fişierul <tt>bad_stack.c</tt>.
 * 5) * Compilaţi programul şi rulaţi executabilul astfel obţinut.
 * 6) * Cum se explică rezultatul afişat. Corectaţi programul astfel încât rezultatul afişat să fie corect

= Soluții =


 * Soluții exerciții laborator 4

= Resurse utile =


 * Linux System Programming - Chapter 8 - Memory Management
 * Windows System Programming - Chapter 5 - Memory Management (Win32 and Win64 Memory Management Architecture, Heaps, Managing Heap Memory
 * Linux Application Programming - Chapter 7 - Memory Debugging Tools
 * Windows Memory Management
 * Virtual Memory Allocation and Paging
 * GDB manual
 * Valgrind Home
 * Using Valgrind to Find Memory Leaks
 * The Memory Management Reference
 * IBM trial download: Rational Purify 7.0
 * Using Purify
 * Memory Management Software
 * Smashing the Stack for Fun and Profit