IT rekvalifikácia. Seniorní programátori zarábajú až 6 000 €/mesiac a rekvalifikácia je prvým krokom. Zisti, ako na to!

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
Inštrukcie 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:

výstup - Programujeme operačný systém v assembleri

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.


 

Predchádzajúci článok
Assembler - Tvorba operačného systému
Všetky články v sekcii
Programujeme operačný systém v assembleri
Preskočiť článok
(neodporúčame)
Assembler - Prevod čísla na reťazec a naopak
Článok pre vás napísal Jakub Verner
Avatar
Užívateľské hodnotenie:
Ešte nikto nehodnotil, buď prvý!
Autor se věnuje programování v x86 Assembleru.
Aktivity