Tato učebnice měla být původně vydána jako skripta pro školu, na které učím. Protože však došlo k mnoha objektivním problémům (prostě nebyly peníze), nabízím je touto cestou všem, kteří programují v Turbo Pascalu. I když v současné době převážně programuji v Delphi, znalosti z této učebnice využívám. Pokud budete informace z tohoto dokumentu někdo používat, čiňte tak prosím s poznámkou o autorovi (copyright najdete na konci textu).
Žijeme v době, kdy nás počítače obklopují na každém kroku. Jejich výhodou je možnost ovlivnit své chování podle našich potřeb. Proto i ten nejlepší počítač nedokáže pracovat bez programu, který mu dává instrukce, jak se za kterých okolností chovat. Proto bude vždy nutné, aby určitá skupina lidí byla schopna tyto programy tvořit. Myslím si, že programátoři budou i při sebedokonalejším technickém vybavení spolutvůrci systémů, které ulehčují lidem práci. Mezi základní znalosti každého programátora patří alespoň minimální znalost Jazyka symbolických adres, kterému říkáme assembler. Protože tvorba složitějších programů jen v ASM86 by byla zdlouhavá, nabízí se možnost vytvářet je ve vyšším programovacím jazyce, a v assembleru tvořit jen ty jeho části, které se často opakují, a přitom není jejich tvorba náročná. Tyto bloky se nejlépe programují v tzv. vloženém assembleru.
Obvod 8086 je univerzální šestnáctibitový mikroprocesor. Má šestnáctibitovou ALU, to znamená, že je schopen provádět operace s šestnáctibitovými čísly. S okolím komunikuje po šestnáctibitové datové a dvacetibitové adresové sběrnici.
Vzhledem k tomu, že obvod 8086 je schopen práce s pamětí o velikosti 1MB a obsahuje jen šestnáctibitové registry, je nutná tzv. segmentace paměti. Jedná se o logické dělení paměti do bloků po 64kB. Tomuto bloku říkáme segment a jeho počátek určuje programátor, případně je mu přidělen podle volného místa v paměti. Jediný požadavek na umístění počátku segmentu je, aby jeho adresa byla násobkem šestnácti. Umístění jednotlivých slabik v segmentu určuje offsetová část adresy (offset). Ta určuje, kolikátá je slabika od počátku segmentu. Adresa se skládá ze dvou částí: segment a offset. Obě tyto části jsou šestnáctibitové. Protože ale pro adresování paměti je nutné dvacet bitů, jsou za segmentovou adresu vyjádřenou binárně přidány čtyři bity s hodnotou nula (proto každý segment začíná na násobku šestnácti). K tomuto dvacetibitovému číslu je potom přičteno šestnáctibitové číslo určující offsetovou adresu. Takto vzniká dvacetibitové číslo znamenající skutečné umístění slabiky v paměti (fyzická adresa).
Výpočet skutečné adresy dvojkově:
segment:ssssssssssssssss0000
+offset:0000oooooooooooooooo
--------------------
adresa:aaaaaaaaaaaaaaaaaaaa
(segment jsou jednotlivé bity segmentové části adresy doplněné na konci o čtyři nuly, offset jsou jednotlivé bity offsetové části adresy doplněné na začátku o čtyři nuly, adresa je součet, tedy jednotlivé bity skutečné adresy)
Výpočet skutečné adresy hexadecimálně:
segment:ssss0
+offset:0oooo
-----
adresa:aaaaa
(segment jsou jednotlivé cifry segmentové části adresy doplněné na konci o jednu nulu, offset jsou jednotlivé cifry offsetové části adresy doplněné na začátku o jednu nulu, adresa je součet, tedy jednotlivé cifry skutečné adresy)
Například: Místo v paměti s adresou segmentu $AB1E a offsetu $1111 má skutečnou adresu:
segment:AB1E0
+offset:01111
-----
adresa:AC2F1
Tento způsob adresace umožňuje snadný přenos programu v paměti a jeho schopnost pracovat v každé její části. Program si pro svoji činnost vyčlení segment pro data, zásobník a strojový kód (instrukce). Na tyto bloky ukazují jednotlivé segmentové registry.
Důsledky segmentace:
Pochopení segmentrace paměti je spíše ve znalosti dvojkové a šestnáctkové číselné soustavy.
POZOR!!! Neměli bychom zaměňovat pojmy segment a selektor. Segment určuje jen umístění bloku paměti. Selektor je použit u vyšších typů procesorů a jedná se vlastně o pořadové číslo v tabulce, která nese informace o vyčleněných místech paměti a jejich vlastnostech.
Z hlediska programátora jsou nejdůležitější registry. Ty se dělí na
Tyto registry se nastavují automaticky, jestliže proběhla instrukce, která je nastavuje. Registr F je doplněn i třemi řídicími registry, které ovlivňují běh programu:
Tyto tři registry může nastavit jen programátor vhodnými instrukcemi. Mikroprocesor je sám nenastavuje. Jestliže s registrem příznaků jako s celkem pracujeme, je šestnáctibitový a má tvar: X, X, X, X, OF, DF, IF, TF, SF, ZF, X, AF, X, PF, X, CF (bity X nejsou obsazeny)
Mikroprocesor musí být schopen pracovat i se vstupy-výstupy. Umístění jednotlivých portů určuje šestnáctibitová adresa umístěná nejčastěji v registru DX. Pro programátora je důležitá i ta skutečnost, že si mikroprocesor vytváří tzv. frontu instrukcí. Jedná se o šest slabik znamenajících několik instrukcí, které budou následovat po právě prováděné instrukci. Tato fronta je průběžně doplňována při operacích nezatěžujících sběrnice z paměti. Protože se ale jedná o deset za sebou jdoucích slabik v paměti, je při instrukcích skoku v paměti vyprázdněna. Z tohoto důvodu je vhodné, aby program obsahoval co nejmenší počet skoků. Proto je v poslední době kladen důraz na programovací jazyky, které podporují tzv. strukturované programování bez nepodmíněných skoků. Mezi ně (částečně) patří Turbo Pascal a C. Je jasné, že programovací jazyk Basic se v tomto smyslu k mikroprocesoru nechová moc šetrně a zpomaluje tak běh programu. Mikroprocesor 80286 je strukturou i vlastnostmi podobný 8086. Je schopen pracovat ve dvou režimech. V základním reálném téměř přesně simuluje obvod 8086. Přesto v tomto režimu přináší některá rozšíření pro některé instrukce. Pokud v následujícím výkladu použiji rozšíření instrukcí pro 80286, uvedu to poznámkou [286]. Zdrojový text programu sestaveného i s pomocí instrukcí 80286 ve vkládaném assembleru stačí na prvním řádku (před uses) označit direktivou {$G+}.
Vzhledem k jednoduchosti a názornosti se programovací jazyk Turbo Pascal vyučuje na školách. My se budeme zabývat tzv. vkládaným assemblerem. Jeho znalost umožní zrychlit námi psané programy, a přitom využívat výhod Pascalu ve snadném zápisu algoritmu. Vkládaný assembler je blok v programu psaném v jazyce Pascal. Tento blok začíná klíčovým slovem Asm a končí end. Řádky programu ve vkládaném assembleru se nečíslují a nemusí končit středníkem v případě, že na jednom řádku není více jak jedna instrukce (při více jak jedné instrukci musíme instrukce středníkem oddělit). Komentáře se píší do složených závorek, nesmějí však být uvnitř označení instrukce. Ve vloženém assembleru můžeme měnit obsahy registrů AX, BX, CX, DX, SI, DI, ES, F. Před návratem z bloku asm musíme obnovit hodnoty v registrech BP,SP, SS, DS.
Každý program musí být schopen přesunů dat a to mezi registry, registry a pamětí, registry a vstupy/výstupy. Při této operaci si musíme vždy uvědomit, kolikabitové číslo přesouváme. Počet bitů je většinou specifikován jménem použitého registru (osmibitové - AH, AL, BH, BL, . . ., šestnáctibitové - AX, BX, BP, DI, ES, DS . . .). V případě, že používáme jen paměť, specifikuje počet bitů pro operaci označení:
Všechny přesuny tohoto typu provedeme univerzální instrukcí:
Použití této instrukce demonstruje příklad:
uses crt;
var slovo:word; {v paměti rezervuj 16 bitů a označ je
slovo}
slabika:byte;{v paměti rezervuj 8 bitů a označ je
slabika}
begin
asm
MOV
AL,10
{do registru AL dosaď 8 bitů, hodnotu 10}
MOV
slabika,AL
{do paměti na místo ozn. slabika dosaď obsah AL}
MOV
BX,10
{do registru BX (16 bitový) dosaď 10}
MOV
slovo, BX
{do paměti na místo ozn. slovo dosaď 16 bitů BX}
end;
writeln (slabika,' ',slovo);
readkey;
end.
Tento program má po překladu na místech proměnných v bloku asm označení paměťového místa, které pro ně bylo vyčleněno. Místo pro proměnné je vždy v segmentu globálních proměnných. Segmentová adresa tohoto bloku je vždy umístěna v registru DS. To, že DS ukazuje na segment dat programu, může vést k chybě, která spočívá v jeho změně a následném čtení z globálních proměnných. Takže pozor! Po změně registru DS je práce s globálními proměnnými nemožná, protože jsme si k nim uřízli cestu. Do segmentových registrů nejde dosadit hodnota přímo. Tu nejrychleji dosadíme tak, že ji vložíme do některého univerzáního registru a z něj teprve do segmentového registru (například MOV AX,adresa; MOV ES,AX).
Místo (offset) v paměti označuje vždy určitá hodnota zapsaná v hranatých závorkách. Instrukce MOV BYTE PTR ES:[$100F], 10 znamená: na adresu slabiky offset 100F ($ označuje použití hexadecimální soustavy) v segmentu určeném adresou v ES, dosaď hodnotu 10. Jestliže segment nespecifikujeme označením a dvojtečkou, vztahuje se adresa k segmentu v DS. V praxi by tato metoda omezovala programátora v rozletu. Proto ASM86 umožňuje i další metody adresace. Ale popořadě . . .
Jejich použití umožní zjistit adresu proměnných deklarovaných v části var (const . . .).
Příklad:
var promenna: byte;
begin
asm
MOV
BYTE
PTR
[offset
promenna], 10 {na adresu slabiky proměnné dosaď 10}
end;
end.
Segmentová adresa se v tomto příkladu nemusí určit. Je v DS, a ten se nemusí uvádět. Překladač Pascalu tuto metodu používá i pro naše globální proměnné. Při překladu je totiž každé proměnné přiděleno místo v paměti s pevnou offsetovou adresou (takže zápis OFFSET proměnná nese právě tuto adresu). Specifikace, jestli se jedná o slabiku, nebo slovo, je nutná, protože jinak by procesor nevěděl, jestli má číslem obsadit jednu, nebo dvě slabiky.
var promenna: byte;
begin
asm
MOV
BX,
offset promenna {do BX dosaď adresu proměnné}
MOV
BYTE
PTR
[BX], 10
{na její adresu dosaď hodnotu 10}
end;
end.
var pole: array [0..9] of byte;
begin
asm
MOV
SI, 0
{nuluj
registr SI}
MOV
BYTE
PTR
offset
pole[SI], 10{adr. pole sečti s SI a dosaď 10}
end;
end.
Program dosadí na první místo pole hodnotu. Protože registr SI můžeme zvyšovat, budeme tímto způsobem realizovat pohyb v poli.
var pole: array [0..9] of byte;
begin
asm
MOV
BX,
offset pole {do registru BX dosaď adresu
pole}
MOV
SI, 0
{do registru SI dosaď 0}
MOV
BYTE
PTR
[BX][SI],
10 {na první prvek v poli ulož 10}
end;
end.
V assembleru mikroprocesoru 8086 se objevuje i nový výraz. Prefix znamená určitou specifikaci pro následující instrukci. Zatím jsme si ukázali, jak změnit specifikaci segmentového registru adresy s pomocí jeho označení a dvojtečky. Dalším způsobem je použití prefixu změny segmentu: SEGDS, SEGES, SEGCS, SEGSS. Tato označení jsou prefixy přeskočení (změny segmentu) pro jednotlivé segmentové registry. Například: MOV AX, ES:[BX] je stejné, jako bychom použili SEGES MOV AX, [BX] (i když zápis je různý, kód programu bude po překladu stejný).
Zásobník je část v paměti počítače vyhrazená k odkládání dat. Je organizovaná tak, že data, která jsou uložena naposledy, vyjímáme jako první. Na vrchol zásobníku ukazují adresy uložené v registrech SS a SP (případně BP). Přidáváním dat do zásobníku se offset v SP automaticky snižuje o dvě (a naopak). Musíme si tedy uvědomit, že do zásobníku můžeme odkládat jen šestnáctibitová data. Pro práci se zásobníkem slouží instrukce:
Příklad:
var promenna:word;
begin
promenna:=10; {do paměti na adresu proměnné dosaď
10}
asm
MOV
AX, promenna
{obsah proměnné dosaď do registru AX}
MOV
BX,$BBBB
{do regisru BX dosaď číslo}
PUSH
AX
{ulož obsah AX}
PUSH
BX
{ulož obsah BX}
MOV
AX,$AAAA
{přepiš obsah AX}
MOV
BX,$CCCC
{přepiš obsah BX}
POP
BX
{obnov obsah BX}
POP
AX
{obnov obsah AX}
MOV
promenna, AX
{vrať obsah AX do proměnné}
end;
end.
Tento program naznačuje postup ukládání a vybírání dat do a ze zásobníku. V zásobníku jsou uloženy i lokální proměnné procedur a funkcí. Jsou zde i parametry, kterými je podprogram volaný. (Proto lokální proměnné NEMAJÍ segmentovou adresu v DS.) Občas potřebuje programátor uložit registr příznaků F, aby ho později mohl obnovit do původního stavu. K tomu požíváme instrukci PUSHF (pro uložení) a POPF (pro obnovení). Jestliže ve vkládaném assembleru chceme měnit některý ze "zakázaných" registrů (DS, BP), můžeme si jeho obsah uložit do zásobníku. Podmínkou je ale to, že nezměníme registry SS, SP. Tím bychom si podřízli větev pod sebou. Další možné použití zásobníku je při práci s částí pamětí, ve které máme pole slov (šestnáctibitových dat). Nasměrováním vrcholu zásobníku (SS:SP) na konec tohoto pole můžeme instrukcemi PUSH a POP s tímto polem pracovat. Přitom se bude automaticky zvyšovat a snižovat adresa. Pozor ale, obsahy SS a SP je nutné zase uschovat, nejlépe do paměti na místa proměnných. V tom případě ale nemůžeme měnit registr DS (ES).
Každý se někdy pokusíme zapsat na port a číst z něj. Je dobré si uvědomit, že můžeme zapisovat osm i šestnáct bitů. Každý port, stejně jako slabika v paměti, má svojí adresu. Při zápisu šestnácti bitů zapisujeme tedy i na port s adresou o jednu vyšší. Práci s porty provedeme instrukcemi:
Data se čtou, nebo zapisují z (do) registru AL (osmibitový přístup), AX (šestnáctibitový přístup). Adresu portu specifikuje buď přímo adresa (IN AL, $0F) při adrese osmibitové (spodních 256 portů), nebo registr DX, ve kterém je šestnáctibitová adresa (MOV DX, $F10; OUT DX, AL).
Mezi přesuny dat patří i:
I když jsme si již popsali, jak dosadit hodnotu adresy do některého z adresových registrů, nebyly možnosti ještě vyčerpány. Nejjednodušší je použití instrukce:
Paměť je v tomto případě označena jako v instrukci MOV. Instrukce LEA BX, BYTE PTR [$FF00] a MOV BX, $FF00 jsou ekvivalentní. Protože druhá instrukce je jednodušší, neměla by instrukce LEA význam. Proto ji častěji použijeme při hledání hodnoty kombinované adresy (LEA DI, 100[BX][SI] - sečte registry s číslem 100 a dosadí výsledek do DI). Pro nás má význam i ve vkládaném assembleru. Zápis LEA BX, proměnná je jednodušší něž MOV BX, offset proměnná (i když instrukce vykonají stejnou práci).
Příklad:
var pole: array [0..9] of byte;
begin
asm
LEA
BX, pole
{do registru BX dosaď adresu pole}
MOV
BYTE
PTR
[BX],10 {na první
místo v poli napiš 10}
end;
end.
Zatím jsme ovlivňovali jen registry s offsetem. Přestože bychom byli schopni dosadit i segment, bylo by nutné použít nejméně tři instrukce (nezapomeňte, že MOV neumí dosadit hodnotu do segmentového registru přímo). Abychom pochopili úspornější instrukci, musíme si zopakovat pojem ukazatel.
Je typ proměnné, který nese celou adresu určitého místa v paměti. S pomocí těchto proměnných můžeme potom dosazovat hodnoty na místa, kam ukazují. Častěji myslíme označením ukazatel právě tyto proměnné.
Příklad:
var cislo:byte; {vyčleň v paměti slabiku,
označ jí číslo}
ukazatel:^byte; {vyčleň v paměti čtyři slabiky,
které ponesou}
{adresu na proměnnou
typu byte, označ je ukazatel}
begin
ukazatel:=@cislo; {ukazateli přiřaď adresu proměnné
číslo}
ukazatel^:=10; {na místo kam směřuje ukazatel
zapiš 10}
writeln ('Hodnota proměnné číslo:',cislo,'=',ukazatel^);
{vypiš}
end.
Kromě ukazatelů na daný typ existují i ukazatele obecně (typ pointer). Tyto typy jsou pro nás důležité. Čtyři slabiky, které jsou pro proměnnou tohoto typu vyčleněny, nesou totiž segment i offset adresy, kam ukazatel směřuje. V assembleru existují dvě instrukce, které jsou schopny adresy uložené v ukazateli dosadit do registrů segmentu i offsetu:
Příklad:
var promenna: byte; {v paměti vyčleň slabiku s
označením proměnná}
ukazatel: poiter; {v paměti vyčleň čtyři slabiky pro
ukazatel}
begin
ukazatel:=@promenna; {nasměruj ukazatele na proměnnou}
asm
LES
BX, ukazatel
{nastav ES:BX na adresu proměnné}
SEGES
MOV
BYTE
PTR
[BX],10 {zapiš na tuto adresu}
end; writeln (promenna); {vypiš obsah proměnné}
end.
Programátor při své činnosti potřebuje nejen přesuny dat. V každém programu jsou nutné i výpočty a to s běžnými daty, nebo s adresami. Ty se v assembleru provádějí jen s celými čísly. Operace s desetinnými čísly jsou zdlouhavé, i když jsou proveditelné pomocí určitých algoritmů. ASM86 pro ně ale nemá instrukce. Většina matematických operací se provádí s čísly v registrech nebo v paměti. Označení operandů je shodné jako při přesunech. Zároveň tyto instrukce nastavují indikátory registru F. Umožní tak větvit program. Informace o nastavovaných indikátorech najdeme v tabulce instrukcí (+).
Při tvorbě programu si musíme ujasnit, jestli chceme k cílovému místu přičíst 1, nebo jiné číslo. Podle toho volíme instrukci:
Příklady:
INC AX - přičti
k registru AX hodnotu 1
INC WORD PTR
[BX] - přičti k slovu na adrese určené DS:BX hodnotu 1
INC BYTE PTR
CS:[adresa] - přičti k slabice na adrese určené CS:adresa
(konstantní) 1
SEGES INC BYTE
[DI + 2] - přičti k slabice na adrese ES:DI + 2 hodnotu 1
ADD AX, BX - ke slovu v registru AX přičti
obsah registru BX (slovo)
ADD AH, 8 - k slabice v registru AH přičti
číslo 8}
SEGCS ADD DX, WORD
PTR [BX] - k registru DX přičti slovo na
adrese CS:BX
ADD promenna, 5 - k deklarované proměnné
přičti 5
ADD BYTE PTR
[SI], 30 - k slabice na adrese DS:SI přičti 30}
ADD BYTE PTR
ES:[BP], AL - k slabice na adrese ES:BP přičti obsah registru
AL
Pokud při těchto operacích dojde k přeplnění cíle, nastaví se registr OF do log. 1. Aby při odlaďování vašich programů nedošlo ke zbytečným hádkám s překladačem, uvědomte si, že zdroj i cíl musí mít stejný počet bitů (tzn. 8, nebo 16).
Instrukce sloužící k odčítání jsou zápisem operandů shodné s instrukcemi pro sčítání. Proto si uvedeme jen jejich seznam:
Přesto jsou zde specifické instrukce:
Instrukce CMP porovnává dvě čísla odečtením. Protože ale nedojde k jejich změně, použijeme tuto instrukci před větvením programu. Za CMP totiž většinou následu+jí instrukce skoku závislé na stavu příznaků registru F.
Příklad:
uses crt;
var a,b,s,r:integer;
begin
clrscr; {vymaž obrazovku}
write ('a=');
readln(a); {vstup hodnoty a}
write ('b=');
readln(b); {vstup hodnoty b}
asm {začátek bloku asm}
MOV
AX, a
{do AX vlož hodnotu proměnné a (z paměti)}
ADD
AX,b
{k AX přičti hodnotu proměnné b}
MOV
s, AX
{do proměnné s vlož součet z registru AX}
MOV
AX,a
{znovu naber a}
SUB
AX,b
{odečti od AX hodnotu b}
MOV
r,AX
{do proměnné r vlož rozdíl z registru AX}
INC
a
{k a přičti 1}
DEC
b
{od b odečti 1}
end; {konec bloku asm}
writeln ('a+b=',s,' a-b=',r);{vypiš obsahy proměnných}
writeln ('a+1=',a,' b-1=',b);
end.
Uvedený příklad ukazuje nejjednodušší použití instrukcí ADD, SUB, INC, DEC. Všimněte si, že se zápisy adres proměnných si nemusí programátor ani moc lámat hlavu. V tom mu totiž pomáhá překladač Pascalu.
I když programátoři neradi používají instrukce násobení a dělení pro jejich dlouhou dobu provádění (na procesoru 8086, u jiných procesorů je už rychlé), ASM86 je má. Někdy dokonce neexistuje jiná možnost než je použít. I tyto operace jsou definovány jen na celých číslech. Rozlišujeme také, jestli je provádíme se znaménkem, nebo bez znaménka.
POZOR!, o kolikabitové násobení se jedná určuje označení místa zdroje.
Tato operace je jednou z nejzdlouhavějších. Její provádění trvá (na 8086) až 190 period hodin (sčítání trvá kolem 3 period). Jeho výhodou je ale to, že je možné zjistit jak výsledek po celočíselném dělení (DIV), tak i zbytek po celočíselném dělení (MOD). A to všechno jen jednou instrukcí.
Příklad:
uses crt;
var a,b,d,z:byte;
s:word;
begin
clrscr;
write ('a=');
readln (a);
write ('b=');
readln (b);
asm
MOV
AL,a {do AL
vlož hodnotu a}
MUL
b
{vynásob hodnotou b (v paměti)}
MOV
s,AX {do
proměnné s vlož součin z registru AX}
MOV
AH,0 {nuluj AH
(číslo je jen 8 bitové)}
MOV
AL,a {do AL
vlož hodnotu a}
DIV
b
{vyděl proměnnou b}
MOV
d,AL
{výsledek vlož do proměnné d}
MOV
z,AH {zbytek
po dělení vlož do proměnné z}
end;
writeln ('a*b=',s);
writeln ('a div b=',d,' a mod b=',z);
readkey;
end.
Často potřebujeme opravit šestnáctibitové číslo na osmibitové a naopak. Při této změně může ale dojít ke ztrátě informace v případě úbytku bitů. Převod čísel bez znaménka provedeme nejjednodušeji využitím půlení registrů.
var b:byte;
w:word;
begin
b:=10;
asm
MOV
AL,
b {do AL osm bitů z proměnné b}
MOV
AH, 0
{nuluj AH}
MOV
w, AX
{do proměnné w vlož všech šestnáct bitů}
end;
end.
Čísla v BCD kódu mohou být uložena v těchto formátech:
Logické instrukce jsou jednou z dobrých pomůcek programátorů. ASM86 je schopen provádět všechny běžné logické operace, a to se slovem nebo slabikou. Chybí zde tedy instrukce pro jednotlivé bity. Ty však volbou vhodných algoritmů můžeme lehce nahradit.
Kolikabitová operace je, určuje opět specifikace zdroje a cíle. Instrukci TEST použijeme k nastavení příznakového registru, a tak můžeme větvit program, aniž bychom ovlivnili hodnoty zdroje a cíle.
Vymaskování slabiky nebo slova
Často potřebuje programátor nastavit některé bity slabiky,
nebo slova do hodnoty log. 1, nebo 0. K tomu mu velmi dobře
poslouží právě logické operace AND nebo OR. Máme-li slabiku
ve tvaru XXXXAXXX v registru AL a chceme, aby bity X měly
hodnotu 0 a hodnota bitu A zůstala zachována, provedeme
instrukci AND AL, $08 (=00001000). Máme-li
slabiku ve tvaru XXXXAXXX v registru AL a chceme, aby bity X
měly hodnotu 1 a hodnota bitu A zůstala zachována, provedeme
instrukci OR AL, $F7 (=11110111).
Nulování registru
Zajímavější než instrukce MOV
registr,0 je nulovat pomocí XOR registr,
registr. Efekt je stejný, doba vykonání operace je kratší.
Zjištění zbytku po
celočíselném dělení mocninami 2
Kdybychom vždy, když chceme zjistit zbytek po dělení
mocninami 2 (a ten často potřebujeme) používali instrukci DIV,
program bychom zdržovali. Stačí si jen uvědomit, že můžeme
zjistit hodnotu bitů v řádech za log. 1 v binárním tvaru
dělence. Chceme-li zjistit zbytek po celočíselném dělení 2
(sudé, liché číslo) čísla v registru AL, stačí jen
použít instrukci AND AL,1. V registru AL je
potom jen buď 1 (liché číslo), nebo 0 (sudé číslo). Pro
lepší orientaci poslouží přehled:
Převod čísla v nezhuštěném
BCD na ASCII
Velmi jednoduchým prostředkem, jak převést číslo v rozsahu
0-9 do hodnoty jeho znaku v tabulce ASCII, je logický součet s
číslem $30 (to je stejné jako přičtení 48). Použitím
této úpravy čísel v kódu BCD je zobrazení i velkých
čísel jednoduché.
Příklad:
var slabika:byte;
znak:char;
begin
repeat
readln (slabika);
until slabika in [0..9];
asm
MOV
AL, slabika
{do registru AL předej hodnotu slabiky}
OR
AL,
$30 {převeď na ASCII}
MOV
znak, AL
{do proměnné znak předej ASCII hodnotu čísla}
end;
writeln (znak);
end.
V příkladu je načtené číslo z intervalu 0..9 převedeno do ASCII s pomocí log. instrukce OR.
Před dalším příkladem si musíme vysvětlit, jak ukládá Pascal řetězce (typu string). Za řetězec je zde považováno pole slabik, které má na prvním místě délku řetězce a na dalších místech jsou kódy ASCII zapsaných znaků. Informace o délce řetězce je důležitá pro jeho správné zobrazení. Ten proto nemusí obsahovat speciální ukončovací znak.
Příklad:
var slovo:string;
i:byte;
begin
for i:=0 to 9 do
asm
MOV
DI,
OFFSET
slovo {do registru DI ulož adresu proměnné slovo}
INC
DI
{posuň se až za slabiku délky
řetězce}
XOR
AH,AH
{nuluj AH}
MOV
AL,i {do AL vlož krok i}
ADD
DI,AX
{přičti krok k adrese (posuv po
řetězci)}
OR
AL,$30
{převeď obsah AL na ASCII znak}
MOV
[DI],AL
{přesuň znak do řetězce}
INC
BYTE
PTR
[OFFSET
slovo]{zvyš délku řetězce}
end;
writeln (slovo);
readln;
end.
Tento příklad vytvoří slovo typu string s čísly od 0 do 9. To, že zatím nevíme, jak se v ASM86 tvoří cykly, není na závadu. Prostě si pomůžeme znalostmi z Pascalu.
Kódování
Každý rád chrání svá data před neoprávněným
přístupem kódováním. K tomu dobře slouží logická operace
XOR. Postup kódování naznačuje postup. Provedeme-li operaci
XOR s konstantou a kódovaným číslem, získáme kódované
číslo. Pokud s kódovaným číslem provedeme opět XOR se
stejnou konstantou, získáme zpět původní číslo. Čísla
kódovaná přidáme do EXE souboru programu. Před jejich
použitím je dekódujeme. Protože tato čísla mohou nést
např. jméno autora (v ASCII), je jméno pro běžného
uživatele po zakódování nečitelné (a tedy lehce
nepřepsatelné v souboru EXE). Pozor! Hodnota konstanty musí
být při kódování i dekódování stejná. Tento postup
můžeme libovolně pozměňovat podle úrovně našich znalostí
(např. xorovat první znak s druhým, druhý s třetím, . . .).
Tyto instrukce jsou dobrým pomocníkem každému, kdo je umí používat. Jedná se o bitový posuv uvnitř slabiky, nebo slova. Počet bitů posuvu je specifikován použitým registrem, nebo označením paměťového místa.
Posuvy:
Rotace:
Kontrola jednotlivých
bitů
Jestliže potřebujeme zkontrolovat, jakou hodnotu
některý z bitů nese, stačí slovo nebo slabiku rotovat přes
registr CF. Hodnotu, kterou bit nese, potom zjistíme kontrolou
registru CF.
Tvorba masky
Jestliže nevíme, jak vytvořit slabiku nebo slovo pro
vymaskování, použijeme instrukci posuvu. MOV AL,
1; SHL AL, 3. Takto získáme slabiku s
nastaveným bitem na čtvrtém místě (00001000).
Celočíselné dělení
mocninou 2 a násobení konstantou
Je to nejdůležitější použití posuvů. Vychází z
faktu, že bitový posuv čísla doleva o jeden krok je stejný,
jako bychom číslo vynásobili dvěma. Naopak bitový posuv
čísla doprava o jeden krok je stejný, jako bychom číslo
dělili dvěma. Dělení: Do registru umístíme dělence. Ten
potom posuneme doprava o tolik, kolikátou mocninou 2 je
dělitel:
Pozor! Toto dělení je sice velmi
rychlé, ale použitelné jen tehdy, jestliže chceme číslo
dělit mocninou 2 (a to bývá naštěstí nejčastěji). Ke
zjištění zbytku po celočíselném dělení použijeme operaci
AND (jak bylo popsáno výše).
Násobení čísla konstantou: Do tolika registrů, kolik je log.
1 v binárním vyjádření konstanty, umístíme hodnotu
čísla. Potom jednotlivé registry posuneme doleva. Každý o
tolik, na kolikátém místě byla log. 1 v binárním
vyjádření konstanty. Nakonec všechny registry přičteme k
jedinému, ve kterém bude výsledek.
Příklad: Vynásobme konstantou 18 vložené číslo:
Logická 1 je tedy na místě č.1 a č.4. Proto použijeme dva registry, ty posuneme o 1 a 4 kroky. Nakonec je sečteme.
{$G+}
var cislo:word;
begin
readln (cislo);
asm
MOV
AX,cislo
{naber číslo do prvního registru}
MOV
BX, AX
{naber číslo do druhého registru}
SHL
AX, 1
{v prvním registru jednou doleva<=>vynásob 2}
SHL
BX, 4
{v druhém registru čtyřikrát doleva<=>vynásob
16}
ADD
AX, BX
{sečti obsahy obou registrů}
MOV
cislo, AX
{vrať přes proměnnou cislo}
end;
writeln ('Číslo*18=',cislo);
end.
Uvedený postup můžete snadno převést na libovolnou konstantu. Vzhledem ke zdlouhavosti násobení instrukcí MUL vám tento algoritmus občas zrychlí program.
Následující příklad vytváří řetězec informací o čase. Ten si zjistí z paměti CMOS. Čtení provádíme tak, že na adresu portu $70 vyšleme číslo čtené slabiky (0 - sekundy, 2 - minuty, 4 - hodiny) v CMOS. Z portu $71 potom přečteme její hodnotu. Ta je v CMOS ve zhuštěném BCD tvaru. Proto ji převedeme na nezhuštěný a teprve potom na kód ASCII. Nakonec data zapíši do proměnné slovo typu string ve tvaru, v jakém je zvykem čas zapisovat. Program jsem optimalizoval tak, aby měl co nejmenší počet instrukcí. Vzhledem k tomu, že ve vloženém assembleru jsem nepoužil cyklus, tvořím jej s pomocí pascalovského for cyklu. Podobným způsobem bychom četli i jiné užitečné informace z paměti CMOS (datum, konfigurace . . .).
Příklad:
uses crt;
var i:byte;
slovo:string;
begin
slovo[0]:=#8;
slovo[3]:='.';
slovo[6]:='.';
clrscr;
repeat
for i:=0 to 2 do
asm
MOV
BX,offset
slovo {naber adresu proměnné slovo do BX}
XOR
AH,AH
{vymaž horní polovinu registru AX}
MOV
AL,i {naber do dolní poloviny AX krok i}
SUB
BX,AX
{odečti od BX obsah AX}
SHL
AL,1
{vynásob, AL:=AL*2}
SUB
BX,AX {odečti od BX obsah AX}
OUT
$70,AL
{pošli na CMOS adresu čtené slabiky}
IN
AL,$71
{přečti z CMOS obsah čtené slabiky}
MOV
AH,AL
{zkopíruj obsah přečtené slabiky do AH}
SHR
AH,4
{desítky posuň do dolní poloviny AH}
AND
AX,$0F0F
{odstraň zbytečné bity}
OR
AX,$3030
{proveď převod do ASCII}
MOV
8[BX],AL
{nastav jednotky v proměnné slovo}
MOV
7[BX],AH
{nastav desítky v proměnné slovo}
end;
gotoxy (1,1);
write(slovo);
until keypressed;
readkey;
end.
Protože si mikroprocesor vytváří frontu instrukcí, nejsou z hlediska rychlosti běhu programu skoky to pravé. Přesto bychom složitější programy bez nich asi těžko tvořili. Abychom mohli instrukce skoku používat, musíme umět vytvořit návěští.
Assembler je správně jen název překladače "Jazyka symbolických adres", který se pro něj čassem vžil. Název "Jazyk symbolických adres" vyjadřuje to, že místo adres instrukcí používáme symboly. V Turbo assembleru nejsme v názvech návěští nijak zvlášť omezováni. Ve vkládaném assembleru můžeme za název návěští použít posloupnost znaků začínající znakem @ (@1, @zacatek, @navesti). Jestliže používáme návěští, deklarované mimo vkládaný assembler (s pomocí LABEL), není přítomnost znaku @ nutná. Návěští s dvojtečkou uvedeme před instrukci, na kterou se odkazujeme. Při překladu je v místech odkazu na návěští jeho název nahrazen skutečnou adresou instrukce.
Je to nepodmíněný skok na jiné místo programu. To musí být označené návěštím. Za instrukcí skoku je potom uveden jeho název.
@navesti: instrukce na kterou bude odkaz
.
.
JMP
@navesti
Jestliže skoky používáme, hrozí vždy nebezpečí, že se program zacykluje (a nikdy neskončí). Proto je důležité si vždy rozmyslet, za jakých okolností by k této kolizi mohlo dojít.
Jedná se o skok podmíněný stavem jednoho nebo více, bitů registru příznaků F. Jen tímto způsobem je možné provádět v assembleru přímé větvení programu. Před instrukcí podmíněného skoku proto vždy provedeme instrukci, která použitý příznak nastaví. V případě, že není splněna podmínka skoku, pokračuje program dál, jako by se nic nedělo. Instrukce podmíněného skoku začínají vždy písmenkem J. Za ním je zkratka udávající na jakých bitech registru F je skok závislý.
Při hledání instrukce podmíněného skoku musíme myslet na to, za jakých okolností chceme skok vykonat. K tomu je také dobré si uvědomit:
Rozdíl čísel v tomto případě provedeme nejlépe instrukcí CMP. Pro tvorbu cyklu můžeme použít jeden z registrů, který si pro krokovací proměnnou vyčleníme. Jednoduchý cyklus pak vytvoříme podmíněným skokem:
begin
asm
MOV
CL, 10 {do
registru CL dosaď 10, počet kroků}
@nav: {návěští, tady umístíme opakovanou
činnost}
DEC
CL
{odečti od CL číslo 1}
JNZ
@nav
{jestliže není nula skoč na návěští}
end;
end.
Program opakuje skok dokud není v registru CL nulový výsledek.
ASM86 má i pro cyklus instrukci. Její použití však předpokládá to, že si rezervujeme registr CX pro čítání. Do něj před cyklem umístíme počet opakování. Instrukce LOOP pak cyklus umožní realizovat.
Příklad:
uses crt;
var pole:array [0..9] of byte;
i:byte;
begin
clrscr;
asm
XOR
DI, DI {nuluj
registr DI}
MOV
CX, 10 {do CX
dej délku pole}
@nav: {návěští, začátek cyklu}
MOV
BYTE
PTR
[DI+OFFSET pole],
cl{přesuň do pole na místo urč. DI}
INC
DI {na
další prvek pole}
LOOP
@nav
{odečti od CX 1, není-li nula na @nav}
end;
for i:=0 to 9 do
writeln (pole[i]);
readkey;
end.
Uvedený příklad naplní pole hodnotami 1-10. Obsah v registru CX je použit ke krokování, a současně se s ním plní pole. Prvky pole jsou slabiky. Proto se obsah registru DI zvyšuje o jednu. V případě, že by se jednalo o slova, musíme k registru DI přičítat 2. Cyklu vytvořenému pomocí LOOP se můžeme programově vyhnout instrukcí JCXZ návěští - jestliže je v CX nula přesuň se na návěští.
Příklad:
uses crt;
var pole1,pole2:array [0..9] of byte;
i:byte;
pocet:word;
begin
clrscr;
repeat {vstup počtu prvků kopie s kontrolou hodnoty počet}
write ('Zadej pocet kopirovanych prvku (0..10):');
{$I-}readln (pocet);{$I+}
until (ioresult=0) and (pocet in [0..10]);
randomize;
for i:=0 to 9 do
begin
pole1[i]:=random(256);
pole2[i]:=random(256);
end;
asm
MOV
CX, pocet
{do registru CX dej počet prvků kopie}
JCXZ
@konec
{jestliže je nulový jdi na konec}
MOV
SI,
OFFSET
pole1 {naber adresu pole1}
MOV
DI,
OFFSET
pole2 {naber adresu pole2}
@cykl: {začátek cyklu}
MOV
AL, [SI]
{do registru AL přesuň prvek z pole1}
MOV
[DI], AL
{z registru AL přesuň prvek do pole2}
INC
SI
{posuň se na další prvek v
polích}
INC
DI
LOOP
@cykl
{sniž CX o jednu, jestli je různé od nuly}
{skok na @cykl}
@konec: {konec bloku asm}
end;
for i:=0 to 9 do
writeln (pole1[i],'..',pole2[i]);
readkey;
end.
Až dosud jsme za podmínku opakování považovali nenulové číslo v registru CX. ASM86 však umožňuje podmínky opakování obohatit testováním příznaku ZF.
Při použití těchto instrukcí dáváme v programu možnost uniknout z cyklu i nastavením příznaku ZF. Nezapomeňte ale, že ZF se musí před koncem cyklu opět nastavit vhodnou instrukcí.
Registr příznaků se částečně nastavuje současně s vykonáváním některých instrukcí. Obsahuje ale i registry, které se automaticky nenastavují (IF, DF, TF). Proto ASM86 má instrukce, kterými můžeme přímo ovlivnit hodnoty některých bitů registru F.
Jestliže chceme nastavit hodnotu v příznaku, pro který instrukce neexistuje, použijeme algoritmus:
Příklad:
var promenna:byte;
begin
asm
MOV
promenna,0
{nastav proměnnou do hodnoty 0}
PUSHF
{ulož registr příznaků do zásobníku}
POP
AX
{přesuň obsah vrcholku zásobníku do
registru AX}
OR
AX,1
{nastav poslední bit (CF) do logické 1}
PUSH
AX
{ulož obsah AX do zásobníku}
POPF
{přesuň nazpátek do registru
příznaků}
JNC
@konec
{otestuj nastavení CF}
MOV
promenna,1
{CF byl v 1, nastav hodnotu proměnné do 1}
@konec:
end;
writeln (promenna); {vypiš obsah proměnné}
end.
Jednotlivé bity části registru příznaků můžeme také ovlivnit vhodným použitím instrukcí LAHF a SAHF.
Ne vždy je vhodné používat pro naše proměnné paměť hlavního programu. Možnost vyčlenit si několik slabik dává i vložený assembler. Ve skutečnosti se jedná o část paměti určenou pro strojový kód. My si ale do ní umístíme hodnoty, na které většinou nezbylo místo v registrech. Protože je tento blok v segmentu programu, musíme tento blok proměnných programově obejít. Mikroprocesor by totiž tyto hodnoty v paměti považoval za instrukce. Vyčlenit místo si můžeme pomocí direktiv:
Za direktivu považujeme příkaz pro překladač, není to tedy instrukce. S pomocí těchto direktiv říkáme překladači, aby v kódu programu rezervoval určitý počet slabik pro naše účely. Za tyto direktivy rovnou píšeme počáteční hodnoty slabik, slov a dvojslov oddělené čárkou. Pokud napíšeme jméno proměnné deklarované pomocí var nebo jméno procedury, jedná se o jejich adresy (za direktivou DW offsetová část adresy, za direktivou DD celá adresa, tedy ukazatel). Pro názornost si rovnou uvedeme program s těmito direktivami.
Příklad:
var promenna:byte;
begin
asm
JMP
@dal
@slabiky:
DB
10,
200,'M','Ahoj'
@slova:
DW
32000,'A',promenna
@dvojslova:
DD
promenna
@dal:
MOV
AL, CS:[
OFFSET
@slabiky] {do AL přesuň slabiku z adresy}
{@slabiky,
AL:=10}
MOV
AL, CS:[
OFFSET
@slabiky+1] {do AL přesuň slabiku}
{z
@slabiky+1, AL:=200}
MOV
AL, CS:[
OFFSET
@slabiky+2] {do AL přesuň hodnotu ASCII}
{znaku
'M'}
MOV
AL, CS:[
OFFSET
@slabiky+3] {do AL přesuň ASCII prvního znaku}
{řetězce
'Ahoj'}
MOV
AL, CS:[
OFFSET
@slabiky+4] {do AL přesuň ASCII druhého znaku}
{řetězce
'Ahoj'}
MOV
AX, CS:[
OFFSET
@slova] {do AX přesuň slovo z adresy}
{@slova,
AX:=32000}
MOV
AX, CS:[
OFFSET
@slova+2] {do AX přesuň hodnotu ASCII znaku}
{'A',
AH:=0,AL:=65}
MOV
BX, CS:[
OFFSET
@slova+4] {do BX přesuň offset proměnné}
{promenna}
MOV
BYTE
PTR
[BX], AL
{do této proměnné zapiš obsah}
{registru
AL}
LES
BX,CS:[
OFFSET
@dvojslova] {naber obsah ukazatele, tedy}
{celou
adresu proměnné do ES:BX}
SEGES
MOV
BYTE
PTR [BX], AL
{na celou adresu proměnné zapiš}
{obsah
AL}
end;
end.
Na takto vytvořená místa můžeme samozřejmě i zapisovat. Pokud nechceme používat návěští pro každou část, stačí si jen pamatovat, kolik místa zabere slabika, slovo, nebo dvojslovo. Potom se na hledanou část dostaneme přičítáním, nebo odčítáním určitých hodnot k offsetu návěští. Zajímavé je i využití adres proměnných. Protože proměnná za direktivou DD je celá adresa, můžeme naplnit instrukcí LES (LDS) oba registry, tedy segment i offset. Pokud zapíšeme DB 4, 'Ahoj', jedná se o klasický pascalovský řetězec z délkou na začátku.
ASM86 má velmi silný nástroj v řetězcových instrukcích. Za řetězec je zde na rozdíl od Pascalovského považován blok dat v paměti o téměř libovolné délce (podle definice jsme omezeni jen velikostí segmentu, to se ale dá snadno obejít). Pro použití řetězcových instrukcí jsou vyčleněny dvojice registrů, které nesou adresy:
V praxi to znamená, že vždy jeden blok v paměti je označen za zdrojový, druhý za cílový. Důležitou roli zde hrají i registry:
Řetězové instrukce pak jsou
Slovo zvýšit v těchto popisech činnosti nahradíme slovem snížit při DF = 1. Tyto instrukce umožní najednou provést určitou činnost a přitom aktualizují adresy podle stavu DF a podle toho, jestli pracujeme se slabikami nebo slovy.
Následující příklad využívá přímého zápisu do videopaměti (VRAM) v textovém režimu VGA k výstupu pascalovského řetězce. VRAM, začíná na adrese $B8000. Je organizovaná jako pole slov nesoucích informace o zobrazovaných znacích. Každé slovo nese slabiku atributů (barva znaku a jeho pozadí) a slabiku s ASCII kódem zobrazeného znaku. 80 slov VRAM je jeden řádek na obrazovce. Proto při zvýšení adresy $B8000 o 160 můžeme pracovat s druhým řádkem atd.
Příklad:
var slovo:string;
begin
slovo:='Ahoj';
asm
PUSH
DS
{ulož obsah DS do zásobníku, budeme ho měnit}
JMP
@dal
{obejdi data}
@vram:
DW
$0000,$B800
{offset:segment VRAM, Pozor! je to obráceně}
@adsl:
DD
slovo {adresa slova, ukazatel na něj}
@dal: {začátek programu}
LDS
SI,CS:[
OFFSET
@adsl] {DS:SI nasměruj na zdroj (na slovo)}
LES
DI,CS:[
OFFSET
@vram] {ES:DI nesměruj na VRAM}
XOR
CH,CH
{nuluj CH}
MOV
CL,[SI]
{do CL dej délku řetězce slovo, 1. slabiku}
INC
SI
{posuň se za slabiku s délkou}
MOV
AH,$6F
{do AH dej atributy nápisu}
@cyk: {cyklus pro znak po znaku}
LODSB
{naber kód znaku z řetězce do AL a zvyš
SI+1}
STOSW
{ulož obsah AX do VRAM, zvyš DI+2}
LOOP
@cyk
{sniž CX o jednu, není-li nula jdi na @cyk}
POP
DS
{vrať registr DS do původního stavu}
end;
end.
Uvedený program změní slabiku na slovo v registru AX s tím, že bude kód znaku doplněn o atributy. Jestliže změníme hodnotu v AH ovlivníme tím barvu výstupu.
Dosud známe jen prefix přeskočení. Prefix opakování se používá před řetězcovými instrukcemi a umožňuje tak jejich podmíněné i nepodmíněné opakování. Jejich použitím zrychlíme a zjednodušíme program. Nepodmíněným prefixem je
Tento prefix píšeme většinou před instrukci MOVSB (MOVSW). Jestliže máme nastavený registr CX na počet prvků řetězce a adresové registry zdrojového a cílového řetězce, zajistí REP jejich zkopírování na jednom řádku programu (např. REP MOVSB).
Příklad:
var slovo1,slovo2:string;
begin
slovo1:='Ahoj';
asm
PUSH
DS
{ulož do zásobníku obsah DS, změníme ho}
JMP
@dal
{skoč na začátek, obejdi data}
@adr:
DD
slovo1,slovo2
{definice ukazatelů na pole}
@dal:
LDS
SI,CS:[
OFFSET
@adr] {naber adresu zdrojového řetězce}
LES
DI,CS:[
OFFSET
@adr+4] {naber adresu cílového řetězce}
XOR
CH,CH
{nuluj CH}
MOV
CL,[SI]
{do CL dej délku řetězce}
INC
CX
{pascalovský řetězec
nese o slabiku více}
REP
MOVSB
{kopíruj řetězce po
slabikách}
POP
DS
{vrať obsah DS ze
zásobníku}
end;
writeln (slovo1,' ',slovo2);
readln;
end.
V příkladu kopírujeme jen tolik prvků, kolik má zdrojové slovo slabik. Tuto informaci si zjistíme z první slabiky proměnné slovo1. K tomu musíme ještě přičíst 1, protože pascalovský řetězec nese navíc informaci o délce. I když veškeré přesuny se odehrávají v datovém segmentu s adresou v DS, je dobré si zvyknout na to, že vždy, když měníme DS, ukládáme jeho obsah pro jistotu do zásobníku.
Řetězcové instrukce vyhledání a porovnání využívají registr příznaků ZF. Proto ASM86 obsahuje navíc prefixy podmíněného opakování:
Příklad:
uses crt;
var pole:array [0..9] of word;
hledany,pozice:word;
i:byte;
begin
clrscr;
randomize;
for i:=0 to 9 do
pole[i]:=random(65535); {do pole náhodná čísla}
hledany:=pole[random(10)]; {vyber hledané číslo}
writeln ('Hledam:',hledany);
asm
JMP
@zac
{skok na začátek}
@adr:
DD
pole
{definice ukazatele na pole}
@zac:
MOV
AX,hledany
{do AX vlož hledané číslo}
MOV
CX,10
{do CX vlož délku řetězce
(pole)}
LES
DI,CS:[
OFFSET
@adr] {naber adresu řetězce}
REPNE
SCASW
{opakuj do shody porovnání}
MOV
pozice,9
{spočítej kolikátý je hledaný,}
SUB
pozice,CX
{k tomu použiješ to, co zbylo v CX}
end;
for i:=0 to 9 do
begin
if i<>pozice then textcolor(15) else textcolor(12);
writeln (pole[i]);
end;
readkey;
end.
Tento program vyhledá slovo v poli. K tomu slouží jen řádek REPNE SCASW. Ten opakuje pohyb po poli, dokud nenajde shodu s hodnotou v registru AX (ta se projeví nastavením ZF do 1) . K zjištění pozice hledaného dobře poslouží zbytek v registru CX. Kdyby byl zbytek nulový, hledaný prvek by v poli nebyl.
Příklad:
uses crt;
var slovo1,slovo2:string;
ukazatel:pointer;
i,misto,delka:word;
begin
slovo1:='Nazdar programátoři! '+
'Zkuste vyhledat nějaké slovo z této
věty.';
slovo2:='slovo';
delka:=length(slovo2);
asm
PUSH
DS {ulož DS, budeme ho měnit}
JMP
@dal
{přeskoč data}
@ukp:
DD
slovo1,slovo2
{ukazatele na řetězce}
@dal:
LDS
SI,CS:[
OFFSET
@ukp] {naber adresu zdroje}
INC
SI
{přeskoč délku řetězce}
@cyk:
LES
DI,CS:[
OFFSET
@ukp+4]{naber adresu cíle, hledaného slova}
INC
DI
{přeskoč slabiku s délkou
řetězce}
MOV
CX,delka
{do CX vlož délku řetězce}
REPE
CMPSB
{opakuj do neshody (konce
hledaného)}
JZ
@konec
{byla shoda tak na konec}
SUB
SI,delka
{nebyla shoda tak se v SI vrať}
INC
SI
ADD
SI,CX
{k návratu v SI použij zbytek v
CX}
JMP
@cyk
{a znovu hledat}
@konec:
POP
DS
{vrať obsah DS, už ho
nebudeme měnit}
MOV
misto,SI
{vypočítej místo v prohledávaném}
MOV
SI,CS:[
OFFSET
@ukp] {k tomu použiješ délku řetězce zdroje}
ADD
SI,delka
{délku cíle, tedy hledaného}
SUB
misto,SI
end;
clrscr;
for i:=1 to length(slovo1) do
begin
if not(i in [misto..misto+delka-1]) then
textcolor (15)
else
textcolor(12);
write(slovo1[i]);
end;
readkey;
end.
V příkladu prohledáváme řetězec slovo1. Hledáme v něm umístění podřetězce slovo2. Program má dva cykly v sobě. První zajišťuje pohyb po prohledávaném řetězci v případě neshody (je realizován JMP). Druhý vnitřní zajišťuje pohyb po prohledávaném s kontrolou s hledaným (je realizován REPE). V případě shody je po cyklu REPE v registru ZF = 1 (prostě nevyskočil neshodou ale nulou v CX=> konec hledaného slova a shoda). Proto cyklus prohledávání ukončíme podmíněným skokem JZ na konec. Zde ze zjistí adresa v prohledávaném řetězci. To je ale adresa za posledním znakem shody. Proto se vrátíme nazpátek o délku slova (tam je hledané slovo).
Když firma Intel navrhovala mikroprocesor 8086, byly vloženy do instrukčního souboru i instrukce, které nebyly oficiálně uvedeny v tabulkách. Přesto je metodou pokusů programátoři objevili. Ve svých programech můžeme tyto instrukce používat. Máme však následující omezení:
Seznam a funkci pro nás použitelných nedokumentovaných instrukcí najdete v tabulce instrukcí.
V úvodu jsem upozornil na to, že využití vkládaného assembleru je v tvorbě podprogramů. Předem si ale musíme ukázat, jak se podprogramy volají.
Volání podprogramu spočívá v uložení parametrů do zásobníku a změně adresy v registru IP (čítač instrukcí) na adresu podprogramu s tím, že je uschována adresa odkud provádíme volání (to aby procesor věděl kam se má vrátit). Parametry do zásobníku ukládáme my, zbytek zařídí instrukce CALL.
V hlavičce procedury (nebo funkce) najdeme téměř vždy definici parametrů volaných:
Například: procedure
soucet (a,b:word;var c:word);
je definice procedury s názvem součet s parametry a, b
volanými hodnotou a c volaným odkazem. Při volání této
procedury z některé části programu psaném v Pascalu na
místa a, b zapíšeme konkrétní hodnoty (nebo proměnné (ty
ale podprogram nezmění) s těmito hodnotami) a na místo c
zapíšeme proměnnou, ve které najdeme hodnotu po provedení
procedury (např. soucet (1,3,promenna_c);
). Z místa volání předáváme parametry
do podprogramů vždy přes zásobník v pořadí definice v
hlavičce podprogramu. Do zásobníku před voláním procedury
ukládáme odlišně u parametrů volaných hodnotou a odkazem.
Musíme rozlišovat volání blízkého podprogramu a vzdáleného. Za vzdálený v tomto případě považujeme podprogram s adresou v odlišném segmentu. I když se pro programátora nic nemění je dobré vědět, že při vzdáleném volání se mění nejen IP, ale i CS. Označení místa skoku nese tedy navíc informaci o segmentové adrese. Skok do podprogramu zajistí instrukce
Ukončení samotného podprogramu zajistí instrukce
Jednoduše napíšeme instrukci CALL se jménem podprogramu (tedy procedury nebo funkce). Ostatní zařídí překladač, který zjistí, jestli se jedná o blízké nebo vzdálené volání. Podle toho dosadí adresu. Návrat si opět zařídí překladač při ukončení podprogramu.
Příklad:
{$G+}
uses crt;
procedure pocitej (a,b:word;var c,d:word);
begin
c:=a+b;
d:=a-b;
end;
var a_,b_,c_,d_:word;
begin
a_:=40;
b_:=5;
clrscr;
asm
PUSH
a_
{proceduře posíláme hodnotu a_}
PUSH
b_
{proceduře posíláme hodnotu b_}
LEA
DI,c_
{zjistíme adresu proměnné c_}
PUSH
DS
{do zásobníku segment adresy c_}
PUSH
DI
{do zásobníku offset adresy c_}
LEA
DI,d_
{to samé pro d_}
PUSH
DS
{stejný segment}
PUSH
DI
{offset d_}
CALL
pocitej {a
zavoláme počítej}
end;
writeln (a_,'+(-)',b_,'=',c_,'(',d_,')');
readkey;
end.
Stejnou posloupnost instrukcí jako blok asm v tomto programu provede řádek počítej (a_,b_,c_,d_);
Funkce je podprogram, který vrací jednu hodnotu typu uvedeného v záhlaví. Vracenou hodnotu zjistíme po návratu z funkce vždy v registrech:
Pokud funkce vrací řetězec, musí být volána i s adresou místa, kam má výsledný řetězec zapsat.
Příklad:
{$G+}
uses crt;
function bez1 (a:word):word;
begin
bez1:=a-1;
end;
var a_,c_:word;
begin
a_:=40;
clrscr;
asm
PUSH a_ {posíláme hodnotu a_}
CALL bez1 {zavoláme }
MOV c_,AX {slovo si vyzvedneme v registru AX}
end;
writeln (a_,'-1=',c_);
readkey;
end.
Bloky programu, které vykonávají činnost často se opakující, nazveme podprogramem. Jejich použitím zjednodušíme program. Za podprogramy pokládáme procedury a funkce. Pascal umožňuje vkládat assembler i do obyčejných podprogramů. Můžeme také tvořit podprogramy pouze v assembleru. To vyjádříme zápisem assembler za definici procedury nebo funkce. Ty potom neobsahují klasické vymezení bloku begin...end, stačí jen assemblerovské asm..end (pokud tedy tvoříme podprogram jen v assembleru, uvedeme za definici označení assembler, blok vymezíme asm...end). S parametry pracujeme v podprogramech v souladu s tím, jak jsme je přes zásobník předávali. To znamená, že k parametrům volaným hodnotou přistupujeme jako ke klasickým proměnným, k parametrům volaným odkazem přistupujeme jako k ukazatelům (dosazujeme jejich adresu instrukcí LES, LDS).
V okamžiku vstupu do podprogramu se na vrcholu zásobníku automaticky vytvoří místa pro lokální proměnné definované v části var podprogramu. V případě, že se jedná o pascalovskou funkci (není označena slovem assembler v definici), je navíc vložena speciální proměnná @RESULT určená k předání funkční hodnoty (ta je i stejného datového typu). Před návratem z funkce je obsah proměnné @RESULT automaticky předán do registrů předepsaných pro návrat hodnoty (pokud tedy tvoříme funkci s vloženým assemblerovským blokem, předáme funkční hodnotu do proměnné @RESULT, ve funkci s označením assembler vracíme funkční hodnotu v registrech, ve kterých funkční hodnotu očekává volající (AL, AX,..), jak bylo uvedeno v části o volání podprogramů). Lokální proměnné používáme stejně jako globální (s tím rozdílem, že jejich segmentová adresa není v DS).
Registr BP je v době vykonávání podprogramu nasměrován na vrcholek zásobníku v okamžiku vstupu do něj. Proto použitím nepřímé bázové adresace s pomocí tohoto registru můžeme přistupovat k:
Vzhledem k tomu, že se o tyto přepočty adres může postarat překladač, je jednodušší používat pro přístupy k proměnným a parametrům jen jejich symboly uvedené v definici podprogramu nebo části var.
Příklad:
uses crt;
procedure pocitej (a,b:word;var c,d:word);assembler;
asm
MOV
AX,a
{do registru ax, vlož hodnotu a}
ADD
AX,b
{přičti b}
LES
DI,c
{do ES:DI vlož adresu c (to je výstup součtu)}
MOV
ES:[DI],AX {na
adresu ES:DI zapiš součet}
MOV
AX,a
{to samé pro rozdíl}
SUB
AX,b
LES
DI,d
MOV
ES:[DI],AX {a na
adresu d zapiš rozdíl}
end;
function bez1 (a:word):word;assembler;
asm
MOV
AX,a
{do AX vlož hodnotu parametru a}
DEC
AX
{kdyby to nebyla čistě assemblerovská funkce,
tak}
{přidám řádek:}
{MOV
@RESULT, AX fce hodnotu pak také vrátí v AX}
end;
var a_,b_,c_,d_:word; {hlavní program}
begin
a_:=40;b_:=5;
clrscr;
pocitej (a_,b_,c_,d_);
writeln (a_,'+(-)',b_,'=',c_,'(',d_,')');
c:=bez1 (a_);
writeln (a_,'-1=',c_);
readkey;
end.
V době vykonávání úlohy musí být zajištěna i programová obsluha některých událostí. Za tyto události považujeme například: stisk klávesy, pohyb myší, hrozící výpadek napájení, kritická chyba v paměti, . . . I když by bylo možné testovat stisk klávesy v rámci prováděné úlohy, je pohodlnější, jestliže obsluhu této události zajistí počítač sám na úrovni technického vybavení. Přesto je k této činnosti nutný mikroprocesor. Proto je dočasně přerušena probíhající úloha. Po obsluze se procesor vrací zpět k té části úlohy, ze které byl přerušen.
Celý mechanismus přerušení se dá popsat v několika krocích:
Za instrukcí INT může být číslo v rozpětí 0..255. Toto číslo v případě obsluhy programové události udává, odkud požadavek přišel. Protože je ale nemožné, aby všech 256 úrovní přerušení bylo obsazeno, jsou některé hodnoty obsazeny tzv. službami.
Za služby můžeme považovat podprogramy, které jsou součástí operačního systému nebo BIOSu. Jsou umístěny v paměti počítače. Umožňují jednoduše provádět činnosti, které se v programech často opakují, jsou pracné nebo se liší na počítačích s různou konfigurací.
Služby voláme stejně jako obsluhy přerušení instrukcí INT číslo. Hodnota číslo určuje, o jakou službu se jedná. Často se v rámci jedné služby může vyskytovat i několik činností. Těm budeme říkat podslužby. Před voláním podslužeb musíme napřed nastavit v určitém registru (nejčastěji v AH) hodnotu jim určenou. Potom teprve voláme službu instrukcí INT. Mnoho služeb se chová jako podprogramy volané parametry. Hodnoty parametrů se neukládají do zásobníku, ale do některých registrů. Výstupy z těchto "podprogramů" najdeme opět v registrech. Informace o službách DOSu i BIOSu najdete v odborných publikacích nebo v SYSMANu. Zde také najdete informace o tom, které registry k čemu použijete.
Nejpoužívanější službou je INT $21. Ta zahrnuje služby DOSu jako je vstup a výstup dat, práce se soubory, čas, . . . Je také použita k výstupu pascalovského řetězce na obrazovku v následujícím příkladu. Výstup řetězce realizuje podslužba AH = $9. Vstupem do podslužby je adresa řetězce v registrech DS, DX. Výstup podslužba nemá. Jediná činnost je výpis na obrazovku. Důležité je označení konce řetězce znakem $. V případě, že tento znak na konci není, vypíše se obsah části paměti až do jeho náhodného výskytu.
Příklad:
procedure outstring (retezec:string);assembler;
asm
PUSH
DS
{ulož DS, budeme ho měnit}
MOV
AH,$09
{nastav hodnotu podslužby}
LDS
DI,retezec {čti
adresu řetězce}
MOV
DX,DI
{vlož ji do registru DX pro podslužbu}
INC
DX
{zvyš adresu až za informaci o délce}
XOR
BH,BH
{nuluj BH}
MOV
BL,[DI]
{do BL vlož délku řetězce}
MOV
BYTE
PTR
[DI+BX+1],'$'{na
konec řetězce dosaď ukončovací znak}
INT
$21
{volej služby DOSu}
POP
DS
{vrať DS}
end;
begin
outstring ('Ahoj'); {zkus vypsat}
end.
Uvedený program převede pascalovský řetězec do podoby řetězce, ve které ho očekává služba. Nastaví registry hodnotami vstupů a zavolá podslužbu DOSu. Výstup řetězce touto procedurou můžeme realizovat na libovolném grafickém adaptéru. Možné odlišnosti si vyřeší právě služba DOS.
Velká skupina programů je schopna pracovat na pozadí prováděné úlohy. Patří mezi ně ovladače (myši, klávesnice, . . .), utility (hodiny, antivirová kontrola, stahovače obrazovek, . . .), viry (bez komentáře). Těmto programům přidáváme označení rezidentní.
Jejich základní vlastností je jejich neustálá přítomnost v paměti počítače a schopnost se vyvolat, jestliže je to nutné. Z toho vyplývají i požadavky na ně: malá délka kódu (musí obsadit co nejméně paměti) a nezávislost na spuštěných aplikacích.
Činnost těchto programů na pozadí aplikací zaručuje jejich volání spolu s obsluhami přerušení. Jestliže tedy dojde k nějaké události (stisk klávesy, přijetí dat na port, uplynutí určité doby, . . .), je voláno přerušení obsluhující tuto událost. Po této obsluze (,nebo před ní) proběhne i část rezidentního programu připojeného k ní. Aby k tomu došlo, musí tvůrce rezidentního programu změnit adresu v tabulce vektorů přerušení na adresu svého podprogramu. Přitom si starou adresu obsluhy uschová, aby mohl zajistit volání původní obsluhy události. Je jen na tvůrci, jestli starou obsluhu bude volat nebo ne (jestliže ji ale nezavolá, mohou se vyskytnout problémy). Programátor se také může rozhodnout, ve které části svého programu bude obsluhu volat (např. nemohu číst jaká klávesa byla stisknuta, když ještě neproběhla obsluha klávesnice). Rezidentní program má tyto části:
GetIntVec
(číslo přerušení, adresa proměnné typu
procedure)
SetIntVec
(číslo přerušení, adresa našeho
podprogramu)
Writeln ('Rezidentní
program instalován.');
) Keep (0)
V Pascalu musíme navíc v rezidentním programu ohraničit podprogramy interrupt direktivou {$F+}, která zajistí, že bude uvnitř použito vzdálené volání (za podprogram napíšeme {$F-} pro návrat do automatického zjišťování vzdálených adres). Navíc musíme zajistit správnou alokaci paměti pro rezidentní program označením v úvodu programu {$M 400,0,0}, které vymezí oblast rezervovanou pro zásobník atd. (hodnoty je nejlepší vyzkoušet).
Nejčastěji se pro rezidentní programy používají přerušení:
Ostatní hodnoty přerušení se dají zjistit z literatury (nebo SYSMANu).
Na jaké přerušení rezident připojíme, závisí do značné míry na tom, co má dělat a na co má reagovat. Občas je dobré si v obsluze jednoho přerušení nastavit proměnné a v závislosti na jejich stavu vykonat (nebo nevykonat) určitou činnost v obsluze jiného přerušení. Často si ani neuvědomíme, že náš podprogram připojený k určitému přerušení, ho nepřímo volá. Dojde tak k zacyklení. Toho se částečně vyvarujeme tím, že veškeré činnosti, spojené se vstupy a výstupy, provádíme sami a nevoláme pascalovské procedury (např. výstup na obrazovku realizujeme přímým zápisem do VRAM, použití writeln vede k chybě).
Příklad:
{$M $400,0,0} {nastav paměť:
zásobník $400 slabik}
uses Dos;
var IntVec : Procedure; {proměnná pro adresu staré obsluhy}
{$F+} {vzdálená
volání}
procedure hodiny;interrupt;assembler; {nová obsluha
přerušení}
asm
JMP
@zac
{přeskoč data}
@vid:
DW
156,$B800
{adresa místa VRAM, kde budou hodiny}
@zac:
MOV
CL,2
{hodiny, minuty, vteřiny (cyklus)}
@c1 : {začátek cyklu}
LES
BX,CS:[
OFFSET
@vid]{naber adresu proměnné slovo do BX}
XOR
AH,AH
{vymaž horní polovinu registru AX}
MOV
AL,CL
{naber do dolní poloviny AX krok i}
SHL
AL,1
{vynásob, AL:=AL*2}
SUB
BX,AX
{odečti od BX obsah AX}
OUT
$70,AL
{pošli na CMOS adresu čtené slabiky}
SHL
AL,1
{vynásob, AL:=AL*2}
SUB
BX,AX
{odečti, to ovlivní tvaru výstupu}
IN
AL,$71
{přečti z CMOS obsah čtené slabiky}
MOV
DL,AL
{zkopíruj obsah této slabiky do AH}
SHR
DL,4
{desítky posuň do dolní poloviny
AH}
AND
AX,$F
{odstraň zbytečné bity}
AND
DX,$F
OR
AX,$1F30
{proveď převod do ASCII, přidej atr.}
OR
DX,$1F30
MOV
ES:2[BX],AX {nastav jednotky ve VRAM}
MOV
ES:[BX],DX
{nastav desítky ve VRAM}
DEC
CL
{snížit CL}
JNS
@c1
{konec cyklu}
MOV
WORD
PTR
ES:[154],$1F00+'.'{ve VRAM odděl vteřiny a minuty}
MOV
WORD
PTR
ES:[148],$1F00+'.'{ve VRAM odděl minuty a hodiny}
PUSHF
{do zásobníku registr
příznaků}
CALL
IntVec
{volej starou obsluhu $1C}
end;
{$F-} {konec vzdálených
volání}
begin {hlavní
program}
GetIntVec($1c,@IntVec); {čti adresu staré obsluhy}
SetIntVec($1c,Addr(hodiny));{na její místo dej adresu mojí
obsluhy}
Writeln('Rezidentní hodiny instalovány.');{informuj o
instalaci}
Keep (0); {ukonči s tím,
že zůstane program v paměti}
end.
Uvedený program čte při obsluze přerušení $1C stav hodin z paměti CMOS. Po přepočtu adres a úpravě znaků z BCD kódu do ASCII je informace o čase zobrazena v pravém horním rohu obrazovky. Hlavní program má za úkol jen změnu adresy původní obsluhy na naší.
© 1996-1997 Mgr. Tomáš Papoušek
Tento text je možné používat pro studijní účely bez omezení. V případě komerčního využití kontaktujte autora.