Assembler (Jazyk symbolických adres)

(9. část)

Registry - pokračování

Nastavení registru vlajek

Registr vlajek se částečně nastavuje současně s vykonáváním některých instrukcí. Předchozí tři řídící bity se ale automaticky nenastavují, může je nastavit jen programátor. Assembler pro to má instrukce, kterými můžeme přímo ovlivnit hodnoty některých bitů registru Flags:

Jestliže chceme nastavit hodnotu vlajky, pro kterou instrukce neexistuje, lze to udělat tak, že registr Flags předáme přes zásobník do některého z registrů pro všeobecné použití, v tomto registru logickou operací nastavíme patřičný bit vlajky a přes zásobník opět předáme obsah registru do registru Flags. Pro spodní slabiku registru vlajek (nejdůležitější vlajky) dokonce existují následující instrukce:

Příklad nastavení vlajky ZF (7. v pořadí):
    PUSHF
    POP AX
    OR AX, 40H ; 2^6 = 64 = 40H
    PUSH AX
    POPF
    

Platí nepsané pravidlo, že vyšší programovací jazyky (včetně C) vždy udržují DF = 0. Pokud budete ve svém kódu potřebovat DF = 1, nezapomeňte po provedení blokové operace vrátit stav zpět na DF = 0! Avšak nastavení DF je potřeba jen velmi, velmi zřídka, spíše je lepší se tomu vyhnout.

Operační systém by měl blokovat změnu vlajky IF, tj. nedovolit aplikaci maskovat přerušení, protože celý systém přerušení ovládá sám. Výjimkou je M$ DOS, kde se IF používá (resp. používala) docela často. Ve všech vyspělejších OS je tedy vždy IF = 1, obě instrukce CLI a STI jsou privilegované, takže proces nemá právo hodnotu IF měnit.

Př. Zkuste změnit hodnotu vlajky IF. Jednak pomocí instrukce CLI a také postupem se zásobníkem.

Přerušení

V době vykonávání úlohy musí být zajištěna i programová obsluha některých HW (a SW) událostí. Za hardwarové události považujeme například stisk a uvolnění klávesy, události na vstupně/výstupních portech (např. pohyb myší), hardwarový časovač, komunikace s pevným diskem (a ostatními datovými zařízeními), za softwarové například diskové, grafické, síťové operace, ovládání klávesnice, myši a ostatních externích zařízení (na portech), softwarový časovač, kritická chyba v paměti, krokování programu, atp. I když by bylo možné (?) testovat např. stisk klávesy v rámci prováděné úlohy, je pohodlnější a hlavně mnohem přijatelnější, jestliže obsluhu této události zajistí počítač sám na úrovni technického vybavení. Přesto je i k této činnosti nutný mikroprocesor. Proto je při obsluze události dočasně přerušena probíhající úloha. Po obsluze se procesor vrací zpět k úloze na místo, na kterém bylo její vykonávání přerušeno (zásobník úlohy a vlajky jsou samozřejmě také obnoveny). Tato z pohledu programu (a programátora) neočekávaná přerušení nazýváme hardwarová přerušení, protože je generuje hardware počítače. Hardwarová přerušení je dále možné rozdělit na nemaskovatelná (NonMaskable Interrupts - NMI) (nelze je zakázat, blokovat) a maskovatelná (Maskable Interrupts - MI), jejichž obsluhu je možné zakázat vynulováním bitu IF v registru vlajek.

Celý mechanismus přerušení se dá popsat v několika krocích:

Otázka: Kolik je a jaká (například) jsou maskovatelná hardwarová přerušení? Jaké číslo (u instrukce INT) má např. hardwarové MI 1 (přerušení od klávesnice)?

Služby

Instrukce INT má jako jediný operand číslo v rozpětí 0 až 255. Toto číslo udává, o jaké hardwarové přerušení se jedná. Protože ale všech 256 možných hodnot není hardwarovými přerušeními zdaleka obsazeno, je většina hodnot obsazena tzv. službami, které se také nazývají softwarová přerušení, protože je generuje software (jsou vyvolávány). Za služby můžeme považovat podprogramy, které jsou součástí BIOSu nebo operačního systému. Jsou uloženy na začátek paměti počítače při jeho startu (v případě BIOSu, stejně jako obsluhy hardwarových přerušení) nebo při startu OS, a jejich adresy jsou uloženy do tabulky vektorů přerušení. Představují činnosti, při nichž se většinou komunikuje s hardware a které se liší na počítačích s různou HW konfigurací, a umožňují tak programátorům unifikovaný přístup k tomuto HW (tvoří abstraktní přístupovou vrstvu).

Služby voláme v Assembleru stejně jako jsou volány obsluhy HW přerušení, instrukcí INT číslo. Hodnota číslo určuje, o jakou službu se jedná. Často je v rámci jedné služby umožněno provádět i několik činností. Těm se říká podslužby. Podslužba v rámci služby se specifikuje uložením hodnoty určující tuto podslužbu do vybraného registru (nejčastěji AH). Potom se zavolá služba instrukcí INT. Většině službám lze, podobně jako podprogramům, předat při jejich volání parametry. Hodnoty parametrů se zde ale neukládají do zásobníku, nýbrž do některých registrů. Výstup služby (návratovou hodnotu) najdeme opět v registru (často AL).

Informace o službách BIOSu a M$ DOSu jsou k nalezení v odborných publikacích nebo v interaktivních helpech jako je např. Sysman (viz seznam literatury). Ke každé službě je kromě výkladu její činnosti a dalších podrobnějších informací uvedeno její číslo (pro instrukci INT), parametry a výstup (spolu s nastavovanými registry) a seznam všech jejích podslužeb (jak ji specifikovat, její parametry, výstup). Při využívání služeb se bez těchto informací neobejdeme a jejich zdroje jsou tedy při programování v Assembleru nepostradatelné.

Protože byly služby BIOSu a M$ DOSu navrženy a iplementovány pro 16-bitové procesory v dobách 16-bitových OS, používají pro práci s pamětí segmentaci a tedy i segmentové registry (hlavně DS a ES). I když by technicky nic nebránilo jejich používání i v nynější době 32-bitových procesorů a OS, od jejich využívání se v poslední době často upouští, protože z důvodů bezpečnosti a nadvlády operačního systému nad HW jsou tímto OS emulovány nebo dokonce blokovány. Navíc jsou všechny důležité funkce, které implementují, a mnohé další, vykonávány bezpečnějšími a rychlejšími rutinami dnešních vyspělejších OS, tzv. systémovými voláními. Ale i tato systémová volání mohou být volána přes nějakou službu (jako softwarové přerušení), protože ani dnes stále není obsazeno všech 256 přerušení. Většinou to funguje tak, že obsluhou služby (přerušení) je část jádra OS, která vyvolá patřičné systémové volání (které v rámci služby figuruje jako podslužba).

Využití služeb v emulovaném prostředí OS M$ DOS

V OS M$ Windows je 32-bitovým aplikacím vyvolávání přerušení zakázáno, tedy služeb BIOSu ani M$ DOSu z nich využívat nelze. Ve vývojovém prostředí M$ Visual C++ lze přeložit kód programu v (inline i external) Assembleru, který obsahuje intrukce INT, ale po spuštění takového programu tento skončí na první instrukci INT s chybou neplatného přístupu do paměti. Při překladu jiným překladačem program nemusí skončit s chybou na instrukci INT, ale žádné přerušení se nevyvolá, tj. je blokováno či ignorováno. Je tedy jasné, že systémová volání v tomto OS nejsou aplikacemi volána jako služby (přerušení), nýbrž jako obyčejná volání funkcí, které implementují systémová volání. Víme ale, že v tomto OS (M$ Windows) lze spouštět i 16-bitové programy a to pomocí emulace 16-bitového OS M$ DOS. Takové programy v tomto emulovaném prostředí mohou vyvolávat přerušení, pracovat s porty a provádět další věci, které ve 32-bitovém prostředí provádět nelze (jsou 32-bitovým OS zakázány). Řešením pro využívání služeb BIOSu a M$ DOSu je tedy vytvořit 16-bitový program.

Ve vývojovém prostředí M$ Visual C++ lze vytvořit pouze 32-bitový program. Navíc se k programu v jazyce C++ ve fázi spojování (linkování) při překladu připojuje spousta 32-bitových sdílených knihoven s implementacemi standardních funkcí (jazyka C nebo např. implementujících systémová volání). Překladačem Macro Assembler (MASM) je ale možné přeložit i 16-bitový zdrojový kód v externím Assembleru. Náš 16-bitový program tedy bude psaný celý v externím Assembleru. Překladem dostaneme binární objektové soubory, které je potřeba spojit (slinkovat) do výsledného spustitelného souboru. Spojovacím programem (Linkerem) vývojového prostředí M$ Visual C++ lze ale vytvořit pouze 32-bitový program, takže musíme použít jiný, který produkuje 16-bitové programy. Použijeme 16-bitový linker pro M$ DOS - Turbo Linker (TLINK). Ve vývojovém prostředí ale nelze nastavit vlastní linker ani vlastní příkaz pro sestavení programu, proto musíme sestavovat 16-bitové programy linkerem TLINK jeho spuštěním z konzole: tlink soubor1.obj soubor2.obj .... Na 16-bitový kód v externím Assembleru pro OS M$ DOS jsou kladeny kromě známých požadavků (jako adresování paměti dvojicí segment:offset, rozlišování blízkých a vzdálených skoků, 16-bitová organizace zásobníku, atd.) ještě další podmínky:

Jelikož vytvoříme program pro OS M$ DOS, musíme jej korektně ukončit funkcí M$ DOSu (jinak se neukončí a po nějaké době zhavaruje), viz také dále příklady služeb:

MOV AX, 4C00H
INT 21H
Program bude napsaný celý v externím Assembleru, musíme tudíž zadat jeho vstupní bod (začátek vykonávání programu), pomocí návěští. Toto návěští se většinou pojmenovává START (ale nemusí). Konec programu musí být také označený, a to klíčovým slovem END následovaným jménem vstupního bodu programu (jménem návěští), tzn. v našem případě END START. Na konci zdrojového souboru samozřejmě zůstává i samostatné klíčové slovo END.

Příklady služeb

Služba M$ DOSu 21H (Systémová volání M$ DOSu)

V dobách minulých bývala jednou z nejpoužívanějších služeb služba M$ DOSu číslo 21H. Ta zahrnuje mnoho podlužeb (přes 100), systémových volání (nebo funkcí) M$ DOSu, jako jsou informace o systému, vstup a výstup dat (klávesnice, obrazovka, disky, atd.), správa paměti, a jiné funkce M$ DOSu. Tyto funkce jsou specifikovány hodnotou v registru AH před vyvoláním přerušení 21H. Dobře známá je např. funkce 4CH, která slouží k ukončení programu s návratovým kódem zadaným jako parametr funkce v registru AL:

MOV AX, 4C00H
INT 21H
Z dalších dříve často používaných funkcí jsou např. načtení a výpis řetězce (funkce 0AH a 09H), návrat z programu při jeho zachování v paměti (tzv. rezidentní programy, funkce 31H - KEEP), alokace a uvolnění paměti (funkce 48H a 49H), a jiné.

Otázka: Tušíte, jak bývalo zařízeno (naprogramováno) vyvolání funkcí rezidentních programů v M$ DOSu, např. při stisku určité kombinace kláves nebo v nějakou dobu, apod.?
Služba obrazovky 10H

Zřejmě nejpoužívanější službou dříve i dnes je služba BIOSu 10H - služba obrazovky. Její podslužby, které se specifikují v registru AH, implementují hojně využívané operace s obrazovkou jako např. zjištění a nastavení videorežimu (rozlišení obrazovky - velikost obrazu a počet barev), čtení a zápis textového znaku nebo barvy grafického bodu obrazu, nastavení kurzoru textového režimu, atd. Z předchozího lze vytušit, že obrazovka může pracovat ve dvou typech režimů - textovém a grafickém. V textovém režimu můžeme číst a zapisovat textové (ASCII) znaky a jejich barvu (podslužby 08H, 09H, 0AH) nebo zjišťovat a měnit velikost/tvar a pozici kurzoru (01H, 02H, 03H). Definované standardní textové režimy jsou 40 sloupců na 25 řádků v 16 barvách (40x25x16) nebo 80x25x16 (běžně používaný). V grafickém režimu lze číst a zapisovat grafické body, tj. jejich barvu (podlužby 0CH a 0DH). Standardních grafických režimů (VGA) není o moc víc, např. 320x200x16, 640x480x16, 320x200x256.

Příklad výpisu znaku dané barvy na pozici kurzoru v textovém režimu:
vypis_znaku PROC USES AX BX CX DX, znak:BYTE, barva:BYTE, radek:BYTE, sloupec:BYTE
   xor bh, bh ; stránka obrazovky, detaily viz. např. Sysman
   mov dh, radek ; od 0
   mov dl, sloupec ; od 0
   mov ah, 2 ; podslužba nastavení pozice kurzoru
   int 10h
   mov al, znak
   mov cx, 1 ; počet opakování znaku
   mov bl, barva
   mov ah, 9 ; podslužba výpisu znaku s barvou
   int 10h
   ret
vypis_znaku ENDP
Příklad kreslení graf. bodu dané barvy na pozici v grafickém režimu:
graf_bod PROC USES AX BX CX DX, barva:BYTE, radek:WORD, sloupec:WORD
   xor bh, bh ; stránka obrazovky, detaily viz. např. Sysman
   mov dx, radek ; od 0
   mov cx, sloupec ; od 0
   mov al, barva
   mov ah, 0Ch ; podslužba kreslení graf. bodu
   int 10h
   ret
graf_bod ENDP

Dnes jsou ale běžná mnohem vyšší grafická rozlišení (i textová). Videorežimy s rozlišeními více než 640x480 s více barvami než 256 obstarává tzv. VESA rozšíření BIOSu, SuperVGA, podslužba 4FH. Tato podslužba obsahuje další podpodslužby, specifikované v registru AL, např. zjišťování informací o SuperVGA (00H a 01H), zjištění a nastavení videorežimu a jeho stavu (02H, 03H, 04H). Ze SuperVGA grafických režimů si už můžeme vybrat mnohem víc, 640x480, 800x600, 1024x768, 128x1024 atd. při 16, 256, 32768, 65536, 16 milionů, atd. barvách. Další textové režimy jsou např. 132x25x16, 132x50x16.

Ale dříve než můžeme s nějakým režimem pracovat (vypisovat, kreslit), musíme jej nastavit (inicializovat). K tomu v případě standardních režimů VGA slouží nejdůležitější podslužba 00H (číslo režimu jako parametr v registru AL), u SuperVGA potom podpodslužba 02H (číslo režimu v registru BX). Po ukončení využívání nějakého videorežimu je nanejvýš vhodné jej vrátit na přechozí videorežim. Abychom věděli, do jakého režimu se máme vrátit, musíme si uložit jeho číslo ještě před inicializací nového režimu. K tomu slouží u VGA podslužba 0FH (vrací číslo aktuálního režimu v registru AL), u SuperVGA potom podpodslužba 03H (vrací číslo aktuálního režimu v registru BX). Po ukončení práce v novém režimu tedy obrazovku přepneme do režimu s číslem, které jsme si uložili.

Příklad inicializace videorežimu 320x200x256:
MOV AH, 0FH
INT 10H
MOV puvodni_rezim, AL
MOV AX, 13H
INT 10H
A návrat do předchozího režimu:
MOV AL, puvodni_rezim
XOR AH, AH
INT 10H

Dobrým zdrojem dalších podrobností o službě obrazovky 10H a jejích podslužbách, o videorežimech VGA a SuperVGA, jsou vřele doporučované interaktivní helpy, např. Sysman.

Př. Napište 16-bitový program pro M$ DOS v externím Assembleru, který vykreslí na obrazovku obdélníkovou spirálu od levého horního rohu do středu (mezi čarami bude mezera). Spirálu vykreslete nejdříve v textovém režimu (např. standardním 80x25x16), po jejím vykreslení program počká na stisk libovolné klávesy (pomocí např. služby klávesnice), potom spirálu vykreslí v grafickém režimu (např. standardním 320x200x256), opět počká na stisk lib. klávesy a skončí. Nezapomeňte na korekní ukončení programu funkcí M$ DOSu 4CH!

Závěr, zbývající zajímavé instrukce

Doposud probrané instrukce jsou všechny běžné instrukce (a téměř všechny celkově) procesorů nejvýše 386, které můžeme při psaní běžných procedur využívat nebo se kterými bychom se mohli v kódu psaném v Assembleru nejčastěji setkat.

Na samotný závěr výkladu instrukcí si ještě všimneme dvou zajímavých instrukcí řízení procesoru. Instrukce HLT zastaví činnost procesoru (přesněji procesor stále vykonává následující a poslední uvedenou instrukci), dokud se nevyvolá nějaké přerušení. Přitom si pamatuje adresu instrukce, před kterou byl zastaven (CS:EIP), a po obsluze přerušení pokračuje instrukcí na této adrese. Poslední probranou instrukcí je instrukce NOP. Trvá 3 cykly procesoru (od 486 jen 1) a v binárním kódu programu má hodnotu 90H. Uvedení této instrukce v programu má pro něj stejný efekt jako její neuvedení, čili velmi zjednodušeně řečeno, tato instrukce nedělá nic. Mohlo by se tedy zdát, že taková instrukce je úplně zbytečná. Chyba, i taková instrukce na využití. V čem?? Právě v tom, že nedělá nic!!

Otázka: Jaká může mít instrukce NOP (a tedy i instrukce HLT) konkrétní využití?

Bonus: Kreslení na obrazovku pomocí přímého přístupu do videopaměti

Při vykreslování grafiky pomocí služby obrazovky 10H brzy zjistíme, že překreslování je velice pomalé. Např. hra vykreslující obraz tímto způsobem by byla naprosto nehratelná! Kreslení tímto způsobem je pomalé jednoduše proto, protože pro zobrazení každého jednotlivého bodu se vyvolává přerušení, přitom se vykonává mnoho operací se zásobníkem (tj. pamětí), pořád znovu a znovu. Určitým zvýšením rychlosti by byla možnost vykreslit více bodů v rámci jednoho přerušení.

Návrháři HW ale přišli s ještě lepším řešením - přímým přístupem do videopaměti, tj. paměti, ve které je uložen právě zobrazený obraz. Řešeno je to tak, že tato videopaměť je namapována do hlavní paměti a tedy obyčejným zápisem nebo čtením z určité adresy můžeme pracovat přímo s obrazem na obrazovce. Grafiká karta počítače zobrazuje data právě z této paměti, tedy projevení změny ve videopaměti na obrazovce je téměř okamžité.

Téměř ve všech textových videorežimech je videpaměť mapována do hlavní paměti na adresu B8000h. Obraz je zde uložen po řádcích, zleva doprava odshora dolů. Pro každý znak na obrazovce jsou dvě slabiky, ASCII hodnota znaku a barva. Čili znak vlevo nahoře je uložen na adrese B8000h a jeho barva na B8001h, adresa znaku na i-té řádce a j-tém sloupci (počítáno od 0) je rovna B8000h + i * počet sloupců + j, jeho barva je o 1 dále.

Ve všech standardních grafických režimech (VGA) je tato adresa A0000h. Obraz je zde opět uložen po řádcích, zleva doprava, odshora dolů. V režimu s 256 barvami nese každá slabika od této adresy hodnotu barvy bodu na obrazovce. čili barva prvního bodu v prvním řádku je na adrese A0000h, bod na i-té řádce a j-tém sloupci má adresu A0000h + i * počet sloupců + j.

Příklad kreslení graf. bodu dané barvy na pozici ve standardním grafickém režimu 320x200x256 pomocí přímého zápisu do videopaměti:
vram_seg dw A000H

graf_bod PROC USES ES DI AX, barva:BYTE, radek:WORD, sloupec:WORD
    mov ax, vram_seg
    mov es, ax
    mov ax, radek
    mov di, ax
    shl di, 2
    add di, ax
    shl di, 6
    add di, sloupec
    mov al, barva
    stosb
    ret
graf_bod ENDP
   

Tento přístup má ale také nevýhodu. Problémem může být příliš velká rychlost. Zobrazovacímu zařízení (monitor) totiž vykreslení obrazu nějakou dobu trvá (danou tzv. obnovovací frekvencí - vertical refresh), a pokud je obraz ve videopaměti měněn rychleji než je zobrazován, dochází k viditelnému a nepříjemnému blikání obrazu a následným efektům, např. trhání animace (protože se vykreslí každý n-tý snímek). Řešením je před každou větší změnou (nejlépe celého) obrazu ve videopaměti počkat, až se stávající obraz vykreslí. Pro toto čekání existuje klasická, notoricky známá a používaná procedura wait_retrace:

wait_retrace PROC USES AX DX
    mov dx, 3DAH
  l1:
    in al, dx
    and al, 8
    jnz l1
  l2:
    in al, dx
    and al,8
    jz l2
    ret
wait_retrace ENDP

Co když je ale naopak změna obrazu v paměti náročná a pomalá, pomalejší než jeho vykreslení? Potom se nový obraz vykresluje po částech a ještě nezměněná část obrazu opět bliká. Tento problém se dá řešit pouze zrychlením změny obrazu ve videopaměti tak, aby byla rychlejší než jeho vykreslení. Co když to už ale optimalizacemi v kódu nelze? Nejrychlejší změna obrazu je pouhé zkopírování změněné části (nebo i celého) obrazu do videopaměti z jiné části paměti. Tím se dostáváme k principu metody zvané doublebuffering: obraz se mění v jiné části paměti (druhý buffer) a do videopaměti (první buffer) se po dokončení všech změn pouze zkopíruje.

Otázka: Je možné změnu obrazu (vykreslování do videopaměti) ještě více zrychlit?!

Tato velice efektivní metoda kreslení (pomocí přímého přístupu do videopaměti) byla hojně využívána v dobách minulých pod OS M$ DOS, ale v dnešních vyspělejších OS naráží na obrovskou překážku: přímý přístup do videopaměti není OS povolen, protože OS má proces vykreslování do videopaměti ve své režii a implementuje vlastní (a mnohdy ještě efektivnější) postupy. Procedura wait_retrace čte z portu, a to také není OS dovoleno. A i když je tato metoda nějakým způsobem operačním systémem umožněna (jako např. v OS M$ Windows pomocí emulace 16-bitového prostředí OS M$ DOS), nemusí vést ke kýženému výsledku (rychlejšímu vykreslování obrazu) nebo naopak nemusí být vůbec nutné ji použít, protože součástí onoho způsobu jejího umožnění může být i implementace vlastních výkonných metod kreslení obrazu.

Př. Upravte příklad na kreslení spirály tak, že kreslení bude řešeno přímým přístupem do videopaměti. Volejte proceduru wait_retrace před vykreslením každého bodu a test na stisk klávesy dejte do cyklu, ve kterém se body nebo celé čáry vykreslují.

Jan Outrata
outrataj@phoenix.inf.upol.cz