int21h

DOS a chráněný režim - 1. díl

Úvod

Pokud přecházíte na Freepascal, tak jste si nemohli nevšimnout, jak se vývojový tým na svých stránkách všude chlubí že FP je 32bitový (eventuálně 64bitový) překladač. Pojmy 32bitový program a 32bitový překladač jsou velice vágní výrazy a obecně vzato to může znamenat všechno možné.
Nicméně v případě Freepascalu to znamená, že generuje kód pro 32bitový chráněný režim procesoru (32bitový protekt). Pro nás programátory to znamená, že máme přístup do celé paměti počítače (ne jenom do základních 640KB) a hlavně, že můžeme deklarovat proměnné větší než 64KB.
Hlavně druhý bod je obrovské plus.
Pokud bychom se tu bavili o jakékoli jiné platformě než DOS, mohl bych už na tomto místě článek ukončit. Jenže se tu bavíme o DOSu a v něm nastává několik vážných problémů.
Uvědomme si, že DOS pracuje v reálném módu a tudíž nevidí protektové programy. Nejen, že nemá funkce, jak takový program vůbec spustit, ale taková aplikace navíc nemůže volat žádnou službu DOSu, žádný přídavný ovladač (ty jsou taky pro reál) a taky BIOS běží z velké většiny v reálu. Protektový program na oplátku za normálních okolností nesahá na paměť pod 1MB (Chápe ji jako rezervovanou paměť).
Co tedy s tím?
Musíme si pomoci malým přemosťovacím prográmkem, který načte do paměti protektovou aplikaci, připraví jí "hnízdo" ve vyšší paměti, přesune ji tam a předá jí řízení. Kromě toho si hlídá všechna volání DOSu a BIOSu z protektové aplikace. Po celou dobu běhu programu tak tvoří jakousi pupeční šňůru mezi aplikací pro chráněný mód a operačním systému pro reálný mód.
Patrně jste u DOSových her někdy viděli soubor DOS4GW.EXE nebo RTM.EXE či další. To jsou právě ony DOSové extendery, ony pupeční šňůry.
Freepascal používá extender CWSDPMI.EXE. Ten funguje trošičku jinak než výše zmíněné, ale jeho účel je stejný.
Na tomto místě je nutné říct, že pokud nechcete něco extra(třeba přímý přístup do videopaměti), tak si vůbec nevšimnete, že tu probíhají nějaké složitosti. V ideálním případě ve Freepascalu přeložíte zdroják určený pro Turbo pascal a překladač se o všechno postará. Důležité je ale jedno:
Váš zkompilovaný program bude v některých případech potřebovat právě CWSDPMI.
CWSDPMI může být buďto ve stejném adresáři jako exáč vaší aplikace anebo v adresáři zahrnutém v proměnné prostředí PATH. Nebuďte jako pan Gates a nespoléhejte na to, že tento soubor uživatel má (tím méně ve složce pokryté proměnnou PATH) a distribujte ho společně s aplikací hned vedle exáče.
Zdůraznil jsem sousloví v některých případech. To máte tak: DOS4GW nebo RTM jsou s protektovou aplikací spojené "napevno". Pokud tyto soubory protektová aplikace nenajde, nemůže se spustit. Nikdy. Aplikace volající CWSDPMI je na tom trochu jinak. CWSDPMI by se neměl nazývat extenderem, ale DPMI serverem. Pokud už nějaký jiný DPMI server běží, tak se CWSDPMI nespustí a aplikace si poradí sama.
Nabízí se otázka "Co je to DPMI?"
DPMI znamená DOS Protected Mode Interface a je to rozhraní, které umožňuje všechny potřebné funkce "pupeční šňůry". DPMI NENÍ součást služeb DOSu, a proto celé tohle povídání o CWSDPMI. Nicméně vězte, že DPMI je součástí služeb Windows. Tento standard se poprvé objevil we Windows 3.0 (někdo tvrdí 3.1) kde nastala přesně stejná situace jako s naší hypotetickou protektovou aplikací.
Proto každá aplikace přeložená pro CWSDPMI se napřed koukne, jestli jsou k dispozici služby DPMI. Když jo, tak se program zahnízdí do vyšší paměti a napojí se na pupeční šňůru. Když ne, tak napřed zavede CWSDPMI, s pak udělá to samé. Při ukončení ještě CWSDPMI odstraní z paměti. Je na místě připomenout, že ovladač EMM386 není server DPMI. (S jedinou výjimkou, a to je EMM386 ze systému DR-DOS, který DPMI poskytuje.) DPMI server si můžeme zavést i když Windows nepoužíváme. Můžete ho zavolat jako obyčejný rezidentní program. Taky můžete napsat na příkazovou řádku
cwsdpmi -r
a DPMI zůstane v paměti. Nepříjemné ale je, že DPMI servery jsou dost velké. CWSDPMI zabírá asi 50KB konvenční paměti, což je hodně. Jiné DPMI servery jsou na tom podobně, až na jednu výjimku - až na HDPMI32. V paměti zabírá pouhých 18KB - malý programátorský zázrak.
Pro úplnost zbývá poznamenat, že jsou dvě verze specifikace - DPMI v0.9 a v1.0
Hádejte, kterou z nich podporují windows. Patrně hádáte správně, tu starší :-(
Jelikož v paměti nemůžou být současně dva DPMI servery, tak nastává situace, že ačkoli některé DOSové servery podporují funkce v1.0 (nebo alespoň část z nich), tak je nemůžete dost dobře použít, protože v okamžiku, až váš program spustí někdo ve windows, tak služby verze 1.0 prostě nebudou a váš program se při jejich volání zhroutí. CWSDPMI nicméně umí taky jenom v0.9, takže Freepascal tyto problémy dělat nebude. (A tudíž nevím, proč to sem vlastně píšu...)

CWSDPMI má ale v sobě patrně chybu přenosu DMA. V praxi to znamená, že programy ve FP, které jsou ozvučené, fungují dobře jenom do doby, kdy je spouštíte ve windows. Když je spustíte v čistém DOSu, tak neuslyšíte zvuk a v některých případech může dokonce zamrznout počítač. A je trapné, když DOSový program v DOSu nefunguje, že. Možná, že vina není vyloženě v CWSDPMI, možná je více ve sračkoidních ovladačích zvukových karet pro DOS, ale výsledek je týž.
Spíše ale přece jen v CWSDPMI, protože když si rezidentně zavedete nějaký jiný DPMI server (Mám dobré zkušenosti s HDPMI32), tak funguje zvuk normálně.
A když nezabere ani to, tak zkuste naroubovat váš program na extender WDOSX. Ale i přesto spouštejte takto patchnutý program s přítomným DDPMI32.)

Volání přerušení pomocí DPMI

Pojďme se už ale konkrétně podívat na služby DPMI. Všechny DPMI služby jsou volány pomocí přerušení 31h.
Za prvé jsou tu služby okolo alokace paměti nad 1MB (DOS samotný to neumí) a za druhé služby "pupeční šňůry".
DPMI se aktivuje při každé instukci INT. Tedy po každém INTu se DPMI podívá, jestli je volané přerušení pro chráněný mód nebo pro reálný. Pokud pro chráněný, tak se normálně vykoná.
Pokud pro reálný, tak přepne procesor do reálného módu, provede volání přerušení INT a vrátí se zpátky do protektu. Zde ale nastává problém s návratovými hodnotami. Jestliže voláme třeba funkci INT 21h/AH=30h - Zjisti verzi DOSu, tak problém ještě nevzniká. Verze DOSu je uložena v registru AX a hodnoty registrů se při přechodu do protektu zachovají.
Pokud ale voláte třeba funkci INT 21h/AH=47h - Zjisti aktuální adresář, tak výstupní hodnota je uložena nikoliv v registru, ale v paměti na adrese popsané [DS:SI]
Jenže DOS nevidí na adresy nad 1MB a protektový program za normálních okolností nevidí pod 1MB (nejjednodušší cestou jak pracovat s pamětí pod 1MB je použití pseodopole Mem[] příp. MemW[] a MemL[]). Pomocí obyčejného volání přerušení by se navzájem OS s aplikací nedomluvili!
Jak tedy DPMI tyto problémy řeší?
V prvé řadě umí emulovat instrukci INT. Samo o sobě to sice nestačí k řešení výše uvedeného problému, ale je to první krok. Takhle by třeba vypadalo přečtení verze DOSu pomocí emulace.
type registry = record
         case integer of
          1: { 32bit } (EDI, ESI, EBP, Res, EBX, EDX, ECX, EAX: longint;
                        Flags, ES, DS, FS, GS, IP, CS, SP, SS: word);
          2: { 16bit } (DI, DI2, SI, SI2, BP, BP2, R1, R2: word;
                        BX, BX2, DX, DX2, CX, CX2, AX, AX2: word);
          3: { 8bit }  (stuff: array[1..4] of longint;
                        BL, BH, BL2, BH2, DL, DH, DL2, DH2,
                        CL, CH, CL2, CH2, AL, AH, AL2, AH2: byte);
end;


Procedure Volej_Preruseni(i:byte;var r:registry);
begin
r.sp:=0;
r.ss:=0;
r.res:=0;      { potencialne nebezpecne veci vynuluju }
asm
push edi       { Windows XP chteji uschovu vsech menenych registru pred volanim DPMI }
push ebx
push fs
   movzx ebx,i
   xor ecx,ecx
   mov edi,r
   {o ES se starat nemusime, vime, ze ES=DS a DPMI spravce se o dalsi postara sam}
   mov eax,0300h
   int 31h          { sluzby DPMI jsou na preruseni 31h }
pop fs
pop ebx
pop edi
end;
end;


var r:registry;
begin
r.ax:=$3000;            { Funkce INT21h/AH=30h }
Volej_preruseni($21,r);
writeln(r.al,':',r.ah);
readln;
end.  

Aby bylo jasno, tohle je jenom příklad. Verzi DOSu samozřejmě můžeme zjistit pomocí vestavěné funkce GetOSversion. Nebo si alespoň pomůžeme jednotkou GO32, která má na starosti právě práci s DPMI rozhraním. Program by se nám takhle scvrknul:
uses Go32;
var r:tRealRegs;
begin
r.ax:=$3000;            { Funkce INT21h/AH=30h }
RealIntr($21,r);
writeln(r.al,':',r.ah);
readln;
end.  

Teď, když už víme o jednotce Go32, se konečně pustíme do funkce GetDir
uses Go32;
var r:tRealRegs;
    p:pchar;
    vysledek:string;


begin
r.ax:=$4700;            { Funkce INT21h/AH=47h }
r.edx:=3;               { jednotka C: }
r.ds:=tb_segment;                { segment Transferoveho bloku }
r.esi:=tb_offset;                { offset Transferoveho bloku }
RealIntr($21,r);


p:=@vysledek;
CopyFromDOS(vysledek,251);  { Precte 251bajtu z Transferoveho bloku }
vysledek:='C:'+p+'';
writeln(vysledek);
readln;
end.  
Je jasné, že tady už nejde o přenos dat jenom skrze registry, ale prostřednictvím bufferu. Nemůžeme si nechat poslat výsledek přímo do proměnné vysledek, protože ta se nachází nad 1MB a DOS ji nevidí.
Musíme použít buffer ležící v konvenční paměti. Jednotka Go32 pro tyto účely poskytuje Transferový blok. Ten je sice skrytý, ale pracuje se s ním pomocí procedur
CopyToDOS a CopyFromDOS Jeho adresu zjistíme funkcemi tb_segment a tb_offset
Je velký 16KB. Pokud používáte nějakého hodně exotického DPMI správce, tak může být velikost jiná, ale všechny DPMI servery co znám, dávají právě 16KB. Nicméně, pokud byste měli pochybnosti, tak velikost zjistíte funkcí tb_size.
Je tristní, že tento jednoduchý způsob práce s konvenční je nejhůře zdokumentován. Častěji se proto setkáte s jiným postupem, kdy si vytvoříme vlastní buffer v konvenční paměti.
uses Go32;
var r:tRealRegs;
    p:pchar;
    vysledek:string;


    konv_buffer:longint; { Vyssich 16 bitu vystupuje jako realmodovy segment }
                         { nizsich 16b jako protektovy selektor }
                         { Realmodove rutiny pracuji se segmenty, a protektove se selektory }


begin
konv_buffer:=Global_DOS_Alloc(256);  { V konvencni pameti jsem alokoval 256 bajtu }


r.ax:=$4700;            { Funkce INT21h/AH=47h }
r.edx:=3;               { jednotka C: }
r.esi:=0;               { Pokud alokujeme pamet pres Global_DOS_Alloc, tak je offset vzdycky 0 }
r.ds:=(word(konv_buffer shr 16));   {predame pouze VYSSICH 16 bitu }
RealIntr($21,r);


p:=@vysledek;
DOSmemGet(word(konv_buffer shr 16),0,vysledek,251);  {Precte 251bajtu z realmodoveho bufferu }
Global_DOS_Free(word(konv_buffer)); { Alokovany blok uvolnim. Vsimnete si, ze tady naopak pouziju NIZSICH 16 bitu }
vysledek:='C:'+p+'';
writeln(vysledek);
readln;
end.  
Z komentářů by mělo být všechno jasné.

Možná si říkáte: "Dobře, ale to není můj problém. Já ve Freepascalu nedělám, já dělám v Turbo pascalu a tam nic takového není."
Omyl. Je. Nezapomeňte že poslední verze Turbo pascalu - Borland pascal 7.0x umějí překládat i pro protekt (byť jenom 16bitový). Pokud programujete jenom pro real, tak samozřejmě můžete zůstat v klidu. Jestli ale překládáte pro protekt, tak zbystřete i vy.
Tenhle zdroják je pro Turbo pascal. FP ho nevezme. Napřed si ho prohlédněte:
uses Strings;
var vysledek:string;
    p:pchar;
begin
p:=@vysledek;
asm
mov ah,47h
mov dx,3
lds si,p
int 21h
end;
vysledek:='C:'+StrPas(p)+'';
writeln(vysledek);
readln;
end.  
Vidíte že:
1) není možné míchání typů PChar a String ve výrazech a Pchar proto musíme převést funkcí StrPas na pascalovský řetězec. Tohle samozřejmě nijak s DPMI nesouvisí, FP má prostě takové rozšíření - zmiňuji se o tom jenom jako o zajímovosti.
2) nepoužil jsem nic z těch keců o DPMI. Klidně si volám přerušení INT 21h/AH=47h a o nic se nestarám.

Schválně si to přeložte napřed pro real - všechno funguje jak má (Proč by taky ne)
A teď pro protekt. Taky všechno běží jak má. A bez jediné změny.
Takže jak je to možné?
Borland pascal 7 taky využívá služeb DPMI, ale přitom pracuje jinak. Každý program přeložený pomocí BP7 pro chráněný mód musí mít u sebe ne cwsdpmi jako FP, ale soubory RTM.EXE a DPMI16BI.OVL
DPMI16BI.OVL je DPMI server. Patrně neposkytuje všechny DPMI služby, ale ty podstatné ano. Jenže tyto služby nejsou volány nikdy přímo, ale volá je druhý správce, a to RTM.EXE
RTM.EXE je emulátor služeb API windows 3.1 a pro nás je podstatné, že se řídí funkci DPMI serveru. A RTM se snaží odchytávat všechna volání přerušení, analyzuje je, hledá v databázi a pokud dané volání zná, a přerušení INT 21h/AX=47h zná, tak provede přibližně ten samý kód jako varianta pro Freepascal a vrátí výsledek v registrech [DS:SI], jako by šlo o realmódový program. Klíčový bod tohoto postupu je jasný. RTM musí danou službu znát. Musí vědět, že tato služba vrací výsledek v [DS:SI], jiná v [ES:DI] a jiná v [DS:BX]. RTM se sice snaží, ale všechno znát nemůže. Obzvlášť služby, které vznikly až po vypuštění BP7.
Bohužel, RTM.EXE nezná služby VESA. Kdybyste volali VESA funkce Zjisti_Info_o_kartě a Zjisti_Info_o_videomódu, tak se zhroutí buďto program, nebo celý počítač. (V nejlepším případě se nezhroutí, nicméně fungovat prostě nebude.)
var vyrobce:pchar;
    buffer:array[0..255] of byte;
    segm,offs:word;
begin
asm
mov ax,4f00h
seges lea di,buffer
int 10h
end;
Move(buffer[6],vyrobce,4);
writeln('Vyrobce tvoji karty je: ',vyrobce);
readln;
end.  
Tenhle prográmek napíše výrobce vaší videokarty. Vidíte, že volá rozhraní VESA (funkci INT 10h/AX=4F00h)
V reálném módu fungovat bude, ale v protektu už ne. RTM standard VESA nezná...

Co teď? Vsadím se, že ve Freepascalu byste to vymysleli spíš, než ve starém dobrém BP7.
BP sice nemá jednotku Go32, ale má jednotku WinAPI. Cože, jaký winapi? My chceme programovat pro DOS! Nebojte, budete programovat pro DOS. O pář řádků výše jsem psal, že RTM.EXE je emulátor API funkcí windows 3.1. Emuluje některé funkce okolo správy paměti (jinými slovy obsluhu DPMI) a pár dalších.
Jako raritu bych dodal, že emuluje i funkci MessageBox. Sice žalostně, ale emuluje.
Při psaní následujícího příkladu ale vyplulo na povrch, že podpora služeb DPMI od jednotky WinAPI je nedostatečná. Je nutné dopsat si v assembleru chybějící funkce.
Pro protekt BP7 program upravíme takto:

{$DEFINE _Bez_WinAPI}  {Jestli chces, tak umaz podtrzitko :-)   }


{$IFNDEF Bez_WinAPI}
uses WinAPI;
{$ENDIF}
type DPMIregs=record
      EDI,ESI,EBP,Reserved,EBX,EDX,ECX,EAX:Longint;
      Flags,ES,DS,FS,GS,IP,CS,SP,SS:word;
     end;


{$IFDEF Bez_WinAPI}
Function GlobalDOSAlloc(w:word):longint;assembler;
asm
mov ax,100h
mov bx,w
shr bx,4  {BX se neuvadi v bajtech, ale v tzv. paragrafech. 1 paragraf=16 bajtu}
int 31h
xchg ax,dx
end;


Procedure GlobalDOSFree(w:word);assembler;
asm
mov ax,101h
mov dx,w
int 31h
end;
{$ENDIF}


Procedure DPMI_Preruseni(i:byte;var r:DPMIregs);assembler;
asm
mov ax,300h
xor bx,bx;mov bl,i
xor cx,cx           { nebudu predavat zadny zasobnik }
les di,r
int 31h
end;


Function Segment_Na_Deskriptor(w:word):word;assembler;
asm
mov ax,2
mov bx,w
int 31h
end;


var vyrobce:pchar;
    buffer:array[0..127] of word;
    konv_buffer:longint;
    real_segment:word;
    r:DPMIregs;
    p:pointer;
    pp:^byte;


begin
konv_buffer:=GlobalDOSAlloc(256);
real_segment:=(word(konv_buffer shr 16));
p:=pointer(0);                 {Za tyhle tri radky se omlouvam, ale}
pp:=@p;inc(pp,2);              {nepodarilo se mi to napsat}
move(konv_buffer,pp^,2);       {srozumitelneji}
{-------------------------}
FillChar(r,sizeof(r),0);
r.es:=real_segment;
r.edi:=0;
r.eax:=$4f00;
DPMI_Preruseni($10,r);
{-------------------------}
Move(p^,buffer,256);
buffer[4]:=Segment_Na_Deskriptor(buffer[4]); { prevod realmodoveho segmentu na selektor pristupny z protektu }
Move(buffer[3],vyrobce,4);


writeln('Vyrobce tvoji karty je: ',vyrobce);
readln;
GlobalDOSFree(word(konv_buffer));
end.  
Uff. Takový miniprográmek a jak mi dal zabrat...
Vidíte, jak se situace šíleným způsobem zkomplikovala. Opravdu nechápu, proč Borlandi, když už se dělali s emulační knihovnou v RTM.EXE nedopsali do jednotky WinAPI funkce Segment_NaDeskriptor a DPMI_preruseni. Takhle je to ještě větší utrpení než ve Freepascalu...
V tomto příkládku nicméně voláme WinAPI jenom kvůli funkcím GlobalDOSAlloc a GlobalDOSFree, které jsou navíc jednoduché a které si můžeme napsat sami a jednotku WinAPI až nebudeme potřebovat.

Na tomto místě Borland pascal opustíme a budeme se věnovat už jenom Freepascalu.

Hardwarová přerušení

Situace je obdobná. Naše aplikace běží ve chráněném módu, jenže rutiny BIOSu pro zpracování hardwarových přerušení (klávesnice, myš,...) pracují v reálném režimu. Správce DPMI proto musí hardwarová přerušení hlídat, vždycky přepnout do reálného módu, přeposlat přerušení na BIOS a po vyřízení přepnout zpátky do protektu a vrátit řízení přerušené aplikaci.
Kdyby tohle správce nedělal, tak by se program zhroutil po prvním přerušení od časovače - tedy za 55ms :-)
Princip neustálého přepínání protekt/real zní dost šíleně a člověk by myslel, že to bude děsivě zdržovat, ale ve skutečnosti zdržuje jenom málo. Mnoho DPMI správců navíc úplně přebírá obsluhu časovače a BIOS k němu vůbec nepouští, takže není třeba každých 55ms přepínat do reálu.
Navíc ve většině případů se nepřepíná přímo do reálného režimu, ale do jeho simulace - do módu Virtual86. O V86 si ale povíme příště.
Klíčové téma u hardwarových přerušení je jejich přeprogramování.
Pokud chceme přeprogramovat obsluhu přerušení, tak si musíme v prvé řadě ujasnit, jestli ho chceme zpracovat v reálném režimu nebo v protektu. Pokud v protektu, tak přerušení zpracuje přímo vaše aplikace. Pokud v reálu, tak ho vaše aplikace zpracovat nemůže. Musí ho zpracovat nějaký rezidentní program v reálném módu. To se využívá zřídka kdy, ale viděl jsem to použít u jedné zvukové knihovny.
Dále se budeme zabývat jenom protektovými přerušeními.
Když nainstalujeme protektovou obsluhu hardwarového přerušení, tak DPMI správce do reálu nepřepíná a předá řízení obslužné proceduře. Pořád je ale připraven může přepnout do reálu později. Příkladem může být obsluha klávesnice, která po každém stisknutí klávesy cvakne(pořád jsme v protektu) a pak zavolá původní obsluhu přerušení (tzn. BIOS), která je v reálu.
Zrovna tak můžeme původní obsluhu nechat být a vůbec ji nevolat (A ušetřit přepnutí protekt/real/protekt).

Tenhle prográmek původní obsluhu zachovává:
uses crt,go32;
{$MODE FPC}


{$DEFINE OPATRNOST}   {Zda se, ze je mozne ji vypnout...}
const kbdint = $9;


var oldint9_handler:tseginfo;
    newint9_handler:tseginfo;


    clickproc:pointer;
    backupDS:Word; external name '___v2prt0_ds_alias';


procedure int9_handler; assembler;  { VSIMNETE SI, ZE TU NENI A NESMI BYT UVEDEN MODIFIKATOR INTERRUPT }
asm
cli
{$IFDEF OPATRNOST}
push ds
push es
push fs
push gs
pusha
{$ENDIF}
   mov ax,cs:[backupDS]
   mov ds,ax
   mov es,ax
   mov ax,dosmemselector
   mov fs,ax
 call clickproc
{$IFDEF OPATRNOST}
popa
pop gs
pop fs
pop es
pop ds
{$ENDIF}
 jmp cs:[oldint9_handler]
sti
end;
procedure int9_dummy; begin end;


procedure clicker;
begin
sound(500);delay(10);nosound;
end;
procedure clicker_dummy; begin end;


procedure install_click;
begin
clickproc:=@clicker;
lock_data(clickproc, sizeof(clickproc));
lock_data(dosmemselector, sizeof(dosmemselector));


lock_code(@clicker,longint(@clicker_dummy) - longint(@clicker));
lock_code(@int9_handler,longint(@int9_dummy)-longint(@int9_handler));
newint9_handler.offset:=@int9_handler;
newint9_handler.segment:=get_cs;
get_pm_interrupt(kbdint, oldint9_handler);
set_pm_interrupt(kbdint, newint9_handler);
end;


procedure remove_click;
begin
set_pm_interrupt(kbdint, oldint9_handler);
unlock_data(dosmemselector, sizeof(dosmemselector));
unlock_data(clickproc, sizeof(clickproc));


unlock_code(@clicker,longint(@clicker_dummy)-longint(@clicker));
unlock_code(@int9_handler,longint(@int9_dummy)-longint(@int9_handler));
end;


var ch:char;


begin
install_click;
Writeln('Neco pis. Enterem skoncis');
while (ch <> #13) do begin ch := readkey; write(ch);end;
remove_click;
end.  

Složitý, že jo? Než se pustíme do rozboru kódu, tak se ještě mrkněte na druhý prográmek. Ten zcela odstraňuje původní zpracování přerušení.
uses crt,go32;
{$MODE FPC}


{$DEFINE OPATRNOST}   {Zda se, ze je mozne ji vypnout...}
const kbdint = $9;
      enter = 28;


var oldint9_handler:tseginfo;
    newint9_handler:tseginfo;
    FromPort:byte;
    clickproc:pointer;
    backupDS:Word; external name '___v2prt0_ds_alias';
    {___v2prt0_ds_alias je promenna uvnitr DPMI serveru. Je tu uchovan DS preruseneho programu}
    {My se na tuto promennou dostaneme prostrednictvim backupDS}


    klavesa:Array[0..127] of boolean; {To nejsou ASCII kody, ale polohove kody}
    zmena:boolean;


procedure int9_handler; assembler;interrupt; { VSIMNETE SI, ZE TU MUSI BYT UVEDEN MODIFIKATOR INTERRUPT }
asm
cli
{$IFDEF OPATRNOST}
push ds
push es
push fs
push gs
pusha
{$ENDIF}
   mov ax,cs:[backupDS]
   mov ds,ax
   mov es,ax
   mov ax,dosmemselector
   mov fs,ax
 call clickproc
{$IFDEF OPATRNOST}
popa
pop gs
pop fs
pop es
pop ds
{$ENDIF}
sti
end;
procedure int9_dummy; begin end;




procedure clicker;
begin
sound(500);delay(10);nosound;


FromPort:=InPortB($60);  {NAPROSTO NUTNE! JE ABSOLUTNE NUTNE PRECIST Z PORTU 60h I KDYZ NAS TO TREBA NEZAJIMA}
if (FromPort and 128)<>0 then klavesa[FromPort-128]:=false else klavesa[FromPort]:=true;
zmena:=true;
OutPortB($20,$20);       {NUTNE}
end;
procedure clicker_dummy; begin end;




procedure install_click;
begin
clickproc:=@clicker;
lock_data(clickproc, sizeof(clickproc));
lock_data(dosmemselector, sizeof(dosmemselector));
lock_data(fromport, sizeof(fromport));
lock_data(klavesa, sizeof(klavesa));
lock_data(zmena, sizeof(zmena));


lock_code(@clicker,longint(@clicker_dummy) - longint(@clicker));
lock_code(@int9_handler,longint(@int9_dummy)-longint(@int9_handler));
newint9_handler.offset:=@int9_handler;
newint9_handler.segment:=get_cs;
get_pm_interrupt(kbdint, oldint9_handler);
set_pm_interrupt(kbdint, newint9_handler);
end;


procedure remove_click;
begin
set_pm_interrupt(kbdint, oldint9_handler);
unlock_data(dosmemselector, sizeof(dosmemselector));
unlock_data(clickproc, sizeof(clickproc));
unlock_data(fromport, sizeof(fromport));
unlock_data(klavesa, sizeof(klavesa));
unlock_data(zmena, sizeof(zmena));


unlock_code(@clicker,longint(@clicker_dummy)-longint(@clicker));
unlock_code(@int9_handler,longint(@int9_dummy)-longint(@int9_handler));
end;


var a:integer;
    s,t:string;


begin
install_click;
zmena:=false;
Writeln('Neco mackej. Enterem skoncis');
writeln;
repeat
if zmena then
   begin
   s:='';
   for a:=0 to 127 do if klavesa[a] then
       begin
       Str(a,t);
       s:=s+'|'+t;
       end;
   gotoxy(1,24);write(s:80);
   zmena:=false;
   end;
until klavesa[enter];
remove_click;
end.  

Podívejte se, v čem se tyto dva prográmky liší. Ve zdrojáku jsem se snažil tato kritická místa zdůraznit. Je na vás, abyste si na to dávali pozor. Programování přerušení je jedním z nejsložitějších problémů vůbec a ve chráněném módu to platí dvojnásob. Jakoukoliv chybu počítač nemilosrdně trestá zatuhnutím nebo zhroucením.

Nejnápadnější věc u obou zdrojáků je jistě sekvence příkazů Lock_Data, Lock_Code, UnLock_Data a UnLock_Code
Všechen programový kód zpracování přerušení musí být zamčen a všechna data, se kterými tento kód pracuje musí být také zamčena.
Většina DPMI serverů totiž umožňuje swapování na disk. Díky tomu se programy nemusí starat o to, jestli má počítač dost RAM a díky tomu mohou dobře fungovat multitasková prostředí. Úlohám v pozadí DPMI přitáhne opratě a omezí jim množství paměti ve prospěch programu na popředí.
Ty to ale nepoznají, protože správce DPMI si s nimi hraje na kočku a na myš a swapuje na disk.
Windows 95 byly mimochodem silně kritizovány, že swapují zbytečně často a zpomalují programy. Další verze se v tom zlepšily. (a hlavně drasticky narostla kapacita paměti PC)
Takže, windows swapují, cwsdpmi taky, wdosx rovněž, a HDPMI32 ne. (Borlandí DPMI16BI/RTM asi ano, ale jen na přání)
Správa přerušení ovšem musí být v RAM, s tou se swapovat nemůže (jinak se program po určité době zhroutí).
Procedurami Lock_Data a Lock_Code říkáme správci DPMI: "Nenene, od toho ruce pryč."
UnLock_Data a UnLock_Code tento kus paměti znovu plně přiděluje do kompetencí správce DPMI.
Není dobré mít zamknuto příliš mnoho, protože to zdržuje paměťový management.

Volání OutPort($20,$20) je v překladu samozřejmě mov ax,20h;out 20h,ax
Je to povel pro CPU, že skončilo zpracování přerušení. Bez toho se nejpozději při následujícím přerušení (patrně přerušení časovače) zhroutí.

Videopaměť

Přístup do videopaměti se realizuje z konvenční paměti ze dvou možných adres, podle toho, jestli jsme v textovém nebo grafickém režimu. Pro textový je to B800:xxxx v segmentovém tvaru neboli B8000h+x v lineárním tvaru a pro grafické režimy je to A000h:xxxx resp. A0000h+x
Jak používat konvenční paměť jsme tu už řešili. Toto je nicméně trošičku jiný případ, protože videopaměť se za prvé nealokuje a za druhé má pevně danou adresu.
Nejjednodušší způsob jak dosáhnout na videopaměť je využití polí Mem, MemW a MemL.
Tento prográmek začárá obrazovku jednolitým vzorem:
var w:word;
begin
for w:=0 to 80*25 do MemW[$B800:w*2]:=$10B1;
readln;
end.  
Je pozoruhodné, že tohle ve Freepascalu projde, protože pro Borland pascal to v případě, že překládáme pro protekt musíme zapsat jinak. S tímhle bychom neuspěli. A proto patří k dobrému stylu NIKDY explicitně neuvádět segmenty. Ani když programujeme pro real.
Pište to takto:
var w:word;
begin
for w:=0 to 80*25 do MemW[SegB800:w*2]:=$10B1;
readln;
end.  
Mnohem lepší ne? BP, FP, TMT i VP definují proměnné SegB800 (tu už známe), SegA000 a Seg0040.
Velice pravděpodobně budete k videopaměti přistupovat z procedur v assembleru. V BP žádný problém ani v protektu (už jsme si vyjasnili, že musíte použít Segxxxx), ale ve FP na to musíme jinak. Tam nás žádný SegB800 ani SegA000 nespasí.
Jeden ze způsobů je tento:
begin
asm
   mov ecx,80*25
   mov ax,10B1h
   mov edi,0b8000h
@cykl:
   mov fs:[edi],ax
   add edi,2
loop @cykl
end;
readln;
end.  
Ve zdrojáku jsou patrny dvě zajímavé věci.
Za prvé použití selektoru FS (v protektových programech používáme výraz segment v jiných souvislostech).
Za druhé lineární adresa B8000h. Oboje souvisí s odlišnostmi protektové a reálné adresace. Nebudeme do toho zabíhat. Pro zatím nám stačí vědět, že do segmentových registrů můžeme dávat jenom hodnoty jiných segmentových registrů nebo hodnotu, kterou nám vygeneruje správce DPMI.
Normálně je celá paměť nad 1MB adresovaná z registru DS(ES) (protože ES=DS). Díky tomu, že offsety používáme 32bitové (obvykle ESI nebo EDI), tak dosáhneme na celou paměť...
...kromě onoho 1MB, který je "pod námi".
Když tedy chceme pracovat s konvenční pamětí, musíme požádat správce DPMI o změnu selektoru a on nám vydá nějaký jiný, ležící pod 1MB hranicí, ze kterého už dosáhneme.
Jak ale vidíte, tak já se tady DPMI server o nic neprosím. Freepascal to totiž dělá automaticky a nechává si vydat selektor pro celou konvenční paměť a ukládá ho do FS. Není to nic samozřejmého, TMT to nedělá a VP taky ne.
Když už vám jednou DPMI vydá selektor, tak si s ním můžete dělat, co chcete. Prográmek můžete modifikovat třeba takto:
begin
asm
   mov ecx,80*25
   mov ax,10B1h
   mov edi,0b8000h
push es
   mov bx,fs
   mov es,bx
   rep stosw
pop es
end;
readln;
end.  
Ještě jsem se nezmínil o lineární adrese... V reálu se lineární adresa spočítá takto Linear:=segment*16+offset
A B8000h je přece B800h*16 = B800h*10h = B8000h
Jednoduché!

Kdybyste náhodou zapomněli na registr FS, tak tu samou hodnotu najdete v proměnné DOSmemSelector
uses Go32;
begin
asm
   mov ecx,80*25
   mov ax,10B1h
   mov edi,0b8000h
push es
   mov bx,dosmemselector
   mov es,bx
   rep stosw
pop es
end;
readln;
end.  

V některých případech ale může být užitečné nechat si od DPMI vypsat nový selektor. Ve FP je takový přístup možná trošku zbytečný, ale například v případě, že si chcete nechat otevřená vrátka k jinému překladači (třeba TMT), tak se to hodí.
uses Go32;
{$DEFINE _Jsem_peclivy}


var w:word;
begin
w:=Segment_to_descriptor(SegB800); {misto SegB800 taky lze $B800}
{$IFDEF Jsem_peclivy}
Set_Segment_Base_Address(w,Get_Linear_Addr(LongInt(SegB800*16),65536));
Set_Segment_Limit(w,65535);
{$ENDIF}
asm
   mov ecx,80*25
   mov ax,10B1h
   xor edi,edi
push es
   mov bx,w
   mov es,bx
   rep stosw
pop es
end;
readln;
end.  
Klíčová je funkce Segment_to_descriptor, která pro adresu danou v realmódovém tvaru vymyslí selektor.
Ty dvě řádky zřejmě nejsou alespoň ve FP nutné, ale jak je to v jiných překladačích, nevím. Jejich význam mi není jasný, ale mám podezření, že v podstatě povinné jsou, ale jejich absence se neprojeví proto, že tento selektor leží jakoby uvnitř selektoru DOSmemSelector, který má všechno vyřízené vzorně. Jelikož oba dva selektory patří tomu samému procesu, tak nenastávají problémy s přístupovými právy.
Ale jsou to jenom moje spekulace.
Všimněte si také, že řádek mov edi,0B8000h je nahrazen řádkem xor edi,edi
Selektor W je totiž vypsán až od adresy B800h:0000 (oproti selektoru DOSmemSelector, který je vypsán od 0000:0000)

Vytvořili jsme si už dostatečné teoretické zázemí pro tak komplexní úkon jako nastavní LFB (celoobrazovkový adresační rámec pro VESA módy).
uses Go32;
const m640x480x256=$101;


Procedure ZacmarejObrazovku(lfb:word);assembler;
asm
push es
   mov ax,lfb
   mov es,ax
   mov ecx,640*480
   xor eax,eax
   xor edi,edi
@cykl:
   mov es:[edi],al
   inc edi
   inc al
loop @cykl
pop es
end;


var buffer:array[0..511] of char;
    w:longint;
    s:word;
    r:TRealRegs;
    _lfb:longint;
    lfb:word;
    vram:longint;


begin
FillChar(buffer,SizeOf(buffer),0);
buffer[0]:='V';          { Pokud se chceme bavit po standardu VESA 2 }
buffer[1]:='B';          { tak musime na zacatek bufferu vlozit retezec VBE2}
buffer[2]:='E';
buffer[3]:='2';


s:=Word(w shr 16);
CopyToDOS(buffer,512);
r.eax:=$4F00;                     { sluzba 4F01h - informace o videokarte }
r.es :=tb_segment;
r.edi:=tb_offset;
RealIntr($10,r);


CopyFromDOS(buffer,512);
Move(buffer[18],vram,2);             {velikost pameti videokarty}
{------------------------------------------------------------------------}
r.eax:=$4F01;                     { sluzba 4F01h - informace o videorezimu }
r.es :=tb_segment;
r.edi:=tb_offset;
r.ecx:=m640x480x256;
RealIntr($10,r);


CopyFromDOS(buffer,512);
Move(buffer[40],_lfb,4);          {fyzicka adresa prostoru LFB}


lfb:=Allocate_LDT_Descriptors(1);                                 {tvorba zcela noveho selektoru}
Set_Segment_Base_Address(lfb,Get_Linear_Addr(_lfb,vram shl 16));  {prirad mu pocatecni adresu}
Set_Segment_Limit(lfb,(vram shl 16)-1);                           {a stanov jeho rozsah}


r.eax:=$4F02;
r.ebx:=m640x480x256 + $4000;
RealIntr($10,r);


ZacmarejObrazovku(lfb);
readln;


r.eax:=3;
RealIntr($10,r);
end.  
Jak vidíte, používám tu Transferové bloky. Jednou, abych zjistil velikost videopaměti grafické karty a podruhé abych zjistil adresu bloku LFB.
Zajímavý je řádek lfb:=Allocate_LDT_Descriptors(1), který tu je namísto funkce Segment_to_descriptor. LFB totiž neleží v konvenční paměti, tudíž nemůže být ani vyjádřen segmentový tvar adresy.
Allocate_LDT_Descriptors tedy vytvoří zcela nový deskriptor, ale zatím nevyplněný. Vyplní ho až dvě následující procedury.

Tak...
Téma jsem prakticky vyčerpal. Dalo by se mluvit ještě o dvou věcech: o volání realmódových procedur z protektových programů, což je velice úzce zaměřené téma, a volání protektových procedur z realmódových rezidentů (typicky callback ovladače myši). Tohle je jakž takž popsáno v nápovědě a já nemám nic, co bych k tomu dodal.
Na tenhle článek jsem hrdý a musím se za něj pochválit. Příště ho zakončím poněkud hlubším výkladem o tom, jak vlastně chráněný mód funguje a jak pracuje DPMI server.
2006-11-30 | Laaca
Datum: 10.3.2010 16:49
Od: coosor
Titulek: problém
Ta nová obsluha int9_handler co je popsána v článku mi v protected modu nového FreePascalu pro DOS nejede (starší verze jsem nezkoušel). Je to vyzkoušené? Nainstaluji přerušení klávesnice a jakmile stisknu ve svém programu jakoukoliv klávesu spadne to s chybou obecné ochrany!
Reklamy: