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_handlers
w śladzie wstecznym). Przeważnie z udziałem std::map
jednej 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::map
korupcją, 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.
/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:
To trudny problem, ale podejrzewam, że w wypadkach, które już widziałeś, jest o wiele więcej wskazówek.
Rzeczy, których używaliśmy w podobnych sytuacjach.
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ą:
Są to części kodu, na których należy się skupić.
źródło
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.
źródło
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 ++ ...
źródło
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ść.
źródło
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.
źródło