EMS - poza 1MB
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 GetEMSFreePages*16*1024 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).



"doczarować" literaturę.
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.