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 bash
biegania 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?
źródło
crsh
to świetny pomysł na tego rodzaju eksperymenty. Dziękujemy za poinformowanie nas o tym i idei, która za tym stoi.crsh
, pomyślałem, że będzie to wymawiane jako „crash”. Nie jestem pewien, czy to równie odpowiednie imię.system()
robi pod maską. Okazuje się, żesystem()
odrodzi się proces powłoki! Twój proces powłoki odradza inny proces powłoki, a ten proces powłoki (prawdopodobnie/bin/sh
lub coś w tym rodzaju) jest tym, który uruchamia program. Sposób/bin/sh
lubbash
działanie polega na użyciufork()
iexec()
(lub innej funkcji wexecve()
rodzinie).man 2 wait
, będzie zawierać makraWIFSIGNALED()
iWTERMSIG()
.(WIFSIGNALED(status) && WTERMSIG(status) == 11)
aby wydrukować coś goofy ("YOU DUN GOOFED AND TRIGGERED A SEGFAULT"
). Kiedy uruchomiłemsegfault
program od wewnątrzcrsh
, wydrukował dokładnie to. Tymczasem polecenia, które wychodzą normalnie, nie powodują wyświetlenia komunikatu o błędzie.Odpowiedzi:
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,6Waż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ń”.
SIGSEGV
jest 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
SIGSEGV
sygnał dla procesu. Załóżmy, że proces nie skonfigurował modułu obsługi sygnałówSIGSEGV
, więc jądro wykonuje domyślne zachowanie, które polega na zakończeniu procesu. Ma to takie same skutki jak_exit
wywoł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.
SIGSEGV
idzie 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 zwait
wywoł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 zachowaniaSIGSEGV
sygnał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
crsh
kod nie zawiera do tego kodu, ale i tak się dzieje, ponieważ procedura biblioteki Csystem
uruchamia w pełni funkcjonalną powłokę/bin/sh
„pod maską”.crsh
jest 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/sh
samo się kończy, ponieważ nie ma już nic do roboty, a implementacja biblioteki Csystem
odbiera 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
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.
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 .
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.
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 ”.Niektóre naruszenia ochrony pamięci generują
SIGBUS
(„Błąd magistrali”) zamiastSIGSEGV
. 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ługiSIGSEGV
, prawdopodobnie dobrym pomysłem jest zdefiniowanie tego samego modułu obsługiSIGBUS
.„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.
Wszystkie inne sposoby, w jakie proces nadrzędny może zostać powiadomiony o zakończeniu dziecka, kończą się tym, że rodzic dzwoni
wait
i otrzymuje status wyjścia. Po prostu coś innego dzieje się najpierw.źródło
mmap
pliku 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.)Powłoka rzeczywiście ma coś wspólnego z tą wiadomością i
crsh
pośrednio wywołuje powłokę, co prawdopodobnie jestbash
.Napisałem mały program C, który zawsze segreguje błędy:
Kiedy uruchamiam go z domyślnej powłoki,
zsh
otrzymuję:Kiedy go uruchamiam
bash
, otrzymuję to, co zauważyłeś w swoim pytaniu:Zamierzałem napisać procedurę obsługi sygnału w moim kodzie, a potem zdałem sobie sprawę, że
system()
wywołanie biblioteki używane przezcrsh
exec jest powłoką,/bin/sh
zgodnie zman 3 system
. To/bin/sh
prawie na pewno drukuje „Błąd segmentacji”, ponieważ nacrsh
pewno nie jest.Jeśli ponownie napiszesz,
crsh
aby użyćexecve()
wywołania systemowego do uruchomienia programu, nie zobaczysz ciągu „Błąd segmentacji”. Pochodzi z powłoki wywoływanej przezsystem()
.źródło
execvp
i 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).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.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ę.
źródło
#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.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
SIGSEGFAULT
sygnał (patrzsignal(2)
na przykład) do procesu obrażającego. Domyślna procedura obsługi tego sygnału zrzuca rdzeń (patrzcore(5)
) i kończy proces.Powłoka absolutnie nie ma w tym udziału.
źródło