Vydělávej až 160.000 Kč měsíčně! Akreditované rekvalifikační kurzy s garancí práce od 0 Kč. Více informací.
Hledáme nové posily do ITnetwork týmu. Podívej se na volné pozice a přidej se do nejagilnější firmy na trhu - Více informací.

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í registr
  • BX - Bázový registr
  • CX - Čítací registr
  • DX - 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.


 

Předchozí článek
Assembler - Bitové operace
Všechny články v sekci
Základy assembleru
Přeskočit článek
(nedoporučujeme)
Assembler - ASCII tabulka a spuštění v DOSBox
Článek pro vás napsal Jakub Verner
Avatar
Uživatelské hodnocení:
12 hlasů
Autor se věnuje programování v x86 Assembleru.
Aktivity