Lekce 5 - Assembler - První program - Hello world!
V minulé lekci, Assembler - Bitové operace, jsme se věnovali instrukcím pracujícím s bity.
Máme připravený projekt a jsme vyzbrojení povědomím o registrech a přerušeních. Pojďme tedy programovat v ASM!
Náš první program - Hello World
Naprogramujeme si klasický Hello world program. Již v úvodní lekci jsme si vlastně ukázali jeho zdrojový kód, ale zde si jej i rozebereme tak, jak se sluší a patří.
Do editoru si vložíme následující kód:
; Náš první program - Hello, World! bits 16 org 0x0100 main: mov ax, cs mov ds, ax mov si, zprava call print_string ret print_string: mov ah, 0x0e xor bh, bh .print_char: lodsb or al, al jz short .return int 0x10 jmp short .print_char .return: ret zprava db "Hello, World!", 0x00
Soubor uložíme jako hlwrld.asm
do složky
HelloWorld/Source/
. Program samozřejmě vypíše na obrazovku
Hello world. Nyní si ho rozebereme.
Struktura programu
Program bychom mohli rozdělit na dvě části:
- main
- print_string
Main představuje kód, který se začne provádět jako první. Zde je
návěstí main
zbytečné, ale kvůli představě jsem ho uvedl.
Druhou částí je funkce print_string
. Aby funkce proběhla, je
nutné ji zavolat. Takou část programu nazýváme podprogram.
Komentář ;
První věc, které si všimneme, je komentář. V Assembleru píšeme
komentáře za středníky - ;
.
Komentáře jsou v programu kvůli přehlednosti. Nyní ještě nejsou potřeba, ale až budeme psát větší programy, bude složité se v kódu vyznat. Také je s nimi vhodné popisovat princip algoritmů, které nemusí být na první pohled jednoduché pochopit, nebo vstupy funkcí.
Komentáře jsou překladačem, stejně jako v jiných jazycích, ignorovány a kromě popisu nemají v kódu žádný jiný význam.
Direktivy
Direktiva je něco mezi komentářem a instrukcí. Ve zkompilovaném souboru bychom ji hledali marně, ale už má určitý význam. Direktiva říká překladači nějaké informace o tom, jak zkompilovat kód.
Direktivami jsou v našem kódu následující dva řádky:
bits 16 org 0x0100
Direktiva BITS
Jako první zde máme direktivu BITS
. Tato direktiva říká
překladači, že píšeme program pro 16bitový procesor. K této direktivě se
zapisuje číslo představující architekturu procesoru, tedy 16, 32, 64.
8bitovou architekturu zde nenalezneme, jelikož píšeme programy pro řadu procesorů x86.
Direktiva ORG
(ORiGin)
Tato direktiva překladači říká, kolik má přičíst k adresám v programu. Toto se vztahuje ke skokům v programu a k tomu, na jaký offset v paměti program nahrajeme. Vysvětlíme si to na následujícím kódu:
mov ax, 0xdead
.cyklus:
inc ax
jmp .cyklus
Tento jednoduchý program nejdříve přesune hexadecimální hodnotu
0xDEAD
do registru AX
a přejde do cyklu, kde se
hodnota v registru AX
bude zvyšovat o jedna.
Po zkompilování a nahrání na offset 0x0000
by vypadal
program takto:
Offset | Instrukce | Velikost instrukce (v bajtech) |
0x0000 | MOV AX, 0xDEAD | 3 |
0x0003 | INC AX | 1 |
0x0004 | JMP 0x0003 | 3 |
Dokud by byl program na tomto offsetu, vše by fungovalo tak, jak má.
Můžeme si všimnout, že skok se provádí na instrukci INC AX
,
jak je tomu v kódu. Problém nastane tehdy, kdy změníme offset, kde je
program nahraný:
Offset | Instrukce | Velikost instrukce (v bajtech) |
0x0100 | MOV AX, 0xDEAD | 3 |
0x0103 | INC AX | 1 |
0x0104 | JMP 0x0003 | 3 |
Nyní si můžeme všimnout, že se offset změnil, ale adresa skoku zůstala
stejná. Proto existuje direktiva ORG
. Pokud ji uvedeme na
začátek našeho kódu v téhle formě org 0x0100
, bude se
počítat s tím, že program bude nahrán na offset 0x0100
a k
adrese skoku se toto číslo přičte. ORG
však není možné
měnit za chodu programu nebo při nahrávání do paměti. Přičtení se
provádí hned při zkompilování, a proto musíme vždy vědět, na jakém
offset chceme program provozovat.
Programy z éry MS-DOS byly nahrávány na offset 0x0100
.
Druhou složkou adresy je segment, který je možné měnit a běh programu to nijak neovlivní. Důležitou informací je, že bez vlastní úpravy mohou mít programy v reálném režimu velikost maximálně 64 KiB, tedy správně se bude vykonávat kód pouze na jednom segmentu.
Segmentové registry
Pod návěstím main
si můžeme všimnout instrukce
MOV
. U této instrukce jsou jako operandy registry CS
,
DS
a AX
. CS
je registrem, který tvoří
pár s registrem IP
. Spolu určují adresu instrukce, která se
bude provádět jako další. Samotný registr CS
je segmentovou
částí adresy. Říká tedy, kde se kód nachází. Protože budeme používat
instrukci LODSB
, která čte data z adresy DS:SI
, je
kromě SI
nutné nastavit i DS
. To ale není možné
udělat instrukcí MOV
, jelikož se jedná o segmentové registry,
ale existují dvě jiné varianty:
Buď můžeme použít prostředníka (nějaký registr), který hodnotu jednoho předá druhému, nebo použijeme zásobník. Jelikož o zásobníku ještě nic nevíme, zvolíme prostředníka.
Jako poslední nám zůstal registr AX
, který plní právě
funkci prostředníka.
Volání funkce
Dále se podíváme na zavolání funkce, tedy pasáž:
mov si, zprava call print_string
Volání funkcí v Assembleru je velmi jednoduché, ale není zde
implementován žádný mechanismus, který by za nás předal parametry.
Existuje hned několik způsobů, které si vysvětlíme u instrukce
CALL
.
Instrukce MOV
(MOVe)
Instrukce MOV
je snad nejpoužívanější instrukcí. Slouží
ke kopírování dat z registru do registru, z paměti do registru, konstanty do
registru, atd. Podle toho, co kam přemísťujeme, se mění její operační
kód, ale také velikost, kterou zabírá v paměti.
Instrukce má dva operandy, kterými jsou cíl a zdroj, dokonce v tomto pořadí.
V našem případě přesouváme do registru SI
adresu
proměnné zprava
.
Registr SI
(Source Index)
Registr SI
je registr určený pro práci s řetězci. V kódu
hraje svou roli jako ukazatel na data, která bude načítat instrukce
LODSB
do registru AL
. Tvoří registrový pár s
registrem DS
a často se používá s registrovým párem
ES:DI
. Jedná se o 16bitový registr.
Instrukce CALL
Funkci voláme instrukcí CALL
. Instrukce je velmi podobná
instrukci JMP
, ale jelikož se jedná pouze o volání, je možné
se z volaného místa vrátit. To instrukce dělá tak, že na zásobníku
uloží adresu následující instrukce. Tu pak instrukce RET
ze
zásobníku vybere a provede skok.
Předání parametrů by mohlo probíhat také přes zásobník. Data bychom
pomocí instrukce PUSH
uložili a instrukcí POP
nebo
MOV
bychom je ze zásobníku vybrali. Pozor bychom si museli dát
na návratovou adresu, která je na zásobníku uložena jako poslední, tudíž
bychom ji v případě vybírání pomocí instrukce POP
dostali
jako první.
Další, jednodušší možností je předání parametrů přes registry. To
jsme udělali s adresou proměnné zprava
.
Některé, již existující, metody pro předávání parametrů funkci tyto varianty kombinují. Např. první tři parametry se předají přes registr a ostatní přes zásobník.
Poslední možností je uložení parametrů na nějaké místo v paměti. To vlastně děláme předávání přes zásobník, ale zde bychom měli pevně stanovené adresy.
Až budeme používat pro předávání parametrů funkci zásobník, budeme si muset dát pozor na vyčištění zásobníku. To musí dělat buď funkce, nebo ten, kdo ji zavolal.
Funkce print_string
Podíváme se na další část programu, kterou je samotná funkce pro výpis textu na obrazovku:
print_string: mov ah, 0x0e xor bh, bh .print_char: lodsb or al, al jz short .return int 0x10 jmp short .print_char .return: ret
Návěstí :
Funkce vlastně není opět nic jiného, než pár instrukcí, u nichž známe adresu první z nich. Jelikož pracujeme s moderním Assemblerem, adresy zde nemusíme nijak počítat, stačí pouze zavést návěstí. To pouze nahrazuje adresu, stejně jako URL adresa IP adresu. Návěstí nesmí začínat číslicí a nemělo by být stejné jako označení některé z instrukcí.
Podnávěstí .:
V NASM máme něco, co bychom mohli označit jako podnávěstí. Normálně
není možné použít název návěstí vícekrát, ale pokud máme např. víc
funkcí, kde potřebujeme použít navěstí se stejným označením, např.
return
, je možné jej uvést jako podnávěstí. Podnávěstí je
něco jako podadresář, takže se vztahuje pouze k nadřazenému
návěstí:
funkce1: ; ... .return: ret funkce2: ; ... .return: ret
Nastavení parametrů
INT 0x10
Aby byl náš algoritmus pro výpis co nejvíc efektivní, nastavíme
nejdřív parametry pro přerušení INT 0x10
.
Jako prnví je nutné zvolit funkci. INT 0x10
poskytuje
základní obrazové funkce, mezi kterými je pod číslem 0x0e
teletype output. Tato funkce nahrává ASCII kód znaku v AL do videopaměti
0xB8000
. Číslo funkce předáváme BIOSovým přerušením
obvykle v registru AH
.
Parametrem funkce je znak v registru AL
, číslo stránky v
registru BH
a barva v registru BL
. Znak budeme do
AL
nahrávat pomocí instrukce LODSB
později.
Stránka se volí, jelikož textový režim, ve kterém pracujeme, má několik
stránek. My se nacházíme na stránce 0, takže registr vynulujeme. Barvu v
textovém režimu nenastavujeme, to je možné pouze v grafickém módu. To ale
neznamená, že to nejde jinak. To si ale ukážeme někdy jindy.
Instrukce
LODSB
- (LOaD String Byte at address DS:SI into AL)
První instrukcí v cyklu je, již zmíněná, instrukce LODSB
.
Tato instrukce vybere, podle varianty (LODSB
,
LODSW
,...), hodnotu na adrese DS:SI
, nahraje ji do
příslušné části registru AX
, v našem případě pouze do
AL
, a zvýší hodnotu v SI
o jedna.
Pokud dosáhne hodnota v registru SI
maxima,
hodnota v registru DS
se nezmění. Je nutné si vytvořit
mechanismus na přepočet adresy.
Jelikož LODSB
změní obsah pouze v registru
AL
, je možné nastavit funkci v AH
pro
INT 0x10
mimo cyklus.
Registry AX
, BX
,
CX
, DX
Pojďme se u registrů na chvíli zastavit a trochu si tuto část osvětlit. Pro své programy máme k dispozici 4 univerzální (anglicky general-purpose) registry. To je první typ registrů, který jsme si minule uvedli. Patří mezi ně:
AX
- Akumulační registrBX
- Bázový registrCX
- Čítací registrDX
- Datový registr
Se všemi těmito registry můžeme pracovat jako s 16bitovými nebo dvěma 8bitovými (písmenem X říkáme, že pracujeme s celým registrem, písmenem H označujeme vyšších 8 bitů a písmenem L nižších 8 bitů).
Pokud si chceme ulehčit práci a šetřit místo v paměti:
mov ah, 0x12 mov al, 0x34
můžeme použít zápis:
mov ax, 0x1234
Instrukce OR
Jak asi tušíte, instrukce OR
představuje bitový
součet. My tuto instrukci využijeme, jelikož je pro naše potřeby
vhodnější a hlavně rychlejší než instrukce CMP
.
Tato instrukce totiž po provedení bitového součtu nastavuje příznakový
registr. Tam hraje roli příznak nuly, který se nastaví, pokud je výsledek
operace roven nule. Bitovým součtem hodnoty v registru AL
s ní
samotnou ji zachováme a zároveň zjistíme, jestli je hodnota v registru
AL
rovna 0x00
- NULL.
Instrukce JZ
Tento nepodmíněný skok se váže na instrukci OR
. Jak už
jsme si řekli, instrukce OR
nastavuje příznakový registr, a
právě s příznakovým registrem podmíněné skoky pracují. JZ
provede skok v případě, že je nastaven příznak nuly.
Jak si můžeme všimnout, posledním bajtem v proměnné zprava
je právě 0x00
- NULL. LODSB
nahraje tuto hodnotu do
AL
a po provedení bitového součtu vyjde nula. Proto se nastaví
příznak nuly a provede se skok.
Přerušení INT 0x10
Následující instrukcí je, již zmíněná, instrukce INT
,
INTerrupt - přerušení. V tomto případě se jedná o
přerušení 0x10
, které zahrnuje obrazové funkce. Je
poskytováno BIOSem.
JMP
(JuMP)
Potom následuje JMP
, nepodmíněný skok,
který nás přesune na instrukci LODSB
, aby se načetl další
znak z DS:SI
. Na rozdíl od podmíněních skokových instrukcí
příznakový registr ignoruje.
SHORT
SHORT
se u skokových instrukcí používá tehdy, kdy
neprovádíme nějaký vzdálenější skok. Nejedná se o něco, bez čeho by
náš program nefungoval, ale zredukuje to velikost skokové instrukce.
Některé kompilátory totiž neumí správně vybírat mezi instrukcemi pro
kratší a vzdálenější skoky.
Normální skok, NEAR
, zabírá v paměti dva bajty. To
umožňuje skoky v rozsahu 0
- 65535
od místa, kde se
nacházíme, ale tak daleko my neskáčeme. Proto si vystačíme s rozsahem 0 -
255, který poskytuje SHORT
skok. Zabírá pouze jeden bajt.
Instrukce RET
(RETurn)
Poslední částí funkce je návěstí .return
a instrukce
RET
, která nás přesune zpátky na místo, odkud jsme funkci
zavolali, resp. na následující instrukci. To provede tak, že vybere
návratovou adresu ze zásobníku. Také se dá použít k vrácení kontroly
operačnímu systému.
Pokračovat budeme zase příště.
V příští lekci, Assembler - ASCII tabulka a spuštění v DOSBox, si vysvětlíme ASCII tabulku, dokončíme popis Hello world! programu a konečně si jej i spustíme jako .com soubor v emulátoru DOSBox.