1. diel - Assembler - Zásobník
V minulej lekcii, Assembler - Tvorba operačného systému , sme sa krátko pozreli na históriu počítačov, a popísali sme si rôzne návrhy OS.
V dnešnom ASM tutoriálu sa pozrieme na to, čo je to zásobník. Na prvý pohľad sa môže zdať zložitý, ale práca s ním je nesmierne jednoduchá. Je tu však veľa vecí, na ktoré si musíme dať pozor.
Zásobník
Zásobník (anglicky stack) je štruktúra pre uloženie dát. Jednotlivé hodnoty v ňom sú ukladané spôsobom LIFO (Last In - First Out, teda čo sa uloží posledná, to pri čítaní zo zásobníka získame ako prvý). Čítanie zo zásobníka prvok rovno odoberie.
Zásobník môžeme v 16 bitových ASM aplikáciách používať v podstate ku 2 účely:
- ako programátor pre dáta, ktoré si chceme uložiť, napr. pre kópiu registrov
- program zásobník používa pre uloženie adries, na ktoré sa má vrátiť
po ukončení volaných funkcií (volaných inštrukcií
CALL
)
Inštrukcie zásobníka
So zásobníkom pracujeme pomocou nasledujúcich inštrukcií.
Inštrukcie PUSH
Inštrukcie PUSH
slúži na uloženie ďalšej hodnoty
na zásobník. Má iba jeden operand, ktorým je premenná, samotná
hodnota či hodnota registra, ktorú chceme uložiť.
Pracuje sa s ňou nasledovne:
mov ax, 0xffff
push ax
Do registra AX
presúvame hodnotu 0xffff
a pomocou
inštrukcie PUSH
ukladáme túto hodnotu z registra na
zásobník.
Inštrukcie POP
Keď používame inštrukciu PUSH
, mali by sme spolu s ňou
používať aj inštrukciu POP
. Tá totiž slúži na
výber dát zo zásobníka. Vráti tú posledne pridanú
hodnotu a zo zásobníka ju zmaže. Má opäť iba jeden operand.
Pracuje sa s ňou takto:
pop ax
Vyberáme hodnotu zo zásobníka a ukladáme ju do registra
AX
.
Výmena hodnôt medzi registrami a zásobníkom
Ako tieto dve inštrukcie správne skombinovať?
Inštrukcie PUSH
a POP
sa najčastejšie
používajú, keď potrebujeme pracovať s nejakým registrom, ale nechceme jeho
hodnotu zmeniť natrvalo. Tým máme na mysli, že si ho požičiame a potom do
neho zas vrátime hodnoty, ktoré v ňom boli predtým.
Tu je ukážka toho, ako si do registra AX
uložíme hodnotu
0x1234
. Potom ju uložíme na zásobník pomocou PUSH
,
hodnotu v registri zmeníme a potom zas obnovíme pôvodnú hodnotu Registry
pomocou POP
:
mov ax, 0x1234 push ax mov ax, 0xffff pop ax
Takto môžeme zálohovať a obnoviť aj viac registrov naraz, čo si ukážeme hneď ako ďalší.
Inštrukcie PUSHA
Inštrukcie PUSHA
mám na zásobník uloží hodnoty
všetkých registrov (AX
, BX
, ...). Možno
ste si už stihli vyvodiť, že to A
naviac znamená ALL - všetko.
Nemá žiadny operand:
mov ax, 0x0123 mov bx, 0x4567 mov cx, 0x89ab mov dx, 0xcdef pusha xor ax, ax xor bx, bx xor cx, cx xor dx, dx popa
Všetkým registrom sme nastavili nejaké hodnoty, prepísali sme ich a opäť obnovili.
Inštrukcie POPA
Určite už ani nemusíme hovoriť, čo táto inštrukcia robí. Skrátka a
jednoducho nám vyberie všetky hodnoty zo zásobníka a uloží ich do
príslušných registrov (AX
, BX
, ...).
Príklad si hneď ukážeme.
Príklad - Implementácia zásobníka
Zásobník implementujeme tak, že vyhradíme určitú oblasť v pamäti a do
registra SP
vložíme adresu konca tejto oblasti a pripočítame
(respektíve odpočítame) 1
. Stačí si pamätať, že ak chceme
mať ako vrchol zásobníka adresu 0xffff
, stačí z tejto adresy
odpočítať 1
, teda adresa bude 0xfffe
.
Dôležité je povedať, že zásobník ide zhora
nadol. Pokiaľ bude vrchol zásobníka na adrese 0xfffe
,
ukazovateľ sa bude s každým uložením znižovať o 1
.
Takto si teda nastavíme zásobník:
push cs
pop ss
mov ax, 0xfffe
mov sp, ax
V kóde najprv ukladáme na zásobník hodnotu registra CS
a
nasledovne ju odoberieme a vložíme do registra SS
. To znamená,
že zásobník je v rovnakej časti pamäti, ako je náš kód. Ďalej naplníme
register AX
hodnotou 0xfffe
a ako posledný presunieme
hodnotu z registra AX
do registra SP
.
Možno vás napadlo, že by to išlo jednoduchšie? Ak uvažujete nad touto variantou, tak je to bohužiaľ zle:
mov sp, 0xfffe
Register SP
sa musí vždy nastaviť pomocou
iného registra (napríklad pomocou registra AX
).
Tí bystrejší si teraz ale určite hovoria, prečo sme v prvom kóde urobili toto:
push cs pop ss
Áno, aj takto môžeme presúvať hodnoty do registrov. Uložíme hodnotu z
registra CS
na zásobník, vyberieme ju a presunieme do registra
SS
.
Páry registrov
Pokiaľ budeme chcieť na zásobník uložiť registre, vždy je ukladáme v
pároch, teda AX
(spojenie registrov AH
a
AL
), BX
(spojenie registrov BH
a
BL
), ...
Asi takto by to vyzeralo:
zásobník | adresa ↓ |
---|---|
AH | 0xfffe |
AL | 0xfffd |
BH | 0xfffc |
BL | 0xfffb |
CH | 0xfffa |
CL | 0xfff9 |
DH | 0xfff8 |
DL | 0xfff7 |
CALL
a RET
To ale nie je všetko. So zásobníkom úzko súvisí aj inštrukcie
CALL
a RET
.
Inštrukcie CALL
Všetkým určite došlo, že pomocou tejto inštrukcie budeme "niečo"
volať. To "niečo" môže byť nami vytvorená funkcie či adresa. Ako súvisí
so zásobníkom? Inštrukcie sa používa, keď počítame s tým, že sa
vrátime na pozíciu, kde sme inštrukciu CALL
použili. Adresa,
kde sme začali volať, sa totiž ukladá na zásobník:
call moje_funkce
inc ax
jmp $
moje_funkce:
mov ax, 0xfffe
ret
V kóde zavoláme funkciu moje_funkce
, ktorá nám do registra
AX
presunie hodnotu 0xfffe
. Z funkcie sa pomocou
inštrukcie RET
vrátime späť, pripočítame 1
a
zastavíme vykonávania kódu.
Inštrukcie RET
Inštrukcie CALL
a RET
opäť tvorí pár. Ako už
sme si mohli všimnúť, inštrukcie RET
slúži na návrat na onú
adresu, odkiaľ sme funkciu volali. Nemá žiadny operand.
Možno ste počuli aj o inštrukciu IRET
, ale o tej
si povieme inokedy v súvislosti s prerušeniami.
Teraz ešte ako to všetko dať dohromady ... Ako takú ukážku si uveďme nasledujúci kód:
push cs pop ss mov ax, 0xfffe mov sp, ax pusha mov ax, 0x21 pridej_jedna: call vypis_znak inc ax cmp ax, 0xff jnz pridej_jedna popa cli hlt vypis_znak: push ax mov ah, 0x0e mov bh, 0x0 int 0x10 pop ax ret
výstup:
Tento kód je až smiešne jednoduchý, že? Najprv si nastavíme zásobník,
uložíme hodnoty všetkých registrov a nastavíme registra AX
hodnotu 0x21
. 0x21
je v desiatkovej sústave
33
, v ASCII tabuľke ide o znak !
. Ďalej prejdeme do
cyklu (použité sú tu vedomosti z predchádzajúceho kurzu), kde ako prvý
zavoláme funkciu vypis_znak
. Tá nám vypíše znak podľa hodnoty
v registri AX
a zároveň nám zabezpečí, aby sa register
AX
neprepísal pomocou páru inštrukcie PUSH
a
POP
. Nasleduje pripočítanie hodnoty 1
k existujúcej
hodnote v registri AX
a kontrola, či nie je hodnota rovná
0xff
(255
). Ak nie, skočíme na
pridej_jedna
a celý cyklus opakujeme. Ak áno, obnovíme hodnoty
všetkých registrov, zrušíme všetky prerušenia a zastavíme vykonávania
kódu.
Kombinácia inštrukcií CLI
a HLT
zaisťuje bezproblémové zastavenie počítača. Ak by sme nezrušili všetky
prerušenia, mohlo by dôjsť k vyvolaniu "triple fault".
V budúcej lekcii, Assembler - Prevod čísla na reťazec a naopak , budeme prevádzať medzi číslom a reťazcom na obe strany.