Programowanie w języku Pascal

EMS - poza 1MB

  • 5 komentarzy
  • 742 odsłony
  • Oceń ten tekst jako pierwszy
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[..] of char = ;
asmend;

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;
asmend;

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;
asmend;

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;
asmend;

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;
asmend;

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;
asmend;

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;
asmend;

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;
asmend;



function MoveFromEMS(handle,page,offs : word;var buf;size : word) : byte; assembler;
var
  emms : TEMMRec;
asmend;

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;
asmend;

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[];

function GetEMSHandleName(h : word) : string8;
var
  s : string8;
begin
asmend;

if pos(,s) >  then s[] := char(pos(,s)-);
GetEMSHandleName := s;
end;

procedure SetEMSHandleName(h : word;name : string8); assembler;
asmend;

Ł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    = ;

  errEmsOK        = ;
  errNoEmsMem     = ;
  errAllocEmsMem  = ;
  errEmsSize      = ;


  EmsPageSize     = ;

type
  THandle = word;


  PEmsObject = ^TEmsObject;
  TEmsObject = object
    Status    : Integer;
    Error     : Integer;
    constructor Init(count : longint);
    
    destructor  Done; virtual;
    
    procedure   Seek(poz : longint); virtual;
    
    procedure   Truncate; virtual;
    
    function    GetPos : longint; virtual;
    
    function    GetSize : longint; virtual;
    
    procedure   Read(var buf;var count : word); virtual;
    
    procedure   Write(var buf;count : word); virtual;
    
  private
    handle    : THandle;
    pos       : longint;
    size      : longint;
    procedure   ChangeSize(newsize : longint);
  end;



implementation

constructor TEmsObject.Init(count : longint);
var
  i : longint;
begin
  if count =  then
  begin
    Error := errEmsSize;
    exit
  end;
  Status := ;
  Error := ;
  handle := ;
  i := count div EmsPageSize;
  if count mod EmsPageSize >  then inc(i);

  pos := ;
  size := ;

  if i <= GetEmsFreePages then
  begin
    handle := GetEmsMem(i);
    if Handle =  then error := errAllocEmsMem else size := count;
  end else error := errNoEmsMem;
end;


destructor TEmsObject.Done;
begin
  Error := ;
  if handle >  then FreeEmsMem(handle);
  handle := ;
end;


procedure TEmsObject.Seek(poz : longint);
begin
  Error := ;
  if poz > size then
  begin
    pos := size - ;
    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 =  then newsize := ;

  i := newsize div EmsPageSize; if newsize mod EmsPageSize >  then inc(i);
  j := size div EmsPageSize; if size mod EmsPageSize >  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 =  then ChangeSize() 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).

5 komentarzy

raq240 2009-12-01 11:52

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

chemik143 2008-02-06 13:13

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ć:/

mikmas 2007-02-20 09:22

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

darktemplar 2005-09-03 09:28

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.

Marcel 2003-06-16 15:31

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!