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).

Łukasz Fronczyk
[email protected]

5 komentarzy

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!