Wyznaczanie adresu fizycznego w trybie rzeczywistym
Adres fizyczny komórki pamięci w trybie rzeczywistym procesorów serii Intel x86 jest wyznaczany z adresu logicznego, wg. następującej zależności:
segment - obszar w przestrzeni logicznej
offset - identyfikuje komórkę pamięci w obszarze pamięci (segmencie); jest to liczba wskazująca na komórkę pamięci względem początku segmentu
Wyznaczanie adresu fizycznego w trybie chronionym
Adres fizycznej komórki pamięci w trybie chronionym jest wyznaczany po wykonaniu szeregu obliczeń. Proces ten możemy podzielić na 2 etapy:
Wyznaczeniu adresu liniowego (segmentacja)
Przekształcenie adresu liniowego do adresu fizycznego (stronicowanie)
Na poniższym rysunku pokazany został proces translacji adresu logicznego do fizycznego.
Daną wejściową w procesie jest adres logiczny opisywany za pomocą selektora i offsetu. Adres liniowy jest wyznaczany na podstawie danych pamiętanych w strukturach deskryptorowych (globalna tablica deksryptorów, lokalna tablica deskryptorów, tablica deskryptorów przerwań). Adres fizyczny wyznaczany jest z adresu liniowego przy wykorzystaniu struktur danych jakimi są tablice katalogów stron i tablice stron. W związku, z tym że decyzje o uruchomieniu stronicowania podejmowania jest przez projektanta oprogramowania, adres liniowy może być równoważny adresowi fizycznemu (przy wyłączonym stronicowaniu).
Poniższy rysunek uszczegóławia proces wyznaczania adresu fizycznego w trybie chronionym.
Poniższy rysunek pokazuje metodę wyliczania adresu liniowego.
Adres liniowy zatem można wyznaczyć wg następującej zależności:
gdzie:
TD - definiuje tablice deskryptorów
base - adres segmentu w przestrzeni liniowej pamiętany w deskryptorze
>> - operacja logicznego przesunięcia o 3 bity w prawo
Modele pamięci
Płaski model pamięci
Cechy modelu:
Wspólna przestrzeń adresowa dla wszystkich uruchamianych procesów, lub programów
Brak ochrony przed próbą adresacji pamięci znajdującą się poza przestrzenią pamięci fizycznej
Najprostszy do implementacji w trybie chronionym
Na poniższym rysunku pokazana została konfiguracja oprogramownaia systemowego (np. systemu operacyjnego), która organizuje pamięć zgodnie z modelem płaskim.
W celu uruchomienia oprogramowania korzystającego z płaskiego modelu pamięci należy wykonać następujące działania:
Utworzyć globalną tablicę deskryptorów
W GDT utworzyć następujące deskryptory:
segment kodu, odczyt i wykonywanie
segment danych, zapis i odczyt
W deskryptorach przypisać następujące wartości do pól:
adres bazowy = 0
wielkość semgnetu = 0FFFFFFh (maksymalną możliwą dla danego typu deskryptora)
poziom ochrony semgnetu (DPL) = 0
pole ziarnistości ziarnistości = 1
Do rejestrów selektorów wpisać:
CS - selektor wskazujący na deskryptor segmentu kodu
DS, ES, FS, GS, SS - selektory wskazujące na segment danych
Po inicjalizacji uzyskuje się środowisko, w którym poszczególne procesy, lub programy współdzielą się dostępną pamięcią. Programista widzi pamięć jako jeden blok o wielkości 4GB.
Płaski chroniony model pamięci
Cechy modelu:
Wspólna przestrzeń adresowa dla wszystkich procesów, lub programów
Adresacja możliwa w obrębie dostępnej pamięci fizycznej; dostęp poza zdefiniowane obszary pamięci fizycznej sygnalizowany wyjątkami
Obszary pamięci przeznaczone na kod i dane mogą być odseparowane
Na poniższym rysunku pokazana została konfiguracja oprogramowaina systemowego (np. systemu operacyjnego), która organizuje pamięć zgodnie z modelem płaskim chronionym.
W celu uruchomienia oprogramowania korzystającego z płaskiego chronionego modelu pamięci należy wykonać następujące działania:
Utworzyć globalną tablicę deskryptorów
W GDT utworzyć następujące deskryptory:
segment kodu, odczyt i wykonywanie
segment danych, zapis i odczyt
W deskryptorze segmentu danych przypisać następujące wartości do pól:
adres bazowy = adres pamięci fizycznej przeznaczonej na dane np. 0
wielkość semgnetu = fizyczna wielkość pamięci
poziom ochrony semgnetu (DPL) = 0
pole ziarnistości = wartość uzależniona od wielkości pamięci
W deskryptorze segmentu kodu przypisać następujące wartości do pól:
adres bazowy = adres pamięci fizycznej, w którym umiejscowiony ma być kod
wielkość semgnetu = wielkość fizycznego obszaru pamięci
poziom ochrony segmentu (DPL) = 0
pole ziarnistości = wartość uzależniona od wielkości dostępnego obszaru pamięci
Do rejestrów selektorów wpisać:
CS - selektor wskazujący na deskryptor segmentu kodu
DS, ES, FS, GS, SS - selektory wskazujące na segment danych
Pozostałe pola danych deskryptorów należy dopasować do organizacji dostępnej pamięci. Model ten ma zastosowanie w przypadkach, gdy system komputerowy posiada zorganizowaną pamięć fizyczną w postaci bloków mapowanych do przestrzeni adresowej w taki sposób, że pamieć ta nie ma ciągłej adresacji (występują obszary, które nie są powiązane z fizyczną pamięcią). Zastosowanie w takim przypadku modelu płaskiego może prowadzić do sytuacji awarii na skutek nieuważnego napisania oprogramowania systemowego.
Model pamięci z segmentacją
Cechy modelu:
Przestrzeń adresowa pamięci dzielona jest na segmenty
Obszary pamięci (segmenty) są przypisywane do określonego oprogramownaia (procesów)
Możliwa separacja obszarów pamięci przypisanych do różnych procesów, lub programów
Wykrywanie naruszeń obszarów pamięci i sygnalizacja ich w postaci wyjątków
Na poniższym rysunku pokazana została ogólna idea konfiguracji oprogramownaia systemowego (np. systemu operacyjnego) w modelu z segmentacją.
Rozwijanie oprogramowania przy organizacji pamięci zgodnie z modelem z segmentacją wymaga od projektanata systemu szeregu decyzji. Funkcjonalność, którą otrzymuje on, jest na tyle duża, że może wybierać w szeregu wariantów. W związku z tym należy podjąć szereg decyzji, do których należy zaliczyć:
Czy wykorzystywana jest tylko globalna tablica deskryptorów, czy także lokalne
Czy każy z uruchamianych procesów ma definiowane segmenty: kodu, danych, stosu, czy może semgent stosu zawsze jest lokowany w segmencie danych
Czy przestrzeń adresowa segmentów danych i kodu procesu (programu) jest wspólna czy rozdzielona (segmentu kodu i danych współdzielą fizyczną przestrzeń adresową)
Tablice deskryptorów
Ogólne infomacje
W poprzednim rozdziale opisane zostały modele pamięci, które można wykorzystać do budowania architektury oprogramowania systemowego. W niniejszym rozdziale uszczegółowione zostaną pojęcia jakimi są tablice deskryptorów (GDT, LDT, IDT).
Tablica deksryptorów to struktura danych wykorzystywana przez mikroprocesor w operacjach wykonywania oprogramowania. Dostarcza ona informacji niezbędnych do wyznaczania adresów fizycznych komórek pamięci i zarządzania dostępem do wykorzystywanych obiektów systemowych (segmenty kodu, danych, zadania, furtki, itp). Przyjęte rozwiązanie ma za zadanie dostarczenie mechanizmów sprzętowych wspierających implementację systemów operacyjnych. Zastosowanie tych mechanizmów pozwala na wspieraną sprzętowo implementację wielozadaniowości (wieloprocesowości), współbieżnego wykonywania się procesów na jednej jednostce wykonawczej, mechanizmów zarządzania pamięcią, w tym tzw. swap'ami, mechanizmami ochrony obszarów pamięci zarządzanych przez system operacyjny i oprogramowanie użytkowników.
Elementem tablicy deskryptorów jest obiekt danych noszący nazwę deskryptora. Deskryptor jest strukturą danych opisujących jakiś obiekt systemowy (np. segment danych, czy kodu). Posiada zawsze jednakową szerokość, wynoszącą 8 bajtów. W tablicy deskryptorów można pomieścić do 8192 deskryptory.
Na poniższym rysunku pokazane zostały elementy systemu wykorzystującego tablice deskryptorów (bez tablicy deskryptorów przerwań).
Wyróżniamy następujące tablice deskryptorów:
1. GDT - globalną tablicę deskryptorów
2. LDT - lokalną tablicę deskryptorów
3. IDT - tablicę deskryptorów przerwań
Globalna tablica deskryptorów
GDT jest jedyną wymaganą, do funkcjonowania oprogramowania, w trybie chronionym tablicą deskryptorów. Położenie i wielkość tej tablicy jest zapisana w rejestrze GDTR. Mogą być w niej zdefniowane wszystkie typy deskryptorów z wyjątkiem furtek przerwania, potrzesku. Pierwszy deskryptor w GDT (indeks 0) nosi nazwę zerowego deskryptora i nie może być wykorzystany dla celów opisu obiektu systemowego.
Lokalna tablica deskryptorów
LDT jest tablicą, która zwyczajowo jest wykorzystywana do definiowania obiektów systemowych związanych z jakimś procesem. W takim przypadku przed uruchomieniem procesu jest ona aktywowana przez załadowanie prawidłowej wartości do rejestru LDTR. Deskryptor definiujący tablicę LDT jest pamiętany w GDT. Załadowanie rejestru LDTR odbywa się przez wskazanie deskryptora w GDT. W lokalnej tablicy deskryptorów mogą być zdefiniowane następujący typy deskryptorów:
1. segmentów kodu
2. segmentów danych
3. furtek wywołania
4. furtek zadania
Tablica deskryptorów przerwań
IDT jest tablicą przenoszącą informację o podprogramach obsługi przerwań (zarówno sprzętowych, programowych i wyjątków). Jej definicja jest niezbędna w momencie, gdy jakiekolwiek oprogramowanie pracujące w trybie chronionym korzysta, lub obsługuje zdarzenia zgłaszane przy pomocy przerwań, lub wyjątków. Tablice można pominąć tylko w przypadku całkowitego zamaskowania przerwań. Położenie i wielkość tabeli jest określane zapomocą rejestru IDTR.
Rejestry GDTR, LDTR, IDTR
Na poniższym rysunku pokazana została struktura rejestrów tablic deskryptorowych.
Jak widać z rysunku:
1. Lokalizację tablic IDT i GDT są opisywane za pomocą 32bitowego adresu liniowego (gdy wyłączone stronicowanie adresu fizycznego)
2. Tablice mogą zajmować conajwyżej 65536 bajtów (maksymalna wartość, którą można wpisać w pole limit to 0FFFFh). Powoduje to, że w tabelach można maksymalnie zmieścić 8192 deskryptory
3. Lokalizacja tablicy LDT jest opisywana za pomocą deskryptora zlokalizowanego w GDT
Selektor
Wprowadzenie
W poprzednich rozdziałach pojęcie selektora było wykorzystanie do opisu metody wyznaczania adresu fizycznej komórki pamięci. Określony został wzór na wyznaczanie adresu liniowego, z którego wynikało, że selektor nie przenosi tylko numeru deskryptora w tablicy deskryptorów. W niniejszy rozdział dostarczy informacji uzupełniających dotyczących selektora.
Struktura selektora
Poniższy rysunek pokazuje budowę slektora.
Selektor jest strukturą danych zawierającą następujące pola:
1. Index - indeks w tabeli deskryptorów (GDT, lub LDT)
2. TI - wskaźnik tablicy deskryptorów (0 - GDT, 1 - LDT)
3. RPL - wymagany poziom uprzywilejowania (pole omawiane w rozdziale poświęconym poziomom ochrony)
Liczba jest interpretowana przez procesor jako selektor w momencie ładowania jej do rejestrów selektorów (CS, DS, ES, FS, GS).
W poniższej tabeli zamieszczone zostały przykłady selektorów
LP |
Wartość |
Interpretacja |
1 |
8h |
Indeks: 1; TI: 0; RPL: 0 Odwołanie do pierwszego deskryptora GDT wymagany poziom uprzywilejowania 0 |
2 |
0Ch |
Indeks: 1; TI: 1; RPL: 0 Odwołanie do pierwszego deskryptora LDT wymagany poziom uprzywilejowania 0 |
3 |
0Fh |
Indeks: 1; TI: 1; RPL: 3 Odwołanie do pierwszego deskryptora LDT wymagany poziom uprzywilejowania 3 |
Poniżej zamieszczone zostały przykłady rozkazów ładujących rejestry selektorów:
1. mov ax, 8 ; modyfikacja rejestru DS
mov ds,ax
2. push 0Ch ; modyfikacja rejestru ES
pop es
3. jmp 0fh:100h ; modyfikacja rejestru CS
Rejestry selektorów
Na poniższym rysunku przedstawiona została budowa rejestru selektorów.
Rejestry selektorów są zbudowane z 2 części:
1. Dostępnej do modyfikacji przez programistę (część widoczna)
2. Części ukrytej, ładowanej z deskryptora w czasie ładowania selektora do rejestru selektora
Deskryptor
Opis struktury deskryptora
Jak już wcześniej zostało to napisane deskryptor to obiekt danych zlokalizowany w tablicy deskryptorów.
Na poniższym rysunku opisana została struktura deskryptora.
Pole: limit
Szerokość: 20 bitów
Opis:
Pole definiuje wielkość opisywanego za pomocą deskryptora segmentu. Procesor interpretuje pole w zależności o od wartości pamiętanej przez bit pola G. W przypadku, gdy pole G jest wyzerowane wówczas wielkość segmentu może dochodzić do maksymalnie 1MB. W przypadku przeciwnym segment może posiadać wielkość nawet do 4GB.
Pole: base
Szerokość: 32 bity
Opis:
Pole definiuje położenie segmentu w przestrzeni liniowej adrsowej (w przypadku, gdy segmentacja jest wyłączona przestrzeń liniowa jest równocześnie przestrzenią fizyczną).
Pole: type
Szerokość: 4 bity
Opis:
Pole definiuje typ segmentu, lub furtki. Intepretacja tego pola jest powiązana z polem S. Zakres możliwych wartości opisany został w rozdziale poświęconym typom segmentów i furtek.
Pole: S
Szerokość: 1 bit
Opis:
Definiuje typ deskryptora. W przypadku gdy pole przyjmuje wartość 0 wówczas deskryptor definiuje segment, lub furtę systemową. W przypadku przeciwnym definiuje segment kodu, lub danych.
Pole: DPL
Szerokość: 2 bitów
Opis:
Definiuje poziom uprzywilejowania segmentu, lub furtki. Dozwolony zakres wartości: 0 (najbardziej uprzywilejowany) - 3 (najmniej uprzywilejowany). Szerszy opis poziomów uprzywilejowania można znaleźć w rozdziale poświęconym ochronie w trybie chronionym.
Pole: P
Szerokość: 1 bit
Opis:
Określa czy segment definiowany przy pomocy deskryptora jest obecny w pamięci, czy też nie. W przypadku, gdy pole jest wyzerowane procesor generuje wyjątek: "segment nieobecny" (#NP). Wyjątek ten może zostać wykorzystany przez oprogramowanie zarządzające pamięcią do załadowania do pamięci fizycznej segmentu np. z dysku twardego. Na poniższym rysunku pokazana została struktura deskryptora w przypadku, gdy pole P jest wyzerowane.
Obszary oznaczone jako "Available" w takim przypadku mogą zostać wykorzystane do składowania niezbędnych danych dla oprogramowania managera pamięci, które ten może wykorzystywać do ładowania do pamięci fizycznej segmentu.
Pole: D/B
Szerokość: 1 bitów
Opis:
Pole jest wykorzystywane do różnych funkcji w zależności od definiowanego segmentu. Dla segmentu:
segmentu kodu - pole nosi nazwę D; pole definiuje domyślną szerokość adresów i operandów wykorzystywanych przez rozkazy oprogramowania zlokalizowanego w segmencie; gdy pole jest ustawione wówczas 32 bitowe adresy, 32 bitowe lub 8 bitowe operandy są przyjmowane jako domyślne w wykonywanym kodzie; w przypadku przeciwnym przyjmowane są 16 bitowe adresy, 16 bitowe, lub 8 bitowe operandy; domyślne ustawienia (niniejsze pole) mogą być zmieniane przez zastosowanie prefiksów rozkazów 66h dla operandów i 67h dla adresów;
segment stosu (segment danych wskazywany przez rejestr SS) - pole nosi nazwę B (od ang. big); określa szerokość wskaźnika stosu wykorzystywanego w operacjach związanych ze stosem; w przypadku, gdy pole jest ustawione do adresowania stosu wykorzystywany jest 32 bitowy rejestr ESP; w przypadku przeciwnym rejestr 16 bitowy (SP); w przypadku, gdy segment danych, w którym zlokalizowany jest segment stosu jest segmentem rozszerzalnym w dół pole to określa wielkość segmentu (patrz niżej);
segment danych rozszerzalny w dół? - pole nosi nazwę B; definiuje wielkość segmentu; w przypadku, gdy jest ustawione wielkość wynosi 0FFFFFFFFh (4GB); w przypadku przeciwnym 0FFFFh (64KB)
Pole: G
Szerokość: 1 bit
Opis:
Określa sposób interpretowania pola limit. W przypadku, gdy pole to jest wyzerowane wielkość segmentu jest interpretowana w jednoskach miary - bajtach. W przypadku, gdy jest ustawione wielkość jest intepretowana w 4KB jednostkach miary. Dodatkowo, gdy pole G jest ustawione 12 najmniej znaczących bitów offsetu nie jest sprawdzane w czasie realizacji testów przekroczenia wielkości segmentu. Pozwala to na definiowanie segmenów o wielkości 4KB (wartość 0 pola limit).
Typy deskryptorów
Wprowadzenie
W poprzednim rozdziale opisana została struktura deskryptora w tym pole S definiujące typ deskryptora. W niniejszym rozdziale wprowadzimy sobie klasyfikację deskryptorów i opiszemy każdy z poszczególnych typów, wskazując różnicę między nimi.
Podział deskryptorów
Pole S deskryptora wprowadzało podział na:
deskryptory segmentów kodu i danych
deskryptory systemowe
Deskryptory segmentów kodu i danych definiują obszary pamięci, wykorzystywane do przechowywania kodu, danych (w tym sotsów).
Do deskryptorów systemowych zaliczamy:
segmenty stanu zadania (dostępny, zajęty, 286, 386)
furtki (przerwania, wywołania, potrzesku, zadania)
lokalna tablica deskryptorów
Obiekty, opisywane za pomocą tych deskryptorów, są przeznaczone do wykorzystania przez oprogramowanie systemowe np. w celu zarządzania procesami i przydzielania im zasobów systemowych.
Różnice w definicji poszczególnych deskryptorów
Na poniższym rysunku przedstawione zostały różnice w definicji poszczególnych typów deskryptorów.
Różnice między poszczególnymi typami deskryptorów:
1. Deskryptory systemowe nie posiadaja pól D/B i AVL
2. Pole type deskryptorów segmentów kodu i danych budowane jest w oparciu o następujące zasady:
Najstarszy bit określa czy to jest segment kodu (1), czy danych (0)
Bit E/C określa:
dla segmentów danych czy segment jest rozszerzalny w dół, czy nie
dla semgneów kodu, czy segment kodu jest zgodny czy też nie
Bit W/R określa:
dla segmentów danych, czy można w segmencie zapisywać dane (uprawnienia do odczytu i zapisu)
dla segmentów kodu, czy oprócz wykonywania można czytać dane z segmentu
Bit A dostarcza informacji o tym czy segment w ostatniej operacji był użyty, czy też nie; procesor ustawia ten bit w czasie operacji załadowania selektora do rejestru segmentu; bit dostarcza informacji, które mogą zostać wykorzystane przez oprogramowanie zarządzające pamięcią, lub także w celach wyszukiwania błędów
Deskryptory segmentów danych i kodu
Typy deskryptorów segmentów kodu i danych
W poniższej tabeli zebrane zostały dozwolone wartości pola typ deskryptora w przypadku definicji deskryptora segmentu kodu, lub danych.
UWAGA
Powyższa tabela definiuje typy segmentów danych i kodu w inny sposób niż w książce "Mikroprocesory 80286, 80386 i i486". Różnice wynikają z różnego potraktowania bitu (pola) A w dostępnych materiałach.
Podsumowując, do dyspozycji programisty są:
segmenty kodu:
tylko wykonanie
wykonanie i odczyt
zgodne tylko wykonanie
zgodne wykonanie i odczyt
segmenty danych:
tylko odczyt
odczyt i zapis
rozszerzalne w dół tylko odczyt
rozszerzalne w dół odczyt i zapis
Wybór uprawnień (odczyt, zapis, wykonanie) ma decydujący wpływ związany z wykonaniem się oprogramowania. Próba zapisu do segmentu danych, który został opisany za pomocą deskryptora segment danych tylko do odczytu, kończy się wyjątkiem. To samo jest związane z próbą odczytu danych z segmentu kodu, jeżeli segment taki nie został opisany za pomocą deskryptora z uprawnieniem do odczyt procesor będzie generował wyjątki.
Deskryptory systemowe
Typy deskryptorów segmentów systemowych
Na poniższym rysunku przedstawione zostały wszystkie możliwe wartości pola typ w zakresie deskryptorów systemowych.
Do deskryptorów systemowych zaliczamy:
lokalną tablicę deskryptorów
segment stanu zadania
furtkę wywołania
furtkę przerwania
furtkę potrzasku
furtkę zadania
Wszystkie powyżej wymienione deskryptory możemy podzielić na 2 grupy:
deskryptory segmentów systemowych (deskryptor: tablicy LDT, segmentu satnu zadania)
deskryptory furtek
Deskryptory segmentów systemowych wskazują na obszary pamięci, w których zlokalizowane są segmenty stanów zadania, lub tablic LDT. Natomiast deskryptory furtek wskazują punkty wejścia do udostępnianych przez kod podprogramów (furtki wywołania, przerwania, potrzasku), lub wskazują na segmenty stanu zadadnia (furtka zadania).
Furtki
Furtki
Furtki są przezonaczone do organizowania kontrolowanego dostępu do segmentów kodu znajdujących się na bardziej uprzywilejowanych poziomach. Wyróżniamy następujące typy furtek:
1. Wywołania
2. Pułapki
3. Przerwania
4. Zadania
Furtki zadań są wykorzystywane do przełączania zadań. Furtki półapek, przerwań są wykorzystywane do obsługi zdarzeń sygnalizowanych przy pomocy przerwań.
Furtka wywołania
Furtka wywołania jest wykorzystywana do organizowania operacji zmiany sterowania miedzy segmentami kodu znajdującymi się na różnych poziomach uprzywilejowania. Na poniższym rysunku pokazany został format deskryptora furtki wywołania.
Pola danych deskryptora furtki:
1. Segment selektor - selektor wskazujący na deskryptor segmentu kodu
2. Offset - offset we wskazywanym segmencie kodu wejścia do uruchamianego furtką kodu
3. Param count - określa liczbę argumentów wywołania procedury uruchamianej furtką; parametr ten definiuje liczbę argumentów pamiętanych na stosie w czasie wywołania, która musi zostać skopiowana do nowego stosu w przypadku gdy następuje zmiana stosów w związku z zmianą poziomów uprzywilejowania; parametr definiuje liczbę słów (furtka 16 bitowa), lub podwójnych słów furtka 32 bitowa
4. DPL - poziom uprzywilejowania deskryptora furtki
Mechanizmy ochrony
Wprowadzenie
Tryb chroniony dostarcza mechanizmów ochrony, które są zarówno dostępne na płaszczyźnie segmentacji jaki i stronicowania. Umożliwiają one do budowy oprogramowania, w którym kluczowe jego moduły są chronione przed nieupoważnionym dostępem. Ochrona polega na limitowaniu dostępu do kluczowych dla funkcjonowania oprogramowania segmentów kodu, danych, stosu z oprogramowania nieuprzywilejowanego. Realizowane to jest przez zastosowanie tzw. poziomów uprzywilejowania (ochrony).
Zastosowanie mechanizmów ochrony powoduje realizacje przez procesor weryfikacji wszystkich odwołań do pamięci. Każda operacja powoduje realizację testów. Każdy błąd jest sygnalizowany w postaci wyjątku.
Testy realizowane przez procesor możemy podzielić na następujące grupy:
przekroczenia dozwolonego zakresu (wielkości segmentów, stron)
uprawnień do realizacji określonych operacji (np. odczyt, zapis, wykonanie)
dostępu do określonych typów obiektów systemowych
dostępu do poziomów uprzywilejowania
ograniczeń dostępu do punktów wejścia procedur
ograniczeń dostępu do rozkazów procesora
W niniejszym rozdziale opisane zostały wszystkie mechanizmy ochrony. Opis wyjątków został przedstawiony w rozdziale poświęconym przerwaniom.
Uruchomienie i wyłączenie mechanizmów ochrony
Sterowanie mechanizmami ochrony dla segmentacji
Mechanizmy ochrony są włączane w momencie uruchomienia trybu chronionego (ustawienie bitu PE rejestru CR0). Są wbudowane w tryb i zawsze działają. Nie ma dostępnego, żadnego rozkazu, lub flagi w rejestrach procesora, które mogą wyłączyć mechanizmy ochrony. Jedyną metodą na "wyłączenie" mechanizmów weryfikacji poziomów uprzywilejowania jest uruchomienie oprogramowania na najbardziej uprzywilejowanym poziomie ochrony (poziom 0). Nie mniej zawsze będą realizowane testy związane z przekroczeniem wielkości segmentów, czy weryfikacją typów.
Sterowanie mechanizmami ochrony dla stronicowania
Podobnie jak w przypadku segmentacji mechanizmy ochrony zostają automatycznie uruchomione w momencie startu stronicowania (ustawiony bit PG w rejestrze CR0). Podobnie jak w segmentacji nie ma bezpośredniej możliwości wyłączenia mechanizmów ochrony. Nie mniej jednak można to zrobić wykonując następujące operacje:
1.Wyzerowanie flagi WP w rejestrze CR0
2. Ustawienie flag R/W (zapis i odczyt) i U/S (użytkownik/ administrator) we wszystkich katalogach stron i stronach
Wykonanie tych operacji spowoduje, że każda strona będzie dostępna do zapisu z wyłączonym mechanizmem ochrony na poziomie stron.
Pola danych wykorzystywane przez mechanizmy ochrony
Pola danych struktur systemowych wykorzystywane przez mechanizmy ochrony
Pola danych, flagi wykorzystywane przez mechanizmy ochrony:
1. S - pole deskryptora określające typ deskryptora (deskryptor segmentu danych, kodu, czy deskryptor systemowy)
2. Type - definiuje typ segmentu opisywanego przez deskryptor
3. Limit - wielkość obiektu systemowego (wspólnie z polami G i E (deskryptor segmentu danych element pola type)
4. G - pole ziarnistości; wspólnie z polem limit określa wielkość segmentu
5. E - flaga (pole type deskryptora segmentu danych) definiująca czy segment danych jest rozszerzalny w dół; co za tym idzie określająca także wielkość segmentu
6. DPL - pole definiujące poziom uprzywilejowania deskryptora
7. RPL - pole selektora określające żądany poziom uprzywilejowania
8. CPL - pole selektora CS (pozycje bitowe 0 i 1); określa bieżący poziom ochrony (uprzywilejowania)
9. U/S - flaga katalogu stron, lub tablicy stron określająca typ strony (użytkownik,supervisor)
10. W/R - flaga strony określająca sposób dostępu do obszaru pamięci wskazywanego przez stronę (zapisy/odczyt, czy tylko odczyt)
Na poniższym rysunku pokazane zostało rozlokowanie opisanych powyżej pól w deskryptorach.
Weryfikacja pola limit
Weryfikacja pola limit
Pole limit deskryptora jest wykorzystywane do weryfikacji, czy realizowana operacja nie próbuje się odwołać poza dozwolony obszar segmentu. Efektywna wartość pola limit jest uzależniona od bitu G deskryptora (ziarnistość). W segmentach danych zależy także od pola E i B. W przypadku gdy G jest wyzerowane wówczas wartość efektywna pola jest równa wartości zapisywanej na 20 bitach (liczba z zakresu 0 - 0FFFFFh (1MB)). Kiedy G jest ustawione wówczas efektywna wartość pola jest liczbą z zakresu 0FFFh (4KB) do 0FFFFFFFFh (4GB). Wyliczana jest przez pomnożenie wartości zapisanej w deskryptorze przez wielkość strony ziarnistości (4KB - 0FFFh).
UWAGA
W przypadku, gdy pole G jest równe 1, wówczas 12 najmłodszych bitów offsetu (adresu) realizowanej operacji nie weryfikowane w operacji weryfikacji przekroczenia dozwolonej wielkości segmentu. Przykładowo w przypadku gdy pole limit przyjmuje wartość 0, wówczas dozwolone są wartości offsetu adresu z zakresu 0 - 0FFFH.
Weryfikacja pola limit dla segmentów z wyjątkiem segmentu rozszerzalnego w dół
Dla wszystkich typów segmentów z wyjątkiem segmentu rozszerzalnego w dół efektywna wartość pola limit jest ostatnim dozwolonym adresem dostępu do segmentu. Oznacza to że ostatni dozwolony offset w segmencie jest równy wartości efektywnej pola limit i jest o conajmniej 1 bajt mniejszy od wielkości segmentu. W przypadku przekroczenia wielkości segmentu generowany jest wyjątek #GP. Przez przekroczenie wielkości segmentu rozumiana jest realizacja operacji, w której adres nie spełnia jednego z poniżej wymienionych warunków:
adresowanie bajtu pod offsetem większym niż efektywny limit
adresowanie słowa pod offsetem większym niż efektywny limit -1
adresowanie podwójnego słowa pod offsetem większym niż efektywny limit -3
adresowanie poczwórnego słowa pod offsetem większym niż efektywny limit - 7
Weryfikacja pola limit dla segmentu danych rozszerzalnego w dół
Dla segmentów rozszerzalnych w dół pole limit ma tą samą funkcje, tylko jest interpretowane w inny sposób. Wartość efektywna pola limit określa ostatni niedozwolony adres. Oznacza to że adresy dozwolone są z zakresu od efektywny limit + 1 do 0FFFFFh, lub 0FFFFFFFFh (w zależności od tego czy pole B jest ustawione czy nie). Segment rozszerzalny w dół ma maksymalną wielkość, gdy pole limit równe jest 0.
Weryfikacja pola limit rejestrów GDTR i IDTR
Rejestry GDTR i IDTR zawierają 16 bitowe pole limit. Pole to określa wielkość segmentów zawierających tabele GDT i IDT. Weryfikacja prawidłowości odczytu desktryptora odbywa się na takich samych zasadach jak w przypadku testów opisanych w rozdziale poświęconym "weryfikacji pola limit dla wszystkich segmentów z wyjątkiem segmentów rozszerzalnych w dół".
Weryfikacja typów segmentów i deskryptorów
Wprowadzenie
Deskryptory zawierają dwa pola definiujące typ:
pole S, określające typ deskryptora
pole type, określające typ segmentu opisywanego przez deskryptor
Procesor wykorzystuje te informacje do wykrywania błędów, które są wynikiem prób wykorzystania segmentów, lub deskryptorów niezgodnie z ich przeznaczeniem. Pola są wykorzystywane w różnych miejscach w dalszej części rozdziału omawiane będą poszczególne przypadki. Nie mniej należy założyć że zamieszczona lista jest niepełna i zawiera tylko najistotniejsze przypadki operacji, w których testy pól typów są realizowane.
Ładowanie selektora do rejestru segmentowego
1. Rejestr CS może być tylko ładowany selektorem segmentu kodu
2. Selektory segmentów kodu, które nie mają zdefiniowanego uprawnienia do odczytu nie mogą być ładowane do rejestrów DS,ES,FS, GS
3. Rejestr SS może być ładowany selektorem wskazującym na deskryptor segmentu danych odczyt i zapis
Ładowanie selektora do rejestru LDTR
1. Rejestr LDTR może być ładowany selektorem wskazującym na deskryptor LDT.
2. Rejestr TR może być ładowany selektorem wskazującym na deskryptor segmentu TSS.
Dostęp do segmentu, którego selektor jest już załadowany do rejestru segmentowego
1. Żaden rozkaz nie może zapisywać do segmentu kodu
2. Żaden rozkaz nie może zapisywać do segmentu danych tylko do odczytu
3. Żaden rozkaz nie może czytać danych z segmentu kodu tylko wykonywanie
Wykonanie rozkazu, którego operand zawiera selektor
1. Skok daleki (CALL, lub JMP) może być realizowany do segmentu kodu zgodnego, segmentu kodu, furtki wywołania, furtki zadania, lub segmentu stanu zadania
2. Rozkaz LLDT może być realizowany przez wskazanie deskryptora LDT
3. Rozkaz LTR może być realizowany przez wskazanie deskryptora segmentu stanu zadania
4. Rozkaz LAR może być realizowany przez wskazanie deskryptora LDT, TSS, furtki wywołania, furtki zadania, segmentu kodu, segmentu danych
5. Rozkaz LSL może być realizowany przez wskazanie deskryptora LDT, TSS, segmentu kodu, segmentu danych
6. Pozycją tabeli IDT musi być: furtka przerwania, pułapki, zadania
Wykonanie wewnętrznych operacji
1. Wykonanie operacji skoku dalekiego (CALL, JMP) powoduje weryfikację przez procesor pola type. Jeżeli rozkaz wskazuje na deskryptor segmentu kodu, furtki wywołania realizowana jest operacja skosku do innego segmentu. Jeżeli rozkaz wskazuje na segment stanu zadania, lub furtkę zadania wykonywana jest operacja przełączenia zadań
2. W przypadkach skoków z wykorzystaniem furtek przerwania, wywołania, pułapki, skoków do podprogramów, procedur obsługi przerwań, wyjątków procesor weryfikuje czy skok odbywa się do segmentu kodu (deskryptor segmenty jest typu segment kodu)
3. W przypadkach skoków z wskazaniem na furtkę zadania, procesor weryfikuje czy wskazany w deskryptorze segment jest segmentem stanu zadania
4. W przypadku skoków do zadania, procesor weryfikuje czy selektor wskazuje na deskryptor segmentu TSS
5. W przypadku powrotu z zagnieżdżonego zadania (rozkaz IRET), procesor sprawdza, czy w polu poprzedniego zagnieżdżonego zadania w segmencie TSS znajduje się wskazanie na segment TSS
Weryfikacja selektora zerowego segmentu
Weryfikacja operacji na selektorze zerowego segmentu
Próba załadowania do rejestru segmentowego CS, lub SS selektora wskazującego na zerowy deskryptor powoduje wygenerowanie wyjątku #GP.
Selektor zerowego segmentu może być ładowany do rejestrów segmentowych: DS, ES, GS, FS. Próba odwołania się do segmentów przez załadowany selektor do rejestru segmentu spowoduje wygnerowanie wyjątku #GP.
Możliwość ładowania zerowego selektora do rejestrów segmentów można wykorzystać jako metodę wykrywania operacji na niewykorzystywanych rejestrach segmentowych, lub nieoczekiwanych próbach dostępu do segmentów danych.
Poziomy uprzywilejowania
Mechanizm ochrony segmentów
Mechanizm ochrony składa się z 4 poziomów uprzywilejowania numerowanych od 0 do 3. Poniższy rysunek pokazuje interpretację poziomów uprzywilejowania jako okręgów ochrony.
Centralny okrąg (zarazerwowany dla najbardziej uprzywilejowanych segmentów danych, kodu i stosu) jest wykorzystywany dla semgnetów najbardziej krytycznego oprogramowania (jądra systemu operacyjnego). Bardziej zewnętrzne okręgi są wykorzystywane przez mniej krytyczne dla funkcjonowania systemu komputerowego oprogramowanie. Systemy, które wykorzystują 2 poziomy uprzywilejowania powinny korzystać z poziomów 0 i 3 (np. Microsoft Windows NT). Generalnym zadaniem zastosowania poziomów ochrony jest ograniczenie dostępu oprogramowania osadzonego na niższych poziomach do zasobów zlokalizowanych na bardziej uprzywilejowanych poziomach. Rozwiązania te są wspomagane przez testy realizowane przez procesor w czasie wykonywania oprogramowania. Każde naruszenie mechanizmów ochrony powoduje wygenerowanie wyjątku #GP.
Poziomy uprzywilejowania są weryfikowane w czasie realizacji operacji ładowania rejestrów segmentowych selektorami.
Dane wykorzystywane do weryfikacji praw dostępu do segmentów
Bieżący poziom uprzywilejowania (CPL)
Bieżący poziom uprzywilejowania jest poziomem na jakim wykonuje się bieżący proces (program, lub zadanie). Jest pamiętany na 2 najmłodszych bitach selektora zapisanego w rejestrze CS. CPL zmienia się zawsze w związku z zmianą sterowania wyniającego z wykonania rozkazu skoku między segmentowego (z wyjątkiem skoku do segmentu zgodnego, wówczas CPL pozostaje na swoim poprzednim poziomie.
Poziom uprzywilejowania deskryptora (DPL)
DPL jest poziomem uprzywilejowania definiowanym w polu DPL deskryptora segmentu, lub furtki. W przypadku realizacji operacji dostępu do segmentu, lub furtki DPL zapisany w deskryptorze jest porównywany z CPL i RPL wykorzystywanego w operacji selektora. Interpretacja DPL jest uzależniona od użytego w operacji deskryptora segmentu lub furtki:
Segment danych:
wartość w DPL określa numerycznie największy numer poziomu uprzywilejowania jaki musi być przypisany do segmentu kodu, który chce mieć dostęp do segmentu danych
przykładowo do segmentu danych o DPL równym 1 ma dostęp oprogramowanie z poziomów uprzywilejowania 0 i 1
Segment kodu (bez użycia furtki wywołania):
wartość zapisana w DPL wskazuje poziom uprzywilejowania jaki musi mieć program lub proces, który chce mieć dostęp do segmentu
przykładowo próba skoku do segmentu kodu znajdującego się na poziomie 2 (DPL =2) jest możliwa tylko z kodu, który wykonuje się na poziomie uprzywilejowania 2 (CPL=2)
Furtka wywołania:
wartość zapisana w DPL deskryptora furtki wywołania określa numerycznie największy poziom uprzywilejowania jaki musi mieć kod chcący skorzystać z furtki
przykładowo do furtki o DPL równym 1 ma dostęp oprogramowanie z poziomów uprzywilejowania 0 i 1
zasada ta jest zgodna z weryfikacją dostępu do segmentu danych
Segment kodu i zgodny segment kodu dostępny przez furtkę wywołania:
wartość DPL zapisana w deskryptorze segmentu określa numerycznie najmniejszą wartość poziomu uprzywilejowania kodu, która ma dostęp do segmentu
przykładowo jeżeli DPL segmentu kodu wynosi 2 to programy uruchomione na poziomach (CPL) 0, lub 1 nie mają dostępu do segmentu
Segment stanu zadania:
wartość w DPL określa numerycznie największy numer poziomu uprzywilejowania jaki musi być przypisany do segmentu kodu, który chce mieć dostęp do TSS
przykładowo do segmentu TSS o DPL równym 2 ma dostęp oprogramowanie z poziomów uprzywilejowania 0, 1, 2
zasada ta jest zgodna z weryfikacją dostępu do segmentu danych
Żądany poziom uprzywilejowania (RPL)
RPL jest poziomem uprzywilejowania definiowanym w selektorze na bitach 0 i 1. Jest wykorzystywany wspólnie z CPL do określania uprawnień do segmentu. W przypadku, gdy wartość zapisana w RPL jest numerycznie większa niż w CPL wówczas brana jest ona pod uwagę w czasie określania uprawnień dostępu do segmentu danych. W przypadku segmentu kodu RPL powinno być mniejsze równe numerycznie niż CPL aby mogła się dokonać operacja zmiany sterowania (szczegółowe zastosowanie RPL opisane jest w dalszych rozdziałach opisujące poszczególne przypadki weryfikacji poziomów uprawnień).
Pole RPL ma zastosowanie w ograniczaniu dostępu oprogramowania użytkowego do struktur systemu operacyjnego w czasie wywołania procedur systemu operacyjnego.
Weryfikacja poziomów uprzywilejowania w czasie dostępu do segmentów danych
Weryfikacja poziomów uprzywilejowania w czasie dostępu do segmentów danych
Dostęp do danych znajdujących się w segmencie danych jest możliwy po załadowaniu rejestru segmentu selektorem wskazującym na deskryptor segmentu danych. Operacja może być zrealizowana przy pomocy następujących rozkazów: MOV, POP, LDS, LES, LFS, LGS, LSS.
Przed załadowanie selektora do rejestru segmentowego wykonywane są testy poziomów uprzywilejowania. Testy polegają na porównaniu bieżącego poziomu uprzywilejowania (CPL) procesu, lub programu, poziomu uprzywilejowania selektora (RPL) i poziomu uprzywilejowania zapisanego w deskryptorze segmentu. Selektor jest ładowany do rejestru segmentowego wtedy, gdy DPL jest numerycznie większy lub równy niż CPL i RPL. W przyciwnym przypadku sygnalizowany jest wyjątek #GP związany z brakiem uprawnienia dostępu do segmentu danych i selektor nie jest ładowany do rejestru segmentowego.
Przykład weryfikacji poziomów uprzywilejowania w dostępie do danych
Na poniższym rysunku pokazane zostały 4 podprogramy, które wykonują operację dostępu do danych. Każdy z podprogramów wykonuje się na innym poziomie ochrony i próbuje uzyskać dostęp do segmentów danych znajdujących się także na różnych poziomach ochrony.
1. Podprogram znajdujący się w segmencie kodu A, próbuje uzyskać dostęp do danych znajdujących się w segmencie danych E przy wykorzystaniu selektora E1. Ponieważ CPL (segment A), RPL (selektor E1) i DPL (segmentu E) mają jednakowe wartości więc dostęp do danych w segmencie E jest możliwy.
2. Podprogram zlokalizowany w segmencie kodu B, próbuje uzyskać dostęp do danych znajdujących się w segmencie E przy wykorzystaniu selektora E2. CPL = RPL=1 i jest mniejszy od DPL segmentu E, więc dostęp do segmentu E jest możliwy.
3. Podprogram zlokalizowany w semgnecie C, próbuje uzyskać dostęp do danych zlokalizowanych w segmencie E przy pomocy selektora E3. Ponieważ CPL=RPL=3 jest większy od DPL (2) dostęp do segmentu E jest zabroniony (znajduje się on na poziomie bardziej uprzywilejowanym niż kod, który próbuje się do niego dostać).
4. Podprogram umiejscowiony w segmencie D próbuje uzyskać dostęp do segmentu danych E przez selektor E3. CPL segmentu kodu jest mniejsze od RPL selektora w związku z tym RPL jest brany pod uwagę do realizacji testów dostępu. Ponieważ RPL jest większy od DPL segmentu E, w związku z tym dostęp do segmentu E jest zabroniony.
WAŻNE
Dostęp do segmentów danych jest możliwy z bieżącego poziomu, lub poziomów bardziej uprzywilejowanych. Program pracujący na poziomie 0 ma dostęp tylko do danych zlokalizowanych na poziomach 0-3.
WAŻNE
W związku z tym, że program, lub proces, może w dowolny sposób zmieniać zawartość selektora, który jest wykorzystywany do operacji dostępu do danych, w tym pola RPL, dlatego w przypadkach przekazania selektora do procedury należy sprawdzić uprawnienia wywołującego przy pomocy rozkazu ARPL.
Weryfikacja poziomów ochrony w czasie ładowania rejestru SS
Weryfikacja poziomów uprzywilejowania w związku z ładowaniem rejestru segmentowego SS jest inna niż w przypadku pozostałych rejestrów reprezentujących segmenty danych. Wymagane jest aby CPL, RPL i DPL były sobie równe. W przypadku, gdy RPL, czy DPL nie są równe CPL wówczas generowany jest wyjątek #GP.
Weryfikacja poziomów ochrony w czasie przenoszenia sterowania między segmentami kodu
Przenoszenie sterowania polega na zmianie zawartości rejestrów licznika rozkazów (CS i EIP). Jest ono równoważne wykonaniu rozkazów skoku, przerwania, przełączenia zadania. Zmiana sterowania polega na załadowaniu do rejestru segmentowego CS selektora wskazującego na deskryptor segmentu kodu, furtki (np. wywołania, zadania), segmentu stanu zadania. W czasie ładowania rejestru CS wykonywanych jest szereg testów, do których można zaliczyć testy przekroczenia wielkości segmentu, weryfikacji typu i weryfikacji uprawnień. Jeżeli testy zakończą się sukcesem wówczas do rejestru CS ładowany jest selektor, część ukryta ładowana jest danymi z deskryptora, a wykonanie kodu rozpoczyna się od miejsca wskazywanego offsetem w rejestrze EIP. Zmiana sterowanie może nastąpić na skutek wykonania rozkazów: JMP, CALL, INT, RET, IRET. Może nastąpić także w odpowiedzi obsługi zdarzenia generowanego przez przerwanie sprzętowe, czy wyjątek.
W niniejszym rozdziale omówione zostaną zagadnienia poświęcone weryfikacji poziomów uprzywilejowania w związku z zmianą sterowania wynikającą z wykonania rozkazów JMP, CALL, RET (pozostałe zagadnienia są omawiane w rozdziałach poświęconych obsłudze wyjątków i przerwań i przełączaniu zadań).
Metody przenoszenia sterowania
Sterowanie rozkazami JMP i CALL może być w następujący sposób:
1. Operand rozkazu zawiera selektor docelowego segmentu kodu
2. Operand rozkazu wskazuje na deskryptor furtki wywołania, który wskazuje na deskryptor docelowego segmentu kodu
3. Operand rozkazu wskazuje na deskryptor segmentu TSS, który zawiera selektor wskazujący na docelowy segment kodu
4. Operand rozkazu wskazuje na furtkę zadania, która wskazuje na segment TSS, zawierający selektor wskazujący na docelowy segment kodu
Bezpośrednie skoki do segmentów kodu
Bezpośrednie skoki do segmentu kodu
Bliskie warianty rozkazów JMP, CALL i RET są wykonywane w obrębie jednego segmentu. W związku z tym w niniejszym rozdziale analizowane będą rozkazy dalekiego skoku.
W czasie przenoszenia sterowania z jednego segmentu kodu do drugiego (bez wykorzystania furtki wywołania) do realizacji testów brane są następujące dane: CPL, RPL (selektor wskazujący na deskryptor docelowego segmentu kodu), DPL i pole C (deskryptor segmentu kodu docelowego). Pole C określa czy docelowy segment jest zgodnym segmentem kodu. W zależności od wartości pola C w inny sposób są wykorzystywane pola CPL, RPL i DPL.
Dostęp do segmentu kodu
W przypadku próby zmiany sterowania z zmianą segmentów kodu obowiązuje reguła, że bieżący poziom uprzywilejowania musi być równy poziomowi uprzywilejowania segmentu kodu docelowego (DPL deskryptora). W przeciwnym przypadku generowany jest wyjątek #GP. Pole RPL selektora wskazującego na deskryptor docelowego segment kodu, ma ograniczony wpływ na weryfikację poziomów uprzywilejowania. RPL musi być numerycznie mniejsze, lub równe CPL bieżącego kodu, żeby skok zakończył się sukcesem (na poniższym przykładzie selektory C1 i C2 mogą być przypisane do poziomu 0,1 i 2, ale nie mogą być przypisane do poziomu 3).
Dostęp do zgodnego segmentu kodu
W przypadku próby zmiany sterowania z wskazaniem docelowego segmentu kodu, który jest zgodnym segmentem kodu obowiązuje zasada, że CPL wykonywanego kodu musi być numerycznie większy, lub równy DPL docelowego segmentu kodu. Wyjątek #GP jest generowany w przypadku gdy CPL jest mniejszy od DPL. W przypadku przenoszenia sterowania do segmentu zgodnego nie realizowane są testy porównania pola RPL selektora z CPL.
Przykłady zmiany sterowania do segmentów kodu i zgodnych segmentów kodu
1. Próba wykonania skoku z segmentu a do C wykona sie bez problemu w związku z tym, że CPL, RPL i DPL są sobie równe.
2. Próba zmiany sterowania z segmentu B do segmentu C przy wykorzystaniu selektora C2 zakończy się niepowodzeniem, gdyż nie spełniony jest warunek równości poziomów uprzywilejowania (CPL=RPL=DPL).
3. Próba zmiany sterowania z segmentów A i B do segmentu zgodnego D zakończy się sukcesem w związku z tym, że spełniona jest zależność, CPL >= DPL segmentu zgodnego.
Zmiana sterowania przez furtki wywołania
Zmiana sterowania przez wywołanie furtki wywołania
Skok z wykorzystaniem furtki wywołania następuje w przypadku, gdy rozkaz skoku dalekiego (JMP, CALL) wskazuje (selektor) na deskryptor furtki (offset jest wymagany w rozkazie, nie jest jednak wykorzystywany przez procesor do operacji skoku).
Poniższy rysunek obrazuje operacje wykonywane przez progesor w czasie realizacji skoku z wskazaniem furtki wywołania.
Rozkaz skoku przez furtkę jest realizowany w następujący sposób:
1. Procesor na podstawie selektora (operand rozkazu skoku) określa deskryptor w LDT, lub GDT furtki wywołania.
2. Z deskryptora odczytuje selektora wskazujący na deskryptor segmentu kodu (może się on znajdować zarówno w GDT jak i LDT)
3. Odczytuje deskryptor segmentu kodu i wyznacza adres w przestrzeni liniowej położenia segmentu kodu
4. Korzystając z offsetu zapisanego w deskryptorze furtki wyznacza adres położenia punktu wejścia do uruchamianego podprogramu.
5. Jeżeli wszystkie testy związane z ochroną zakończą się sukcesem wykonywany jest wskazany przez furtkę podprogram.
Na poniższym rysunku pokazane zostały pola wykorzystywane przez mechanizmy ochrony do weryfikacji uprawnień dostępu.
Pola danych wykorzystywane w procesie weryfikacji poziomów uprzywilejowania w czasie realizacji operacji skoków przy wykorzystaniu furtki wywołania:
CPL - bieżący poziom ochrony
RPL - poziom ochrony selektora furtki wywołania
DPL - poziom ochrony deskryptora furtki wywołania
DPL - poziom ochrony deskryptora docelowego segmentu kodu
Regóły weryfikacji uprawnień są różne w zależności od rozkazu, który jest sprawcą operacji przenoszenia sterowania.
Reguły weryfikacji poziomu ochrony dla rozkazu CALL
1. CPL <= DPL furtki wywołania; RPL <= DPL furtki wywołania
2. Jeżeli docelowym segmentem kodu jest segment zgodny: DPL segmentu docelowego <= CPL
3. Jeżeli docelowym segmentem kodu jest zwykły segment kodu: DPL segmentu docelowego <= CPL
Reguły weryfikacji poziomu ochrony dla rozkazu JMP
1. CPL <= DPL furtki wywołania; RPL <= DPL furtki wywołania
2. Jeżeli docelowym segmentem kodu jest segment zgodny: DPL segmentu docelowego <= CPL
3. Jeżeli docelowym segmentem kodu jest zwykły segment kodu: DPL segmentu docelowego = CPL
Powyższe reguły można podsumować:
1. Furtka wywołania (DPL) może się znajdować na tym samym poziomie, lub mniej uprzywilejowanym poziomie ochrony (o numerze większym równym CPL
2. RPL selektora wskazującego na furtkę wywołania jest większe lub równe bieżącemu poziomowie ochrony
3. Rozkaz CALL umożliwia przeniesienie sterowania do bardziej uprzywilejowanego poziomu
4. Rozkaz JMP pozwala na przeniesienie sterowania do bardziej uprzywilejowanego poziomu tylko w przypadku, gdy docelowy segment kodu jest segmentem zgodnym
Na poniższym rysunku pokazane zostały przykłady operacji skoku z wykorzystaniem furtek wywołania.
1. Segment kodu A może wykonać skok przez furtkę A do segmentu kodu E, gdyż: CPL = RPL = DPL furtki, a DPL segmentu kodu <= od CPL.
2. Segment kodu A nie może wykonać skoku przez furtkę B, gdyż nie został spełniony warunek, że DPL furtki jest większy równy od CPL (tak jak to ma miejsce w przypadku skosku z segmentu C)
Przełączanie stosów
W każdym przypadku, gdy na skutek wywołania furtki wywołania następuje skok ze zmianą poziomu ochrony procesor realizuje operacje przełączenia na stos odpowiadający poziomowi uprzywilejowania docelowego segmentu kodu. Każde zadanie (program) uruchamiane w systemie powinno tworzyć do 4 stosów (dla każdego poziomu uprzywilejowania, z którego będzie korzystało). W przypadku, gdy uruchamiane jest oprogramowanie pracujące na 3 poziomie ochrony powinno ono zdefiniować conajmniej 2 stosy: dla poziomu 3 i dla poziomu 0 (poziom oprogramowania systemowego). Każdy z tworzonych stosów powinien być ulokowany w oddzielnym segmencie danych. Poziom uprzywilejowania przypisany do niego definiowany jest za pomocą pola DPL deskryptora segmentu danych, w którym stos jest lokalizowany. Selektor segmentu danych stosu i wskaźnik wierzchołka stosu poziomu 3 jest zapamiętywany w rejestrach: SS, ESP (w przypadku, gdy oprogramowanie jest uruchamiane na poziomie 3). Selektory i wskaźniki stosów dla poziomów 0 - 2 są definiowane za pomocą wpisów do segmentu stanu zadania TSS. Każdy segment stosu musi posiadać zdefiniowane uprawnienia do zapisu i odczytu danych. Minimalna wielkość stosu powinna pozwalać na:
zapis zawartości rejestrów: SS, ESP, CS i EIP dla uruchamianej procedury (podprogramu)
zapis arumentów wywołania procedury, zmiennych tymczasowych
rejestru EFLAGS, kodu błędu, gdy nastąpi wywołanie wyjątku
Każdy stos powinien umożliwiać zapis wielu powyższych zestawów danych (ramek) w związku z wykonaniem zagnieżdżonym procedur.
Jeżeli oprogramowanie nie wykorzystuje mechanizmów wielozadaniowości wbudowanych w procesor powinno utworzyć jeden segment TSS dla obsługi przełączania stosów w związku z wykorzystaniem furtek wywołania.
W momencie wywołania procedury przez furtkę wywołania realizowane są następujące operacje w związku z przełączaniem stosów:
1. Procesor wykorzystuje DPL docelowego segmentu kodu do znalezienia stosu w danych zapisanych w segmencie TSS.
2. Odczytuje z segmentu TSS selektor segmentu danych stosu i adres wierzchołka stosu. W przypadku, gdy wystąpią błędy w czasie odczytu danych generowany jest wyjątek #TS.
3. Weryfikuje segment stosu (testy wielkości, typów, poziomu uprzywilejowania) i w przypadku błędu generuje wyjątek #TS.
4. Zapamiętuje tymczasowo bieżące wartości rejestrów SS i ESP.
5. Ładuje do rejestrów SS i ESP wartości związane z stosem bieżącego poziomu ochrony.
6. Zapamiętuje na stosnie tymczasowo zapamiętane wartości położenia stosu kodu wywołującego procedurę.
7. Kopiuje parametry wywołania procedury ze stosu wywołującego (liczba jest określona w deskryptorze furtki wywołania) do nowego stosu.
8. Odkłada na nowym stosie adres powrotu (CS, EIP).
9. Ładuje do rejestrów CS, EIP adres procedury zapisany w furtce wywołania.
Na poniższym rysunku pokazane zostały stosy w wyniku wykonania operacji przełączania w czasie skoku przez furtkę wywołania.
Parametr count w deskryptorze furtki określa ilość danych (do 31), które procesor powinien skopiować ze stosu procedury wywołującej na stos procedury wywoływanej. Jeżeli przekazywana jest większa niż 31 ilość argumentów wywołania wówczas jeden z argumentów powinien wskazywać na obszar pamięci, gdzie pozostałe argumenty są składowane.
Powrót z wywoływanej procedury
Do powrotu z podprogramu może być wykorzystywany rozkaz RET. Rozkaz ten umożliwia portów z procedury, która została wywołana w wyniku wykonania rozkazu CALL. Rozkaz jest wykorzystywany zarówno do powrotów, które były wynikiem wykonania skoku bez zmiany poziomu ochrony, jak również ze zmianą poziomu uprzywilejowania. W przypadku, powrotu ze skoku bliskiego ze stosu zdejmowany jest adres powrotu, który jest ładowany do licznika rozkazów. W przypadku powrotu ze skoku dalekiego realizowane są następujące operacje:
1. Sprawdzane czy pole RPL zapisanego na stosie rejestru CS jest różne od CPL. Jeżeli tak oznacza to że skok nastąpił ze zmianą poziomu uprzywilejowania.
2. Ze stosu zdejmowany jest licznik rozkazów (CS i EIP) i następuje załadowanie wartościami rejetrów CS i EIP. Z operacją zmiany wartości rejestru CS są realizowane testy typu, poziomów uprzywilejowania. Jeżeli wystąpią błędy są one sygnalizowane wyjątkami.
3. W przypadku, gdy powrót następuje na skutek wywołania rozkazu RET n (n - operand, liczba) i następuje zmiana poziomów uprzywilejowania:
procesor dodaje n do bieżącej wartości rejestru ESP (po zdjęciu ze stosu rejestrów CS i EIP)
w wyniku tej operacji ESP wskazuje na zapamiętany na stosie adres wierzchołka stosu procedury wywołującej
4. W przypadku, gdy następuje zmiana poziomu uprzywilejowania, ze stosu zdejmowany jest adres wierzchołka stosu procedury wywołującej. Poszczególne wartości są zapisywane do rejestrów SS i ESP. Jednocześnie realizowane są testy weryfikacji naruszeń, które w przypadku błędów generują wyjątki.
5. W przypadku, gdy powrót następuje na skutek wywołania rozkazu RET n (n - operand, liczba) do rejestru ESP dodawana jest wartość n. W wyniku tej operacji wierzchołek stosu wraca na pozycję z przed wywołania procedury. Przypadki naruszenia wielkości stosu nie są sygnalizowane wyjątkiem.
6. W przypadku, gdy następuje powrót ze zmianą poziomu uprzywilejowania zawartości rejestrów segmentowych DS, ES, FS, GS są weryfikowane na zgodność z bierzącym poziomiem uprzywilejowania. W przypadku, gdy DPL segmentów wskazywanych przez nie są mniejsze niż CPL wówczas do rejstru segmentowego wpisywana jest wartość null (selektor zerowy)
WAŻNE
Operand rozkazu RET n jest liczbą liczoną w jednostkach bajt. Parametr count furtki wywołania jest mierzony w zależności od typu furtki albo w słowach, albo w podwójnych słowach. Wartość operandu n musi być tak dobrana, aby była identyczna z wielkością kopiowanego obszaru reprezentowaną parametr count furtki wywołania. Przykładowo jeżeli furtka wywołania jest typu 32 bitowa furtka wywołania, wartość parametru count jest 2, to operand n musi przyjąć wartość 8.
Inicjalizacja trybu chronionego
Wprowadzenie
W niniejszym rozdziale opisane zostały operacje wykonywane w czasie uruchamiania trybu chronionego procesorów serii x86.
Przyjęto następujące założenia:
1. Inicjalizacja następuje przez uruchomienie programu napisanego dla systemu operacyjnego DOS.
2. Uruchomienie następuje w środowisku systemu DOS.
3. Konfiguracja systemu operacyjnego: brak jakichkolwiek sterowników do zarządzania pamięcią górna (np. emm386).
Operacje wykonywane w czasie inicjalizacji trybu chronionego
Etapy inicjalizacji trybu chronionego
Maskowanie/blokowanie przerwań
Operacje do wykonania:
1. Blokada przerwań procesora.
2. Maskowanie przerwania NMI.
3. Maskowanie przerwań kontrolerów 8259.
Przygotowanie struktur deskryptorowych, które będą wykorzystywane przez oprogramowanie
Operacje do wykonania:
1. Przygotowanie GDT.
2. Przygotowanie IDT.
3. Przygotowanie wszystkich wykorzystywanych LDT.
4. Przygotowanie wartości wykorzystywanych selektorów.
5. Przygotowanie danych we wszystkich wykorzystywanych segmentach TSS.
6. Przygotowanie wartości, które zostaną załadowane do rejestrów: GDTR, IDTR, LDTR, TR.
7. Dane niezbędne do uruchomienia stronicowania.
8. Załadowanie danych wskazujących na tablicę GDT do rejestru GDTR.
Dane, które są przygotowywane są uzależnione od wybranego modelu pamieci i architektury oprogramowania, które będzie uruchamiane w trybie chronionym. W minimalnym zakresie do przygotowania są następujące dane:
1. Globalna tablica deskryptorów.
2. Wykorzystywane selektory.
3. Inicjalizacyjny segment TSS (wykorzystywany tylko przy pierwszym przełączeniu zadań).
4. Wartości ładowane do rejestrów: GDTR i TR.
Konfiguracja urządzeń zewnętrznych komputera, które będą wykorzystywane w trybie chronionym
Realizowane operacje:
1. Zapamiętanie danych pozwalających na odtworzenie konfiguracji pracy kontrolerów przerwań (układy 8259)
2. Inicjalizacja systemu przerwań wykorzystywanego w trybie chronionym.
3. Przygotowanie nowych masek dla obsługiwanych przerwań sprzętowych.
4. Odczyt danych konfiguracji układu 8254 i zapamiętanie ich do odtworzenia.
5. Przeprogramowanie układu 8254.
6. Przeprogramowanie innych wykorzystywanych układów.
Zapamiętanie danych potrzebnych do powrotu do systemu operacyjnego DOS
Do powrotu do DOS potrzebne jest zapamiętanie stanu wszystkich zmienianych przez nas rejestrów, głównie rejestrów segmentowych (CS, DS, ES, SS).
Uruchomienie trybu chronionego
Ustawienie bitu PM w rejestrze CR0 procesora powoduje uruchomienie trybu chronionego. Po wykonaniu tej czynności należy wykonać rozkaz skoku (JMP, lub CALL), który wyczyści kolejkę rozkazów procesora.
Inicjalizacja rejestrów segmentowych
W związku z zmianą interpretacji zawartości rejestrów segmentowych, aby możliwe było wykonywanie się oprogramowania w trybie chronionym należy:
1. Załadować rejestr LDTR wskazaniem na deskryptor tablicy LDT (jeżeli jest wykorzystywana).
2. Załadować rejestr IDTR wskazaniem na tablicę IDT.
3. Załadować rejestr TR selektorem wskazującym na inicjalizacyjny segment TSS (jeżeli uruchomienie docelowego kodu odbędzie się rozkazem przełączenia zadania).
4. Zainicjalizować rejestry selektorów wartościami wskazującymi na deskryptory segmentów (w przypadku uruchamiania docelowego oprogramowania rozkazem skoku od zadania, operacje mogą być pomijane).
Aktywacja przerwań
Do wykonania są następujące operacje:
1. Odblokowanie przerwań na kontrolerach 8259 (zgodnie z przygotowanymi wcześniej maskami).
2. Odblokowanie przerwania NMI.
3. Odblokowanie przerwań procesora (STI).
Uruchomienie programu w trybie chronionym
Proces ten można wykonać na wiele sposobów zaczynając od skoku bezpośredniego do segmentu kodu oprogramownaia, przez przełączenie zadania, a skończywszy na uruchomieniu kodu po wykonaniu procedury obsługi przerwania np. timera. Ważne jest aby wybrana metoda pokrywała się z przygotowanymi w czasie inicjalizacji danymi. W związku z tym jężeli uruchomienie ma się odbyć przez przełączenie zadania to wcześniej rejestr TR musi zostać załadowany wskazaniem na segment TSS (inicjalizacyjny). Jeżeli uruchomienie ma się odbyć przez skok do segmentu kodu, to muszą zostać najpierw przygotowane wartości wszystkich rejestrów segmentów.