!tutorials, tryb chroniony 2


5

Poza zakresem

Fault

-

Po instrukcji BOUND

6

Nieznana instrukcja

Fault

-

Błędny opcode dla danego CPU

7

Brak koprocesora

Fault

-

Kiedy nie ma koprocesora

8

Double Fault

Abort

+ (0)

Błąd podczas wykonywania przerwania

9

Coprocessor Segment Overrun

Abort

-

?

10

Błędny TSS

Fault

+

Zadanie ma błędny TSS

11

Brak segmentu

Fault

+

Flaga P segmentu ustawiona na "0"

12

Błąd stosu

Fault

+

Przekroczony limit stosu lub błędny SS

13

GP

Fault

+

Dostęp do chronionego segmentu

14

Błąd strony

Fault

+

Brak strony pamięci

15

Błąd koprocesora

Fault

-

Błąd koprocesora

0..255

Software interrupts

Trap

-

Przerwania programowe

0. Intro
1. O co chodzi z tymi trybami?
2. Po co mi pmode?
3. Czy pmode ma jakieś wady?
4. Deskryptory segmentowa - czyli przechodzeń do meritum :-)
5. Tablice deskryptorów i selektory.
6. Jak przebiega adresowanie w PM?
7. Przerwania w trybie chronionym.
8. Instrukcje specyficzne dla pmode i instrukcje uprzywilejowane.
9. Przełączanie procesora w pmode.
10. Dos Protected Mode Interface = DPMI.
11. Outro


0. Intro

Zdecydowałem się napisać ten tutorial mając na uwadze fakt, ze w polskich zasobach sieci jest stosunkowo mało informacji na temat trybu chronionego - przynajmniej takich, które mogłyby być pomocne beginnerom. Również trudno znaleźć cokolwiek na ten temat w powszechnie dostępnej literaturze IT.

W tym tutorialu będę się starał wyjaśniać wszystko od absolutnego początku z pominięciem wielu "dobrodziejstw" trybu chronionego typu wielozadaniowość czy tez stronicowanie. Będzie to spojrzenie na pmode oczami kogoś, komu po prostu brakuje pamięci lub prędkości przy pisaniu własnych programów. Na razie naszym celem będzie napisanie poprawnie działającego klienta DPMI, który umożliwi prace w trybie chronionym.

Zaznaczam, ze zajmiemy się tutaj wyłącznie 32-bitowym trybem chronionym (ma ktoś jeszcze 286? ;-) Ale do rzeczy...

1. O co chodzi z tymi trybami?

Zasadniczo istnieją 4 tryby pracy procesora 386+. Oto one:

  • tryb rzeczywisty (real mode, dalej zwany RM)

  • tryb chroniony (protected mode, w skrócie pmode lub PM)

  • tryb wirtualny V86 - kombinacja 2 powyższych (nie będziemy się nim zajmować; używany np. przez Windows ;)

  • tryb nierzeczywisty (tzw. unreal mode - procesor pracuje w RM, ale można używać adresowania 32-bitowego; to jest raczej sztuczka programistyczna)

Każdy procesor z rodziny x86 po rozpoczęciu pracy (włączeniu zasilania ;-) działa w trybie rzeczywistym. W takim trybie procek "widzi" tylko pierwszy megabajt RAM'u i dopiero system operacyjny (typu Winda lub Linux) przełącza go w pmode podczas bootowania. W dodatku pamięć w RM jest podzielona na segmenty po 64kB co np. bardzo utrudnia używanie w programach tablic o rozmiarach przekraczających ten limit.

W jaki sposób procesor lokalizuje komórkę pamięci w RM? Zobaczmy następujący przykład:

mov es:[si + 2], ax

Procesor robi tak: pobiera wartość rejestru segmentowego ES, mnoży ja przez 16 (10h) i dodaje przesuniecie (tutaj wartość rejestru indeksowego SI oraz +2). W ten sposób powstaje adres fizyczny komórki pamięci i dopiero wtedy kopiowana jest tam zawartość akumulatora. Jak się później okaże, w PM adresowanie przebiega zupełnie inaczej.

W RM każdy segment ma długość 64kB (nie można jej zmieniać) i zaczyna się na granicy 16 b
ajtów (tzw. paragraf). Oznacza to, że kolejne segmenty częściowo zachodzą na siebie. Na przykład: bajt 16 segmentu 0 i bajt 0 segmentu 1 to jest to samo.

Tryb rzeczywisty oprócz podanych wyżej wad ma jeszcze jedna: każdy program ma możliwość manipulowania obszarami pamięci nie należącymi do niego. Np. w systemie DOS możliwe jest nadpisanie handlera jakiegoś ważnego przerwania i tym samym, przy najbardziej optymistycznej wersji, zawieszenie komputera.

2. Po co mi pmode?

Przede wszystkim tryb chroniony stwarza możliwość użycia naprawdę dużej ilości pamięci (do 4 GB na 386, w przeciwieństwie do dotychczasowego 1 MB). Nie trzeba już używać powolnych EMS i XMS by dostać się do pamięci rozszerzonej. Ponieważ rozmiar segmentu nie jest ograniczony do 64kB, można w praktyce wyeliminować rejestry segmentowe i operować w tzw. trybie flat - jest to ważne w przypadku kiedy potrzebujemy dużej prędkości działania programu, DS jest zajęty a my nie chcemy prefixowac ES'em, FS'em itd. Inne zastosowanie: operacje na buforach ekranowych przy dużych rozdzielczościach i/lub dużej liczbie kolorów, które to bufory wymagają więcej niż 64kB pamięci (spróbujcie napisać procke rysującą linie w trybie 640x480x256 pod RM - wooooolne...).

3. Czy pmode ma jakieś wady?

Tak. Wszystkie przerwania BIOS lub DOS'a, których używaliśmy, przykładowo, do obsługi plików, staja się bezużyteczne bo nie mogą działać w PM (oczywiście można to obejść poprzez chwilowe wychodzenie z pmode do rmode na czas wykonywania przerwania, ale nie jest to już takie łatwe). Dlatego trzeba będzie pisać własne handlery do klawiatury, zegara etc.

Nie można także beztrosko używać samomodyfikujacego się kodu - czytanie lub zapis w segmencie wykonywalnym spowoduje wygenerowanie wyjątku GP.

4. Deskryptory segmentów - czyli przechodzeń do meritum :-)

Wprowadzenie mechanizmów ochrony pamięci wymagało podjęcia specjalnych środków. Każdy segment (kod, dane, stos) opisywany jest przez tzw. deskryptor. Zadaniem deskryptora jest określenie BAZY SEGMENTU (jego fizycznego adresu w pamięci, np. dla bufora VGA adres bazowy wynosi 0A0000h), LIMITU (rozmiaru segmentu - może wynosić od 1B do 4GB) oraz jego TYPU/FLAG (czy segment zawiera dane, kod, etc.)

Technicznie rzecz biorąc deskryptor jest to 8-bajtowa struktura danych, która rezyduje gdzieś w pamięci operacyjnej i ma następujący format:

0x01 graphic


Znaczenie kolejnych bajtów jest takie:
0, 1 - młodsze słowo długości segmentu (limitu)
2, 3 - młodsze słowo adresu bazowego segmentu
4 - młodszy bajt starszego słowa adresu bazowego
5 - flagi segmentu
6 - starszy półbajt limitu i tzw. flagi dostępu
7 - starszy bajt starszego słowa adresu bazowego

Jak powiedziałem wcześniej, rozmiar segmentu w pmode może być dowolny (a wiec nie tylko 64kB, ale także każda inna wielkość od 1B do 4GB). Jak wynika z opisu limit w deskryptorze jest to liczba 20-bitowa (8 + 8 + 4) co daje maksymalnie 1048576 czyli 1MB. Hmm... a miało być 4 giga? O tym w jaki sposób zostaje wykorzystana ta liczba napisze za chwile.

Adres bazowy to 32-bitowa liczba wyznaczająca
początek segmentu w pamięci. Zazwyczaj pisząc typowy DOS'owy extender bazę możemy wyznaczyć poprzez pomnożenie przez 16 numeru segmentu, w którym znajduje się nasz kod 32-bitowy. Wartość ta będzie dla nas bardzo ważna jeśli będziemy chcieli np. dobrać się do pamięci ekranu. W pmode bowiem, jak się okaże, pod adresem 0A0000h może się niekiedy znajdować coś zupełnie innego niż bufor VGA... O tym później.

Teraz czas na flagi segmentu (bajt 5):

0x01 graphic


Bit 7 - flaga P (Presence - obecność). Używana przy pamięci wirtualnej. Ustawiajmy ja zawsze na "1".
Bity 5 i 6 - Descriptor Privilege Level czyli poziom uprzywilejowania segmentu. Ponieważ mamy tylko 2 bity, istnieją 4 poziomy: 0, 1, 2 i 3 przy czym 0 to najwyższy poziom uprzywilejowania. O co w tym chodzi? Omówię to na przykładzie: system operacyjny przydzielił procesowi obszar pamięci i opisał go deskryptorem, który określa, ze ten obszar (segment) ma, załóżmy, 3 poziom uprzywilejowania (DPL = 3). Ten proces nie
może się odwoływać do segmentów, których DPL jest mniejsze od 3. W praktyce oznacza to, ze ten proces nie nadpisze sobie jądra systemu, bo ono najprawdopodobniej działa na poziomie 0. Osobiście sugerowałbym w DOS extenderze ustawić DPL na 0 (na maksa, a co?). Bity 5 i 6 będą wiec równe 0. W przypadku aplikacji będącej klientem hosta DPMI wymagane jest DPL = 3.
Bit 4 - Descriptor Type. Jeśli równy "1" to mamy do czynienia z segmentem aplikacji. "0" jest dla segmentów systemowych.
Bity 0, 1, 2 i 3 wyznaczają tzw. typ segmentu. Myślę, ze nas interesować będą na razie tylko
0010 - segment danych (ew. stosu) do odczytu i zapisu oraz 1010 - segment kodu do wykonywania i odczytu.

Bajt 6 deskryptora jest chyba najbardziej skomplikowany:

0x01 graphic


Bit 7 - ziarnistość (Granularity).
Bit ten decyduje o sposobie traktowania podanego limitu segmentu. Jeśli bit jest ustawiony na "1" wtedy rozmiarem segmentu jest limit pomnożony przez 4096. A wiec segment o rozmiarze 4GB będzie miał limit równy 0FFFFFh i G = 1. Kiedy G = 0 to segment ten zostanie potraktowany jako 1-megabajtowy.
Bit 6 - określa czy segment jest 16 czy 32-bitowy. Jeśli równy "1" to wówczas instrukcje 32-bitowe (typu
mov eax, ebx) nie będą poprzedzone przedrostkiem rozmiaru (66h). Natomiast do instrukcji 16-bitowych (np. add cx, ax) zostaną dodane przedrostki.
Bit 5 - nieużywany?
Bit 4 - określa czy segment jest "dostępny dla systemu". Co to w praktyce znaczy? Nie wiem ;-) Tak jak bit 5, lepiej ustawić na "0".
Bity 3, 2, 1, 0 stanowią najstarszy połbajt limitu.

Istnieje 18 różnych typów deskryptorów, ale w praktyce część z nich jest nieużywana (zarezerwowane lub dostępne tylko dla 28
6). Oprócz deskryptorów segmentów, zajmiemy się tu także deskryptorami przerwań.

5. Tablice deskryptorów i selektory.

Pojawia się pytanie: skąd procesor wie, gdzie w pamięci znajduje się dany deskryptor? Postanowiono grupować deskryptory w tablice tzn. kolejno następujące po sobie deskryptory. Np:

desc_null   db 0, 0, 0, 0, 0, 0, 0, 0
desc_code32 db 0FFh, 0FFh, 0h, 0h, 0h, 0FAh, 0CFh, 0h
desc_data32 db 0FFh, 0FFh, 0h, 0h, 0h, 0F2h, 0CFh, 0h

Tablica ta zawiera 3 deskryptory: tzw. Null Descriptor, deskryptor segmentu kodu oraz deskryptor segmentu danych. Ten pierwszy pojawia się tylko w tablicach GDT i zawsze zawiera same zera.

Wyróżniamy 3 typy tablic: GDT (Globalna Tablica Deskryptorów), LDT (Lokalna Tablica Deskryptorów; używana przez pojedynczy proces w systemie) oraz IDT (Tablica Deskryptorów Przerwań).

Procesor musi wiedzieć, gdzie takie tablice się znajdują i dlatego dysponuje specjalnymi rejestrami, które zawierają 32-bitowy fizyczny adres danej tablicy oraz jej długość. Są 3 rejestry: GDTR (przechowuje adres i długość GDT) oraz, analogicznie, LDTR i IDTR. Każdy z tych rejestrów jest 48-bitowy i ma następujący format:

0x01 graphic


4 pierwsze bajty to adres, 2 kolejne to długość tablicy. Ponieważ długość jest liczba 16-bitowa a rozmiar deskryptora liczy 8 bajtów, maksymalnie można używać 8192 deskryptorów w jednej tablicy.

Jednak sama znajomość położenia tablicy w RAM nie wystarczy. Trzeba poinformować procesor, który segment używa którego deskryptora. Np. jaki deskryptor opisuje aktualny segment danych? W pmode rejestry segmentowe NIE zawierają numerów segmentów jak miało to miejsce w RM. Zawierają za to
selektory. Oto taki selektor:

0x01 graphic


Bity 0 i 1 zawierają tzw. RPL czyli wymagany poziom uprzywilejowania. Poziom ten musi być większy lub równy od DPL'a deskryptora, do którego selektor się odn
osi. Bit 2 informuje o tym czy selektor wskazuje na deskryptor w GDT (wtedy ustawiony na "1") lub w LDT ("0"). Reszta bitów to NUMER danego deskryptora w tablicy deskryptorów (przy czym pierwszy z nich ma numer 0). Na jego podstawie procesor lokalizuje dany deskryptor. W powyższym przykładzie selektor umieszczony w rejestrze DS będzie wyglądał tak: 0000000000010 0 00 - DS wskazuje na drugi deskryptor w tablicy GDT na poziomie 0.

6. Jak przebiega adresowanie w PM?

W tym miejscu celowo pominę mechanizm stronicowania i role jednostki MMU bo na razie nas to nie interesuje (stronicowanie traktujemy jako wyłączone). Najlepiej omawiać to na przykładzie:

mov [esi + 2], eax

W trybie chronionym wpisanie akumulatora pod wskazany adres odbywa się tak: procesor pobiera selektor z DS (ponieważ dla (E)SI domyślny jest właśnie DS - wiadomo to już z RM). Następnie bierze z tego selektora numer deskryptora i mnoży go przez 8 (rozmiar deskryptora). Po dodaniu do tego adresu tablicy GDT/LDT pochodzącego z rejestru GDTR/LDTR powstaje lokalizacja konkretnego deskryptora. Teraz procek może wykonać szereg czynności związanych z ochrona pamięci - sprawdza czy RPL nie jest większy od DPL, czy segment opisywany w tym deskryptorze istnieje, czy segment ma typ "data" i ma ustawione prawa do zapisu etc. Kiedy cos jest nie tak to wywoływane jest stosowne przerwanie (najczęściej 13h - General Protection Fault; o tym później). W przeciwnym razie można kontynuować. Z deskryptora pobierana jest BAZA segmentu, do której procesor dodaje PRZESUNIECIE (u nas ESI oraz np. +2). W ten sposób powstaje adres FIZYCZNY i można już tam zapisać 4 bajty z EAX. Tak to wszystko wygląda w pewnym uproszczeniu.

Zwrócić należy uwagę, ze podawane przesuniecie to jest LOGICZNY adres w RAM. Po co nam to wiedzieć? Np. dostanie się do pamięci ekranu (fizycznie 0A0000h) jest odrobinę problematyczne. Możemy zrobić sobie specjalny deskryptor, który opisze nam segment o adresie bazowym = 0A0000h i limicie 64kB. No tak, ale mięliśmy pracować w trybie flat zapominając o rejestrach segmentowych! Jeśli ustalimy sobie jeden duży segment o maksymalnym rozmiarze 4GB obejmujący cala teoretyczna przestrzeń adresowa to adres prawdziwego bufora VGA obliczymy tak:

mov esi, 0A0000h
sub esi, [segment_base]

Kiedy do tak zmniejszonego 0A0000h procek doda bazę segmentu to wszystko będzie OK. Inaczej trafilibyśmy na obszar pamięci o adresie 0A0000h + segment_base. Nie można przewidzieć, co tam się znajduje.

W pmode istnieje możliwość adresowania nie tylko rejestrami indeksowymi (ESI, EDI) oraz bazowymi (EBP, EBX). Można także używać wszystkich rejestrów ogólnego przeznaczenia (EAX, EBX, ECX i EDX) i dodatkowo stosować skalowanie poprzez mnożenie ich przez 2, 4, 8 itd. Np:

mov eax, [edi + edx*2]

Dygresja: w połączeniu z instrukcja LEA można tego użyć do szybkiego mnożenia (1 cykl na Pentium). Poniższą instrukcja mnoży EAX przez 5:

lea eax, [eax + eax*4]

7. Przerwania w trybie chronionym.

W RM pojawienie się przerwania (wywołanego przez instrukcje INT lub urządzenie zewnętrzne typu klawiatura) powodowało, ze procesor, po odłożeniu na stos rejestru flag oraz CS i IP, skakał pod adres określony w tablicy wektorów przerwań. Tablica ta znajduje się zawsze na początku pamięci pod adresem 0000:0000 i ma długość 1kB (256 przerwań * 4 czyli rozmiar wektora CS:IP). W pmode, jak się można domyślić, ta tablica jest już bezużyteczna.

Rejestr IDTR, podobnie jak omawiany GDTR, zawiera adres fizyczny i długość nowej tablicy. Jest to tzw. Interrupt Descriptor Table czyli Tablica Deskryptorów Przerwań. W przeciwieństwie do RM, tablica ta może znajdować się w dowolnym miejscu w pamięci. Maksymalnie zawiera 256 deskryptory. Gdy w PM ma miejsce przerwanie, procek odszukuje dany deskryptor mnożąc numer przerwania przez 8 i dodając adres tablicy zawarty w IDTR
. Format deskryptora przerwania jest nieco inny od formatu segmentowego:

0x01 graphic


Bajt 0 i 1 (młodsze słowo) oraz 6 i 7 (starsze słowo) zawierają przemieszczenie (offset) handlera przerwania w danym segmencie. Jest to po prostu miejsce gdzie znajduje się kod przerwania.
Bajt 2 i 3 to miejsce na selektor danego deskryptora tzn. tego, który opisuje segment zawierający handler.
Bajt 4 jest nieużywany i zawsze pusty.
Bajt 5 zawiera flagi deskryptora pr
zerwania:

0x01 graphic


Najmłodsze 4 bity zawsze są równe
1110. Jest to identyfikator deskryptora przerwania.
Bity 5 i 6 stanowią DPL czyli poziom uprzywilejowania handlera.
Bit 7 czyli Presence - zawsze równy "1".

Zwrócić należy uwagę, ze deskryptor przerwania jest to przypadek tzw. bramki wywołania służącej do wywoływania funkcji znajdujących się w różnych segmentach o różnych poziomach uprzywilejowania.

Przerwanie w trybie chronionym zwyczajowo nazywane jest wyjątkiem (exception). Są 3 typy wyjątków:
trap, fault i abort. Wyjątki abort wywoływane są na skutek błędów w tablicach systemowych (GDT, LDT, IDT) lub w przypadku wystąpienia problemów sprzętowych. Nie ma możliwości powrotu do naszego programu, nie jest zwracany także kod błędu. Wyjątki trap to zwykle przerwania generowane przez instrukcje INT lub podczas debuggowania (wywoływanie przerwania po każdej instrukcji). Ostatni typ, fault, to wyjątki spowodowane przewidywalnymi, udokumentowanymi błędami np. słynny General Protection Fault wywalający w Windows "niebieskie ekrany śmierci". Na stosie umieszczany jest kod błędu, a rejestry CS:EIP (odłożone na stos) zawsze wskazują na instrukcje, która spowodowała błąd. Oto tabelka z wyjątkami:

Nr

Nazwa

Typ

Błąd

Opis

0

Dzielenie przez 0

Fault

-

Dzielenie przez 0

1

Praca krokowa

Trap

-

Wywoływane po każdej instrukcji

2

NMI - niemaskowalne

Abort

-

Błąd sprzętowy

3

Breakpoint

Trap

-

Używane przy debuggowaniu

4

Przepełnienie

Trap

-

Wywoływane po INTO jeśli CF = 1


Przypominam, ze handler przerwania w pmode powinien być zakończony instrukcja IRETD (pobranie 32-bitowych EIP oraz EFLAGS oraz selektor
a CS).

Na razie tyle wiedzy o przerwaniach w PM nam wystarczy.

8. Instrukcje specyficzne dla pmode i instrukcje uprzywilejowane.

Programy działające na niskim poziomie uprzywilejowania (np. 3) nie mogą mieć dostępu do niektórych instrukcji. Dowolny proces w systemie nie powinien zmieniać zawartości tablicy GDT lub wychodzić z trybu chronionego do RM. Użycie takich instrukcji przez zwykły program spowoduje wygenerowanie wyjątku. Typowym przykładem jest pojawiający się w Windows komunikat, który informuje, ze program należy uruchomić w środowisku DOS - to po prostu efekt działania wyjątku gdy procesor jest zmuszony np. wyzerować bit trybu chronionego w rejestrze MSW. Oto niektóre instrukcje, które będą nas interesować:

LGDT/LIDT mem48
Ładuje 48-bitowy reje
str GDTR/IDTR pod wskazany adres.

SGDT/SIDT mem48
Zapisuje rejestr GDTR/IDTR pod wskazanym adresem.

LMSW/SMSW reg16/mem16
Ładuje/zapisuje rejestr stanu procesora. Najmłodszy bit tego rejestru decyduje o trybie pracy procka. Jeśli ustawiony na "1" to jesteśmy w pmode. W przeciwnym razie w real mode.

Pozostale instrukcje: ARPL, CLTS, HLT, LTR, STR, LSL, VERR, VERW.

9. Przełączanie procesora w pmode.

Potrzebujemy teraz kodu, który ustawi wszystkie niezbędne tablice i przełączy tryb pracy procesora z RM na PM. Po wykonaniu tych czynności będzie można operować już w trybie chronionym.

Metoda, która tu przedstawię zadziała wyłącznie w DOS w trybie "raw" (bez zainstalowanych himem.sys lub qemm). Program potrzebuje 3 segmentowa: 16-bitowego segmentu kodu, 32-bitowego segmentu kodu i danych (będą to jednocześnie dane dla 16-bit) oraz stosu. Nasz program startuje w trybie rzeczywistym pod kontrola DOS'a. Rozpoczynamy od kodu 16-bit (dla TASM 4.1):

; udostepnienie instrukcji 386 trybu chronionego

.386p

code16 segment para public use16

assume cs:code16, ds:code32

start16:

; punkt wejscia naszego programu

; ustawienie segmentu danych (patrz dalej)

mov ax, code32

mov ds, ax

W tym miejscu wypadałoby sprawdzić czy jesteśmy na pewno w RM i w czystym DOS (żeby zabezpieczyć się przed sytuacja gdy nasz program zostanie odpalony w V86 np. pod Windows):

mov eax, cr0

test eax, 1

je v86_detected

; ok, można przelaczac...

v86_detected:

; obsluga bledu

W tym celu pobraliśmy zawartość rejestru cr0 (jest to de facto Machine Status Word, które można pobrać przy pomocy LMSW, np: lmsw ax). Jeśli najmłodszy bit jest ustawiony to znaczy, ze jesteśmy już w trybie chronionym i musimy wyjść z błędem.

Dygresja: na początku kodu można oczywiście sprawdzić czy program uruchamiany jest na procesorze 386+ ale tutaj, z uwagi na maksymalne uproszczenie kodu, pominę to.

Jeśli jesteśmy już pewni, ze można bezpiecznie przełączyć tryb, znajdujemy adres bazowy naszego segmentu code32. Zadeklarowany wcześniej code32 wygląda tak:

; to jest segment dla danych i kodu jednocześnie (możliwa

; praca w trybie flat)

code32 segment para public use32

assume cs:code32, ds:code32

; tutaj tablice deskryptorów i inne dane

start32:

; tutaj wlasciwy 32-bitowy kod, do którego

; skoczymy zaraz po przelaczeniu się do pmode

code32 ends

Jak znaleźć bazę segmentu? Pomnożymy numer segmentu code32 jaki przydzieli mu DOS przez 16:

mov eax, code32

shl eax, 4

mov small dword ptr [code32_base], eax

; "small" jest dlatego, ze dane są w segmencie 32-bitowym

; a my operujemy w 16-bit. Musimy zatem używać tylko najmlodszych

; 16 bitów adresu

Tak otrzymana wartość zapamiętaliśmy sobie w zmiennej code32_base (przyda się do obliczania adresów logicznych, np. bufora VGA). Adres ten musi zostać także dodany do deskryptorów. Robimy to tak:

mov ebx, eax

shr ebx, 16

mov di, small offset desc_code32

mov word ptr [di + 2], ax

mov byte ptr [di + 4], bl

mov byte ptr [di + 7], bh

mov di, small offset desc_data32

mov word ptr [di + 2], ax

mov byte ptr [di + 4], bl

mov byte ptr [di + 7], bh

Co to robi? Najpierw przenosimy do BX starsze słowo adresu bazowego. W AX jest młodsze. Nasze deskryptory (dwóch segmentowa - dane i kod 32-bit) są w segmencie code32. Musimy wpisać do nich adres bazowy segmentu, który opisują. Robimy to zgodnie z formatem deskryptora.

Deskryptory segmentu code32 wyglądają następująco:

; tzw. Null Descriptor (MUSI być obecny w tablicy GDT)

desc_null db 0, 0, 0, 0, 0, 0, 0, 0

; kod 32-bit, limit 4GB, DPL = 0

desc_code32 db 0ffh, 0ffh, 0h, 0h, 0h, 10011010b, 11001111b, 0h

; dane 32-bit, limit 4GB, DPL = 0

desc_data32 db 0ffh, 0ffh, 0h, 0h, 0h, 10010010b, 11001111b, 0h

Puste bajty wypełnione zerami to właśnie miejsce na adres bazowy, który właśnie został wyliczony i tam przepisany. Jedyne co nam pozostaje do zrobienia z deskryptorami segmentowymi to przygotowanie i załadowanie rejestru GDTR. Definiujemy sobie zmienna:

gdt_reg dd 0

dw 0

Do adresu bazowego segmentu code32 dodajemy przesuniecie tablicy GDT. Zapisujemy także jej rozmiar:

mov eax, small offset desc_null

add eax, small dword ptr [code32_base]

mov small dword ptr [gdt_reg + 0], eax

; dopisanie rozmiaru

mov small word ptr [gdt_reg + 4], 3 * 8

Ładujemy GDTR i przy okazji wyłączamy przerwania (w tym najprostszym przykładzie nie będziemy używać przerwań):

lgdt small fword ptr [gdt_reg]

; wylacz przerwania

cli

Teraz czas przejść do trybu chronionego. Uzyskamy to poprzez ustawienie pierwszego bitu w rejestrze MSW (czyli cr0):

mov eax, cr0

or eax, 1

mov cr0, eax

Znajdujemy się już w trybie chronionym, ale to nie wszystko. Musimy natychmiast załadować CS selektorem deskryptora segmentu kodu, a EIP przesunięciem etykiety start32 (początku kodu 32-bit). Jedyna instrukcja, która to umożliwia to daleki skok JMP lub powrót RETF. Skorzystamy z drugiej metody:

push 8

push large offset start32

retf

Ale co to za "8"? Jest to selektor pierwszego (nie zerowego bo ten jest pusty!) deskryptora w GDT - 0000000000001 0 00. Selektor ma RPL = 0 i znajduje się właśnie w GDT. Teraz kontynuujemy działanie od punktu start32 w segmencie code32:

; ładujemy selektory wskazujące na deskryptor 2 w GDT

mov ax, 16

mov ds, ax

mov es, ax

mov ss, ax

; inicjujemy stos: obliczamy jego adres logiczny w segmencie code32

; i ustawiamy szczyt stosu

mov esp, stack_seg

sub esp, large code32

shl esp, 4

add esp, STACK_SIZE - 4

; zawieszamy komputer

hang_me:

jmp hang_me

Ten program nie potrafi nic więcej zrobić. Wszystkie przerwania są zablokowane i nie można ich używać. W przeciwnym razie nastąpi zresetowanie procesora (automatyczny restart komputera). Stanie się tak także wtedy gdy nastąpi dowolny błąd wywołujący w normalnych warunkach przerwanie (np. General Protection Fault). Dlaczego? Ponieważ nie ma zdefiniowanej tablicy IDT i tym samym nie ma obsługi wyjątków. Najlepsze co procesor może zrobić w takiej sytuacji to reset.

A oto nasz program w całej okazałości (dodatkowo po ustawieniu stosu wypisuje na ekranie standardowe "Hello World"):

.386p

STACK_SIZE equ 4096

;------------------------------------------------------------

; 16-bit code

;------------------------------------------------------------

code16 segment para public use16

assume cs:code16, ds:code32

start16:

mov ax, code32

mov ds, ax

mov eax, cr0

test eax, 1

je v86_detected

jmp mode_ok

v86_detected:

mov ah, 4ch

mov al, 1

int 21h

mode_ok:

mov eax, code32

shl eax, 4

mov small dword ptr [code32_base], eax

mov ebx, eax

shr ebx, 16

mov di, small offset desc_code32

mov word ptr [di + 2], ax

mov byte ptr [di + 4], bl

mov byte ptr [di + 7], bh

mov di, small offset desc_data32

mov word ptr [di + 2], ax

mov byte ptr [di + 4], bl

mov byte ptr [di + 7], bh

mov eax, offset desc_null

add eax, small dword ptr [code32_base]

mov small dword ptr [gdt_reg + 0], eax

mov small word ptr [gdt_reg + 4], 3 * 8

lgdt small fword ptr [gdt_reg]

cli

mov eax, cr0

or eax, 1

mov cr0, eax

push 8

push large offset start32

retf

code16 ends

;------------------------------------------------------------

; 32-bit code

;------------------------------------------------------------

code32 segment para public use32

assume cs:code32, ds:code32

desc_null db 0, 0, 0, 0, 0, 0, 0, 0

desc_code32 db 0ffh, 0ffh, 0h, 0h, 0h, 10011010b, 11001111b, 0h

desc_data32 db 0ffh, 0ffh, 0h, 0h, 0h, 10010010b, 11001111b, 0h

gdt_reg dd 0

dw 0

txt_hello db 'Hello World!'

TXT_HELLO_LEN equ $ - txt_hello

start32:

mov ax, 16

mov ds, ax

mov es, ax

mov ss, ax

mov esp, stack_seg

sub esp, large code32

shl esp, 4

add esp, STACK_SIZE - 4

mov esi, offset txt_hello

mov edi, 0b8000h

sub edi, dword ptr [code32_base]

mov ecx, TXT_HELLO_LEN

mov ah, 14

txt_print:

lodsb

stosw

loop txt_print

hang_me:

jmp hang_me

code32 ends

;------------------------------------------------------------

stack_seg segment para stack 'stack'

db STACK_SIZE dup(?)

stack_seg ends

end start16

10. Dos Protected Mode Interface = DPMI.

Potrafimy już uruchomić program w pmode startując z czystego DOS'a. Istnieje jednak prosty sposób na używanie pmode'a w programach dosowych uruchamianych pod Windows lub w formie tzw. klientów DPMI pod DOS-extenderami. W tych przypadkach sam proces przełączania jest bardzo uproszczony. Nie musimy się także martwic o przerwania - odpada wiec pisanie handlerów, budowanie tablicy IDT i przekierowywanie przerwań. Dodatkowo większość hostow DPMI posiada wbudowane funkcje alokowania pamięci, badania typu procesora etc.

DPMI jest zbiorem norm określających np. numery przerwań i funkcji oferowanych przez DOS-extendery. Standard ten powstał w czasach kiedy piwo było mocniejsze a kobiety ładniejsze. Jednym słowem wtedy gdy pojawił się procesor 386 a rożni koderzy tworzyli własne DOS-extendery (tj. programy przełączające się do pmode i pełniące usługi na rzecz programow-klientow). Te extendery były ze sobą niekompatybilne dlatego postanowiono żeby do poważnych zastosowań używać ściśle określonego zestawu funkcji i jednego przerwania - 31h. Teraz pisząc DOS-extender kompatybilny z DPMI sprawiamy, ze rożni klienci naszego 'hosta' będą działać poprawnie jeśli korzystają z 31h i np. używają funkcji AX = 100h do alokowania bloków pamięci podstawowej. Z kolei pisząc klienta mamy pewność, ze pod danymi numerami znajdują się właściwe funkcje.

Istnieje wiele powszechnie używanych i sprawdzonych extenderów np. cwsdpmi (używał go Quake I). Także w środowisku Windows zainstalowany jest rezydentnie serwer DPMI. Co zrobić żeby
uruchomić dosowy program-klient pracujący w trybie chronionym pod Winda? Podobnie jak w rozdziale 9 potrzebować będziemy 3 segmentowa plus jeden dodatkowy dla tak zwanego "prywatnego" użytku serwera:

dpmi_area segment para public use16

db DPMI_AREA_SIZE dup(?)

dpmi_area ends

Zaczynamy w segmencie code16 od sprawdzenia czy serwer DPMI jest obecny w pamięci. Odpowiada za to funkcja AX = 1687h multipleksowego przerwania naprzemiennego 2fh:

start16:

mov ax, code32

mov ds, ax

; sprawdzamy czy host DPMI jest zainstalowany w pamięci

mov ax, 1687h

int 2fh

or ax, ax

jz dpmi_installed

; błąd - nie ma hosta !

dpmi_installed:

; ok, możemy kontynuować

Bezpośrednio po tym sprawdzamy najmłodszy bit rejestru BL. Jeśli jest ustawiony - host obsługuje 32-bitowy pmode. W przeciwnym razie mamy do czynienia z 16-bitowym pmode ala 286.

test bl, 1

jnz mode32_ok

; błąd !

mode32_ok:

; 32-bit pmode obslugiwany

Teraz możemy zapamiętać adres procedury DPMI, pod która należy skoczyć celem "przełączenia" trybu (w cudzysłowie gdyż np. pod Windows cały czas jesteśmy w V86 i nie trzeba nic przełączać). Skąd wziąć ten adres? Zwróciła go w rejestrach ES:DI funkcja z początku programu. Zapamiętujemy do zmiennej dpmi_switch:

mov small word ptr dpmi_switch[0], di

mov small word ptr dpmi_switch[2], es

Następnie, tak jak w przykładzie z rozdziału 9, obliczamy adres fizyczny segmentu code32 i uzupełniamy deskryptory. UWAGA! Klienci hosta DPMI musza działać na poziomie uprzywilejowania DPL = 3, dlatego nasze deskryptory wymagają malej zmiany:

; kod 32-bit, limit 4GB, DPL = 3

desc_code32 db 0ffh, 0ffh, 0h, 0h, 0h, 11111010b, 11001111b, 0h

; dane 32-bit, limit 4GB, DPL = 3

desc_data32 db 0ffh, 0ffh, 0h, 0h, 0h, 11110010b, 11001111b, 0h

Jak łatwo zauważyć nie ma tutaj null-deskryptora gdyż używamy tablicy LDT (GDT posiada host DPMI, a jego klienci tylko tablice lokalne czyli LDT).

Pozostaje nam przypisanie segmentu "prywatnego" do ES i skok do procedury przełączającej:

mov ax, dpmi_area

mov es, ax

; ax = 1 tzn. chcemy 32-bitowego pmode

mov ax, 1

call small dword ptr dpmi_switch

jnc mode_switched

; błąd - nie można przełączyć trybu

mode_switched:

Jesteśmy już w pmode. Przed wykonaniem skoku do segmentu code32 musimy jeszcze poinformować hosta jakich deskryptorów używamy i otrzymać od niego odpowiednie selektory (nie możemy ustalać ich samemu jak w czystym DOS). Zrealizujemy to za pomocą funkcji przerwania 31h (to przerwanie jest dostępne wyłącznie w trybie chronionym):

; jesteśmy już w trybie chronionym, ale 16-bit

mov ax, ds

mov es, ax

; alokujemy deskryptor segmentu kodu

xor ax, ax

mov cx, 1

int 31h

mov small word ptr [sel_code32], ax

; ustawiamy deskryptor segmentu kodu

mov bx, ax

mov ax, 0ch

mov edi, offset desc_code32

int 31h

; alokujemy deskryptor segmentu danych

xor ax, ax

mov cx, 1

int 31h

mov small word ptr [sel_data32], ax

; ustawiamy deskryptor segmentu danych

mov bx, ax

mov ax, 0ch

mov edi, offset desc_data32

int 31h

Selektory, jako ze nie pochodzą od nas, musimy sobie zapamiętać. Do tego posłużyły nam zmienne:

sel_code32 dw ?

sel_data32 dw ?

Należy teraz przeskoczyć do segmentu 32-bitowego i tam kontynuować wykonywanie programu. Skaczemy podobnie jak poprzednio:

push small dword ptr sel_code32

push large offset start32

retf

Tak wygląda pełny kod opisanego właśnie programu inicjującego środowisko pracy klienta DPMI:

.386p

STACK_SIZE equ 1000h

DPMI_AREA_SIZE equ 1000h

;------------------------------------------------------------

; 16-bit code

;------------------------------------------------------------

code16 segment para public use16

assume cs:code16, ds:code32

start16:

mov eax, code32

mov ds, ax

; sprawdzamy czy host DPMI jest zainstalowany w pamięci

mov ax, 1687h

int 2fh

or ax, ax

jz dpmi_installed

mov dx, small offset dpmi_err1

jmp err_quit

dpmi_installed:

; sprawdzamy czy jest obslugiwany 32-bitowy pmode

test bl, 1

jnz mode32_ok

mov dx, small offset dpmi_err2

jmp err_quit

mode32_ok:

; zapamiętujemy adres procedury, która przełączy nas

; w pmode

mov small word ptr dpmi_switch[0], di

mov small word ptr dpmi_switch[2], es

; znajdujemy adres fizyczny segmentu code32

mov eax, code32

shl eax, 4

mov small code32_base, eax

; uzupełniamy deskryptory o obliczony adres

mov ebx, eax

shr ebx, 16

mov di, small offset desc_code32

mov word ptr [di + 2], ax

mov byte ptr [di + 4], bl

mov byte ptr [di + 7], bh

mov di, small offset desc_data32

mov word ptr [di + 2], ax

mov byte ptr [di + 4], bl

mov byte ptr [di + 7], bh

; przypisujemy ES do "prywatnego" segmentu hosta DPMI

mov ax, dpmi_area

mov es, ax

; "przelaczamy" się w pmode (jeśli odpalamy proga pod Windows

; to nie trzeba naprawde się przełączać - cały czas bylismy w V86).

mov ax, 1

call small dword ptr dpmi_switch

jnc mode_switched

mov dx, small offset dpmi_err3

jmp err_quit

mode_switched:

; jesteśmy już w trybie chronionym, ale 16-bit

mov ax, ds

mov es, ax

; alokujemy deskryptor segmentu kodu (poprzez funkcje DPMI)

xor ax, ax

mov cx, 1

int 31h

mov small sel_code32, ax

; ustawiamy deskryptor segmentu kodu (dopisujemy go do tablicy

; deskryptorów?)

mov bx, ax

mov ax, 0ch

mov edi, offset desc_code32

int 31h

; alokujemy deskryptor segmentu danych

xor ax, ax

mov cx, 1

int 31h

mov small sel_data32, ax

; ustawiamy deskryptor segmentu danych

mov bx, ax

mov ax, 0ch

mov edi, offset desc_data32

int 31h

; skaczemy do segmentu code32 - ładujemy CS selektorem segmentu

; kodu, a EIP przemieszczeniem etykiety start32

push small dword ptr sel_code32

push large offset start32

db 66h, 0cbh

err_quit:

mov ah, 09h

int 21h

mov ah, 4ch

int 21h

code16 ends

dpmi_area segment para public use16

db DPMI_AREA_SIZE dup(?)

dpmi_area ends

;------------------------------------------------------------

; 32-bit code

;------------------------------------------------------------

code32 segment para public use32

assume cs:code32, ds:code32

dpmi_err1 db 'DPMI host not installed!', 10, 13, '$'

dpmi_err2 db '32-bit PMODE unsupported!', 10, 13, '$'

dpmi_err3 db 'Unable to switch mode!', 10, 13, '$'

dpmi_switch dd ?

code32_base dd ?

; nasza tablica LDT

; fa = 11111010b code read / exec

; cf = 11001111b

; f2 = 11110010b data read / write

; 32-bitowy segment kodu, DPL = 3, limit = 4GB

desc_code32 db 0ffh, 0ffh, 0h, 0h, 0h, 0fah, 0cfh, 0h

; 32-bitowy segment danych, DPL = 3, limit = 4GB

desc_data32 db 0ffh, 0ffh, 0h, 0h, 0h, 0f2h, 0cfh, 0h

sel_code32 dw ?

sel_data32 dw ?

start32:

; ustawiamy selektory na deskryptor segmentu data32

mov ax, sel_data32

mov ds, ax

mov es, ax

mov ss, ax

; inicjujemy stos

mov esp, stack_seg

sub esp, large code32

shl esp, 4

add esp, STACK_SIZE-4

; skok do glownej procedury programu

call _main

; powracamy do rmode i wychodzimy do DOS'a

mov ah, 4ch

int 21h

code32 ends

;------------------------------------------------------------

stack_seg segment para stack 'stack'

db STACK_SIZE dup(?)

stack_seg ends

end start16

Jakkolwiek przejście do PM nawet w DPMI nie jest proste, to wyjście do RM sprowadza się do wywołania dobrze znanej funkcji DOSa - 4ch. Jest to możliwe ponieważ serwer DPMI przejął przerwanie 21h. Supportowana jest jednak tylko ta (i jedyna!) funkcja, która zamyka program i usuwa go z pamięci.

Spośród wielu funkcji przerwania 31h oferowanych przez hosta DPMI dość ważna jest AX = 501h alokująca blok pamięci. Rozmiar bloku w bajtach podajemy w rejestrach BX:CX. Oto napisana przeze mnie procedura, która upraszcza alokacje pamięci:

;------------------------------------------------------------

; in: eax = size of block in bytes

; out: eax = handle (0 if error occured)

; ebx = base address

;------------------------------------------------------------

alloc_mem proc

mov cx, ax

shr eax, 16

mov bx, ax

mov ax, 0501h

int 31h

jnc @@ok

xor eax, eax

ret

@@ok:

shl ebx, 16

mov bx, cx

sub ebx, code32_base

mov ax, si

shl eax, 16

mov ax, di

ret

endp

W EAX otrzymujemy uchwyt bloku, który należy zapamiętać do późniejszego zwolnienia pamięci. W EBX znajduje się logiczny adres bloku. Tak można zdealokować pamięć:

;------------------------------------------------------------

; in: eax = handle

; out: none

;------------------------------------------------------------

free_mem proc

mov di, ax

shr eax, 16

mov si, ax

mov ax, 0502h

int 31h

ret

endp

Pod DPMI możemy także wywoływać przerwania DOS i BIOS. Wprawdzie większość z nich posiada swoje odpowiedniki przepisane pod pmode, ale bezpieczniejsze wydaje mi się skorzystanie z funkcji AX = 300h. W BX podajemy numer przerwania, które chcemy wywołać, w EDI adres struktury danych "symulującej" rejestry procesora (dzięki niej będziemy przekazywać do przerwania argumenty). Struktura taka ma następującą postać:

dpmi_regs struc

_edi dd ?

_esi dd ?

_ebp dd ?

_none1 dd ?

_ebx dd ?

_edx dd ?

_ecx dd ?

_eax dd ?

_flags dw ?

_es dw ?

_ds dw ?

_fs dw ?

_gs dw ?

_none2 dw ?

_none3 dw ?

_sp dw ?

_ss dw ?

ends

Oto przykład pokazujący włączenie trybu graficznego 13h pod DPMI:

; czyszczenie struktury

mov edi, offset _regs

push edi

xor eax, eax

mov ecx, (size dpmi_regs) / 4

cld

rep stosd

pop edi

; wywolanie przerwania

mov eax, 0300h

mov ebx, 10h

mov dword ptr [edi._eax], 000013h

int 31h

_regs dpmi_regs ?

11. Outro

Mam nadzieje, ze
przybliżyłem Wam nieco zagadnienie trybu chronionego. Oczywiście wielu rzeczy nie byłem w stanie tutaj wyjaśnić. Oto zbiór linków, pod którymi możesz znaleźć więcej informacji na temat pmode:

Na zakończenie pragnę podziękować członkom grupy NAAG, do której de facto należę, za konstruktywna krytykę mojego tutoriala. Bez ich sugestii nigdy bym go nie ukończył.

3



Wyszukiwarka

Podobne podstrony:
!tutorials, tryb chroniony 3, Modele pamięci
!tutorials tryb chroniony 1
!tutorials, tryb chroniony 1
Tryb chroniony i rzeczywisty
!tutorials ~$yb chroniony 2
Tryb chroniony(1)
Tryb rzeczywisty i chroniony procesora
Tryb rzeczywisty, chroniony i wirtualny
Chronic Hepatitis
Zespół kanału łokciowego i nerw pachowy (tryb edytowalny)
2012 KU W5 tryb dzienny moodle tryb zgodnosci
(W7a Stale do kszta t na zimno cz I [tryb zgodno ci])
II rok tryb rozkazujacy
2 Sieci komputerowe 09 03 2013 [tryb zgodności]
bugzilla tutorial[1]
freeRadius AD tutorial
Alignmaster tutorial by PAV1007 Nieznany
free sap tutorial on goods reciept
Microsoft PowerPoint IP5 klasyfikacje tryb zgodnosci

więcej podobnych podstron