Jak działa awaria segmentacji pod maską?


266

Nie mogę znaleźć żadnych informacji na ten temat oprócz „MMU procesora wysyła sygnał” i „jądro kieruje je do szkodliwego programu, kończąc je”.

Zakładałem, że prawdopodobnie wysyła sygnał do powłoki i powłoka obsługuje ją, kończąc proces obrażeń i drukowanie "Segmentation fault". Przetestowałem to założenie pisząc niezwykle minimalną powłokę, którą nazywam crsh (skorupa crap). Ta powłoka nie robi nic poza pobieraniem danych wejściowych od użytkownika i podawaniem jej do system()metody.

#include <stdio.h>
#include <stdlib.h>

int main(){
    char cmdbuf[1000];
    while (1){
        printf("Crap Shell> ");
        fgets(cmdbuf, 1000, stdin);
        system(cmdbuf);
    }
}

Więc uruchomiłem tę powłokę w czystym terminalu (bez bashbiegania pod spodem). Następnie przystąpiłem do uruchamiania programu, który powoduje awarię. Jeśli moje założenia byłyby poprawne, spowodowałoby to: a) awarię crsh, zamknięcie xtermu, b) nie wydrukowanie "Segmentation fault", lub c) jedno i drugie.

braden@system ~/code/crsh/ $ xterm -e ./crsh
Crap Shell> ./segfault
Segmentation fault
Crap Shell> [still running]

Chyba powrót do pierwszego. Właśnie pokazałem, że to nie powłoka to robi, ale system pod spodem. W jaki sposób drukowany jest „błąd segmentacji”? „Kto” to robi? Jądro? Coś innego? W jaki sposób sygnał i wszystkie jego skutki uboczne rozprzestrzeniają się ze sprzętu do ostatecznego zakończenia programu?


43
crshto świetny pomysł na tego rodzaju eksperymenty. Dziękujemy za poinformowanie nas o tym i idei, która za tym stoi.
Bruce Ediger,

30
Kiedy po raz pierwszy zobaczyłem crsh, pomyślałem, że będzie to wymawiane jako „crash”. Nie jestem pewien, czy to równie odpowiednie imię.
jpmc26

56
To niezły eksperyment ... ale powinieneś wiedzieć, co system()robi pod maską. Okazuje się, że system()odrodzi się proces powłoki! Twój proces powłoki odradza inny proces powłoki, a ten proces powłoki (prawdopodobnie /bin/shlub coś w tym rodzaju) jest tym, który uruchamia program. Sposób /bin/shlub bashdziałanie polega na użyciu fork()i exec()(lub innej funkcji w execve()rodzinie).
Dietrich Epp

4
@BradenBest: Dokładnie. Przeczytaj stronę podręcznika man 2 wait, będzie zawierać makra WIFSIGNALED()i WTERMSIG().
Dietrich Epp

4
@DietrichEpp Tak jak powiedziałeś! Próbowałem dodać pole wyboru, (WIFSIGNALED(status) && WTERMSIG(status) == 11)aby wydrukować coś goofy ( "YOU DUN GOOFED AND TRIGGERED A SEGFAULT"). Kiedy uruchomiłem segfaultprogram od wewnątrz crsh, wydrukował dokładnie to. Tymczasem polecenia, które wychodzą normalnie, nie powodują wyświetlenia komunikatu o błędzie.
Braden Best

Odpowiedzi:


248

Wszystkie nowoczesne procesory mają zdolność przerywania aktualnie wykonywanej instrukcji maszyny. Zapisują wystarczający stan (zwykle, ale nie zawsze, na stosie), aby umożliwić późniejsze wznowienie wykonania, tak jakby nic się nie wydarzyło (zwykle przerwana instrukcja jest restartowana). Następnie zaczynają wykonywać procedurę obsługi przerwań , która jest po prostu większym kodem maszynowym, ale umieszczona w specjalnej lokalizacji, aby procesor wiedział z wyprzedzeniem. Procedury obsługi przerwań są zawsze częścią jądra systemu operacyjnego: komponent, który działa z największymi uprawnieniami i jest odpowiedzialny za nadzorowanie wykonywania wszystkich innych komponentów. 1,2

Przerwania mogą być synchroniczne , co oznacza, że ​​są wyzwalane przez sam procesor jako bezpośrednią odpowiedź na coś, co zrobiła aktualnie wykonywana instrukcja, lub asynchroniczne , co oznacza, że ​​zdarzają się w nieprzewidywalnym czasie z powodu zdarzenia zewnętrznego, takiego jak dane przychodzące do sieci Port. Niektórzy ludzie rezerwują termin „przerwanie” dla przerwania asynchronicznego i zamiast tego nazywają przerwanie synchroniczne „pułapkami”, „błędami” lub „wyjątkami”, ale wszystkie te słowa mają inne znaczenie, więc będę się trzymał „przerwania synchronicznego”.

Obecnie większość nowoczesnych systemów operacyjnych ma pojęcie procesów . Mówiąc najprościej, jest to mechanizm, za pomocą którego komputer może uruchamiać więcej niż jeden program w tym samym czasie, ale jest to również kluczowy aspekt tego, jak systemy operacyjne konfigurują ochronę pamięci , co jest cechą większości (ale niestety wciąż nie wszystkie ) nowoczesne procesory. To idzie w parze z pamięcią wirtualną, czyli możliwość zmiany mapowania między adresami pamięci a rzeczywistymi lokalizacjami w pamięci RAM. Ochrona pamięci pozwala systemowi operacyjnemu nadać każdemu procesowi osobną część pamięci RAM, do której tylko on może uzyskać dostęp. Pozwala także systemowi operacyjnemu (działającemu w imieniu niektórych procesów) wyznaczyć regiony pamięci RAM jako tylko do odczytu, wykonywalne, współużytkowane przez grupę współpracujących procesów itp. Część pamięci będzie dostępna tylko dla jądro. 3)

Tak długo, jak każdy proces uzyskuje dostęp do pamięci tylko w sposób, w jaki procesor jest skonfigurowany tak, aby pozwalał, ochrona pamięci jest niewidoczna. Kiedy proces łamie reguły, CPU generuje synchroniczne przerwanie, prosząc jądro o uporządkowanie. Regularnie zdarza się, że proces tak naprawdę nie złamał reguł, tylko jądro musi trochę popracować, aby proces mógł być kontynuowany. Na przykład, jeśli strona pamięci procesu musi zostać „eksmitowana” do pliku wymiany, aby zwolnić miejsce w pamięci RAM na coś innego, jądro oznaczy tę stronę jako niedostępną. Następnym razem, gdy proces spróbuje go użyć, CPU wygeneruje przerwanie ochrony pamięci; jądro pobierze stronę z wymiany, umieści ją z powrotem tam, gdzie była, oznaczy ją ponownie jako dostępną i wznowi wykonanie.

Załóżmy jednak, że proces naprawdę złamał zasady. Próbował uzyskać dostęp do strony, na której nigdy nie było zmapowane RAM, lub próbował wykonać stronę oznaczoną jako niezawierająca kodu maszynowego ani nic takiego. Rodzina systemów operacyjnych zwanych ogólnie „Unix” używa sygnałów do radzenia sobie z tą sytuacją. 4 Sygnały są podobne do przerwań, ale są generowane przez jądro i przetwarzane przez procesy, a nie generowane przez sprzęt i uruchamiane przez jądro. Procesy mogą definiować procedury obsługi sygnałówwe własnym kodzie i poinformuj jądro, gdzie się znajdują. Te procedury obsługi sygnałów zostaną następnie wykonane, przerywając w razie potrzeby normalny przepływ sterowania. Wszystkie sygnały mają numer i dwa nazwiska, z których jedno jest tajemniczym akronimem, a drugie nieco mniej tajemniczym zwrotem. Sygnał generowany, gdy proces łamie zasady ochrony pamięci, jest (zgodnie z konwencją) numerem 11, a jego nazwy to SIGSEGV„Błąd segmentacji”. 5,6

Ważną różnicą między sygnałami a przerwaniami jest to, że dla każdego sygnału istnieje domyślne zachowanie . Jeśli system operacyjny nie zdefiniuje modułów obsługi dla wszystkich przerwań, jest to błąd w systemie operacyjnym, a cały komputer ulegnie awarii, gdy procesor spróbuje wywołać brakujący moduł obsługi. Ale procesy nie są zobowiązane do definiowania procedur obsługi sygnałów dla wszystkich sygnałów. Jeśli jądro generuje sygnał dla procesu, a sygnał ten pozostawił swoje domyślne zachowanie, jądro po prostu pójdzie naprzód i zrobi wszystko, co domyślne, i nie przeszkadza procesowi. Większość domyślnych zachowań sygnałów to „nic nie rób” lub „zakończ ten proces i być może również zrzuć rdzeń”. SIGSEGVjest jednym z tych ostatnich.

Podsumowując, mamy proces, który złamał zasady ochrony pamięci. Procesor zawiesił proces i wygenerował przerwanie synchroniczne. Jądro wypełniło to przerwanie i wygenerowało SIGSEGVsygnał dla procesu. Załóżmy, że proces nie skonfigurował modułu obsługi sygnałów SIGSEGV, więc jądro wykonuje domyślne zachowanie, które polega na zakończeniu procesu. Ma to takie same skutki jak _exitwywołanie systemowe: otwarte pliki są zamykane, pamięć jest zwalniana itp.

Do tego momentu nic nie wydrukowało żadnych wiadomości, które człowiek może zobaczyć, a powłoka (lub, bardziej ogólnie, proces nadrzędny właśnie zakończonego procesu) nie była w ogóle zaangażowana. SIGSEGVidzie do procesu, który złamał zasady, a nie jego rodzic. Następny krok w sekwencji jest jednak to, aby powiadomić procesu nadrzędnego, że jej dziecko zostało zakończone. To może zdarzyć się na kilka różnych sposobów, z których najprostsza jest, gdy rodzic jest już czeka na tego zgłoszenia, korzystając z jednej z waitwywołań systemowych ( wait, waitpid, wait4, etc). W takim przypadku jądro po prostu spowoduje powrót tego wywołania systemowego i dostarczy procesowi nadrzędnemu kod o nazwie „ status wyjścia”. 7 Status wyjścia informuje rodzica, dlaczego proces potomny został zakończony; w takim przypadku dowie się, że dziecko zostało zakończone z powodu domyślnego zachowania SIGSEGVsygnału.

Proces nadrzędny może następnie zgłosić zdarzenie człowiekowi, drukując wiadomość; programy powłoki prawie zawsze to robią. Twój crshkod nie zawiera do tego kodu, ale i tak się dzieje, ponieważ procedura biblioteki C systemuruchamia w pełni funkcjonalną powłokę /bin/sh„pod maską”. crshjest dziadkiem w tym scenariuszu; powiadomienie o procesie nadrzędnym jest oznaczone przez /bin/sh, co powoduje wydruk jego zwykłej wiadomości. Następnie /bin/shsamo się kończy, ponieważ nie ma już nic do roboty, a implementacja biblioteki C systemodbiera to powiadomienie o wyjściu. Możesz zobaczyć to powiadomienie o wyjściu w kodzie, sprawdzając wartość zwracanąsystem; ale nie powie ci to, że proces wnuka zmarł w wyniku awarii, ponieważ został pochłonięty przez proces powłoki pośredniej.


Przypisy

  1. Niektóre systemy operacyjne nie implementują sterowników urządzeń jako części jądra; jednak wszystkie programy obsługi przerwań nadal muszą być częścią jądra, podobnie jak kod konfigurujący ochronę pamięci, ponieważ sprzęt nie pozwala na nic innego niż jądro.

  2. Może istnieć program o nazwie „hypervisor” lub „manager maszyny wirtualnej”, który jest nawet bardziej uprzywilejowany niż jądro, ale dla celów tej odpowiedzi można go uznać za część sprzętu .

  3. Jądro jest programem , ale to nie to proces; jest bardziej jak biblioteka. Wszystkie procesy od czasu do czasu wykonują części kodu jądra, oprócz własnego kodu. Może istnieć wiele „wątków jądra”, które tylko wykonują kod jądra, ale nie dotyczą nas tutaj.

  4. Jedynym systemem operacyjnym, z którym prawdopodobnie będziesz mieć do czynienia, którego nie można uznać za implementację Uniksa, jest oczywiście Windows. W tej sytuacji nie używa sygnałów. (Rzeczywiście, nie ma sygnałów; w systemie Windows <signal.h>interfejs jest całkowicie sfałszowany przez bibliotekę C.) Zamiast tego używa czegoś zwanego „ obsługą wyjątków strukturalnych ”.

  5. Niektóre naruszenia ochrony pamięci generują SIGBUS(„Błąd magistrali”) zamiast SIGSEGV. Linia między nimi jest nieokreślona i różni się w zależności od systemu. Jeśli napisałeś program, który definiuje moduł obsługi SIGSEGV, prawdopodobnie dobrym pomysłem jest zdefiniowanie tego samego modułu obsługi SIGBUS.

  6. „Błąd segmentacji” to nazwa przerwania wygenerowanego z powodu naruszenia ochrony pamięci przez jeden z komputerów z oryginalnym Uniksem , prawdopodobnie PDP-11 . „ Segmentacja ” jest rodzajem ochrony pamięci, ale obecnie termin „ błąd segmentacji ” odnosi się ogólnie do każdego rodzaju naruszenia ochrony pamięci.

  7. Wszystkie inne sposoby, w jakie proces nadrzędny może zostać powiadomiony o zakończeniu dziecka, kończą się tym, że rodzic dzwoni waiti otrzymuje status wyjścia. Po prostu coś innego dzieje się najpierw.


@zvol: ad 2) Nie sądzę, aby słuszne było twierdzenie, że procesor wie coś o procesach. Powinieneś powiedzieć, że wywołuje on funkcję obsługi przerwań, która przekazuje kontrolę.
user323094,

9
@ user323094 Współczesne procesory wielordzeniowe naprawdę dużo wiedzą o procesach; wystarczająco, aby w tej sytuacji mogli zawiesić tylko wątek wykonania, który spowodował błąd ochrony pamięci. Starałem się też nie wchodzić w szczegóły niskiego poziomu. Z punktu widzenia programisty zajmującego się przestrzeniami użytkownika najważniejszą rzeczą do zrozumienia w kroku 2 jest to, że to sprzęt wykrywa naruszenia ochrony pamięci; tym mniej dokładny podział pracy między sprzętem, oprogramowaniem układowym i systemem operacyjnym, jeśli chodzi o identyfikację „procesu naruszającego prawo”.
zwolnienie

Inną subtelnością, która może wprowadzić w błąd naiwnego czytelnika, jest: „Jądro wysyła procesowi obrażającemu sygnał SIGSEGV”. który używa zwykłego żargonu, ale w rzeczywistości oznacza, że ​​jądro każe sobie radzić sobie z sygnałem foo na pasku procesu (tzn. kod użytkownika nie bierze udziału, chyba że zainstalowany jest moduł obsługi sygnału, pytanie to jest rozwiązywane przez jądro). Kiedyś wolę z tego powodu podnosi sygnał SIGSEGV” .
dmckee

2
Istotna różnica między SIGBUS (błąd magistrali) i SIGSEGV (błąd segmentacji) jest następujący: SIGSEGV występuje, gdy CPU wie, że nie powinieneś uzyskiwać dostępu do adresu (a więc nie wysyła żadnego żądania magistrali pamięci zewnętrznej). SIGBUS występuje, gdy CPU dowiaduje się o problemie z adresowaniem dopiero po umieszczeniu żądania na swojej zewnętrznej magistrali adresowej. Na przykład prośba o fizyczny adres, na który nic w magistrali nie odpowiada, lub prośba o odczyt danych na błędnie wyrównanej granicy (która wymagałaby dwóch fizycznych próśb zamiast jednego)
Stuart Caie

2
@StuartCaie Opisujesz zachowanie przerwań ; w rzeczywistości wiele procesorów dokonuje rozróżnienia, które naszkicujesz (chociaż niektóre tego nie robią, a linia między nimi jest różna). Sygnały SIGSEGV i SIGBUS są jednak nie wiarygodnie odwzorowane na tych dwóch warunków CPU szczebla. Jedynym warunkiem, w którym POSIX wymaga SIGBUS zamiast SIGSEGV, jest przejście mmappliku do obszaru pamięci większego niż plik, a następnie uzyskanie dostępu do „całych stron” poza końcem pliku. (Poza tym POSIX jest dość niejasny co do tego, kiedy zdarzy się SIGSEGV / SIGBUS / SIGILL / itp.)
zwolnij

42

Powłoka rzeczywiście ma coś wspólnego z tą wiadomością i crshpośrednio wywołuje powłokę, co prawdopodobnie jest bash.

Napisałem mały program C, który zawsze segreguje błędy:

#include <stdio.h>

int
main(int ac, char **av)
{
        int *i = NULL;

        *i = 12;

        return 0;
}

Kiedy uruchamiam go z domyślnej powłoki, zshotrzymuję:

4 % ./segv
zsh: 13512 segmentation fault  ./segv

Kiedy go uruchamiam bash, otrzymuję to, co zauważyłeś w swoim pytaniu:

bediger@flq123:csrc % ./segv
Segmentation fault

Zamierzałem napisać procedurę obsługi sygnału w moim kodzie, a potem zdałem sobie sprawę, że system()wywołanie biblioteki używane przez crshexec jest powłoką, /bin/shzgodnie z man 3 system. To /bin/shprawie na pewno drukuje „Błąd segmentacji”, ponieważ na crshpewno nie jest.

Jeśli ponownie napiszesz, crshaby użyć execve()wywołania systemowego do uruchomienia programu, nie zobaczysz ciągu „Błąd segmentacji”. Pochodzi z powłoki wywoływanej przez system().


5
Właśnie rozmawiałem o tym z Dietrich Epp. I hacked razem wersję crsh który używa execvpi zrobił test ponownie, aby zauważyć, że podczas gdy skorupa nadal nie psuje (to znaczy SIGSEGV nigdy nie jest wysyłany do muszli), to jednak nie drukować „segmentacji Fault”. W ogóle nic nie jest drukowane. Wydaje się to wskazywać, że powłoka wykrywa, kiedy jej procesy potomne zostają zabite i jest odpowiedzialna za wydrukowanie „błędu segmentacji” (lub niektórych jego wariantów).
Braden Best,

2
@BradenBest - Zrobiłem to samo, mój kod jest niechlujny niż Twój kod. W ogóle nie dostałem żadnej wiadomości, a moja jeszcze szybsza skorupa nic nie drukuje. Użyłem waitpid()na każdym rozwidleniu / exec i zwraca inną wartość dla procesów, które mają błąd segmentacji, niż procesy kończące się na 0.
Bruce Ediger

21

Nie mogę znaleźć żadnych informacji na ten temat oprócz „MMU procesora wysyła sygnał” i „jądro kieruje je do szkodliwego programu, kończąc je”.

To trochę zniekształcone podsumowanie. Mechanizm sygnału uniksowego różni się całkowicie od zdarzeń specyficznych dla procesora, które rozpoczynają proces.

Zasadniczo po uzyskaniu dostępu do złego adresu (lub zapisaniu go w obszarze tylko do odczytu, próbie wykonania sekcji niewykonywalnej itp.), Procesor generuje pewne zdarzenia specyficzne dla procesora (w tradycyjnych architekturach innych niż VM było to zwane naruszeniem segmentacji, ponieważ każdy „segment” (tradycyjnie „tekst” wykonywalny tylko do odczytu, zapisywalne i „dane” o zmiennej długości oraz stos tradycyjnie na przeciwległym końcu pamięci) miał ustalony zakres adresów - w nowoczesnej architekturze bardziej prawdopodobne jest uszkodzenie strony [w przypadku niezmapowanej pamięci] lub naruszenie dostępu [w przypadku problemów z odczytem, ​​zapisem i wykonywaniem uprawnień], i skupię się na tym do końca odpowiedzi).

Teraz w tym momencie jądro może robić kilka rzeczy. Błędy strony są również generowane dla pamięci, która jest ważna, ale nie jest załadowana (np. Zamieniona, w pliku mmapa itp.). W takim przypadku jądro zamapuje pamięć, a następnie ponownie uruchomi program użytkownika z instrukcji, która spowodowała błąd. W przeciwnym razie wysyła sygnał. Nie jest to dokładnie „bezpośrednie [oryginalne zdarzenie] do programu naruszającego prawa”, ponieważ proces instalowania procedury obsługi sygnału jest inny i w większości niezależny od architektury, w przeciwieństwie do tego, czy program ma symulować instalację programu obsługi przerwań.

Jeśli program użytkownika ma zainstalowaną procedurę obsługi sygnału, oznacza to utworzenie ramki stosu i ustawienie pozycji wykonania programu użytkownika na procedurę obsługi sygnału. To samo dzieje się w przypadku wszystkich sygnałów, ale w przypadku naruszenia segmentacji rzeczy są ogólnie ustawione tak, że jeśli procedura obsługi sygnału zwróci, ponownie uruchomi instrukcję, która spowodowała błąd. Program użytkownika mógł naprawić błąd, np. Poprzez mapowanie pamięci na adres naruszający - zależy to od architektury, czy jest to możliwe). Procedura obsługi sygnału może również przeskoczyć w inne miejsce w programie (zazwyczaj przez longjmp lub przez zgłoszenie wyjątku), aby przerwać wszelkie operacje powodujące zły dostęp do pamięci.

Jeśli program użytkownika nie ma zainstalowanej procedury obsługi sygnału, zostaje po prostu zakończony. W przypadku niektórych architektur, jeśli sygnał zostanie zignorowany, może wielokrotnie uruchamiać instrukcję, powodując nieskończoną pętlę.


+1, tylko odpowiedź, która dodaje coś do zaakceptowanej. Ładny opis historii „segmentacji”. Ciekawostka: x86 nadal ma ograniczenia segmentów w 32-bitowym trybie chronionym (z włączonym lub bez stronicowania (pamięć wirtualna)), więc instrukcje, które może wygenerować pamięć dostępu #PF(fault-code)(błąd strony) lub #GP(0)(„Jeśli efektywny adres argumentu pamięci znajduje się poza CS, Limit segmentów DS, ES, FS lub GS. ”). Tryb 64-bitowy ogranicza sprawdzanie limitów segmentów, ponieważ systemy operacyjne właśnie użyły zamiast tego stronicowania oraz płaski model pamięci dla przestrzeni użytkownika.
Peter Cordes

Właściwie uważam, że większość systemów operacyjnych na x86 używa segmentacji stronicowania: kilka dużych segmentów w płaskiej, stronicowanej przestrzeni adresowej. W ten sposób chronisz i mapujesz pamięć jądra w każdej przestrzeni adresowej: pierścienie (poziomy ochrony) są połączone z segmentami, a nie ze stronami
Lorenzo Dematté

Również w NT (ale chciałbym wiedzieć, czy w większości Uniksów jest taki sam!) „Błąd segmentacji” może się zdarzać dość często: na początku przestrzeni użytkownika jest segment chroniony na 64k, więc wyłuskowanie wskaźnika NULL powoduje wzrost (prawda?) błąd segmentacji
Lorenzo Dematté

1
@ LorenzoDematté Tak, prawie wszystkie współczesne Unixy pozostawiają fragment trwale niezmapowanych adresów na początku przestrzeni adresowej w celu wychwycenia zerowych dereferencji. Może być dość duży - w systemach 64-bitowych może to być cztery gigabajty , więc przypadkowe obcięcie wskaźników do 32 bitów zostanie natychmiast złapane. Jednak segmentacja w ścisłym sensie x86 jest ledwo używana; jest jeden płaski segment dla przestrzeni użytkownika i jeden dla jądra, a może kilka specjalnych trików, takich jak wykorzystanie FS i GS.
zwolnienie

1
@ LorenzoDematté NT używa raczej wyjątków niż sygnałów; w tym przypadku STATUS_ACCESS_VIOLATION.
Random832

18

Błąd segmentacji to dostęp do adresu pamięci, który jest niedozwolony (nie jest częścią procesu lub próbuje zapisać dane tylko do odczytu lub wykonać dane niewykonywalne, ...). Jest to wychwytywane przez MMU (jednostka zarządzania pamięcią, dziś część procesora), powodując przerwanie. Przerwanie jest obsługiwane przez jądro, które wysyła SIGSEGFAULTsygnał (patrz signal(2)na przykład) do procesu obrażającego. Domyślna procedura obsługi tego sygnału zrzuca rdzeń (patrz core(5)) i kończy proces.

Powłoka absolutnie nie ma w tym udziału.


3
Więc twoja biblioteka C, podobnie jak glibc na pulpicie, definiuje ciąg?
Drawbenn

7
Warto również zauważyć, że SIGSEGV można obsługiwać / ignorować. Możliwe jest więc napisanie programu, który nie zostanie przez niego zakończony. Java Virtual Machine jest jednym z godnych uwagi przykładów, które używają SIGSEGV wewnętrznie do różnych celów, jak wspomniano tutaj: stackoverflow.com/questions/3731784/…
Karol Nowak

2
Podobnie w Windowsie .NET nie zawraca sobie głowy dodawaniem zerowych kontroli wskaźnika w większości przypadków - po prostu wychwytuje naruszenia zasad dostępu (odpowiednik segfault).
immibis
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.