Złożoność
Architektura x86 dojrzewała przez lata. Inżynierowie przez cały czas starali się zachować zgodność wsteczną, wysoką przystosowalność do potrzeb programistów oraz oczywiście dodawać nowe i ulepszać stare funkcje. Z tych powodów IA-32 jest dosyć skomplikowana. Nim zanurzymy się w pełen dobrodziejstw tryb chroniony, warto poznać podstawowe elementy architektoniczne, które będziemy oprogramowywali :)
W tym celu posłużymy się schematem dostępnym w polecanych przeze mnie podręcznikach Intela:
GDT
Znajdująca się w centralnym punkcie obrazka struktura danych zwana Global Descriptor Table (GDT) jest „sercem” architektury x86. Zawiera ona deskryptory segmentów (struktury opisujące segmenty), na które podzielona jest pamięć. Tak jak wspominałem, segmentacja jest zawsze obecna w architekturze x86. Oprócz „zwykłych” segmentów w pamięci, zawierających dane czy kod, w GDT znajdują się również deskryptory TSS oraz LDT. Warto jednak wspomnieć, że bitowo patrząc, wszystkie deskryptory mają taką samą strukturę. Z każdym deskryptorem skojarzony jest odpowiedni selektor, czyli przesunięcie w tablicy GDT wskazujące na dany deskryptor, lokalność/globalność, czyli to, czy mamy do czynienia z wpisem GDT czy LDT oraz prawa dostępu. Adres (liniowy) tablicy GDT, wraz z limitem (długością) znajduje się w rejestrze GDTR.
Odwołując się w jakikolwiek sposób do pamięci, musimy więc podać selektor segmentu oraz przesunięcie wewnątrzsegmentowe. Z selektora pobierany jest numer deskryptora, a z deskryptora następnie adres bazowy, do którego następnie dodawane jest przesunięcie dając adres liniowy – w taki sposób następuje odwołanie do elementu w pamięci.
LDT
Local Descriptor Table (LDT) jest siostrzaną tablicą do GDT. Aktualnie jest już prawie nieużywana. Jej użycie jest istotne w systemach ze starymi procesorami np. 80826, w których nie było jednostki stronicowania – a więc nie było możliwości realizowania separacji, tym samym ochrony pamięci poszczególnych procesów. Aktualnie LDT używane jest wyłącznie do uruchomiania 16 bitowych programów. Niegdyś, każdy proces użytkownika, posiadał własną tablicę LDT, której pozycja znajdowała się w rejestrze LDTR. LDT oferowała ochronę pamięci polegającą na możliwości dostępu tylko przez deskryptory znajdujące się w tejże tablicy. Kernel oraz współdzielona pamięć opisana była w GDT.
TSS
Task State Segment (TSS) to segment opisujący zadanie (ang. task). Używany jest głównie przy sprzętowej wielozadaniowości, w której z każdym procesem skojarzony jest pojedynczy TSS. W strukturze TSS znajdują się wszystkie informacje potrzebne do „odtworzenia” zadania – rejestry ogólnego przeznaczenia i te bardziej szczególne, takie jak EIP. W przypadku programowej obsługi wielozadaniowości potrzebna tylko jednego lub dwóch segmentów TSS. Selektor segmentu TSS jest trzymany w rejestrze TR – Task Register.
IDT
Interrupt Descriptor Table (IDT), to trzecia, po GDT i LDT tablica systemowa. Wpisy w tej tablicy nazywamy bramkami (ang. gates) i mogą być to bramki: przerwań (interrupt), zadań (task) oraz pułapek (trap). Powstrzymam się z omawianiem konkretnych bramek – zrobię to przy okazji implementowania IDT. Na tę chwilę powiem, że wpisy w IDT służą wywołaniu odpowiedniej procedury obsługi konkretnego zdarzenia, np. przerwania, pułapki. Adres liniowy IDT znajduje się w rejestrze IDTR.
Stronicowanie
Na obrazku, stronicowanie (ang. paging) przedstawione jest na dole (dla pamięci „płaskiej”, rozmiaru strony 4KiB). Jest to bardzo istotny element architektury x86, o którym napiszę więcej w przyszłości. Na chwilę obecną przedstawię ogólną ideę działania jednostki stronicowania. Po przejściu przez wszystkie zawiłości związane z GDT „na wyjściu” otrzymujemy adres liniowy zbudowany, tak jak wspomniałem, z adresu bazowego znajdującego się w deskryptorze segmentu i przesunięcia wewnątrzsegmentowego. Gdy jednostka stronicowania jest wyłączona, adres ten jest bezpośrednim adresem w pamięci liniowej. W przeciwnym wypadku, adres interpretowany jest jako wirtualny. W adresie tym można wydzielić 3 części: dir, table oraz offset. Dir wskazuje wpis w katalogu stron, który z kolei wskazuje na odpowiednią tablicę. Następnie table wskazuje wpis w tablicy stron, który wskazuje konkretną stronę. Offset to przesunięcie w tej konkretnej stronie, które jest już elementem, który chcemy pobrać/zapisać. Jak widać, pojedynczy dostęp do pamięci, np. mov [eax], 42 nie oznacza fizycznie jednego dostępu do pamięci, ale sporo więcej. Stronicowanie ma więc istotny wpływ na wydajność systemu operacyjnego, dając nam jednak elegancką ochronę pamięci.
Rejestry systemowe
Oprócz wspomnianych wyżej, architektura x86 posiada 5 rejestrów kontrolnych (CR0, CR1, CR2, CR3, CR4, XCR0), rejestr flag EFLAGS, rejestry debuggowania oraz rejestry specyficzne dla konkretnej platformy. Myślę, że nie ma co opisywać ich zawiłych funkcji – zrobię to przy okazji używania ich.
Podsumowanie
Jak widać, architektura x86 jest dosyć bizantyjska. Posiada wiele funkcjonalności, których aktualnie się nie używa, które jednak znalazły swoje zastosowanie w przeszłości. Jest z tego powodu bardzo ciekawa z punktu widzenia tworzenia systemów operacyjnych – można tworzyć bardzo skomplikowane twory, całkowicie zgodne z wizją programisty, łącząc old school z tym co trendy ;).
O wspomnianych wyżej elementach architektury x86 będę jeszcze dokładniej pisał przy okazji ich używania. Na pierwszy ogień pójdzie GDT.