Aktuálně: Postihly zákazy tvou profesi? Poptávka po ajťácích prudce roste, využij slevové akce 30% výuky zdarma!
Pouze tento týden sleva až 80 % na e-learning týkající se PHP
Discount week - April - 30

Lekce 1 - Assembler - Zásobník

V dnešním ASM tutoriálu se podíváme na to, co je to zásobník. Na první pohled se může zdát složitý, ale práce s ním je nesmírně jednoduchá. Je tu však spoustu věcí, na které si musíme dát pozor.

Zásobník

Zásobník (anglicky stack) je struktura pro uložení dat. Vlastně se jedná o vyhrazený úsek paměti, se kterým se pracuje pomocí instrukcí pro práci se zásobníkem. Jednotlivé hodnoty v něm jsou ukládané způsobem LIFO (Last In - First Out, tedy co se uloží poslední, to při čtení ze zásobníku získáme jako první).

Zásobník můžeme v 16bitových ASM aplikacích používat v podstatě ke 2 účelům:

  • jako programátor pro data, která si chceme uložit, např. pro kopii registrů
  • některé instrukce používají zásobník pro uložení a čtení adres (například pár instrukcí CALL a RET)

Instrukce pro práci se zásobníkem

Se zásobníkem pracujeme pomocí následujících instrukcí.

Instrukce PUSH

Instrukce PUSH slouží k uložení další hodnoty na zásobník. Má pouze jeden operand, kterým může být proměnná, samotná hodnota, adresa, či hodnota registru, kterou chceme uložit.

Pracuje se s ní následovně:

mov ax, 0xffff
push ax

Do registru AX přesouváme hodnotu 0xffff a pomocí instrukce PUSH ukládáme tuto hodnotu z registru na zásobník.

Instrukce POP

Když používáme instrukci PUSH, měli bychom spolu s ní používat i instrukci POP. Ta totiž slouží k výběru dat ze zásobníku. Vrátí tu posledně přidanou hodnotu a ze zásobníku ji smaže. Má opět pouze jeden operand.

Samotné používání jedné či druhé instrukce může vést k přetečení nebo podtečení zásobníku.

Pracuje se s ní takto:

pop ax

Vybíráme hodnotu ze zásobníku a ukládáme ji do registru AX.

Výměna hodnot mezi registry a zásobníkem

Jak tyto dvě instrukce správně zkombinovat?

Instrukce PUSH a POP se nejčastěji používají, když potřebujeme pracovat s nějakým registrem, ale nechceme jeho hodnotu změnit trvale. Tím máme na mysli, že si jej půjčíme a pak do něj zas vrátíme hodnoty, které v něm byly předtím.

Tento výukový obsah pomáhají rozvíjet následující firmy, které dost možná hledají právě tebe!

Zde je ukázka toho, jak si do registru AX uložíme hodnotu 0x1234. Pak ji uložíme na zásobník pomocí PUSH, hodnotu v registru změníme a pak zas obnovíme původní hodnotu registru pomocí POP:

mov ax, 0x1234
push ax
mov ax, 0xffff
pop ax

Někdy ale potřebujeme uložit více hodnot najednou, což si ukážeme hned jako další.

Instrukce PUSHA

Instrukce PUSHA nám na zásobník uloží hodnoty všech registrů (AX, BX,...). Možná jste si již stihli vyvodit, že to A navíc znamená ALL - vše. Nemá žádný 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šem registrům jsme nastavili nějaké hodnoty, přepsali jsme je a opět obnovili.

Instrukce POPA

Určitě už ani nemusíme říkat, co tato instrukce dělá. Zkrátka a jednoduše nám vybere všechny hodnoty ze zásobníku a uloží je do příslušných registrů (AX, BX, ...). Tyto instrukce by se měly používat jako instrukční pár.

Jako příklad můžeme použít předchozí kód.

Přetečení a podtečení zásobníku

Nyní si povíme o chybách, které můžeme jako programátor u zásobníku udělat.

Podtečení zásobníku

Pokud bychom opakovaně používali instrukci POP, nebo POPA, došlo by k podtečení zásobníku. Při práci se zásobníkem se stále mění ukazatel SP (Stack pointer). Pokud bychom hodnoty pouze vybírali, hodnota by se stále zvyšovala a zásobník by podtekl. Vlastně bychom začali vybírat hodnoty, které už jsou mimo vyhrazenou oblast paměti.

Přetečení zásobníku

Opak podtečení je přetečení. To můžeme vyvolat opakovaným ukládáním na zásobník pomocí instrukce PUSH, nebo PUSHA. Často stačí vyhradit pouze malý prostor. Zde bychom hodnoty zase ukládali přes rámec vyhrazeného prostoru.

Implementace zásobníku

Zásobník implementujeme tak, že vyhradíme určitou oblast v paměti a nastavíme segment, kde se bude zásobník nacházet (k tomu slouží registr SS). Do registru SP vložíme adresu, nejlépe konce této oblasti a přičteme (respektive odečteme) 1. Stačí si pamatovat, že pokud chceme mít jako vrchol zásobníku adresu 0xffff, stačí z této adresy odečíst 1, tedy adresa bude 0xfffe. Je to taková menší prevence před podtečením zásobníku.

Důležité je říct, že zásobník jde shora dolů. Pokud bude vrchol zásobníku na adrese 0xfffe, ukazatel se bude s každým uložením snižovat o 1.

Takto si tedy nastavíme zásobník:

mov ax, 0x0000
mov ss, ax
mov sp, 0xfffe

V kódu nejprve uložíme do registru AX hodnotu 0x0000 a následovně ji přesuneme do registru SS. Pak už jen přesuneme hodnotu 0xfffe do SP.

Možná vás napadlo, že by to šlo jinak? Jestli uvažujete nad touto variantou, tak je to bohužel špatně:

mov ss, 0xfffe

Veškeré segmentové registry (např.: ES, SS) se nastavují pomocí jiného registru (např.: AX, BX).

Zásobník můžeme nastavit dokonce i pomocí jeho samotného:

push cs
pop ss

Ano, dá se to použít i jako způsob pro přesouvání hodnot do registrů. Uložíme hodnotu z registru CS na zásobník, vybereme ji a přesuneme do registru SS.

Páry registrů

Pokud budeme chtít na zásobník uložit registry, vždy je ukládáme v párech, tedy AX (spojení registrů AH a AL), BX (spojení registrů BH a BL),...

Asi takto by to vypadalo:

Zásobník Adresa ↓
AH 0xfffe
AL 0xfffd
BH 0xfffc
BL 0xfffb
CH 0xfffa
CL 0xfff9
DH 0xfff8
DL 0xfff7

Instrukce CALL a RET

To ale není vše. Se zásobníkem úzce souvisí i, již zmíněné, instrukce CALL a RET.

Instrukce CALL

Všem určitě došlo, že pomocí této instrukce budeme "něco" volat. To "něco" může být námi vytvořená funkce či adresa (např. nahraného programu). Jak souvisí se zásobníkem? Instrukce se používá, když počítáme s tím, že se vrátíme na pozici, kde jsme instrukci CALL použili. Adresa, kde jsme začali volat, se totiž ukládá na zásobník:

call moje_funkce
inc ax
jmp $

moje_funkce:
mov ax, 0xfffe
ret

V kódu zavoláme funkci moje_funkce, která nám do registru AX přesune hodnotu 0xfffe. Z funkce se pomocí instrukce RET vrátíme zpět, přičteme 1 a zastavíme provádění kódu.

jmp $ můžeme nahradit i kombinací instrukcí cli a hlt. Toto si ještě podrobně vysvětlíme.

Instrukce RET

Instrukce CALL a RET opět tvoří pár. Jak už jsme si mohli všimnout, instrukce RET slouží k návratu na onu adresu, odkud jsme funkci volali. Nemá žádný operand.

Možná jste slyšeli i o instrukci IRET, ale o té si povíme jindy v souvislosti s přerušeními.

Teď ještě jak to všechno dát dohromady... Jako takovou ukázku si uveďme následující kód:

mov ax, 0x0000
mov ss, ax
mov sp, 0xfffe

pusha

mov al, 0x21

pridej_jedna:
call vypis_znak

inc al
cmp al, 0xff
jnz pridej_jedna
popa

cli
hlt

vypis_znak:
pusha
mov ah, 0x0e
mov bh, 0x00
int 0x10
popa
ret

Výstup:

Výstup

Tento kód je až směšně jednoduchý, že? Nejprve si nastavíme zásobník, uložíme hodnoty všech registrů a nastavíme registru AL hodnotu 0x21. 0x21 je v desítkové soustavě 33, v ASCII tabulce jde o znak !. Dále přejdeme do cyklu (využité jsou zde znalosti z předchozího kurzu), kde jako první zavoláme funkci vypis_znak. Ta nám vypíše znak podle hodnoty v registru AL a zároveň nám zajistí, aby se registry nepřepsaly pomocí páru instrukce PUSHA a POPA. Následuje přičtení hodnoty 0x01 ke stávající hodnotě v registru AL a kontrola, zda není hodnota rovna 0xff (255). Pokud ne, skočíme na pridej_jedna a celý cyklus opakujeme. Pokud ano, obnovíme hodnoty všech registrů, zrušíme všechna přerušení a zastavíme provádění kódu.

Kombinace instrukcí CLI a HLT zajišťuje bezproblémové zastavení počítače. Pokud bychom nezrušili všechna přerušení, mohlo by dojít k vyvolání "triple fault".

V příští lekci, Assembler - Převod čísla na řetězec a naopak, budeme převádět mezi číslem a řetězcem na obě strany.


 

Všechny články v sekci
Programujeme operační systém v assembleru
Článek pro vás napsal Jakub Verner
Avatar
Jak se ti líbí článek?
Ještě nikdo nehodnotil, buď první!
Autor se věnuje programování v Assembleru.
Aktivity (4)

 

 

Komentáře

Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zatím nikdo nevložil komentář - buď první!