Co to jest schrödinbug?


52

Ta strona wiki mówi:

Schrödinbug jest błędem, który pojawia się tylko wtedy, gdy ktoś czyta kod źródłowy lub używa programu w nietypowy sposób, zauważa, że ​​nigdy nie powinien był działać, w którym to momencie program natychmiast przestaje działać dla wszystkich, dopóki nie zostanie naprawiony. Plik żargonu dodaje: „Chociaż… to brzmi niemożliwie, zdarza się; niektóre programy od lat kryją ukryte schrödinbugs”.

To, o czym się mówi, jest bardzo niejasne ...

Czy ktoś może podać przykład tego, jak wygląda schrödinbug (na przykład w sytuacji fikcyjnej / rzeczywistej)?


15
Pamiętaj, że cytat jest żartobliwie.

11
Myślę, że lepiej byś zrozumiał shrodinbug, gdybyś wiedział o kocie Shrodingera: en.wikipedia.org/wiki/Shrodingers_cat
Eimantas

1
@Eimantas Jestem teraz bardziej zdezorientowany, ale to ciekawy artykuł :)

Odpowiedzi:


82

Z mojego doświadczenia wynika, że ​​wzór jest następujący:

  • System działa, często przez lata
  • Zgłoszony błąd
  • Deweloper bada błąd i znajduje fragment kodu, który wydaje się całkowicie wadliwy, i oświadcza, że ​​„nigdy nie zadziałał”
  • Błąd został naprawiony, a legenda o kodzie, który nigdy nie działał (ale działał przez lata), rośnie

Bądźmy logiczni tutaj. Kod, który nigdy nie zadziałałby ... nigdy nie zadziałałby . Jeśli to nie poskutkuje, to stwierdzenie jest fałszywe.

Powiem więc, że błąd dokładnie taki, jak opisano (obserwując, że wadliwy kod przestaje działać) jest ewidentnie nonsensowny.

W rzeczywistości to, co się stało, jest jedną z dwóch rzeczy:

1) Deweloper nie w pełni zrozumiał kod . W tym przypadku kod jest zwykle bałaganem i gdzieś w nim ma dużą, ale nieoczywistą wrażliwość na niektóre warunki zewnętrzne (powiedzmy konkretną wersję lub konfigurację systemu operacyjnego, która reguluje działanie niektórych funkcji w niewielki, ale znaczący sposób). Ten warunek zewnętrzny jest zmieniany (powiedzmy przez aktualizację lub zmianę serwera, która uważana jest za niepowiązaną) i powoduje to uszkodzenie kodu.

Następnie programista patrzy na kod i nie rozumiejąc kontekstu historycznego lub mając czas na prześledzenie każdej możliwej zależności i scenariusza, oświadczył, że nigdy nie zadziałałby i przepisał go.

W tej sytuacji należy zrozumieć, że idea, że ​​„nigdy nie zadziałałoby”, jest prawdopodobnie fałszywa (ponieważ tak się stało).

Nie oznacza to, że przepisywanie jest złą rzeczą - często nie jest, chociaż miło jest wiedzieć dokładnie, co często było nie tak, co jest czasochłonne, a przepisywanie fragmentu kodu jest często szybsze i pozwala mieć pewność, że wszystko zostało naprawione.

2) Właściwie to nigdy nie zadziałało, po prostu nikt tego nie zauważył . Jest to zaskakująco powszechne, szczególnie w dużych systemach. W tym przypadku ktoś nowy zaczyna i zaczyna patrzeć na sprawy w sposób, w jaki nikt wcześniej tego nie robił, lub zmiany w procesie biznesowym, wprowadzając do wcześniejszego procesu niewielką przewagę i coś, co tak naprawdę nigdy nie działało (lub działało, ale nie wszystkie czas) został znaleziony i zgłoszony.

Deweloper patrzy na to i deklaruje, że „nigdy by to nie zadziałało”, ale użytkownicy mówią „nonsens, używamy go od lat” i mają rację, ale coś, co uważają za nieistotne (i zwykle nie wspominają, dopóki deweloper znajdzie dokładny stan, w którym punkt idą „Och tak, my zrobimy to teraz, a nie przed”) został zmieniony.

Tutaj deweloper ma rację - nigdy nie mógł działać i nigdy nie działał.

Ale w obu przypadkach jedna z dwóch rzeczy jest prawdą:

  • Twierdzenie „nigdy by nie zadziałało” jest prawdziwe i nigdy nie zadziałało - ludzie po prostu tak myśleli
  • Udało się, a stwierdzenie „nigdy nie mogło zadziałać” jest fałszywe i wynika z (zwykle rozsądnego) braku zrozumienia kodu i jego zależności

1
Tak często mi się zdarza
geneza

2
Świetny wgląd w realizm tych sytuacji
StuperUser

1
Domyślam się, że jest to zazwyczaj moment „WTF”. Miałem to raz. Ponownie przeczytałem napisany przeze mnie kod i zdałem sobie sprawę, że zauważony niedawno błąd powinien spowodować awarię całej aplikacji. Właściwie po dalszej kontroli inny komponent, który napisałem, był tak dobry, że zrekompensował błędy.
Thaddee Tyl

1
@Thaddee - Widziałem to wcześniej, ale widziałem również dwa błędy w modułach kodu, które nawzajem się nawzajem anulowały, więc tak naprawdę działało. Spójrz na jedno z nich, a one były zepsute, ale razem były w porządku.
Jon Hopkins

7
@Jon Hopkins: Dostałem również przypadek 2 błędów, które wzajemnie się znoszą, i to jest naprawdę zaskakujące. Znalazłem błąd, wypowiadałem niesławne stwierdzenie „nigdy nie mogło zadziałać”, spojrzałem głębiej, by dowiedzieć się, dlaczego i tak działało, i znalazłem inny błąd, który w pewnym stopniu poprawił pierwszy, przynajmniej w większości przypadków. Byłem naprawdę oszołomiony odkryciem i faktem, że przy JEDNYM z błędów, konsekwencje byłyby katastrofalne!
Alexis Dufrenoy,

54

Ponieważ wszyscy wspominają o kodzie, który nigdy nie powinien działać, dam wam przykład, na który natknąłem się, około 8 lat temu, na umierający projekt VB3, który był konwertowany na .net. Niestety projekt musiał być aktualizowany do czasu ukończenia wersji .net - i byłem tam jedynym, który nawet zdalnie rozumiał VB3.

Była jedna bardzo ważna funkcja, która była wywoływana setki razy w każdym obliczeniu - obliczała miesięczne odsetki za długoterminowe plany emerytalne. Odtworzę ciekawe części.

Function CalculateMonthlyInterest([...], IsYearlyInterestMode As Boolean, [...]) As Double
    [about 30 lines of code]
    If IsYearlyInterestMode Then
        [about 30 lines of code]
        If Not IsYearlyInterestMode Then
            [about 30 lines of code (*)]
        End If
    End If
End Function

Część oznaczona gwiazdką miała najważniejszy kod; była to jedyna część, która faktycznie wykonała obliczenia. Najwyraźniej to nigdy nie powinno działać, prawda?

To wymagało dużo debugowania, ale w końcu znalazłem przyczynę: IsYearlyInterestModebyło Truei Not IsYearlyInterestModebyło również prawdą. Dzieje się tak dlatego, że gdzieś wzdłuż linii ktoś rzucił ją na liczbę całkowitą, a następnie w funkcji, która ma ustawić ją na prawdziwą, zwiększyła ją (jeśli jest to 0, Falseto byłoby ustawione na 1, czyli VB True, więc widzę logikę tam), a następnie wrzuć go z powrotem na boolean. Pozostał mi stan, który nigdy nie może się zdarzyć, a jednak zdarza się cały czas.


7
Epilog: Nigdy nie naprawiłem tej funkcji; Właśnie załatałem witrynę z nieudanymi połączeniami, aby wysłać 2, jak wszystkie pozostałe.
konfigurator

więc masz na myśli, że jest używany, gdy ludzie mylnie interpretują kod?
Pacerier

1
@Pacerier: Częściej, gdy kod jest takim bałaganem, że działa poprawnie tylko przypadkowo. W moim przykładzie żaden programista nie miałby IsYearlyInterestModeoceniać zarówno jako prawdy, jak i nieprawdy; oryginalny programista, który dodał kilka wierszy (w tym jeden z nich iftak naprawdę nie rozumiał, jak to działa - po prostu działało, więc było wystarczająco dobre.
konfigurator

16

Nie znam przykładu z prawdziwego świata, ale upraszczając go na przykładowej sytuacji:

  • Błąd nie jest zauważany przez pewien czas, ponieważ aplikacja nie uruchamia kodu w warunkach, które powodują jego awarię.
  • Ktoś to zauważa, robiąc coś poza normalnym użytkowaniem (lub sprawdzając źródło).
  • Teraz, gdy zauważono błąd, aplikacja kończy się niepowodzeniem również do normalnych warunków, dopóki błąd nie zostanie naprawiony.

Może się tak zdarzyć, ponieważ błąd spowoduje uszkodzenie niektórych aplikacji, które powodują awarie w poprzednio normalnych warunkach.


4
Jednym z wyjaśnień jest to, że wystąpiły przypadkowe awarie oprogramowania, których nikt nie był w stanie połączyć mentalnie. Dlatego te błędy zostały uznane za naturalne przyczyny (takie jak przypadkowe awarie sprzętu). Po odczytaniu kodu źródłowego ludzie są teraz w stanie powiązać wszystkie wcześniejsze losowe błędy z tą jedną przyczyną i zdają sobie sprawę, że nigdy nie powinien on działać.
rwong

4
Drugim wyjaśnieniem jest to, że w oprogramowaniu jest część, która jest implementowana według wzorca odpowiedzialności. Każdy moduł obsługi jest napisany w solidny sposób, mimo że jeden moduł obsługi ma krytyczny błąd. Teraz pierwszy moduł obsługi zawsze kończy się niepowodzeniem, ale ponieważ drugi moduł obsługi (który pokrywa się z odpowiedzialnością) próbuje wykonać to samo zadanie, wydaje się, że ogólna operacja się powiodła. Jeśli w drugim module wystąpią jakiekolwiek zmiany, takie jak zmiana obszaru odpowiedzialności, spowodowałoby to ogólną awarię, chociaż prawdziwy błąd znajduje się w innym miejscu.
rwong

13

Przykład z prawdziwego życia. Nie mogę pokazać kodu, ale większość ludzi będzie się z tym odnosić.

Mamy dużą wewnętrzną bibliotekę funkcji narzędziowych, w których pracuję. Pewnego dnia szukam funkcji do wykonania określonej czynności i Frobnicate()próbuję jej użyć. Uh-oh: okazuje się, że Frobnicate()zawsze zwraca kod błędu.

Zagłębiając się w implementację, znajduję kilka podstawowych błędów logicznych, Frobnicate()które powodują, że zawsze kończy się to niepowodzeniem. W kontroli źródła widzę, że funkcja nie została zmodyfikowana od czasu jej napisania, co oznacza, że ​​funkcja nigdy nie działała zgodnie z przeznaczeniem. Dlaczego nikt tego nie zauważył? Przeszukuję resztę rejestracji źródłowej i stwierdzam, że wszyscy istniejący wywołujący Frobnicate()ignorują wartość zwracaną (a zatem zawierają własne subtelne błędy). Jeśli zmienię te funkcje, aby sprawdzić zwracaną wartość tak, jak powinny, to i one zaczną zawodzić.

Jest to częsty przypadek warunku nr 2, o którym wspomniał Jon Hopkins w swojej odpowiedzi, i jest przygnębiająco powszechny w dużych bibliotekach wewnętrznych.


... co stanowi dobry powód, aby unikać pisania biblioteki wewnętrznej wszędzie tam, gdzie jest użyteczna biblioteka zewnętrzna. Będzie bardziej testowany, a tym samym będzie miał o wiele mniej takich paskudnych niespodzianek (biblioteki open source są lepsze, ponieważ można je naprawić, jeśli tak się stanie).
Jan Hudec

Tak, ale jeśli programiści zignorują kody zwrotne, nie jest to wina biblioteki. (Nawiasem mówiąc, kiedy ostatni raz sprawdzałeś printf()
ponowne kodowanie

Właśnie dlatego wymyślono sprawdzone wyjątki.
Kevin Krumwiede

10

Oto prawdziwy Schrödinbug, który widziałem w kodzie systemu. Demon root musi komunikować się z modułem jądra. Tak więc kod jądra tworzy niektóre deskryptory plików:

int pipeFDs[1];

następnie konfiguruje komunikację przez potok, który zostanie dołączony do nazwanego potoku:

int pipeResult = pipe(pipeFDs);

To nie powinno działać. pipe()zapisuje dwa deskryptory plików w tablicy, ale jest tylko miejsce na jeden. Ale przez około siedem lat to zrobił pracę; tablica znajdowała się przed jakimś nieużywanym miejscem w pamięci, które przekształciło się w deskryptor pliku.

Pewnego dnia musiałem przenieść kod do nowej architektury. Przestał działać, a błąd, który nigdy nie powinien był działać, został wykryty.


5

Następstwem Schrödinbug jest Heisenbug - który opisuje błąd, który znika (lub czasami pojawia się) podczas próby zbadania i / lub naprawienia go.

Heisenbugs to mityczne sprytne małe zarazy, które biegają i chowają się po załadowaniu debuggera, ale wychodzą z stolarki, gdy przestaniesz oglądać.

W rzeczywistości są one zazwyczaj spowodowane przez jedną z następujących przyczyn:

  • wpływ tej optymalizacji, w której skompilowany kod -DDEBUGjest zoptymalizowany na inny poziom niż kompilacja wydania
  • subtelne różnice czasowe wynikające z rzeczywistych magistrali komunikacyjnych lub przerwań, które różnią się nieznacznie od symulowanych „idealnych” manekinów

Oba podkreślają znaczenie testowania kodu wersji na sprzęcie wersji, a także testu jednostki / modułu / systemu przy użyciu emulatorów.


Dlaczego nie zauważyłem odpowiedzi S.Lote i komentarza delnana przed opublikowaniem tego?
Andrew

Niewiele mam doświadczenia, ale znalazłem kilka z nich. Pracowałem w środowisku Android NDK. Kiedy debugger znalazł punkt przerwania, zatrzymał tylko wątki Java, a nie C ++, umożliwiając niektóre wywołania, ponieważ elementy zostały zainicjowane w C ++. Jeśli pozostanie bez debugera, kod Java będzie działał szybciej niż C ++ i spróbuje użyć wartości, które nie zostały jeszcze zainicjowane.
MLProgrammer-CiM

Kilka miesięcy temu odkryłem Heisenbug API API bazy danych Django : Kiedy DEBUG = Truenazwa „parametrów” zmienia się w surowe zapytanie SQL. Używaliśmy go jako arg słowa kluczowego dla jasności ze względu na długość zapytania, które zepsuło się całkowicie, gdy nadszedł czas, aby przejść do strony beta, gdzieDEBUG = False
Izkata

2

Widziałem kilka Schödinbugs i zawsze z tego samego powodu:

Polityka firmy wymagała, aby każdy miał korzystać z programu.
Nikt tak naprawdę tego nie wykorzystał (głównie dlatego, że nie było na to szkolenia).
Ale nie mogli powiedzieć zarządowi. Więc wszyscy musieli powiedzieć: „Używam tego programu od 2 lat i nigdy nie spotkałem się z tym błędem do dziś”.
Program nigdy tak naprawdę nie działał, z wyjątkiem mniejszości użytkowników (w tym programistów, którzy go napisali).

W jednym przypadku program został poddany wielu testom, ale nie w rzeczywistej bazie danych (która została uznana za zbyt wrażliwą, dlatego użyto fałszywej wersji).


1

Mam przykład z własnej historii, to było jakieś 25 lat temu. Byłem dzieckiem, które programowałem w podstawową grafikę w Turbo Pascal. TP miała bibliotekę o nazwie BGI, która zawierała niektóre funkcje, które pozwalają skopiować region ekranu do bloku pamięci opartego na wskaźnikach, a następnie zablokować go w innym miejscu. W połączeniu z xor-blittingiem na czarno-białym ekranie można go użyć do wykonania prostej animacji.

Chciałem pójść o krok dalej i zrobić duszki. Napisałem program, który narysował duże bloki i elementy sterujące, aby je pokolorować, a ty zrobiłeś to jako piksele, tworząc prosty program do rysowania do tworzenia duszków, które następnie można skopiować do pamięci. Był tylko jeden problem: aby użyć tych blitowanych duszków, trzeba je zapisać w pliku, aby inne programy mogły je odczytać. Ale TP nie miała możliwości szeregowania przydzielania pamięci na podstawie wskaźników. Instrukcje wyraźnie stwierdzają, że nie można ich zapisać do akt.

Wymyśliłem fragment kodu, który z powodzeniem napisał do pliku. I zacząłem pisać program testowy, który wykasował duszka z mojego programu do rysowania na tle - na mojej drodze do stworzenia gry. I działało pięknie. Jednak następnego dnia przestało działać. Nie pokazywał nic poza zniekształconym bałaganem. Nigdy więcej nie zadziałało. Stworzyłem nowego duszka i działało idealnie - dopóki nie zadziałało, i znów był zniekształcony.

Zajęło to dużo czasu, ale w końcu zorientowałem się, co się dzieje. Program do rysowania nie zapisywał, tak jak myślałem, skopiowanych danych pikseli do pliku - zapisywał sam wskaźnik. Kiedy następny program odczytał plik, skończył się wskaźnikiem do tego samego bloku pamięci - który wciąż zawierał to, co napisał ostatni program (to było na MS-DOS, zarządzanie pamięcią nie istniało). Ale działało ... aż do momentu ponownego uruchomienia komputera lub uruchomienia czegoś, co ponownie wykorzystało ten sam obszar pamięci, a następnie pojawił się zniekształcony bałagan, ponieważ blokowałeś grupę całkowicie niepowiązanych danych w bloku pamięci wideo.

Nigdy nie powinien był działać, nigdy nie powinien wydawać się działać (i na żadnym prawdziwym systemie operacyjnym nie miałby), ale nadal działał, a kiedy się zepsuł - pozostał zepsuty.


0

Dzieje się tak przez cały czas, gdy ludzie używają debuggerów.

Środowisko debugowania różni się od rzeczywistego środowiska produkcyjnego - bez debuggera.

Uruchamianie z debuggerem może maskować rzeczy takie jak przepełnienie stosu, ponieważ ramki stosu debugera maskują błąd.


Nie sądzę, żeby odnosiło się to do różnicy między kodem uruchomionym w debuggerze a kompilacją.
Jon Hopkins

26
To nie jest schrödinbug, to heisenbug .

@delnan: Jest na krawędzi, IMO. Uważam, że jest to kwestia nieokreślona, ​​ponieważ istnieją niepoznawalne stopnie swobody. Chciałbym zarezerwować heisenbuga na rzeczy, w których pomiar jednej rzeczy faktycznie przeszkadza drugiej (np. Warunki wyścigu, ustawienia optymalizatora, ograniczenia przepustowości sieci itp.)
S.Lott

@ S.Lott: Sytuacja, którą opisujesz, wiąże się z obserwacją zmieniającą się przez bałagan w ramkach stosu lub tym podobnych. (Najgorszy taki przykład, jaki widziałem, to debugger, który pokojowo i „poprawnie” wykonuje ładunki nieprawidłowych wartości rejestrów segmentów w trybie jednoetapowym. W rezultacie powstały pewne procedury w RTL, które zostały wysłane pomimo załadowania wskaźnika trybu rzeczywistego w trybie chronionym Ponieważ był tylko kopiowany, a nie dereferencjonowany, zachowywał się doskonale.)
Loren Pechtel,

0

Nigdy nie widziałem prawdziwego schrodinbuga i nie sądzę, żeby istniał - znalezienie go nie zepsuje wszystkiego.

Zmieniło się raczej coś, co ujawniło błąd, który czai się od wieków. Cokolwiek się zmieniło, wciąż się zmienia, dlatego błąd pojawia się, a jednocześnie ktoś go znalazł.

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.