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.