Czy system operacyjny wstrzykuje własny kod maszynowy podczas otwierania programu?

32

Studiuję procesory i wiem, jak odczytuje program z pamięci i wykonuję jego instrukcje. Rozumiem również, że system operacyjny oddziela programy w procesach, a następnie przełącza się między nimi tak szybko, że wydaje się, że działają one w tym samym czasie, ale w rzeczywistości każdy program działa sam na procesorze. Ale jeśli system operacyjny to także fragment kodu działający w procesorze, jak może zarządzać procesami?

Myślałem i jedynym wyjaśnieniem, jakie mogłem wymyślić, jest: kiedy system operacyjny ładuje program z pamięci zewnętrznej do pamięci RAM, dodaje własne instrukcje w środku oryginalnych instrukcji programu, więc program jest wykonywany, program może zadzwonić do systemu operacyjnego i zrobić kilka rzeczy. Wierzę, że istnieje instrukcja, którą system operacyjny doda do programu, która pozwoli procesorowi na powrót do kodu systemu operacyjnego na jakiś czas. Ponadto uważam, że gdy system operacyjny ładuje program, sprawdza, czy istnieją jakieś zabronione instrukcje (które przeskakują do zabronionych adresów w pamięci) i wtedy eliminuje.

Czy myślę o sztywności? Nie jestem studentem CS, ale tak naprawdę studentem matematyki. Jeśli to możliwe, chciałbym mieć dobrą książkę na ten temat, ponieważ nie znalazłem nikogo, kto wyjaśniłby, w jaki sposób system operacyjny może zarządzać procesem, jeśli system operacyjny jest również zbiorem kodu działającego w procesorze i nie może działać w tym samym czasie czas programu. Książki mówią tylko, że system operacyjny może zarządzać rzeczami, ale teraz jak.

Cofanie Sumody
źródło
7
Zobacz: Przełącznik kontekstu System operacyjny przełącza kontekst na aplikację. Aplikacja może następnie zażądać usług systemu operacyjnego, które wykonują kontekst z powrotem do systemu operacyjnego. Po zakończeniu aplikacji kontekst przełącza się z powrotem na system operacyjny.
Guy Coder,
4
Zobacz także „syscall”.
Raphael
1
Jeśli komentarze i odpowiedzi nie odpowiadają Twojemu pytaniu na twoje zrozumienie lub satysfakcję, poproś o więcej informacji jako komentarz i wyjaśnij, co myślisz, gdzie jesteś zagubiony lub na czym dokładnie potrzebujesz więcej szczegółów.
Guy Coder
2
Myślę, że główne, potrzebne słowa kluczowe to interrupt , hooking (of a interrupt), sprzętowy timer (z harmonogramem obsługującym hook) i stronicowanie (częściowa odpowiedź na twoją uwagę na temat zabronionej pamięci). System operacyjny musi ściśle współpracować z procesorem, aby uruchomić swój kod tylko w razie potrzeby. Dlatego większość mocy procesora może być wykorzystana do obliczeń, a nie do zarządzania.
Palec,

Odpowiedzi:

35

Nie. System operacyjny nie zadziera z kodem programu wstrzykującym do niego nowy kod. Miałoby to szereg wad.

  1. Byłoby to czasochłonne, ponieważ system operacyjny musiałby przeskanować cały plik wykonywalny, wprowadzając zmiany. Zwykle część pliku wykonywalnego jest ładowana tylko w razie potrzeby. Ponadto wkładanie jest drogie, ponieważ musisz przenieść ładunek rzeczy na bok.

  2. Z powodu nierozstrzygalności problemu zatrzymania nie można wiedzieć, gdzie wstawić instrukcje „Powrót do systemu operacyjnego”. Na przykład, jeśli kod zawiera coś podobnego while (true) {i++;}, zdecydowanie musisz wstawić hak do tej pętli, ale warunki w pętli ( truetutaj) mogą być dowolnie skomplikowane, więc nie możesz zdecydować, jak długo pętla będzie. Z drugiej strony wstawianie haczyków do każdej pętli byłoby bardzo nieefektywne : na przykład przeskakiwanie z powrotem do systemu operacyjnego for (i=0; i<3; i++) {j=j+i;}znacznie spowolniłoby ten proces. Z tego samego powodu nie można wykryć krótkich pętli, aby zostawić je w spokoju.

  3. Z powodu nierozstrzygalności problemu zatrzymania nie można ustalić, czy wstrzyknięcia kodu zmieniły znaczenie programu. Załóżmy na przykład, że używasz wskaźników funkcji w swoim programie C. Wstrzyknięcie nowego kodu spowodowałoby przesunięcie lokalizacji funkcji, więc po wywołaniu go przez wskaźnik przeskoczyłbyś w niewłaściwe miejsce. Gdyby programista był na tyle chory, aby użyć skoków obliczeniowych, również one by się nie udały.

  4. Odgrywałoby to wesołe piekło z każdym systemem antywirusowym, ponieważ zmieniałby również kod wirusa i zniszczył wszystkie sumy kontrolne.

Możesz obejść problem z zatrzymaniem, symulując kod i wstawiając haki w dowolnej pętli, która wykonuje więcej niż określoną stałą liczbę razy. Wymagałoby to jednak wyjątkowo kosztownej symulacji całego programu, zanim mógł on zostać uruchomiony.

Właściwie, jeśli chcesz wstrzyknąć kod, kompilator byłby naturalnym miejscem do zrobienia tego. W ten sposób musiałbyś to zrobić tylko raz, ale nadal nie działałoby z drugiego i trzeciego powodu podanego powyżej. (I ktoś mógłby napisać kompilator, który nie grał razem).

Istnieją trzy główne sposoby odzyskania kontroli przez system z procesów.

  1. W systemach współpracujących (lub nieprzekazujących) istnieje yieldfunkcja, którą proces może wywołać w celu przywrócenia kontroli nad systemem operacyjnym. Oczywiście, jeśli jest to twój jedyny mechanizm, polegasz na procesach, które zachowują się ładnie, a proces, który nie daje rezultatu, spowoduje zatrzymanie procesora aż do jego zakończenia.

  2. Aby uniknąć tego problemu, stosuje się przerwanie timera. Procesory pozwalają systemowi operacyjnemu rejestrować wywołania zwrotne dla wszystkich rodzajów przerwań realizowanych przez procesor. System operacyjny używa tego mechanizmu do rejestrowania wywołania zwrotnego dla przerwania czasomierza uruchamianego okresowo, co umożliwia mu wykonanie własnego kodu.

  3. Za każdym razem, gdy proces próbuje odczytać z pliku lub wchodzić w interakcje ze sprzętem w jakikolwiek inny sposób, prosi system operacyjny o wykonanie pracy. Gdy system operacyjny zostanie poproszony o zrobienie czegoś przez proces, może zdecydować o wstrzymaniu tego procesu i uruchomieniu innego. Może to zabrzmieć nieco makiawelicznie, ale jest to słuszne: dyskowe operacje we / wy są powolne, więc równie dobrze możesz pozwolić procesowi B na uruchomienie, podczas gdy proces A czeka na wirujące bryły metalu w odpowiednim miejscu. Sieć we / wy jest jeszcze wolniejsza. Klawiatura I / O jest lodowata, ponieważ ludzie nie są istotami gigahercowymi.

David Richerby
źródło
5
Czy możesz rozwinąć więcej na swoim 2. punkcie? Jestem ciekawa tego pytania i czuję, że wyjaśnienie zostało pominięte tutaj. Wydaje mi się, że pytanie brzmi „jak system operacyjny odbiera procesor z procesu”, a twoja odpowiedź mówi „system operacyjny sobie z tym radzi”. ale jak? Weźmy nieskończoną pętlę w swoim pierwszym przykładzie: jak nie zamraża komputera?
BiAiB
3
Niektóre systemy operacyjne tak robią, większość z nich przynajmniej ma problem z kodem do wykonania „łączenia”, więc program można załadować pod dowolnym adresem
Ian Ringrose
1
@BiAiB Kluczowym słowem jest tutaj „przerwać”. Procesor nie jest tylko czymś, co przetwarza dany strumień instrukcji, może być również asynchronicznie przerywany z osobnego źródła - co najważniejsze dla nas, wejścia / wyjścia i przerwania zegara. Ponieważ tylko kod przestrzeni jądra może obsługiwać przerwania, system Windows może być w stanie „ukraść” pracę z dowolnego uruchomionego procesu w dowolnym momencie. Procedury obsługi przerwań mogą wykonywać dowolny kod, w tym „przechowywać gdzieś rejestry procesora i przywracać je stąd (inny wątek)”. Niezwykle uproszczone, ale taka jest zmiana kontekstu.
Luaan
1
Dodanie do tej odpowiedzi; styl wielozadaniowości, o którym mowa w punktach 2 i 3, nazywa się „zapobiegawczym wielozadaniowością”, nazwa odnosi się do zdolności systemu operacyjnego do zatrzymania uruchomionego procesu. Wspólna wielozadaniowość była często używana w starszych systemach operacyjnych; w systemie Windows przynajmniej zapobiegawcza wielozadaniowość została wprowadzona dopiero w Windows 95. Czytałem o co najmniej jednym stosowanym dzisiaj przemysłowym systemie kontroli, który nadal używa Windows 3.1 wyłącznie ze względu na jego zachowanie podczas wielozadaniowości w czasie rzeczywistym.
Jason C
3
@BiAiB W rzeczywistości się mylisz. Procesory do komputerów stacjonarnych nie uruchamiają kodu sekwencyjnie i synchronicznie od czasu około i486. Jednak nawet starsze procesory nadal miały asynchroniczne wejścia - przerwania. Wyobraź sobie żądanie przerwania sprzętowego (IRQ), podobnie jak pin na samym procesorze - gdy się pojawi 1, procesor zatrzymuje wszystko, co robi, i rozpoczyna przetwarzanie przerwania (co w zasadzie oznacza „zachowaj stan i przeskocz do adresu w pamięci”). Sama obsługa przerwań nie jest żadnym x86kodem, jest dosłownie podłączona. Po skoku ponownie wykonuje (dowolny) x86kod. Wątki są znacznie wyższą abstrakcją.
Luaan,
12

Chociaż odpowiedź Davida Richerby'ego jest dobra, w pewien sposób rzuca światło na to, w jaki sposób nowoczesne systemy operacyjne zatrzymują istniejące programy. Moja odpowiedź powinna być dokładna dla architektury x86 lub x86_64, która jest jedyną powszechnie używaną w komputerach stacjonarnych i laptopach. Inne architektury powinny mieć podobne metody osiągnięcia tego celu.

Podczas uruchamiania systemu operacyjnego tworzona jest tabela przerwań. Każdy wpis w tabeli wskazuje trochę kodu w systemie operacyjnym. Kiedy dochodzi do przerwań, które są kontrolowane przez CPU, patrzy na tę tabelę i wywołuje kod. Istnieje wiele przerwań, takich jak dzielenie przez zero, niepoprawny kod i niektóre zdefiniowane przez system operacyjny.

W ten sposób proces użytkownika komunikuje się z jądrem, na przykład, jeśli chce czytać / zapisywać na dysku lub coś innego, co kontroluje jądro systemu operacyjnego. System operacyjny skonfiguruje również licznik czasu, który wywołuje przerwanie po zakończeniu, więc działający kod jest siłą zmieniany z programu użytkownika na jądro systemu operacyjnego, a jądro może wykonywać inne czynności, takie jak kolejkowanie innych programów do uruchomienia.

Z pamięci, kiedy tak się dzieje, jądro systemu operacyjnego musi zapisać tam, gdzie był kod, a kiedy jądro skończy robić to, co trzeba, przywraca poprzedni stan programu. Dlatego program nawet nie wie, że został przerwany.

Proces nie może zmienić tabeli przerwań z dwóch powodów. Pierwszy polega na tym, że działa w środowisku chronionym, więc jeśli spróbuje wywołać określony kod chronionego zestawu, procesor wyzwoli kolejne przerwanie. Drugim powodem jest pamięć wirtualna. Lokalizacja tabeli przerwań ma wartość od 0x0 do 0x3FF w rzeczywistej pamięci, ale w procesach użytkownika lokalizacja ta zwykle nie jest mapowana, a próba odczytania niezapisanej pamięci spowoduje kolejne przerwanie, więc bez chronionej funkcji i możliwości zapisu do prawdziwej pamięci RAM , proces użytkownika nie może tego zmienić.

Programmdude
źródło
4
Przerwania nie są zdefiniowane przez system operacyjny, lecz przez sprzęt. Większość współczesnych architektur ma specjalne instrukcje do wywoływania systemu operacyjnego. i386 użył do tego (wygenerowanego programowo) przerwania, ale nie dzieje się tak już w przypadku następców.
vonbrand
2
Wiem, że przerwania są definiowane przez procesor, ale jądro ustawia wskaźniki. Prawdopodobnie źle to wytłumaczyłem. Pomyślałem również, że linux używał int 9, aby nadal rozmawiać z jądrem, ale może są teraz lepsze sposoby.
Programmdude
Jest to dość myląca odpowiedź, chociaż poprawne jest przekonanie, że harmonogramy wyprzedzające są sterowane przez przerwania timera. Po pierwsze warto zauważyć, że zegar jest sprzętowy. Również w celu wyjaśnienia, że ​​proces „zapisz ... przywróć” nazywa się przełączaniem kontekstu i polega głównie między innymi na zapisywaniu wszystkich rejestrów procesora (w tym wskaźnika instrukcji). Procesy mogą również skutecznie zmieniać tabele przerwań, co nazywa się „trybem chronionym”, który również definiuje pamięć wirtualną, i istnieje już od 286 - wskaźnik do tablicy przerwań jest zapisywany w rejestrze do zapisu.
Jason C
(Również tabela przerwania trybu rzeczywistego jest relokowalna - nie jest zablokowana na pierwszej stronie pamięci - od 8086.)
Jason C
1
W tej odpowiedzi brakuje krytycznego szczegółu. Po uruchomieniu przerwania procesor nie przełącza się bezpośrednio na jądro. Zamiast tego najpierw zapisuje istniejące rejestry, następnie przełącza się na inny stos i dopiero wtedy wywoływane jest jądro. Wywołanie jądra z losowym stosem z losowego programu byłoby raczej złym pomysłem. Również ostatnia część wprowadza w błąd. Nie dostaniesz „próby” odczytania niezapisanej pamięci; to po prostu niemożliwe. Czytasz z adresów wirtualnych, a niezamapowana pamięć po prostu nie ma adresu wirtualnego.
MSalters
5

Jądro systemu operacyjnego odzyskuje kontrolę nad działającym procesem z powodu procedury obsługi przerwań zegara procesora, a nie poprzez wstrzyknięcie kodu do procesu.

Powinieneś przeczytać o przerwaniach, aby uzyskać więcej wyjaśnień na temat ich działania oraz sposobu, w jaki jądra systemu operacyjnego je obsługują i implementują różne funkcje.

Ankur
źródło
Nie tylko przerwanie zegara: każde przerwanie. A także instrukcje zmiany trybu.
Gilles „SO- przestań być zły”
3

Nie jest to metoda podobna do tego, co można opisać: wielozadaniowość spółdzielczej . System operacyjny nie wstawia instrukcji, ale każdy program musi być napisany w celu wywołania funkcji systemu operacyjnego, które mogą wybrać uruchomienie innego procesu współpracy. Ma to wady, które opisujesz: awaria jednego programu usuwa cały system. Windows do wersji 3.0 włącznie włącznie działał w ten sposób; 3.0 w „trybie chronionym” i wyżej nie.

Zapobiegawcza wielozadaniowość (obecnie jest normalna) opiera się na zewnętrznym źródle przerwań. Przerwania zastępują normalny przepływ kontroli i zwykle zapisują gdzieś rejestry, więc CPU może zrobić coś innego, a następnie przejrzyście wznowić program. Oczywiście system operacyjny może zmienić rejestr „kiedy wychodzisz z przerwań, wznów tutaj”, więc wznawia się w innym procesie.

(Niektóre systemy zrobić przepisywania instrukcjami w ograniczonym stopniu na obciążenia program o nazwie „thunking”, a procesor Transmeta dynamicznie zrekompilowane do własnego zestawu instrukcji)

pjc50
źródło
AFAICR 3.1 również współpracował. Win95 był miejscem, w którym pojawiła się zapobiegawcza wielozadaniowość. Tryb chroniony przyniósł głównie izolację przestrzeni adresowej (co poprawia stabilność, ale z bardzo niezwiązanych powodów).
cHao
Thunking nie przepisuje ani nie wstrzykuje kodu do aplikacji. Zmodyfikowany moduł ładujący jest oparty na systemie operacyjnym, a nie produktem aplikacji. Języki interpretacyjne, które są kompilowane, na przykład przy użyciu kompilatorów JIT, nie modyfikują kodu ani nie wstrzykują niczego do kodu. Tłumaczą kod źródłowy na plik wykonywalny. Znowu nie jest to to samo, co wstrzykiwanie kodu do aplikacji.
Dave Gordon,
Transmeta wziął kod wykonywalny x86 jako źródło, a nie język interpretacyjny. I pomyślałem o jednym przypadku, w którym wstrzykiwany jest kod : działający pod debuggerem. Systemy X86 zwykle nadpisują instrukcję w punkcie przerwania „INT 03”, która pułapkuje na debugger. Po wznowieniu przywracany jest oryginalny kod operacji.
pjc50
Debugowanie nie jest sposobem, w jaki ktokolwiek uruchamia aplikację; poza twórcą aplikacji. Więc nie sądzę, żeby to naprawdę pomogło OP.
Dave Gordon
3

Wielozadaniowość nie wymaga wprowadzania kodu. W systemie operacyjnym, takim jak Windows, jest element kodu systemu operacyjnego zwany harmonogramem, który polega na przerwaniu sprzętowym wywołanym przez zegar sprzętowy. Jest to wykorzystywane przez system operacyjny do przełączania się między różnymi programami i samym sobą, co sprawia, że ​​wszystko wydaje się naszym ludzkim wyobrażeniem, że dzieje się to jednocześnie.

Zasadniczo system operacyjny programuje sprzętowy zegar tak, aby uruchamiał się tak często ... może 100 razy na sekundę. Kiedy licznik czasu się wyłącza, generuje przerwanie sprzętowe - sygnał, który informuje procesor, aby przerwał to, co robi, zapisać swój stan na stosie, zmienić tryb na bardziej uprzywilejowany i wykonać kod, który znajdzie w specjalnie wyznaczonym miejsce w pamięci. Ten kod bywa częścią harmonogramu, który decyduje o tym, co należy zrobić dalej. Może być konieczne wznowienie jakiegoś innego procesu, w którym to przypadku będzie on musiał wykonać tak zwany „przełącznik kontekstowy” - zastępując cały jego obecny stan (w tym tabele pamięci wirtualnej) stanem innego procesu. Wracając do procesu, musi przywrócić cały kontekst tego procesu,

„Specjalnie wyznaczone” miejsce w pamięci nie musi być znane tylko systemowi operacyjnemu. Implementacje są różne, ale ich sedno polega na tym, że procesor zareaguje na różne przerwania, wykonując wyszukiwanie w tabeli; lokalizacja tabeli znajduje się w określonym miejscu w pamięci (określonym przez konstrukcję sprzętową procesora), zawartość tabeli jest ustalana przez system operacyjny (zazwyczaj w czasie rozruchu), a „rodzaj” przerwania określa, który wpis w tabeli ma służyć jako „procedura obsługi przerwań”.

Żadna z tych czynności nie obejmuje „wstrzykiwania kodu” ... opiera się na kodzie zawartym w systemie operacyjnym we współpracy z funkcjami sprzętowymi CPU i jego obwodów pomocniczych.

Zenilogix
źródło
2

Myślę, że najbliższym przykładem tego, co opisujesz, jest jedna z technik używanych przez VMware , pełna wirtualizacja z wykorzystaniem tłumaczenia binarnego .

VMware działa jak warstwa pod co najmniej jednym systemem wykonującym jednocześnie na tym samym sprzęcie.

Większość wykonywanych instrukcji (np. W zwykłych aplikacjach) można zwirtualizować za pomocą sprzętu, ale samo jądro systemu operacyjnego korzysta z instrukcji, których nie można zwirtualizować, ponieważ jeśli kod maszynowy zgadywanego systemu operacyjnego zostałby wykonany niezmodyfikowany, „wybuchłby ”kontroli hosta VMware. Na przykład system operacyjny gościa musiałby działać w najbardziej uprzywilejowanym pierścieniu ochronnym i skonfigurować tabelę przerwań. Gdyby było to dozwolone, VMware straciłoby kontrolę nad sprzętem.

VMware przepisuje te instrukcje w kodzie systemu operacyjnego przed ich wykonaniem, zastępując je skokami do kodu VMware, który symuluje pożądany efekt.

Ta technika jest więc nieco analogiczna do tego, co opisujesz.

Daniel Earwicker
źródło
2

Istnieje wiele przypadków, w których system operacyjny może „wstrzyknąć kod” do programu. Wersje systemu Apple Macintosh oparte na 68000 budują tabelę wszystkich punktów wejścia segmentu (znajdujących się bezpośrednio przed statycznymi zmiennymi globalnymi, IIRC). Kiedy program się uruchamia, każdy wpis w tabeli składa się z instrukcji pułapki, po której następuje numer segmentu i przesunięcie do segmentu. Jeśli pułapka zostanie wykonana, system sprawdzi słowa po instrukcji pułapki, aby zobaczyć, jaki segment i przesunięcie jest wymagane, załaduj segment (jeśli jeszcze nie jest), dodaj adres początkowy segmentu do odsunięcia i następnie zastąp pułapkę skokiem do nowo obliczonego adresu.

W starszych programach komputerowych, chociaż technicznie nie było to zrobione przez „system operacyjny”, często budowano kod z instrukcjami pułapki zamiast instrukcji matematycznych koprocesora. Jeśli nie zainstalowano koprocesora matematycznego, moduł obsługi pułapek go emulował. Jeśli koprocesor został zainstalowany, to przy pierwszym wzięciu pułapki program obsługi zastąpi instrukcję pułapki instrukcją koprocesora; przyszłe wykonania tego samego kodu będą korzystać bezpośrednio z instrukcji koprocesora.

supercat
źródło
Metoda FP jest nadal używana w procesorach ARM, które w przeciwieństwie do procesorów x86 nadal mają warianty bez FP. Ale jest to rzadkie, ponieważ większość zastosowań ARM odbywa się w dedykowanych urządzeniach. W tych środowiskach zwykle wiadomo, czy procesor będzie miał możliwości FP.
MSalters
W żadnym z tych przypadków system operacyjny nie wstrzyknął kodu do aplikacji. Aby system operacyjny mógł wprowadzić kod, potrzebowałaby licencji producenta oprogramowania na „modyfikację” aplikacji, której nie otrzymuje. System operacyjny NIE wstrzykuje kodu.
Dave Gordon,
@DaveGordon Trapped instrukcje można słusznie powiedzieć, że są aplikacjami wstrzykującymi kod systemu operacyjnego.
Gilles 'SO - przestań być zły'
@MSalters Instrukcje uwięzione często zdarzają się na maszynach wirtualnych.
Gilles 'SO - przestań być zły'