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: (EDI, ESI, EBP, Res, EBX, EDX, ECX, EAX: longint;
Flags, ES, DS, FS, GS, IP, CS, SP, SS: word);
2: (DI, DI2, SI, SI2, BP, BP2, R1, R2: word;
BX, BX2, DX, DX2, CX, CX2, AX, AX2: word);
3: (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;
asm
push edi
push ebx
push fs
movzx ebx,i
xor ecx,ecx
mov edi,r
mov eax,0300h
int 31h
pop fs
pop ebx
pop edi
end;
end;
var r:registry;
begin
r.ax:=$3000;
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;
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;
r.edx:=3;
r.ds:=tb_segment;
r.esi:=tb_offset;
RealIntr($21,r);
p:=@vysledek;
CopyFromDOS(vysledek,251);
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;
begin
konv_buffer:=Global_DOS_Alloc(256);
r.ax:=$4700;
r.edx:=3;
r.esi:=0;
r.ds:=(word(konv_buffer shr 16));
RealIntr($21,r);
p:=@vysledek;
DOSmemGet(word(konv_buffer shr 16),0,vysledek,251);
Global_DOS_Free(word(konv_buffer));
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:
uses WinAPI;
type DPMIregs=record
EDI,ESI,EBP,Reserved,EBX,EDX,ECX,EAX:Longint;
Flags,ES,DS,FS,GS,IP,CS,SP,SS:word;
end;
Function GlobalDOSAlloc(w:word):longint;assembler;
asm
mov ax,100h
mov bx,w
shr bx,4
int 31h
xchg ax,dx
end;
Procedure GlobalDOSFree(w:word);assembler;
asm
mov ax,101h
mov dx,w
int 31h
end;
Procedure DPMI_Preruseni(i:byte;var r:DPMIregs);assembler;
asm
mov ax,300h
xor bx,bx;mov bl,i
xor cx,cx
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);
pp:=@p;inc(pp,2);
move(konv_buffer,pp^,2);
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]);
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;
const kbdint = $9;
var oldint9_handler:tseginfo;
newint9_handler:tseginfo;
clickproc:pointer;
backupDS:Word; external name '___v2prt0_ds_alias';
procedure int9_handler; assembler;
asm
cli
push ds
push es
push fs
push gs
pusha
mov ax,cs:[backupDS]
mov ds,ax
mov es,ax
mov ax,dosmemselector
mov fs,ax
call clickproc
popa
pop gs
pop fs
pop es
pop ds
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;
const kbdint = $9;
enter = 28;
var oldint9_handler:tseginfo;
newint9_handler:tseginfo;
FromPort:byte;
clickproc:pointer;
backupDS:Word; external name '___v2prt0_ds_alias';
klavesa:Array[0..127] of boolean;
zmena:boolean;
procedure int9_handler; assembler;interrupt;
asm
cli
push ds
push es
push fs
push gs
pusha
mov ax,cs:[backupDS]
mov ds,ax
mov es,ax
mov ax,dosmemselector
mov fs,ax
call clickproc
popa
pop gs
pop fs
pop es
pop ds
sti
end;
procedure int9_dummy; begin end;
procedure clicker;
begin
sound(500);delay(10);nosound;
FromPort:=InPortB($60);
if (FromPort and 128)<>0 then klavesa[FromPort-128]:=false else klavesa[FromPort]:=true;
zmena:=true;
OutPortB($20,$20);
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;
var w:word;
begin
w:=Segment_to_descriptor(SegB800);
Set_Segment_Base_Address(w,Get_Linear_Addr(LongInt(SegB800*16),65536));
Set_Segment_Limit(w,65535);
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';
buffer[1]:='B';
buffer[2]:='E';
buffer[3]:='2';
s:=Word(w shr 16);
CopyToDOS(buffer,512);
r.eax:=$4F00;
r.es :=tb_segment;
r.edi:=tb_offset;
RealIntr($10,r);
CopyFromDOS(buffer,512);
Move(buffer[18],vram,2);
r.eax:=$4F01;
r.es :=tb_segment;
r.edi:=tb_offset;
r.ecx:=m640x480x256;
RealIntr($10,r);
CopyFromDOS(buffer,512);
Move(buffer[40],_lfb,4);
lfb:=Allocate_LDT_Descriptors(1);
Set_Segment_Base_Address(lfb,Get_Linear_Addr(_lfb,vram shl 16));
Set_Segment_Limit(lfb,(vram shl 16)-1);
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.
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!