Dlaczego nie można programowo zrealizować ochrony pamięci?

0

Jakiś czas temu dowiedziałem się, że ochrona pamięci musi być realizowa sprzętowo, bo programowa jej emulacja spowolniłaby wykonanie nawet ze 100 razy.

Nie za bardzo rozumiem, dlaczego?

Ot, pomysł. System operacyjny działa na procesorze nie posiadającym wsparcia dla ochrony pamięci. W związku z tym, za każdym razem, gdy uruchamiany jest jakiś program, OS wpierw skanuje binarkę w poszukiwaniu odwołań do pamięci operacyjnej (ale już nie np. rejestrów procesora) i każde takie odwołanie otacza ifami sprawdzającymi, czy adres jest w porządku.

Pewnie napisałem bzdury…?

3
  1. A zdajesz sobie sprawę że programy cały czas się odwołują do pamięci? Wiesz ile byś miał takich ifów? o_O Poza tym czym jest "odwołanie do pamięci"? Skok pod jakis adres to też jakieś odwołanie do pamięci. Chcesz testować też każdy skok?
  2. A co z alokacjami/realokacjami? Przecież przestrzeń adresowa procesu cały czas może ulegać zmianie... więc i te twoje "poprawne zakresy" też by musiały być płynne i zmienne.
1

No, i gdyby kod byłby self-modifying to już w ogóle ify (i statyczna analiza też) leżą i kwiczą.
Ale przykładowo pomijając nowoczesne systemy, na 8086 (i podobnych) gdzie właściwie nie było ochrony pamięci poza segmentacją to w pewnym stopniu było by chyba możliwe, jako taki proof-of-concept. Wykluczając też że kod będzie się sam modyfikował. Deasemblujesz i parsujesz sobie binarkę i np. Emulujesz jakoś (w sensie na wyrywki) stan procesora przed każdą instrukcją która odnosi się do pamięci.
// edit
A najfajniejszy to byłby chyba framework do emulacji może niekoniecznie całego procka od zera, ale coś trochę jak Wine, który odpalałby program w emulowanym środowisku i jednocześnie dawał pełny wgląd do jego bebechów.
Jeśli piszę głupoty to sorry, ale tak też sie ostatnio nad podobnymi sprawami zastanawiałem :P

3

każde takie odwołanie otacza ifami sprawdzającymi, czy adres jest w porządku.

Samo takie otaczanie zmienia adresy w kodzie. A OS nie jest w stanie wykryć co jest kodem, a co danymi, bo mogą być one przemieszane z sobą. Dodatkowo x86 zezwala na dowolną arytmetykę wskaźników i możesz mieć zmienne typu połowaAdresuMagicznejZmiennej, którą to potem skalujesz przez 2 podczas użycia. W jaki sposób parser miałby się zorientować statycznie, że coś takiego będzie miało miejsce w czasie wykonania? W dość ekstremalnym przypadku to nawet program może sobie ściągnąć adresy z neta. I co wtedy? Ściąganie całego Internetu przed odpaleniem programu?

0
kmph napisał(a):

Jakiś czas temu dowiedziałem się, że ochrona pamięci musi być realizowa sprzętowo, bo programowa jej emulacja spowolniłaby wykonanie nawet ze 100 razy.

Nie to, że musi. Jest to wydajniejsze. Tak samo jak nowoczesny krzem zawiera specjalne instrukcje do obslugi np. aes albo kodekow wideo.

kmph napisał(a):

Ot, pomysł. System operacyjny działa na procesorze nie posiadającym wsparcia dla ochrony pamięci. W związku z tym, za każdym razem, gdy uruchamiany jest jakiś program, OS wpierw skanuje binarkę w poszukiwaniu odwołań do pamięci operacyjnej (ale już nie np. rejestrów procesora) i każde takie odwołanie otacza ifami sprawdzającymi, czy adres jest w porządku.

  1. System nie wie jeszcze jakie adresu są w porządku. Zakres adresów może być przydzielony podczas działania programy poprzez syscalle albo bibliotekę standardową (ktora i tak uzywa syscalli).
  2. Generalnie coś podobnego jest realizowane z pomocą sprzętu. Tzn:
  • system ładuje binarkę, zapisuje sobie mapowania pamieci, tworzy podstawowe mapowania stron dla kontrolera pamieci
  • gdy program odnosi się do jakiegos miejsca w pamieci kontroler sprawdza czy dany adres wirtualny jest zamapowany
  • jezeli adres nie jest zmapowany w kontrolerze to jest wysylany sygnal (przerwanie sprzetowe)
  • przerwanie powoduje wykonanie procedury obslugi pamieci, system sprawdza czy dany adres jest poprawny dla danego procesu, jezeli jest poprawny ale nie zamapowany w kontolerze to go domapowuje i przydziela strony fizyczne, jak jest niepoprawny to wysyla odpowiedni sygnal do procesu, co skutkuje zazwyczaj jego zabiciem
  1. Widzisz proces powyżej? To się dzieje non stop podczas czasu życia programu jak odnosi się do jakiegoś adresu wirtualnego. Dlatego wykorzystywane jest wsparcie sprzętowe.
Wibowit napisał(a):

każde takie odwołanie otacza ifami sprawdzającymi, czy adres jest w porządku.

Samo takie otaczanie zmienia adresy w kodzie. A OS nie jest w stanie wykryć co jest kodem, a co danymi, bo mogą być one przemieszane z sobą.

A sekcje .text i .data w binarce (elf), uprawnienia sekcji oraz oddzielny adress space dla instrukcji i danych w architekurze Harvardzkiej coś Ci mówią? :)

PS.
http://sva.cs.illinois.edu/index.html (http://sva.cs.illinois.edu/sva.html)

0

A sekcje .text i .data w binarce (elf), uprawnienia sekcji oraz oddzielny adress space dla instrukcji i danych w architekurze Harvardzkiej coś Ci mówią? :)

Kodziłem przez wiele lat w czystym asemblerze pod 16-bit i 32-bit. Wiem o czym mówię. Da się spokojnie pomieszać dane z kodem w architekturze x86. Da się też tworzyć samo-modyfikowalny kod. Zakładam też, że autora interesuje architektura x86.

Ponadto, za https://en.wikipedia.org/wiki/Modified_Harvard_architecture :

The Modified Harvard architecture is a variation of the Harvard computer architecture that allows the contents of the instruction memory to be accessed as if it were data. Most modern computers that are documented as Harvard architecture are, in fact, Modified Harvard architecture.

0

Samo takie otaczanie zmienia adresy w kodzie. A OS nie jest w stanie wykryć co jest kodem, a co danymi, bo mogą być one przemieszane z sobą.

Jestem w stanie sobie wyobrazić maszynę wirtualną na wzór Javy czy .NETa, której kod pośredni byłby zgodny z x86 a VM implementowałaby programowo kontrolę pamięci.
Ale wtedy to przecież będzie po prostu emulator.

Tu nie chodzi o "programowo" kontra "sprzętowo". Kontrola pamięci musi być transcendentna względem uruchamianego programu - musi pochodzić z zewnątrz – czy to od sprzętu czy od udającego sprzęt emulatora – a nie da się jej zrobić sensownie w ramach wykonywanego kodu (i nie jest to kwestia uprawnień user vs kernel).

0
Wibowit napisał(a):

A sekcje .text i .data w binarce (elf), uprawnienia sekcji oraz oddzielny adress space dla instrukcji i danych w architekurze Harvardzkiej coś Ci mówią? :)

Kodziłem przez wiele lat w czystym asemblerze pod 16-bit i 32-bit. Wiem o czym mówię. Da się spokojnie pomieszać dane z kodem w architekturze x86. Da się też tworzyć samo-modyfikowalny kod. Zakładam też, że autora interesuje architektura x86.

W czystym asm bez kernala owszem. Ale da się też temu zapobiec. To system operacyjny definiuje ABI. System definiuje format dla kontenera kodu. System narzuca mechanizmy ochronne. Kompilator dorzuca mechanizmy ochronne do twojego kodu i to on emituje binarke, ktora jest w stanie sie prozumiec z systemem. Teoretycznie wiec jest to realizowalne. Ale po co, skoro mamy do tego sprzęt. Rozwój sprzętu i softu są ze sobą ściśle powiązane.

Azarien napisał(a):

Jestem w stanie sobie wyobrazić maszynę wirtualną na wzór Javy czy .NETa, której kod pośredni byłby zgodny z x86 a VM implementowałaby programowo kontrolę pamięci.
Ale wtedy to przecież będzie po prostu emulator.

Wirtualizacja. Rolą systemu jest między innymi wirtualziacja pamięci, czasu procesora, zasobów, etc. Inna sprawa, że obecnie wirtualizacja jest realizwoana sprzętowo gdzie tylko się da.

0

Jeśli będziesz narzucał coraz to kolejne ograniczenia to w końcu będziesz mógł dojść do takiego stanu, że programowa realizacja ochrony pamięci stanie się łatwa. Skoro kontrolujesz ładowanie programów to możesz wymusić by np każde odwołanie do pamięci było poprzez wywołanie funkcji typu odczytajBajt, zapiszBajt, etc Brak wydajnej programowej ochrony pamięci sugeruje jednak, że nikt poważny nie planuje tak ścisłych ograniczeń.

Z architekturą x86 jest też kolejny problem jeśli chodzi o statyczną analizę - instrukcje mogą być różnej długości i zaczynać się w dowolnym miejscu. To drastycznie utrudnia deasemblację. Każdy skok może skoczyć w dowolne miejsce wewnątrz dozwolonej przestrzeni adresowej i przez to statyczny analizator może przeoczyć występujące instrukcje wymagające wysokich uprawnień (czyli chociażby dostępu do zabronionego obszaru pamięci).

0

Nie tylko x86 istnieje. ARM na staly rozmair instrukcji. Poza tym statyczna analzia nie musi odbywać się na poziomie asemblera, może na reprezentacji posredniej.
Wchodząc na wyższy poziom abstrackji, system nie musi nawet pozwalać na wykonywanie natywnego kodu z poziomu userspaca.

Mam nadzieję, że autor tematu rozumie o czym mówimy. Najlepiej jakby zainwestował własny czas nad pogłębienie wiedzy o architekturze komputerów.

0
kmph napisał(a):

...

"
Ochrona pamięci polega na ograniczeniu zakresu dostępnych adresów do pewnego podzbioru ograniczonego poprzez podanie najwyższego i najniższego dopuszczalnego adresu. W tym schemacie, najniższy adres jest przechowywany w rejestrze bazowym. Rejestr graniczny z kolei, określa wielkość dostępnego obszaru pamięci. Musi zostać zapewniona pamięć co najmniej dla przerwania sprzętowego oraz systemowych procedur obsługi przerwań. Pamięć poza zdefiniowanym zakresem jest chroniona."

0

Nie można?

Singularity (oraz Midori) używały wyłącznie programowej izolacji procesów, i to chyba właśnie w taki sposób, jak myśli @kmph: przez analizę statyczną, kontrakty i te wszystkie inne elementy bezpieczeństwa w Singularity. Oczywiście, programy dla Singularity były pisane w języku zarządzanym, bez bloków unsafe.

Polecam całą serię wpisów blogowych od autora Midori: http://joeduffyblog.com/2015/11/03/blogging-about-midori/, w szczególności dla tego tematu polecam http://joeduffyblog.com/2015/11/03/a-tale-of-three-safeties/.

0

Oczywiście, programy dla Singularity były pisane w języku zarządzanym, bez bloków unsafe.

No i to właśnie całkowicie zmienia postać rzeczy. Język zarządzany zawiera tyle ograniczeń w porównaniu do języka natywnego, że łatwo zrobić dla niego statyczną analizę.

Jak piszę pod JVMa i nie korzystam z JNI ani z klasy Unsafe to też w zasadzie mam programową ochronę pamięci. Nie mam arytmetyki wskaźników, a JVM chroni mnie przed wyjeżdżaniem poza zakres tablicy czy dereferencjonowaniem pustego wskaźnika. Ochrona polega na wyrzuceniu Javowych wyjątków obsługiwanych wewnątrz JVMa zamiast robienia nieprzewidywalnych rzeczy, gdyby tej ochrony nie było.

0

Kolejnym przykladem programowej ochrony pamięci opartej na analizie statycznej może być Rust. Ale tam masz ograniczenia jeszcze bardziej restrykcyjne niż w JVM / CLR.

1 użytkowników online, w tym zalogowanych: 0, gości: 1