19 października 2020

Valgrind - pomaga wyszukać wycieki pamięci w programach

Chwilę pomyślałem sobie dzisiaj o problemach jakie mogą mieć urządzenia wbudowane z oprogramowaniem i przyszła mi taka myśl: 

Ciekawe, czy zależnie od języka programowania w standardzie możemy dostawać wycieki pamięci lub nie?

Z punktu widzenia różnych znanych mi systemów w urządzeniach mobilnych zastanawia mnie słabe rozwiązanie tego problemu - tzn. klikanie zwolnij zaalokowaną pamięć telefonu na żądanie. Inny wariant, to dedykowane programy z klasy "odśmiecacze pamięci", czyli dedykowane oprogramowanie, którego celem jest przegląd wszelkich niezwolnionych bloków i za zgodą usera próba zwolnienia. Brzmi znajomo, może Twój telefon z Androidem też przymula po otwarciu kilku aplikacji (nie koniecznie gier) ... i dopiero restart naprawia sprawę. Cóż, może mamy tam do czynienia z tzw. wyciekiem pamięci operacyjnej. Koncept nieskomplikowany do sprawdzenia na prostym przykładzie. 


Wprowadzenie do wycieków pamięci

Wyciek pamięci (ang. memory leak) to zaalokowanie pamięci w systemie operacyjnym przez program, który nie zwalania pamięci po zakończeniu swojego działania. 

Wszystko fajnie, kiedy bawimy się telefonem i po np: tygodniu działania zaczyna zamulać, robimy restart i działa dalej. Inaczej sprawa się ma na rynku np: serwerów, czy sprzętu ratującego życie, medycznego, diagnostycznego, kalibracyjnego. Są branże, gdzie wycieki pamięci to nie jest norma a poważny błąd. Każdy bajt wykradziony z puli w takich systemach wbudowanych, to proszenie się o zawieszenie systemu / programu / procesu, czyli o niestandardową awarię.

Nie jest tylko tak, że wycieki pamięci generują programiści danego softu. Mogą istnieć ukryte wycieki pamięci, które generują zewnętrzne bibioteki lub biblioteki współdzielone lub systemowe. Ta klasa błędów posiada dość szeroki zasięg ze względu na magię używania dwóch podstawowych instrukcji niższego poziomu w jężyka programowania t.j. C dobrze znanych. Są to:

 - alloc / malloc / calloc / tallloc i pochodne - rezerwują pamięć w programie dla zmiennej

- free i pochodne - zwalniają przydzieloną pamięć (po podaniu zmiennej z przydziału pamięci jako argumentu) 

Właściwie to chodzi tutaj o fakt, że nie wywołuje się FREE tam gdzie zachodzi *ALLOC. Niby takie proste w opisie a do osiągnięcia nirvany jest sporo kombinacji i pracy ;-)


Valgrind - ratunek na wycieki pamięci w oprogramowaniu

Na szczęście w fazie tworzenia oprogramowania, debugowania i testowania, jesteśmy w stanie sprawdzić jak zachowuje się oprogramowanie i dokonać rozpoznania wycieków pamięci. Narzędziem najpopularniejszym na rynku otwartego oprogramowania jest Valgrind 




Narzędzie to doczekało się dojrzałej wersji i bardzo bogatego portfolio, jeśli chodzi o wspierane architektury.

Valgrind obejmuje obecnie siedem narzędzi o jakości produkcyjnej: 
- detektor błędów pamięci 
- dwa detektory błędów wątków 
- pamięć podręczną i profiler predykcji rozgałęzień 
- wykres wywołań generujący pamięć podręczną 
- profiler przewidywania rozgałęzień 
- dwa różne profilery sterty. 
Zawiera również eksperymentalny generator podstawowych wektorów blokowych SimPoint. 
Działa na następujących platformach: X86 / Linux, AMD64 / Linux, ARM / Linux, ARM64 / Linux, PPC32 / Linux, PPC64 / Linux, PPC64LE / Linux, S390X / Linux, MIPS32 / Linux, MIPS64 / Linux, X86 / Solaris , AMD64 / Solaris, ARM / Android (2.3.x i nowsze), ARM64 / Android, X86 / Android (4.0 i nowsze), MIPS32 / Android, X86 / Darwin i AMD64 / Darwin (Mac OS X 10.12).

W Ubuntu możemy zainstalować narzędzie valgrind z takiego onelinera:

$ snap install valgrind  --classic



Test dwóch znanych języków programowania - Python i Golang - na wycieki pamięci w najprostszym programie

Wracam tutaj do założenia mojego wpisu. Mamy dwa bardzo popularne dzisiaj języki programowania: Python i Golang, którymi posługuję się biegle na codzień. Pomyślałem, spróbuję napisać jednolinijkowca i zobaczymy, czy valgrind coś wykryje :-) Poniżej zrzut z testu i rezultaty:


Program w Python + valgrind


$ cat memleaks_test.py 
print("a" * 255)

$ python3 memleaks_test.py 
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

Ok, odpalmy teraz z badaniem wycieków pamięci i co widzimy:

....
==19177== Invalid read of size 4
==19177==    at 0x5139AE: PyGrammar_RemoveAccelerators (in /usr/bin/python3.8)
==19177==    by 0x67EE59: Py_FinalizeEx (in /usr/bin/python3.8)
==19177==    by 0x6B614C: Py_RunMain (in /usr/bin/python3.8)
==19177==    by 0x6B63BC: Py_BytesMain (in /usr/bin/python3.8)
==19177==    by 0x4C890B2: (below main) (libc-start.c:308)
==19177==  Address 0x51f0020 is 384 bytes inside a block of size 732 free'd
==19177==    at 0x4A39078: free (vg_replace_malloc.c:538)
==19177==    by 0x51ACB8: PyGrammar_AddAccelerators (in /usr/bin/python3.8)
==19177==    by 0x5FD6C4: PyParser_New (in /usr/bin/python3.8)
==19177==    by 0x517293: ??? (in /usr/bin/python3.8)
==19177==    by 0x67BA7A: PyParser_ASTFromStringObject (in /usr/bin/python3.8)
==19177==    by 0x67BEFD: PyRun_StringFlags (in /usr/bin/python3.8)
==19177==    by 0x600581: ??? (in /usr/bin/python3.8)
==19177==    by 0x5C4D7F: ??? (in /usr/bin/python3.8)
==19177==    by 0x56B26D: _PyEval_EvalFrameDefault (in /usr/bin/python3.8)
==19177==    by 0x5F7145: _PyFunction_Vectorcall (in /usr/bin/python3.8)
==19177==    by 0x56B26D: _PyEval_EvalFrameDefault (in /usr/bin/python3.8)
==19177==    by 0x569559: _PyEval_EvalCodeWithName (in /usr/bin/python3.8)
==19177==  Block was alloc'd at
==19177==    at 0x4A37ECB: malloc (vg_replace_malloc.c:307)
==19177==    by 0x51ACCF: PyGrammar_AddAccelerators (in /usr/bin/python3.8)
==19177==    by 0x5FD6C4: PyParser_New (in /usr/bin/python3.8)
==19177==    by 0x517293: ??? (in /usr/bin/python3.8)
==19177==    by 0x67BA7A: PyParser_ASTFromStringObject (in /usr/bin/python3.8)
==19177==    by 0x67BEFD: PyRun_StringFlags (in /usr/bin/python3.8)
==19177==    by 0x600581: ??? (in /usr/bin/python3.8)
==19177==    by 0x5C4D7F: ??? (in /usr/bin/python3.8)
==19177==    by 0x56B26D: _PyEval_EvalFrameDefault (in /usr/bin/python3.8)
==19177==    by 0x5F7145: _PyFunction_Vectorcall (in /usr/bin/python3.8)
==19177==    by 0x56B26D: _PyEval_EvalFrameDefault (in /usr/bin/python3.8)
==19177==    by 0x569559: _PyEval_EvalCodeWithName (in /usr/bin/python3.8)
==19177== 
==19177== 
==19177== HEAP SUMMARY:
==19177==     in use at exit: 301,190 bytes in 135 blocks
==19177==   total heap usage: 2,186 allocs, 2,051 frees, 3,158,779 bytes allocated
==19177== 
==19177== LEAK SUMMARY:
==19177==    definitely lost: 0 bytes in 0 blocks
==19177==    indirectly lost: 0 bytes in 0 blocks
==19177==      possibly lost: 1,632 bytes in 3 blocks
==19177==    still reachable: 299,558 bytes in 132 blocks
==19177==         suppressed: 0 bytes in 0 blocks
==19177== Rerun with --leak-check=full to see details of leaked memory
==19177== 
==19177== Use --track-origins=yes to see where uninitialised values come from
==19177== For lists of detected and suppressed errors, rerun with: -s
==19177== ERROR SUMMARY: 1106 errors from 128 contexts (suppressed: 0 from 0)

Kilkaset bajtów program pożarł ;-) a to nowina dla wszystkich milionów użytkowników języka Pytohn :-) Polecam prztestować bardziej skomplikowane programy oraz serwery aplikacji.



Program w Golang + valgrind


$ cat main.go 
package main

import "fmt"
import "strings"

func main() {
fmt.Println(strings.Repeat("a", 255))
}


$ go build main.go 
$ ./main 
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa


Program w GOlangu robi dosłownie to samo i badamy go przy pomocy valgrind:


==19372== Conditional jump or move depends on uninitialised value(s)
==19372==    at 0x40C4FD: runtime.mallocgc (/snap/go/6633/src/runtime/malloc.go:1152)
==19372== 
==19372== Conditional jump or move depends on uninitialised value(s)
==19372==    at 0x40C4B1: runtime.mallocgc (/snap/go/6633/src/runtime/malloc.go:1136)
==19372==    by 0x4474A8: runtime.growslice (/snap/go/6633/src/runtime/slice.go:230)
==19372==    by 0x43593E: runtime.allgadd (/snap/go/6633/src/runtime/proc.go:473)
==19372==    by 0x43D1F8: runtime.newproc1 (/snap/go/6633/src/runtime/proc.go:3572)
==19372==    by 0x45DBD2: runtime.newproc.func1 (/snap/go/6633/src/runtime/proc.go:3528)
==19372==    by 0x461605: runtime.systemstack (/snap/go/6633/src/runtime/asm_amd64.s:370)
==19372==    by 0x43745F: ??? (<autogenerated>:1)
==19372== 
==19372== Conditional jump or move depends on uninitialised value(s)
==19372==    at 0x40C4FD: runtime.mallocgc (/snap/go/6633/src/runtime/malloc.go:1152)
==19372==    by 0x4474A8: runtime.growslice (/snap/go/6633/src/runtime/slice.go:230)
==19372==    by 0x43593E: runtime.allgadd (/snap/go/6633/src/runtime/proc.go:473)
==19372==    by 0x43D1F8: runtime.newproc1 (/snap/go/6633/src/runtime/proc.go:3572)
==19372==    by 0x45DBD2: runtime.newproc.func1 (/snap/go/6633/src/runtime/proc.go:3528)
==19372==    by 0x461605: runtime.systemstack (/snap/go/6633/src/runtime/asm_amd64.s:370)
==19372==    by 0x43745F: ??? (<autogenerated>:1)
==19372== 
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
==19372== 
==19372== HEAP SUMMARY:
==19372==     in use at exit: 0 bytes in 0 blocks
==19372==   total heap usage: 0 allocs, 0 frees, 0 bytes allocated
==19372== 
==19372== All heap blocks were freed -- no leaks are possible
==19372== 
==19372== Use --track-origins=yes to see where uninitialised values come from
==19372== For lists of detected and suppressed errors, rerun with: -s
==19372== ERROR SUMMARY: 3447 errors from 761 contexts (suppressed: 0 from 0)

... i ku naszemu zdziwieniu jednak GOlang nie ma wycieków pamięci na takim samym banalnym programie.



Podsumowanie

Bardzo ciekawa obserwacja pomyślałem sobie, jak łatwo przy pomocy valgrind znajdziemy wycieki pamięci. Szczególnie kiedy chciałbym coś napisać na urządzenie wbudowane, którego wielkość pamięci RAM to nie są GB a MB, przemyślę jednak w jakim języku napisać kod. 
Nie chciałbym wpaść w totalne błędne wnioski, że nie można tego robić w Pythonie i tylko w GOlang, gdyż może to być błąd mojej wersji i w kolejnej zostanie naprawiony. Oczywiście, tak błachy błąd dla jednych programistów jest powodem do kolejnych eksperymentów dla innych. 

W związku z tym zapraszam do wypowiedzi, jakie macie doświadczenia z valgrind? Z jakich ciekawych sytuacji wyratował Was ten tool w pracy zawodowej? 


Brak komentarzy: