EMS - poza 1MB
ŁF
Dawno temu pojawiło się zapotrzebowanie na komputery z RAMem mieszczącym
więcej niż 1MB. Wtedy to jakiś geniusz wymyślił, że dostęp do niej będzie
się odbywać poprzez tryb chroniony. Jest to w sumie fajna sprawa, ale niestety
komplikuje wiele prostych w trybie rzeczywistym rzeczy (m.in. brak przerwań).
Tryb rzeczywisty, czyli to czym dysponujemy w Turbo Pascalu (Uwaga: w Borland
Pascalu można kompilować programy do trybu rzeczywistego, chronionego i pod
Windowsa!), został brutalnie zamordowany. Bo na co komu coś takiego, co nie
może obsłużyć pamięci ponad 1MB w czasach, kiedy komputery mają jej tysiące
razy więcej?
Ale my reanimujemy tryb rzeczywisty.
Po pierwsze: bez żadnych tricków możesz dossać się do pierwszych 64 kB
ponad 1MB - tzw HMA; jest to po prostu adres $FFFF:$0010 i wyżej;
jak widzisz $FFFF0+$10 daje $100000, czyli adres pierwszego megabajta
(swoją drogą powinien być to miły, nie sygnalizowany błąd przekroczenia zakresu
dodawania w procesorze 386 - tak jest na 286, ale kto tam pamięta te starocie).
Jeśli program emm386 nie został uruchomiony z opcją RAM, to HMA jest wolny
i możesz po nim pisać ile wlezie. Zawsze to o 64kB (minus 16 bajtów) więcej.
Jednak istnieje coś dużo lepszego: dorwiemy się do pewnej furtki "przypadkiem"
pozostawionej przez Intela i namiętnie wykorzystywanej przez różnego rodzaju
menedżery pamięci rozszerzonej (chodzi o to, że można uruchomić niejako w tle
program w trybie chronionym, wrócić do trybu rzeczywistego, a za pośrednictwem
przerwań komunikować się z tym programem z trybu chronionego). Za pośrednictwem
sterownika emm386 (standardowo ładowanego na kompach z Windozą 9x/Me), który
udostępnia nam coś, co się nazywa EMS, wgryziemy się w tyle pamięci, ile jest
na komputerze, a przy odrobinie szczęścia zrobimy obiekt piszący po EMS jak
po pliku, oraz coś w rodzaju menedżera pamięci trybu chronionego - tzn. będziemy
korzystać nie tylko z EMSu, ale i dysku.
Nie będę wnikać w teorię dotyczącą działania pamięci rozszerzonej (EMS), jeśli ktoś jest chętny to mój adres mejlowy jest na dole artykułu. Zaczniemy od poznania działania przerwania 67h, bo to ono nam posłuży do komunikacji z EMM.
Co potrzebujemy? Najpierw sprawdzamy, czy aby emm386 jest uruchomiony, posługując się odpowiednią funkcją DOSu (int 21h).
function DetectEMS : boolean; assembler; const EMM : array[1..8] of char = 'EMMXXXX0'; asm mov ax,3567h int 21h mov di,0Ah lea si,EMM mov cx,8 repz cmpsb jnz @@1 mov ax,1 jmp @@2 @@1: mov ax,0 @@2: end;
Jeśli ta funkcja zwróci wartość true, to pędzimy dalej: sprawdzamy, czy
aby jest trochę wolnej pamięci EMS:
function GetEMSFreePages : word; assembler;
asm
mov ah,42h
int 67h
mov AX,BX
end;
Funkcja zwróci ilość wolnych stron ("ramek"). Jedna ramka to 16kB pamięci,
więc GetEMSFreePages161024 zwróci ilość wolnej pamięci EMS. Przy okazji
możemy sprawdzić, ile pamięci EMS jest w ogóle:
function GetEMSAllPages : word; assembler;
asm
mov ah,42h
int 67h
mov AX,DX
end;
Funkcja ta zwróci ilość wszystkich stron pamięci, każda po 16kB.
Skoro już wiemy, że EMM jest zainstalowany, i że mamy mnóstwo wolnej
pamięci (tzn. więcej niż zero stron), to już jesteśmy gotowi do
upychania danych w pamięć ponad pierwszym megabajtem. jeszcze tylko mała
uwaga: środowisko tpx.exe bez zastanowienia połyka nam całą pamięć EMS,
i dla nas nie zostaje nic. Co na to poradzić? Albo posługiwać się kompilatorem
tpc.exe czy turbo.exe (co jest bardzo niewygodne), albo bawić się pod Windozą
(uruchamiamy tpx w oknie DOSowym i jest tak jak wcześniej: tpx zabiera całą
pamięć, ale... ale Windows doczarowuje drugie tyle na potrzeby naszego programu :)).
Możemy też w autoexecu ustawić wartość odpowiedniej zmiennej poleceniem set:
niestety nie pamiętam nazwy tej zmiennej (czy ktoś mógłby?...).
Teraz małe słowo o organizacji pamięci EMS: w wyniku alokacji pamięci dostajemy
nie wskaźnik ale uchwyt do naszego bloku pamięci. mając go korzystamy z kilku
funkcji, które wrzucą nasze dane do pamięci EMS bądź je stamtąd pobiorą.
Alokować możemy dowolnie dużą ilość pamięci, jednak dane możemy kopiować
i pobierać tylko i wyłącznie w maksymalnie szesnastokilobajtowych porcjach.
I koniecznie trzeba pamiętać o zwalnianiu pamięci, kiedy już nie jest potrzebna,
bo może się po prostu skończyć.
To popatrzmy na funkcje do obsługi pamięci EMS:
function GetEMSmem(pages : word) : word; assembler;
asm
mov ax,4300h
mov bx,pages
int 67h
cmp ah,0
jne @@1
mov ax,dx
jmp @@2
@@1:
xor ax,ax
@@2:
end;
Alokuje blok pamięci EMS, a następnie zwraca uchwyt do niej i coś
w rodzaju wskaźnika, (przeważnie równego wartości ptr($0E0000)),
co umożliwi nie do końca prawidłowe korzystanie z naszej pamięci :).
Jeśli uchwyt (tzn. wartość zwracana przez funkcję) jest równy zeru,
to znaczy że coś jest nie w porządku.
function FreeEMSmem(handle : word) : word; assembler;
asm
mov ax,4500h
mov dx,handle
int 67h
end;
Ta procedura zwolni blok pamięci EMS o podanym uchwycie (spróbujcie
"na sztywno" uruchomić FreeEMSmem(0) i FreeEMSmem(1) - oczywiście po
backupie całego dysku... - podane uchwyty to pamięć bufora dysku - smartdrv).
Teraz pierwsza metoda na przyssanie się do danych - bezpośrednio z pamięci
DOS: wywołujemy procedurkę
procedure ResEMSmem(handle : word); assembler;
asm
mov ah,48h
mov dx,handle
int 67h
end;
która wpisze pierwsza (i tylko pierwszą) ramkę naszej pamięci do
okna znajdującego się pod adresem, który obczaimy sobie następną
funkcją:
function GetEMSSegment : word; assembler;
asm
mov AH,41h
int 67h
mov ax,bx
end;
Wtedy modlimy się, żeby
żaden inny program w tym czasie nie korzystał z pamięci EMS, i
bezczelnie traktujemy pierwsze 16kB pod adresem GetEMSSegment:0 jako naszą
pamięć: co tam zmienimy, to zostanie zmienione również w pamięci EMS.
Ale jeśli jakiś inny program w międzyczasie też korzystał z pamięci EMS,
to mamy problem, bo nie zmienimy naszych danych, tylko tego programu.
Czyli nieźle napsujemy, bo tym "innym" programem jest najczęściej bufor
dysku :) Dlatego polecam poniższą metodę.
function MoveToEMS(var buf;handle,page,offs : word;size : word) : byte; assembler; var emms : TEMMRec; asm xor ax,ax cmp size,0 je @@1 cmp handle,0 je @@1 mov ax,size mov emms.size.word[0],ax mov emms.size.word[2],0 mov emms._from,0 mov emms.srchandle,0 les di,buf mov emms.source.word[2],es mov emms.source.word[0],di mov emms._to,1 mov ax,handle mov emms.dsthandle,ax mov ax,offs mov emms.dest.word[0],ax mov ax,page mov emms.dest.word[2],ax push ds lea si,emms mov ax,ss mov ds,ax mov ax,5700h int 67h pop ds mov al,ah @@1: end; function MoveFromEMS(handle,page,offs : word;var buf;size : word) : byte; assembler; var emms : TEMMRec; asm xor ax,ax cmp size,0 je @@1 cmp handle,0 je @@1 mov ax,size mov emms.size.word[0],ax mov emms.size.word[2],0 mov emms._from,1 mov ax,handle mov emms.srchandle,ax mov ax,offs mov emms.source.word[0],ax mov ax,page mov emms.source.word[2],ax { logical page } mov emms._to,0 mov emms.dsthandle,0 les di,buf mov emms.dest.word[2],es mov emms.dest.word[0],di push ds lea si,emms mov ax,ss mov ds,ax mov ax,5700h int 67h pop ds mov al,ah @@1: end;
MoveToEMS(var p; h,offs,count : word) - z pamięci konwencjonalnej (DOS)
kopiujemy dane do pamięci EMS. p to bufor z danymi, h - uchwyt bloku EMS,
offs - jeśli nie chcemy upychać danych od zerowego bajta (coś jak seek,
np.: jeśli chcemy upchnąć do pamięci EMS dwie kopie bufora, robimy tak:
MoveFromEMS(bufor,f,0,SizeOf(bufor)); MoveCE(bufor,f,SizeOf(bufor),SizeOf(bufor))
i już). count - liczba bajtów do skopiowania.
Analogicznie działa MoveEC(h,offs : word;var p;count : word) - z pamięci
EMS do pamięci DOS. h - uchwyt bloku EMS, offs - przesunięcie względem
początka danych, p - bufor, cound - ile bajtów skopiować.
Proste?
Przydać się może jeszcze jedna procedurka - analogiczna do truncate:
function ReallocateEmsPages(handle,newsize : word) : byte; assembler;
asm
mov AH,51h
mov DX,handle
mov BX,newsize
int 67h
mov al,ah
end;
I to właściwie wszystko, co jest potrzebne do efektywnego korzystania
z pamięci EMS.
Można skorzystać z jeszcze jednej, rzadko używanej funkcji do nazwania
badź odczytania nazwy przypisanej do uchwytu (tak! można nazywać
nasz blok pamięci; konwencja nazewnicza jak w plikach, ale bez
rozszerzenia).
type string8 = string[8]; function GetEMSHandleName(h : word) : string8; var s : string8; begin asm mov byte ptr s[0],8 lea di,s[1] mov ax,ss mov es,ax mov ax,5300h mov dx,[h] int 67h cmp ax,0 jz @@1 mov byte ptr s[1],0 @@1: end; if pos(#0,s) > 0 then s[0] := char(pos(#0,s)-1); GetEMSHandleName := s; end; procedure SetEMSHandleName(h : word;name : string8); assembler; asm push ds push si lea si,name[1] mov ax,ss mov ds,ax mov ax,5301h mov dx,[h] int 67h pop si pop ds end;
Ładne, dość łatwe i przyjemne :)
Teraz coś ambitniejszego: obiekt pozwalający pisać i czytać z pamięci EMS
jak z pliku (analogiczny obiekt znajduje się w pakiecie Turbo Vision).
unit ems_obj; interface uses EMS; const errSeekError = $000F; errEmsOK = $0100; errNoEmsMem = $0101; errAllocEmsMem = $0102; errEmsSize = $0103; {reszta błędów: $0100+ 00h successful 80h internal error 81h hardware malfunction 83h invalid handle 84h undefined function requested by application 85h no more handles available 86h error in save or restore of mapping context 87h insufficient memory pages in system 88h insufficient memory pages available 89h zero pages requested 8Ah invalid logical page number encountered 8Bh invalid physical page number encountered 8Ch page-mapping hardware state save area is full 8Dh save of mapping context failed 8Eh restore of mapping context failed 8Fh undefined subfunction 90h undefined attribute type 91h feature not supported 92h successful, but a portion of the source region has been overwritten 93h length of source or destination region exceeds length of region allocated to either source or destination handle 94h conventional and expanded memory regions overlap 95h offset within logical page exceeds size of logical page 96h region length exceeds 1M 97h source and destination EMS regions have same handle and overlap 98h memory source or destination type undefined 9Ah specified alternate map register or DMA register set not supported 9Bh all alternate map register or DMA register sets currently allocated 9Ch alternate map register or DMA register sets not supported 9Dh undefined or unallocated alternate map register or DMA register set 9Eh dedicated DMA channels not supported 9Fh specified dedicated DMA channel not supported A0h no such handle name A1h a handle found had no name, or duplicate handle name A2h attempted to wrap around 1M conventional address space A3h source array corrupted A4h operating system denied access } EmsPageSize = $4000; type THandle = word; PEmsObject = ^TEmsObject; TEmsObject = object Status : Integer; Error : Integer; constructor Init(count : longint); {Inicjalizacja pamięci EMS, nadanie początkowego rozmiaru} destructor Done; virtual; {Zwolnienie pamięci (koniecznie trzeba o tym pamiętać, bo po kilku uruchomieniach programu może zabraknąć pamięci EMS)} procedure Seek(poz : longint); virtual; {Ustawianie miejsca, od którego nastąpi odczyt/zapis} procedure Truncate; virtual; {Zwolnienie części pamięci (obcięcie)} function GetPos : longint; virtual; {Pozycja ustawiana poprzez seek/read/write} function GetSize : longint; virtual; {Rozmiar zaalokowanej pamięci EMS} procedure Read(var buf;var count : word); virtual; {odczyt z EMS do zwykłej} procedure Write(var buf;count : word); virtual; {zapis ze zwykłej do EMS} private handle : THandle; pos : longint; size : longint; procedure ChangeSize(newsize : longint); end; implementation constructor TEmsObject.Init(count : longint); var i : longint; begin if count = 0 then begin Error := errEmsSize; exit end; Status := 0; Error := 0; handle := 0; i := count div EmsPageSize; if count mod EmsPageSize > 0 then inc(i); pos := 0; size := 0; if i <= GetEmsFreePages then begin handle := GetEmsMem(i); if Handle = 0 then error := errAllocEmsMem else size := count; end else error := errNoEmsMem; end; destructor TEmsObject.Done; begin Error := 0; if handle > 0 then FreeEmsMem(handle); handle := 0; end; procedure TEmsObject.Seek(poz : longint); begin Error := 0; if poz > size then begin pos := size - 1; Error := errSeekError; end else pos := poz; end; function TEmsObject.GetPos : longint; begin GetPos := pos end; function TEmsObject.GetSize : longint; begin GetSize := size end; procedure TEmsObject.Read(var buf;var count : word); var i : word; begin if pos + count > size then count := size - pos; Error := MoveFromEms(handle,pos div EmsPageSize,pos mod EmsPageSize,buf,count)+errEmsOK; if Error = errEmsOK then inc(pos,count) end; procedure TEmsObject.Write(var buf;count : word); begin if pos + count > size then ChangeSize(pos+count); Error := MoveToEms(buf,handle,pos div EmsPageSize,pos mod EmsPageSize,count)+errEmsOK; if Error = errEmsOK then inc(pos,count) end; procedure TEmsObject.ChangeSize(newsize : longint); var i,j : longint; begin if newsize = 0 then newsize := 1; i := newsize div EmsPageSize; if newsize mod EmsPageSize > 0 then inc(i); j := size div EmsPageSize; if size mod EmsPageSize > 0 then inc(j); if j = i then begin size := newsize; exit end; if i <= GetEmsFreePages+j then begin Error := ReallocateEmsPages(handle,i) + errEmsOK; if Error = errEmsOK then size := newsize end else error := errNoEmsMem; end; procedure TEmsObject.Truncate; begin if pos = 0 then ChangeSize(1) else ChangeSize(pos); end; end.
Korzystanie jest na tyle proste i podobne do pisania po pliku, że nie będę
tego opisywać. Czekam na chętego, który napisze menedżera obsługującego
przydzielanie i zarządzanie pamięciami DOS+EMS+swap...
Miłego wykraczania poza pierwszy megabajt.
Aha - w dziale TP->źródła znajdują się źródła bibliotek ems
i ems_obj, żeby się nikt aby nie przemęczył. I nie biorę odpowiedzialności za ewentualne
zamieszanie w Twoim komputerze wywołane eksperymentami z pamięcią EMS (u mnie wszystko działa OK).
[email protected]
Kompletnie OLEWAJĄCE podejście autora do wyjaśnienia zagadnienia. Całkowicie niestosowny styl formułowania zdań. Drogi autorze prosze się nie popisywać i...
"doczarować" literaturę.
MIKMAS, poczytaj o XMS. Nawet dość szybka sprawa, a możesz sobie zadeklarować tablice tak wielka na ile Ci starczy pamięci;) nawet 4GB :D Chciałem napisać o tym artykuł, ale moduł z którego korzystam nie jest mojego autorstwa a nie chce robić plagiatów;) Tak czy siak fajna sprawa. Tylko też (nie wiem dlaczego..) jest tak, że kiedy czytasz coś z tej pamięci, a wystąpi przerwanie zrobione chociażby przez Ciebie (w moim wypadku tak było...), które także czyta z XMS to zaczynają się szopki dziać:/
A co, jak będę musiał bezpośrednio ciągnąć z pamięci wyższej? Kopiowanie bloków z/do pamięci wyższej nie jest problemem. W każdej książce o kompach można znaleść przerwania za to odpowiedzialne. W przypadku gier przecież potrzeba olbrzymich często zmiennych z tak szybkim dostępem jak w przypadku pamięci konwencjonalnej
Tryb chroniony nie może być uruchamiany razem z trybem rzeczywistym gwoli ścisłości. Masz trzy wyjścia - chroniony, v86 albo tryb nierzeczywisty, a raczej jedno bo musisz napisać managera do v86 takiego jak Windows na przykład. Przełączanie się do trybu chronionego za każdym razem kiedy chcesz pisać w pamięci to już jakieś wyjście.Jeśli mówisz jednak o 'furtce' to zapewne chodzi ci o tryb nierzeczywisty który działa tak:
Ustawiasz sobie GDT(w chronionym możesz[ba, musisz] sam definiować sobie segmenty - skrót od Global Descriptor Table, Globalna Tablica Deskryptorów - deskryptor to coś co definiuje segment) taki fajny od zera do 4 GB albo od końca 1 MBtu do 4 GB, potem hop do chronionego(tu możesz sobie stos ustawić gdzieś na końcu pamięci, bardzo przydatne bo nie używasz pamięci dostępnej, następnie przełazisz do trybu rzeczywistego i dostajesz rejestr(y) z selektorem segmentu 4 GBajtowego. UWAGA! Jakikolwiek zapis do tego selektora(nie mylić z segmentem, który jest częścią pamięci) rozwala dostęp do segmentu. Dlatego że pisać możesz tylko do części SELEKTORA(coś co określa do jakiego segmentu chcesz się dossać), a w trybie chronionym gdy to robisz procek zapisuje do części ukrytej dane z GDT., a w rzeczywistym TYLKO do części selektora 16bitowej(czyli taki WYSIWYG). Tak dosysają się do pamięci menagery przeróżne. Taki menedżer żeby zostawić ci wszystkie rejestry wykonuje tą operację(GDT+2 skoki trybów) tylko gdy sam potrzebuje dostępu do pamięci powyżej 1 MB albo gdy ty mu każesz. Jeszcze raz - nie może jej wrzucić na stos bo na stos nie wlatuje część ukryta determinująca superwłaściwości naszego segmentu 4 GBajtowego, czy w ogóle nie może części ukrytej gdzieś zapisać i potem przepisać do procka.
Nie wiem jak to z tym stosem, ale jak pisałem mojego OS'a to działało(OS był RealMode - nie śmiać się bo miał kupę bajerów)
Zainteresowanych zapraszam na http://www.nondot.org/sabre/os/articles w dziale Protected Mode, gdzieś na dole jest opis Unreali/VOODOO/Flat mode czyli trybu nierzeczywistego. Implementacja w asemblerze.
Hmm... Artykuł OK (i moduł nawet dobrze działa!). Denerwuje mnie tylko trochę OLEWAJĄCE podejscie autora do całego problemu. EMS to sprawa moim zdaniem ciekawa i dla ludzi którzy nieco poważniej "gnębią" TP7 jakies POWAŻNIEJSZE rozszerzenie tej wiedzy jest NIEZBĘDNE! Czekam... POZDROWIENIA!