Debugowanie uszkodzenia pamięci

23

Po pierwsze, zdaję sobie sprawę, że nie jest to idealne pytanie w stylu pytań i odpowiedzi z absolutną odpowiedzią, ale nie mogę wymyślić żadnego sformułowania, które poprawiłoby jego działanie. Nie wydaje mi się, żeby istniało absolutne rozwiązanie tego problemu i jest to jeden z powodów, dla których zamieszczam go tutaj zamiast Przepełnienia stosu.

W ciągu ostatniego miesiąca przepisałem dość stary fragment kodu serwera (mmorpg), aby był bardziej nowoczesny i łatwiejszy do rozszerzenia / modyfikacji. Zacząłem od części sieciowej i zaimplementowałem bibliotekę innej firmy (libevent) do obsługi rzeczy dla mnie. Po wszystkich zmianach faktoringu i zmianach kodu wprowadziłem gdzieś uszkodzenie pamięci i starałem się dowiedzieć, gdzie to się dzieje.

Nie mogę w wiarygodny sposób odtworzyć go w moim środowisku deweloperskim / testowym, nawet gdy implementuję prymitywne boty do symulacji obciążenia, nie dostaję już awarii (naprawiłem poważny problem, który powodował pewne rzeczy)

Do tej pory próbowałem:

Walcząc z tego do diabła - Żadnych niepoprawnych zapisów, dopóki coś się nie zawiesi (co może zająć ponad 1 dzień w produkcji ... lub tylko godzinę), co naprawdę mnie zaskakuje, na pewno w pewnym momencie uzyska dostęp do nieprawidłowej pamięci i nie nadpisze rzeczy przez szansa? (Czy istnieje sposób na „rozłożenie” zakresu adresów?)

Narzędzia do analizy kodu, a mianowicie coverity i cppcheck. Podczas gdy wskazywali na niektóre… nieprzyjemności i przewrotne przypadki w kodzie, nie było nic poważnego.

Nagrywanie procesu aż do awarii z gdb (przez undodb), a następnie przejście do tyłu. To / brzmi / powinno to być wykonalne, ale albo skończę z awarią gdb przy użyciu funkcji autouzupełniania, albo skończę w jakiejś wewnętrznej strukturze libevent, w której się zgubię, ponieważ istnieje zbyt wiele możliwych gałęzi (jedna z nich powoduje uszkodzenie i tak dalej) na). Myślę, że byłoby miło, gdybym mógł zobaczyć, do czego pierwotnie należy wskaźnik / gdzie został przydzielony, co wyeliminowałoby większość problemów z rozgałęzianiem. Nie mogę uruchomić valgrind z undodb, a ja normalny rekord gdb jest wyjątkowo wolny (jeśli to działa nawet w połączeniu z valgrind).

Przegląd kodu! Sam (dokładnie) i znajomi, którzy przeglądają mój kod, choć wątpię, by był wystarczająco dokładny. Myślałem o wynajęciu programisty, który przeprowadziłby ze mną przegląd / debugowanie kodu, ale nie mogę sobie pozwolić na włożenie w to zbyt dużo pieniędzy i nie wiedziałbym, gdzie szukać kogoś, kto byłby skłonny pracować za mało… brak pieniędzy, jeśli nie znajdzie problemu lub ktoś w ogóle się kwalifikuje.

Powinienem także zauważyć: zazwyczaj otrzymuję spójne ślady wstecz. Jest kilka miejsc, w których dochodzi do awarii, głównie związanych z jakimś uszkodzeniem klasy gniazd. Czy to niepoprawny wskaźnik wskazujący na coś, co nie jest gniazdem, czy sama klasa gniazda zostaje nadpisana (częściowo?) Bełkotem. Chociaż podejrzewam, że tam najczęściej się zawiesza, ponieważ jest to jedna z najczęściej używanych części, więc jest to pierwsza uszkodzona pamięć, która się wykorzystuje.

Podsumowując, ten problem był dla mnie zajęty przez prawie 2 miesiące (włączanie i wyłączanie, bardziej projekt hobby) i naprawdę frustruje mnie do tego stopnia, że ​​jestem zrzędliwy IRL i zastanawiam się nad tym, jak się poddać. Po prostu nie mogę myśleć o tym, co jeszcze mam zrobić, aby znaleźć problem.

Czy są jakieś przydatne techniki, za którymi tęskniłem? Jak sobie z tym radzisz? (To nie może być tak powszechne, ponieważ nie ma o tym dużo informacji ... czy jestem naprawdę ślepy?)

Edytować:

Niektóre specyfikacje w przypadku, gdy ma to znaczenie:

Używanie c ++ (11) przez gcc 4.7 (wersja dostarczona przez debian wheezy)

Baza kodów ma około 150 000 linii

Edytuj w odpowiedzi na post david.pfx: (przepraszam za powolną odpowiedź)

Czy prowadzisz staranne rejestry wypadków, aby szukać wzorów?

Tak, wciąż mam na sobie zrzuty ostatnich awarii

Czy kilka miejsc jest naprawdę podobnych? W jaki sposób?

Cóż, w najnowszej wersji (wydaje się, że zmieniają się za każdym razem, gdy dodam / usuwam kod lub zmieniam pokrewne struktury), zawsze zostałby złapany w metodzie timera przedmiotów. Zasadniczo element ma określony czas, po upływie którego wygasa i wysyła zaktualizowane informacje do klienta. Nieprawidłowy wskaźnik gniazda znajdowałby się w (wciąż o ile wiem) klasie odtwarzacza, głównie z tym związanej. Mam również dużo awarii w fazie czyszczenia po normalnym zamknięciu, w którym niszczą wszystkie klasy statyczne, które nie zostały jawnie zniszczone ( __run_exit_handlersw śladzie wstecznym). Przeważnie z udziałem std::mapjednej klasy, zgadywanie to tylko pierwsza rzecz, jaka się pojawia.

Jak wyglądają uszkodzone dane? Zera? Ascii? Wzory?

Nie znalazłem jeszcze żadnych wzorów, wydaje mi się to trochę przypadkowe. Trudno powiedzieć, ponieważ nie wiem, gdzie zaczęła się korupcja.

Czy jest to związane ze stertą?

Jest to całkowicie związane ze stosem (włączyłem ochronę stosu gcc i to niczego nie złapało).

Czy korupcja ma miejsce po free()?

Będziesz musiał nieco rozwinąć ten temat. Czy masz na myśli wskaźniki leżące wokół już uwolnionych obiektów? Ustawiam każde odniesienie na zero, gdy obiekt zostanie zniszczony, więc chyba że gdzieś coś przeoczyłem, nie. Powinno to pojawić się w valgrind, ale tak się nie stało.

Czy jest coś charakterystycznego w ruchu sieciowym (rozmiar bufora, cykl odzyskiwania)?

Ruch sieciowy składa się z surowych danych. Tak więc tablice char, (u) intX_t lub struktury spakowane (w celu usunięcia dopełnienia) dla bardziej złożonych rzeczy, każdy pakiet ma nagłówek składający się z identyfikatora i samego rozmiaru pakietu, który jest sprawdzany względem oczekiwanego rozmiaru. Są to około 10-60 bajtów, a największy (wewnętrzny pakiet „bootup”, uruchamiany raz przy starcie) ma rozmiar kilku Mb.

Wiele twierdzi, że produkcja. Crash wcześnie i przewidywalnie, zanim rozprzestrzeni się uszkodzenie.

Kiedyś miałem awarię związaną z std::mapkorupcją, każdy byt ma mapę swojego „widoku”, każdy byt może to zobaczyć i odwrotnie. Dodałem bufor 200 bajtów z przodu i po, wypełniłem go 0x33 i sprawdziłem przed każdym dostępem. Zepsucie właśnie magicznie zniknęło, musiałem coś poruszyć, co spowodowało, że zepsuło to coś innego.

Rejestrowanie strategiczne, dzięki czemu dokładnie wiesz, co działo się przed chwilą. Dodaj do rejestrowania, gdy zbliżysz się do odpowiedzi.

Działa .. do pewnego stopnia.

W desperacji, czy możesz zapisać stan i automatyczne ponowne uruchomienie? Mogę wymyślić kilka programów, które to robią.

Trochę to robię. Oprogramowanie składa się z głównego procesu „pamięci podręcznej” i niektórych innych procesów roboczych, z których wszystkie uzyskują dostęp do pamięci podręcznej w celu pobierania i zapisywania danych. Więc w przypadku awarii nie tracę dużo postępu, wciąż odłącza wszystkich użytkowników i tak dalej, to zdecydowanie nie jest rozwiązanie.

Współbieżność: wątki, warunki wyścigu itp

Istnieje wątek mysql do wykonywania zapytań „asynchronicznych”, ale wszystko to pozostaje nietknięte i dzieli informacje z klasą bazy danych tylko poprzez funkcje z całą blokadą.

Przerwania

Istnieje zegar przerwania, który zapobiega blokowaniu się, który po prostu przerywa, jeśli nie ukończy cyklu przez 30 sekund, ten kod powinien być jednak bezpieczny:

if (!tics) {
    abort();
} else
    tics = 0;

tiki jest volatile int tics = 0;zwiększane za każdym razem, gdy cykl się kończy. Stary kod też.

zdarzenia / wywołania zwrotne / wyjątki: niepoprawny stan korupcji lub stosu

Wykorzystywanych jest wiele wywołań zwrotnych (asynchroniczne we / wy sieciowe, timery), ale nie powinny robić nic złego.

Nietypowe dane: nietypowe dane wejściowe / czas / stan

Miałem kilka związanych z tym przypadków. Odłączenie gniazda podczas przetwarzania pakietów skutkowało uzyskaniem dostępu do wartości zerowej i tak dalej, ale do tej pory były one łatwe do wykrycia, ponieważ każde odwołanie jest czyszczone zaraz po poinformowaniu samej klasy, że zostało zrobione. (Samo zniszczenie jest obsługiwane przez pętlę usuwającą wszystkie zniszczone obiekty w każdym cyklu)

Zależność od asynchronicznego procesu zewnętrznego.

Możesz rozwinąć temat? Tak jest nieco w przypadku wspomnianego powyżej procesu buforowania. Jedyną rzeczą, jaką mogłem sobie wyobrazić z góry głowy, byłoby to, że nie kończyło się wystarczająco szybko i nie używało śmieciowych danych, ale tak nie jest, ponieważ używa to również sieci. Ten sam model pakietu.

Rudzik
źródło
7
Niestety jest to bardzo popularne w nietrywialnych aplikacjach C ++. Jeśli używasz kontroli źródła, testowanie różnych zestawów zmian w celu zawężenia przyczyny zmiany kodu może pomóc, ale w tym przypadku może nie być możliwe.
Telastyn
Tak, w moim przypadku naprawdę nie jest to wykonalne. Zasadniczo przeszedłem z pracy na całkowicie i całkowicie zepsuty przez 2 miesiące, a następnie na etap debugowania, gdzie mam nieco działający kod. Stary system naprawdę nie pozwolił mi zaimplementować mojego nowego, elastycznego kodu sieciowego bez zepsucia wszystkiego.
Robin,
2
W tym momencie być może będziesz musiał spróbować odizolować każdą część. Weź każdą klasę / podzbiór rozwiązania, zrób sobie z niego próbę, aby mógł funkcjonować, i przetestuj żywe piekło, aż znajdziesz sekcję, która zawodzi.
Ampt
zacznij od komentowania części kodów, dopóki nie będziesz mieć awarii.
cpp81,
1
Oprócz Valgrind, Coverity i cppcheck, powinieneś dodać Asan i UBsan do swojego systemu testowania. Jeśli twój kod to corss-platofrm, dodaj także Microsoft Enterprise Analysis ( /analyze) oraz zabezpieczenia Malloc i Scribble firmy Apple. Powinieneś także używać jak największej liczby kompilatorów, używając jak największej liczby standardów, ponieważ ostrzeżenia kompilatora są diagnostyczne i z czasem stają się lepsze. Nie ma srebrnej kuli, a jeden rozmiar nie pasuje do wszystkich. Im więcej narzędzi i kompilatorów używasz, tym bardziej pełny zasięg, ponieważ każde narzędzie ma swoje mocne i słabe strony.

Odpowiedzi:

21

To trudny problem, ale podejrzewam, że w wypadkach, które już widziałeś, jest o wiele więcej wskazówek.

  • Czy prowadzisz staranne rejestry wypadków, aby szukać wzorów?
  • Czy kilka miejsc jest naprawdę podobnych? W jaki sposób?
  • Jak wyglądają uszkodzone dane? Zera? Ascii? Wzory?
  • Czy w grę wchodzi wielowątkowość? Czy to może być stan wyścigu?
  • Czy jest to związane ze stertą? Czy uszkodzenie następuje po free ()?
  • Czy jest to związane ze stosami? Czy stos jest uszkodzony?
  • Czy zwisające odniesienie jest możliwe? Wartość danych, która w tajemniczy sposób się zmieniła?
  • Czy jest coś charakterystycznego w ruchu sieciowym (rozmiar bufora, cykl odzyskiwania)?

Rzeczy, których używaliśmy w podobnych sytuacjach.

  • Wiele twierdzi, że produkcja. Crash wcześnie i przewidywalnie, zanim rozprzestrzeni się uszkodzenie.
  • Dużo strażników. Dodatkowe elementy danych przed i po zmiennych lokalnych, obiektach i mallocs () ustawione na wartość, a następnie często sprawdzane.
  • Rejestrowanie strategiczne, dzięki czemu dokładnie wiesz, co działo się przed chwilą. Dodaj do rejestrowania, gdy zbliżysz się do odpowiedzi.

W desperacji, czy możesz zapisać stan i automatyczne ponowne uruchomienie? Mogę wymyślić kilka programów, które to robią.

Dodaj szczegóły, jeśli w ogóle możemy pomóc.


Czy mogę dodać, że tak poważne, nieokreślone błędy nie są tak powszechne i nie ma wielu rzeczy, które mogą (zwykle) je powodować. Zawierają:

  • Współbieżność: wątki, warunki wyścigu itp
  • Przerwania / zdarzenia / wywołania zwrotne / wyjątki: niepoprawny stan uszkodzenia lub stosu
  • Nietypowe dane: nietypowe dane wejściowe / czas / stan
  • Zależność od asynchronicznego procesu zewnętrznego.

Są to części kodu, na których należy się skupić.

david.pfx
źródło
+1 Wszystkie dobre sugestie, zwłaszcza twierdzenia, strażnicy i logowanie.
andy256,
W odpowiedzi na twoje pytanie zredagowałem więcej informacji. To sprawiło, że pomyślałem o awariach podczas zamykania, na które jeszcze nie patrzyłem, więc chyba się tym zajmę.
Robin,
5

Użyj debugującej wersji malloc / free. Zawiń je i w razie potrzeby napisz własne. Dużo zabawy!

Wersja, której używam, dodaje bajty ochronne przed i po każdym przydziale, i utrzymuje listę „przydzielonych”, z której darmowe kontrole zwalniają porcje. Wyłapuje to większość przepełnienia bufora i wiele lub nieuczciwych błędów „wolnych”.

Jednym z najbardziej podstępnych źródeł korupcji jest nadal wykorzystywanie fragmentu po uwolnieniu. Wolny powinien wypełnić wolną pamięć znanym wzorcem (tradycyjnie 0xDEADBEEF). Pomaga to, jeśli przydzielone struktury zawierają element „magicznej liczby” i swobodnie obejmują sprawdzanie odpowiedniej magicznej liczby przed użyciem struktury.

ddyer
źródło
1
Valgrind powinien jednak złapać podwójne zwolnienia / wykorzystanie darmowych danych, prawda?
Robin,
Pisanie tego rodzaju przeciążeń dla nowego / usuwania pomogło mi zlokalizować liczne problemy z uszkodzeniem pamięci. Zwłaszcza bajty ochronne, które są weryfikowane podczas usuwania i powodują, że program wyzwala punkt przerwania, który automatycznie upuszcza mnie do debuggera.
Emily L.,
3

Parafrazując to, co mówisz w swoim pytaniu, nie jest możliwe udzielenie ostatecznej odpowiedzi. Najlepsze, co możemy zrobić, to zasugerować rzeczy, których należy szukać oraz narzędzia i techniki.

Niektóre sugestie będą naiwne, inne mogą być bardziej odpowiednie, ale mam nadzieję, że jedna z nich wywoła myśl, którą możesz podjąć. Muszę powiedzieć, że odpowiedź david.pfx zawiera solidne porady i sugestie.

Od objawów

  • dla mnie to brzmi jak przepełnienie bufora.

  • pokrewnym problemem jest używanie nieważnych danych gniazda jako indeksu dolnego lub klucza itp.

  • czy to możliwe, że używasz gdzieś globalnej zmiennej, masz globalną i lokalną o tej samej nazwie, czy dane jednego gracza w jakiś sposób przeszkadzają innemu?

Podobnie jak w przypadku wielu błędów, prawdopodobnie przyjmujesz gdzieś nieprawidłowe założenia. A może więcej niż jeden. Wiele błędów interakcji jest trudnych do wykrycia.

  • Czy każda zmienna ma opis? Czy potrafisz zdefiniować potwierdzenie ważności?
    Jeśli nie, dodaj te, przejrzyj kod, aby zobaczyć, czy każda zmienna wydaje się być używana poprawnie. Dodaj to twierdzenie tam, gdzie ma to sens.

  • Sugestia dodania asercji partii jest dobra: pierwsze miejsce na umieszczenie ich jest w każdym punkcie wejścia funkcji. Sprawdź poprawność argumentów i dowolnego odpowiedniego stanu globalnego.

  • Używam dużo logowania do debugowania kodów długo działających / asynchronicznych / w czasie rzeczywistym.
    Ponownie wstaw zapis dziennika do każdego wywołania funkcji.
    Jeśli pliki dziennika stają się zbyt duże, funkcje rejestrowania mogą zawijać / przełączać pliki / itp.
    Jest to najbardziej przydatne, jeśli komunikaty dziennika są wcięte z głębokością wywołania funkcji.
    Plik dziennika może pokazywać, jak błąd się rozprzestrzenia. Przydatne, gdy jeden fragment kodu robi coś niezupełnie właściwego, co działa jak bomba o opóźnionym działaniu.

Wiele osób ma własny domowy kod logowania. Mam gdzieś stary system dzienników makr C i może wersję C ++ ...

andy256
źródło
3

Wszystko, co zostało powiedziane w innych odpowiedziach, jest bardzo istotne. Jedną ważną rzeczą częściowo wspomnianą przez ddyera jest to, że owijanie malloc / free ma zalety. Wspomina kilka, ale chciałbym dodać do tego bardzo ważne narzędzie do debugowania: możesz zalogować każdy malloc / free do zewnętrznego pliku wraz z kilkoma liniami callstack (lub pełnym callstack, jeśli ci zależy). Jeśli jesteś ostrożny, możesz łatwo zrobić to dość szybko i użyć go w produkcji, jeśli o to chodzi.

Z tego, co opisujesz, sądzę, że możesz trzymać odniesienie do wskaźnika gdzieś w celu uwolnienia pamięci i może w końcu uwolnić wskaźnik, który już nie należy do ciebie lub do niego pisać. Jeśli możesz wywnioskować zakres wielkości do monitorowania za pomocą powyższej techniki, powinieneś być w stanie znacznie zawęzić rejestrowanie. W przeciwnym razie, gdy znajdziesz już uszkodzoną pamięć, możesz dowiedzieć się, jaki wzór Malloc / Free doprowadził do tego z dzienników.

Ważną uwagą jest to, że jak wspomniałeś, zmiana układu pamięci może ukryć problem. Dlatego bardzo ważne jest, aby rejestrowanie nie dokonywało żadnych alokacji (jeśli możesz!) Lub było ich jak najmniej. Pomoże to w odtwarzalności, jeśli jest związane z pamięcią. Pomoże również, jeśli jest tak szybko, jak to możliwe, jeśli problem dotyczy wielu wątków.

Ważne jest również, aby wychwytywać alokacje z bibliotek stron trzecich, aby można je również poprawnie rejestrować. Nigdy nie wiadomo, skąd może pochodzić.

Ostatnią alternatywą jest również utworzenie niestandardowego programu przydzielającego, w którym przydzielasz co najmniej 2 strony dla każdego przydziału i odblokowujesz je po zwolnieniu (wyrównanie przydziału do granicy strony, przydzielenie strony przed i oznaczenie jako niedostępne lub wyrównanie alokuj na końcu strony i alokuj stronę po, a oznaczenie jako niedostępne). Pamiętaj, aby przynajmniej raz nie używać adresów pamięci wirtualnej do nowych przydziałów. Oznacza to, że będziesz musiał samodzielnie zarządzać pamięcią wirtualną (zarezerwuj ją i używaj, jak chcesz). Pamiętaj, że spowoduje to obniżenie wydajności i może spowodować zużycie znacznej ilości pamięci wirtualnej w zależności od tego, ile alokacji ją zasilasz. Aby to złagodzić, pomoże to w uruchomieniu 64-bitowym i / lub zmniejszeniu zakresu przydziałów, które tego potrzebują (na podstawie wielkości). Valgrind może już to zrobić, ale może być zbyt wolny, abyś mógł złapać problem. Wykonanie tego tylko dla kilku rozmiarów lub obiektów (jeśli wiesz, które możesz użyć specjalnego alokatora tylko dla tych obiektów) zapewni minimalny wpływ na wydajność.

Nicholas Frechette
źródło
0

Spróbuj ustawić punkt obserwacyjny na adresie pamięci, w którym się zawiesza. GDB złamie się na polecenie, które spowodowało niepoprawną pamięć. Następnie za pomocą śledzenia wstecznego możesz zobaczyć swój kod, który powoduje uszkodzenie. Może to nie być źródłem korupcji, ale powtarzanie uwagi na temat każdej korupcji może prowadzić do źródła problemu.

Nawiasem mówiąc, ponieważ pytanie jest oznaczone jako C ++, rozważ użycie współużytkowanych wskaźników, które dbają o własność, utrzymując liczbę referencji i bezpiecznie usuwaj pamięć po tym, jak wskaźnik zniknie z zakresu. Ale używaj ich ostrożnie, ponieważ mogą powodować impas w rzadkich przypadkach zależności cyklicznej.

Mohammad Azim
źródło