Jak używać Valgrind, aby znaleźć wycieki pamięci?


183

Jak używać valgrind, aby znaleźć wycieki pamięci w programie?

Proszę, niech mi ktoś pomoże i opisz kroki, aby przeprowadzić procedurę?

Używam Ubuntu 10.04 i mam program a.c, pomóż mi.


16
Używasz valgrind do testowania skompilowanego programu, a nie kodu źródłowego.
Tony,

6
Odpowiedź podana poniżej przez @RageD jest poprawna, dlaczego jej nie akceptujesz?
Pratik Singhal

1
Przeciek jest spowodowany przez coś, czego nie zrobisz - tj. wolna przydzielona pamięć. Dlatego Valgrind nie może pokazać, „gdzie” jest wyciek - tylko Ty wiesz, gdzie przydzielona pamięć nie jest już potrzebna. Jednakże, mówiąc ci, która alokacja nie jest free () d, śledząc użycie tej pamięci w twoim programie, powinieneś być w stanie określić, gdzie powinna uzyskać free () d. Częstym błędem jest wyjście z funkcji bez zwolnienia przydzielonej pamięci.
Mike W.

Odpowiedzi:


297

Jak uruchomić Valgrind

Nie po to, aby obrażać OP, ale dla tych, którzy przychodzą do tego pytania i wciąż są nowicjuszami w Linuksie - być może będziesz musiał zainstalować Valgrind w swoim systemie.

sudo apt install valgrind  # Ubuntu, Debian, etc.
sudo yum install valgrind  # RHEL, CentOS, Fedora, etc.

Valgrind jest łatwo użyteczny dla kodu C / C ++, ale może być nawet używany dla innych języków, jeśli jest poprawnie skonfigurowany (zobacz to dla Pythona).

Aby uruchomić Valgrind , przekaż plik wykonywalny jako argument (wraz z parametrami do programu).

valgrind --leak-check=full \
         --show-leak-kinds=all \
         --track-origins=yes \
         --verbose \
         --log-file=valgrind-out.txt \
         ./executable exampleParam1

Flagi to w skrócie:

  • --leak-check=full: „każdy wyciek zostanie szczegółowo pokazany”
  • --show-leak-kinds=all: Pokaż wszystkie „określone, pośrednie, możliwe, osiągalne” rodzaje wycieków w „pełnym” raporcie.
  • --track-origins=yes: Preferuj użyteczną wydajność niż prędkość. Śledzi pochodzenie niezainicjowanych wartości, które mogą być bardzo przydatne w przypadku błędów pamięci. Rozważ wyłączenie, jeśli Valgrind jest niedopuszczalnie powolny.
  • --verbose: Może opowiedzieć o nietypowym zachowaniu programu. Powtórz, aby uzyskać więcej szczegółowości.
  • --log-file: Napisz do pliku. Przydatne, gdy dane wyjściowe przekraczają przestrzeń terminala.

Na koniec chciałbyś zobaczyć raport Valgrind, który wygląda następująco:

HEAP SUMMARY:
    in use at exit: 0 bytes in 0 blocks
  total heap usage: 636 allocs, 636 frees, 25,393 bytes allocated

All heap blocks were freed -- no leaks are possible

ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

Mam wyciek, ale GDZIE ?

Więc masz wyciek pamięci, a Valgrind nie mówi nic znaczącego. Być może coś takiego:

5 bytes in 1 blocks are definitely lost in loss record 1 of 1
   at 0x4C29BE3: malloc (vg_replace_malloc.c:299)
   by 0x40053E: main (in /home/Peri461/Documents/executable)

Rzućmy okiem na kod C, który również napisałem:

#include <stdlib.h>

int main() {
    char* string = malloc(5 * sizeof(char)); //LEAK: not freed!
    return 0;
}

Cóż, utracono 5 bajtów. Jak to się stało? Raport o błędzie mówi tylko maini malloc. W większym programie polowanie byłoby bardzo kłopotliwe. Dzieje się tak z powodu sposobu skompilowania pliku wykonywalnego . W rzeczywistości możemy uzyskać szczegółowe informacje o tym, co poszło nie tak, wiersz po wierszu. Przekompiluj swój program z flagą debugowania (używam gcctutaj):

gcc -o executable -std=c11 -Wall main.c         # suppose it was this at first
gcc -o executable -std=c11 -Wall -ggdb3 main.c  # add -ggdb3 to it

Teraz, dzięki tej kompilacji debugowania, Valgrind wskazuje dokładną linię kodu przydzielającą pamięć, która wyciekła! (Sformułowanie jest ważne: może nie być dokładnie miejsce wycieku, ale to , co wyciekło. Śledzenie pomaga ustalić, gdzie ).

5 bytes in 1 blocks are definitely lost in loss record 1 of 1
   at 0x4C29BE3: malloc (vg_replace_malloc.c:299)
   by 0x40053E: main (main.c:4)

Techniki debugowania wycieków i błędów pamięci

  • Skorzystaj z www.cplusplus.com ! Ma świetną dokumentację dotyczącą funkcji C / C ++.
  • Ogólne porady dotyczące wycieków pamięci:
    • Upewnij się, że twoja pamięć przydzielana dynamicznie faktycznie została zwolniona.
    • Nie przydzielaj pamięci i zapomnij przypisać wskaźnik.
    • Nie nadpisuj wskaźnika nowym, chyba że zostanie zwolniona stara pamięć.
  • Ogólne porady dotyczące błędów pamięci:
    • Uzyskuj dostęp i pisz do adresów i indeksów, które na pewno należą do Ciebie. Błędy pamięci różnią się od wycieków; często są to tylko IndexOutOfBoundsException problemy typu.
    • Nie używaj ani nie zapisuj w pamięci po jej zwolnieniu.
  • Czasami twoje przecieki / błędy mogą być ze sobą powiązane, podobnie jak IDE, które odkrywa, że ​​nie wpisałeś jeszcze nawiasu zamykającego. Rozwiązanie jednego problemu może rozwiązać inne, więc poszukaj takiego, który wygląda na dobrego winowajcę, i zastosuj niektóre z tych pomysłów:

    • Wypisz funkcje w swoim kodzie, które zależą / są zależne od „naruszającego” kodu, który zawiera błąd pamięci. Śledź wykonanie programu (być może nawet w gdb) i poszukaj błędów warunku wstępnego / warunku końcowego. Chodzi o to, aby śledzić wykonanie programu, koncentrując się na czasie życia przydzielonej pamięci.
    • Spróbuj wykomentować „obraźliwy” blok kodu (w granicach rozsądku, aby kod nadal się kompilował). Jeśli błąd Valgrind zniknie, znalazłeś, gdzie on jest.
  • Jeśli wszystko inne zawiedzie, spróbuj to sprawdzić. Valgrind też ma dokumentację !

Spojrzenie na typowe wycieki i błędy

Uważaj na swoje wskazówki

60 bytes in 1 blocks are definitely lost in loss record 1 of 1
   at 0x4C2BB78: realloc (vg_replace_malloc.c:785)
   by 0x4005E4: resizeArray (main.c:12)
   by 0x40062E: main (main.c:19)

A kod:

#include <stdlib.h>
#include <stdint.h>

struct _List {
    int32_t* data;
    int32_t length;
};
typedef struct _List List;

List* resizeArray(List* array) {
    int32_t* dPtr = array->data;
    dPtr = realloc(dPtr, 15 * sizeof(int32_t)); //doesn't update array->data
    return array;
}

int main() {
    List* array = calloc(1, sizeof(List));
    array->data = calloc(10, sizeof(int32_t));
    array = resizeArray(array);

    free(array->data);
    free(array);
    return 0;
}

Jako asystent nauczyciela często widziałem ten błąd. Uczeń korzysta ze zmiennej lokalnej i zapomina zaktualizować oryginalny wskaźnik. Błąd polega na zauważeniu, że w reallocrzeczywistości może przenieść przydzieloną pamięć w inne miejsce i zmienić lokalizację wskaźnika. Następnie wychodzimy, resizeArraynie mówiąc, array->datagdzie tablica została przeniesiona.

Nieprawidłowy zapis

1 errors in context 1 of 1:
Invalid write of size 1
   at 0x4005CA: main (main.c:10)
 Address 0x51f905a is 0 bytes after a block of size 26 alloc'd
   at 0x4C2B975: calloc (vg_replace_malloc.c:711)
   by 0x400593: main (main.c:5)

A kod:

#include <stdlib.h>
#include <stdint.h>

int main() {
    char* alphabet = calloc(26, sizeof(char));

    for(uint8_t i = 0; i < 26; i++) {
        *(alphabet + i) = 'A' + i;
    }
    *(alphabet + 26) = '\0'; //null-terminate the string?

    free(alphabet);
    return 0;
}

Zauważ, że Valgrind wskazuje nam skomentowaną linię kodu powyżej. Tablica o rozmiarze 26 jest indeksowana [0,25], dlatego *(alphabet + 26)zapis jest nieprawidłowy - jest poza zakresem. Nieprawidłowy zapis jest typowym wynikiem błędów typu „off-by-one”. Spójrz na lewą stronę operacji przypisania.

Nieprawidłowy odczyt

1 errors in context 1 of 1:
Invalid read of size 1
   at 0x400602: main (main.c:9)
 Address 0x51f90ba is 0 bytes after a block of size 26 alloc'd
   at 0x4C29BE3: malloc (vg_replace_malloc.c:299)
   by 0x4005E1: main (main.c:6)

A kod:

#include <stdlib.h>
#include <stdint.h>

int main() {
    char* destination = calloc(27, sizeof(char));
    char* source = malloc(26 * sizeof(char));

    for(uint8_t i = 0; i < 27; i++) {
        *(destination + i) = *(source + i); //Look at the last iteration.
    }

    free(destination);
    free(source);
    return 0;
}

Valgrind wskazuje nam na skomentowaną linię powyżej. Spójrz na ostatnią iterację tutaj, czyli
*(destination + 26) = *(source + 26);. Jednak *(source + 26)ponownie jest poza zakresem, podobnie jak nieprawidłowy zapis. Nieprawidłowe odczyty są również częstym skutkiem błędów typu off-by-one. Spójrz na prawą stronę swojej operacji przypisania.


Topia Open Source (U / Dys)

Skąd mam wiedzieć, że wyciek jest mój? Jak znaleźć wyciek, gdy używam kodu innej osoby? Znalazłem wyciek, który nie jest mój; powinienem coś zrobić? Wszystkie są uzasadnione pytania. Po pierwsze, dwa przykłady ze świata rzeczywistego, które pokazują dwie klasy typowych spotkań.

Jansson : biblioteka JSON

#include <jansson.h>
#include <stdio.h>

int main() {
    char* string = "{ \"key\": \"value\" }";

    json_error_t error;
    json_t* root = json_loads(string, 0, &error); //obtaining a pointer
    json_t* value = json_object_get(root, "key"); //obtaining a pointer
    printf("\"%s\" is the value field.\n", json_string_value(value)); //use value

    json_decref(value); //Do I free this pointer?
    json_decref(root);  //What about this one? Does the order matter?
    return 0;
}

To jest prosty program: odczytuje ciąg JSON i analizuje go. Podczas tworzenia używamy wywołań bibliotek, aby wykonać za nas analizę. Jansson dokonuje niezbędnych alokacji dynamicznie, ponieważ JSON może zawierać własne zagnieżdżone struktury. Nie oznacza to jednak, że decref„uwolnimy” pamięć przekazaną nam z każdej funkcji. W rzeczywistości ten kod, który napisałem powyżej, generuje zarówno „Invalid read”, jak i „Invalid write”. Te błędy znikają, gdy usuniesz decreflinię dla value.

Czemu? Zmienna valuejest traktowana jako „pożyczona referencja” w Jansson API. Jansson śledzi swoją pamięć za Ciebie, a Ty po prostu musisz mieć decref niezależne od siebie struktury JSON. Lekcja tutaj: przeczytaj dokumentację . Naprawdę. Czasami trudno to zrozumieć, ale mówią ci, dlaczego tak się dzieje. Zamiast tego mamy istniejące pytania dotyczące tego błędu pamięci.

SDL : biblioteka grafiki i gier

#include "SDL2/SDL.h"

int main(int argc, char* argv[]) {
    if (SDL_Init(SDL_INIT_VIDEO|SDL_INIT_AUDIO) != 0) {
        SDL_Log("Unable to initialize SDL: %s", SDL_GetError());
        return 1;
    }

    SDL_Quit();
    return 0;
}

Co jest nie tak z tym kodem ? Ciągle wycieka dla mnie ~ 212 KB pamięci. Pomyśl o tym przez chwilę. Włączamy i wyłączamy SDL. Odpowiedź? Nie ma w tym niczego złego.

Na początku może to brzmieć dziwnie . Prawdę mówiąc, grafika jest niechlujna i czasami trzeba zaakceptować niektóre przecieki jako część standardowej biblioteki. Lekcja tutaj: nie musisz tłumić każdego wycieku pamięci . Czasami wystarczy stłumić wycieki, ponieważ są to znane problemy, z którymi nie można nic zrobić . (To nie jest moja zgoda na ignorowanie twoich własnych przecieków!)

Odpowiedzi na próżnię

Skąd mam wiedzieć, że wyciek jest mój?
To jest. (W każdym razie na 99%)

Jak znaleźć wyciek, gdy używam kodu innej osoby?
Są szanse, że ktoś już to znalazł. Wypróbuj Google! Jeśli to się nie powiedzie, użyj umiejętności, które ci przekazałem powyżej. Jeśli to się nie powiedzie i najczęściej widzisz wywołania API i niewiele własnego śladu stosu, zobacz następne pytanie.

Znalazłem wyciek, który nie jest mój; powinienem coś zrobić?
Tak! Większość interfejsów API ma sposoby zgłaszania błędów i problemów. Użyj ich! Pomóż oddać narzędzia, których używasz w swoim projekcie!


Dalsze czytanie

Dzięki, że zostałeś ze mną tak długo. Mam nadzieję, że się czegoś nauczyłeś, ponieważ starałem się dotrzeć do szerokiego spektrum ludzi dochodzących do tej odpowiedzi. Mam nadzieję, że po drodze zapytałeś kilka rzeczy: Jak działa alokator pamięci w C? Czym właściwie jest wyciek pamięci i błąd pamięci? Czym się różnią od segfaultów? Jak działa Valgrind? Jeśli miałeś którykolwiek z nich, proszę zaspokoić swoją ciekawość:


4
O wiele lepsza odpowiedź, szkoda, że ​​to nie jest akceptowana odpowiedź.
A. Smoliak

Uważam, że zrobienie czegoś takiego to dobra praktyka, kilka zrobiłem sam
A. Smoliak

1
Czy mogę oznaczyć tę odpowiedź gwiazdką i wykorzystać ją jako odniesienie w przyszłości? Dobra robota!
Zap

czy memchecknarzędzie jest domyślnie włączone?
abhiarora

@abhiarora Yes. Strona memcheckpodręcznika mówi nam, że jest to domyślne narzędzie:--tool=<toolname> [default: memcheck]
Joshua Detwiler

146

Spróbuj tego:

valgrind --leak-check=full -v ./your_program

Dopóki valgrind jest zainstalowany, będzie przeglądał twój program i informował, co się stało. Może dostarczyć wskazówek i przybliżonych miejsc, w których można znaleźć wycieki. Jeśli segfaultinging, spróbuj go przepuścić gdb.


Co oznacza „twój_program”? Czy to lokalizacja kodu źródłowego lub nazwa aplikacji, taka jak plik APK?
HoangVu

7
your_program== nazwa pliku wykonywalnego lub jakiekolwiek polecenie, którego używasz do uruchamiania aplikacji.
RageD

27

Możesz biegać:

valgrind --leak-check=full --log-file="logfile.out" -v [your_program(and its arguments)]

1

Możesz utworzyć alias w pliku .bashrc w następujący sposób

alias vg='valgrind --leak-check=full -v --track-origins=yes --log-file=vg_logfile.out'

Więc kiedy chcesz sprawdzić wycieki pamięci, po prostu zrób to

vg ./<name of your executable> <command line parameters to your executable>

Spowoduje to wygenerowanie pliku dziennika Valgrind w bieżącym katalogu.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.