NASM Lehele | 2. Praktikum >>

NASM – Praktikum1: Sissejuhatus, Töökeskkond ja Hello World

Programmeerimine x86 Assembleris on võimas töörist riistvaraga kõige madalamal tasemel töötamiseks, andes kiireid ning kindlaid tulemusi. Isegi kui Assembleris programmeerimist (nüüdsest asm) ei saa arvestada rapid development keskkonnana, on asm sellegipoolest asendamatu tööriist, mille abil saame programmeerida jõudluskriitilisi funktsioone ning hiljem kasutada neid oma C/C++ programmides.

Miks Assembler?

1. Asm on Kiire – Asmis kirjutatud programmid on väga kiired – enamus C stdlib funktsioone on kirjutatud just Asmis.
2. Asm on Võimas – Sul kogu kontroll CPU ja mälu üle – midagi mis on kõrgtaseme keeltes sageli võimatu.
3. Asm on Väike – Assembleri programmid on palju väiksemad, mis on tihti kasulik kui mäluruum on piiratud.

Järgnevad x86 Assembleri praktikumid keskenduvad ainuüksi kompilaatorile NASM – Netwide Assembler, mis on saadaval nii Windows kui ka Linux keskkonnas. NASM kompilaatori saab alla laadida NASM-i kodulehelt: http://www.nasm.us. NASM-i kompileeritud objektfailid saame linkida käsurealt kasutades MinGW/GCC kompilaatorit.

1. Registrid

Üks tähtsamaid peatükke Assembleri programmeerimises on Registrid. Protsessori aritmeetika-loogika üksus (ALU – protsessori moodul, mis tegeleb aritmeetiliste operatsioonidega) ei suuda teha arvutusi välises DRAM-is, sest bittide pööramine ja nihutamine võtaks tohutu hulga aega. Et seda probleemi vältida on juba alates varasematest protsessoritest alati kasutatud ülikiiret sisemist vahemälu – Registreid. Välisest mälust Registritesse loetud andmetega saab ALU teha arvutusi ning kirjutada need vajaduse korral tagasi välisesse DRAM-i.

Registrid on üldjuhul otseselt seotud individuaalse ALU-ga ning protsessoril on sageli mitu erinevat ALU-t. Mõningad neist on varuüksused kiirema käitamise jaoks, teised on väga spetsiifilised või erilised ALU-d (nt SSE ALU). Registrite otsene seotus ALU-ga tähendab, et arvutused registrites on ülimalt kiired. Lisaks veel, Registrite ning välise DRAM-i eraldamine annab meile ka võimaluse optimeerimiseks - enamus mälust loetud andmeid pole tarvis kohe tagasi kirjutada.

Tähtsamad 32-bit registrid x86 arhitektuuris on:

EAX - Accumulator Register
EBX - Base Register
ECX - Counter Register
EDX - Data Register
ESI - Source Index
EDI - Destination Index
EBP - Base Pointer
ESP - Stack Pointer

Iga 32-bitine register toetab kõiki üldisi aritmeetika ning mälu operatsioone. Osad registrid omavad aga erilist tähendust või konteksti milles neid tuleks kasutada:

EAX - Faster arithmetics
EDX - Extension to EAX, (64-bit emulation), also Fast arithmetics
EBX - General purpose pointer
ECX - Equivalent of 'i'. Used in loops and looping instructions
ESI - Source pointer for reading array values in loops
EDI - Destination pointer for writing array results in loops
EBP - Stack Frame pointer
ESP - Stack pointer, core of x86 function call system

32-bitised Üldregistrid, milleks on EAX, EBX, ECX, EDX; võimaldavad 16-bit ja 8-bit ligipääsu registri enda sisule. Antud juhul on see lihtsalt mugavdus, et lugeda hõlpsamini registri alumisi bitte:

Accumulator Register

Registrid EAX, EDX ja ECX omavad veel lisaks erilist konteksti funktsioonikutsetes ajutiste registrite näol – ehk väärtused nendes registrites ei pruugi funktsioonikutsete vahel säilida (üldiselt ei säiligi, kuna tulemus on alati EAX registris). Vastukaaluks, registrid EBX, ESI, EDI peab programmeerija säilitama funktsioonikutsete vahel. See saavutatakse funktsiooni enda proloogis, kus kasutatavad registrid lükatakse stack-i ja hiljem taastatakse funktsiooni epiloogis. Programmeerijad peavad seda reeglit hoolikalt jälgima – muidu tekib ootamatuid registrite väärtuste muudatusi.

2. Segmendid

Nagu loengus räägitud on kõik programmid mälus jaotatud segmentideks ja x86 Assembleris peab programmeerija ise määrama andmesegmendid. Kunas me arendame programme mis jooksevad operatsioonisüsteemi peal, seatakse need segmendid automaatselt üles kui programm käivitub. Ilma operatsioonisüsteemita keskkonnas peab programmeerija ise segmendid üles seadma.

Tähtsad eeldefineeritud segmendid NASM-is:

section .data  - DATA  segment (static initialized RW data)
section .const - RDATA segment (static initialized RO data)
section .bss   - BSS   segment (static uninitialized RW data)
section .text  - TEXT  segment (executable data)

DATA segment (static initialized RW data) – Andmed on Read-Write ja kompileeritakse binaarfaili.
RDATA segment (static initialized RO data) – Andmed on Read-Only ja kompileeritakse binaarfaili. Seda segmenti kutsutakse ka CONST segmendiks.
BSS segment (static uninitialized RW data) – Andmed on Read-Write ja märgitakse reservmäluks mille allokeerib operatsioonisüsteem programmi käitamisel.
TEXT segment (executable data) – Andmed on Read-Only-Executable ja on mõeldud protsessori interpreteerimiseks. Seda segmenti kutsutakse ka CODE segmendiks.

On tähtis märkida, et kõik sildistused on lihtsalt 'offsetid' segmendi algusest. Lõppaadress on kujul [SegmentRegister + Offset], mis võimaldab operatsioonisüsteemil randomiseerida segmentide alguseid. Järgnevas näites on NASM-is deklareeritud andmeid koos vastava illustreeritud mälujäljendiga vasakul:

0x4000 | section .data
0x4000 |   my_string  db  'Hello!',0 ; array[7]  (0x4000 - 0x4006)
0x4007 |   my_short   dw  10         ; 16bit int (0x4007 - 0x4008)
0x4009 |   my_dword   dd  10h        ; 32bit int (0x4009 - 0x400C)
0x400D |   my_array   times 40 db 0  ; array[40] init. to 0
                                     ; (0x400D - 0x4034)
0x4035 | ; ...
0x4100 | section .const
0x4100 |   ro_string  db 'ReadOnly',0 ; array[9](0x4100 - 0x4108)
0x4109 | ; ...
0x8000 | section .bss
0x8000 |   an_int    resd  1         ; uninit. int(0x8000 - 0x8003)
0x8004 |   byte_arr  resb  80        ; uninit. array[80]
                                     ; (0x8004 - 0x8053)
0x8054 | ; ...

3. Töökeskkonna seadistamine

Enne kui saame lähtekoodi kallale asuda, peame veenduma, et meil on olemas NASM ja MinGW kompilaatorid. Lisaks sellele peame ka seadistama Windows PATH süsteemimuutujat, et NASM ja GCC käsud oleksid käsurealt kättesaadavad.

Kompilaatorite paigaldamine

NASM zip on saadaval siit: nasm-2.11.06-win32.zip. NASM-i peaks lahti pakkima kausta "C:\NASM\".
MinGW get-installer: mingw-get-setup.exe.
MinGW installer on natuke keerulisem, kuna kaasas on spetsiifiline paketihaldur (package manager). Installatsioonikaust peaks olema "C:\MinGW\". Paketihaldurist tuleb kindlasti valida mingw32-base ja msys-base paketid:

Pakettide mingw32-base ja msys-base valimine
Pakettide lisamiseks tuleb kontekstimenüüst valida "Apply Changes":
Muudatuste rakendamine

Käskude lisamine PATH-i

Nagu enne mainitud peame Windows PATH süsteemimuutujat seadistama, et käsurealt oleksid tuntavad nasm, gcc ja make käsud. Kõige lihtsam viis selleks on kasutada käsurida. Lähtudes eelmistest installatsioonisätetest on meile tähtsad kaustad:
C:\NASM\
C:\MinGW\bin\
C:\MinGW\msys\1.0\bin\
Nende lisamiseks system PATH-i, kasutame SETX käsku, mis tekitab User PATH laienduse koos antud väärtusega:

>setx path ";C:\NASM\;C:\MinGW\bin\;C:\MinGW\msys\1.0\bin\;"
SUCCESS: Specified value was saved.
Seejärel sulgeme kõik lahtiolevad käsurea aknad, et muutujad uueneksid.

4. Hello World – Konsooli Programm

Programmeerimine Assembleris ei ole kahjuks nii triviaalne kui tundub - isegi lihtsa “Hello World” kuvamiseks peame kulutama üpriski palju eeltööd. Õnneks on meie käsutuses GCC linker, mis teeb ära suure osa tööst ja seega on programmi kirjutamine lihtsam kui eelnevatel aastatel. Järgnevalt läbime rida rida haaval “Hello World” programmi, kattes samal ajal lihtsamad aspektid assembleri programmis.

Lähtekoodi loomine

Esmalt peame looma uue tekstifaili, näiteks: ‘C:\NASM\helloworld\helloworld.asm’. Loodud tekstifaili peaks esimese asjana lisama kommentaari antud assembleri programmi kohta. Antud juhul ei pea me palju kirjutama ning hea tavana järgime Javadoc stiili. Kõik peale semikoolonit ‘;’ on koodikommentaarid:

; ----------------------------------------------------
; helloworld.asm
; @brief	Simply displays "Hello world!" in stdout
; @author	Nimi Nimi
; @date 	05.11.2013
; ----------------------------------------------------

Nüüd saame alustada programmi endaga. Kõigepealt peame deklareerima globaalse sildi "_main", mis on vajalik selleks, et MinGW linker leiaks üles programmi alguse. Kohe järgi lisame ka välise ('extern') sümboli "_printf". Extern ütleb kompilaatorile, et sümbol tuleks otsida alles linkimise ajal.

global 	_main        ; make label visible for linker
extern	_printf      ; link with printf

Nüüd saame alustada segmentide deklareerimisega. Segmentide järjekord ei ole tähtis, kuna Makroassembler teeb sildistamise segmenteerimise automaatselt. Antud juhul on meil vaja deklareerida andmesegmendi algus, kuhu saame paigutada sõne "Hello World!\n". Kusjuures, antud sõne võiks olla Read-Only kaitstud andmesegmendis. Sellisele segmendile vastab ".const" segment:

; -------------------------------
section	.const
  hello db 'Hello world!', 10, 0   ; "Hello world!\n"

Segmente deklareeritakse kujul 'section <seg>'. Andmeid deklareeritakse kujul 'label <datatype> <data...>'. Kusjuures, andmeid võib lihtsalt eraldada komadega, mis loob automaatselt massiivi. Baitide jada 'db' puhul võib kasutada ka ühekordseid jutumärke jada kirjapanekuks. Kahjuks ei eksisteeri Assembleris sõnetöötlust, seega on C-s tuntud "\r\n\0" sõned lihtsalt tähemärkide jadad.

Pärast CONST andmesegmenti võime deklareerida programmi alguspunkti, ehk "_main" funktsiooni. Selleks peame esmalt deklareerima TEXT segmendi alguse:

section .text  ; start of code segment

Ning peale seda deklareerime funktsiooni sildi, mille me tegime globaalseks. Nagu enne mainitud, et sümbolid/sildid/funktsioonid oleksid linkerile nähtavad, peavad nad olema globaalsed.

; -------------------------------
_main:

Järgmise sammuna võiksime juba välja kutsuda funktsiooni _printf. Selleks peame tegema eel- ja järeltööd. Eeltööna peame lükkama stack-i funktsiooni argumendid. Antud juhul on ainult 1 argument, milleks on sümbol 'hello'. Kuna sümbol hello on lihtsalt aadress, siis on tegu 4-baidise väärtusega. Järeltööna peame tasakaalustama stack-i just niipalju kui oli argumentides baite - milleks on 4 baiti. Sellist süsteemi kutsutakse kui CDECL - C calling convention.

	; printf("Hello world!\n");
	push	hello        ; push address to label hello
	call	_printf
	add		esp, 4       ; balance stack pointer

Pärast printf-i kutsumist võiksime lõpetada programmi töö. Selleks tuleb kirjutada EAX registrisse main funktsiooni resultaat ning seejärel teha täiesti tavaline return.

	; return 0;
	mov		eax, 0       ; EAX := 0
	ret
; -------------------------------

Kogu lähtekood:

; ----------------------------------------------------
; helloworld.asm
; @brief	Simply displays "Hello world!" in stdout
; @author	Nimi Nimi
; @date 	05.11.2013
; ----------------------------------------------------
global 	_main        ; make visible for linker
extern	_printf      ; link with printf
; -------------------------------
section	.const
  hello db 'Hello world!', 10, 0
section .text
; -------------------------------
_main:
	; printf("Hello world!\n");
	push	hello        ; push address to label hello
	call	_printf
	add		esp, 4       ; balance stack pointer
	; return 0;
	mov		eax, 0       ; EAX := 0
	ret
; -------------------------------

Lähtekoodi kompileerimine

Nüüd kui meil on olemas lihtne Asmi lähtekood, võime proovida seda kompileerida ning linkida käivitatavaks binaariks. Esimene samm on kompileerida vastav objektfail. Objektfail on binaarne kogum: andmetest, globaalsed/privaatsed sümbulod ja kompileeritud käsud. NASM oskab genereerida mitmeid erinevaid objektfaile, aga Windows OS all kasutame vaikimisi COFF objekte. Kui kogu lähtekood on kompileeritud objekt-failideks, saame linkeri abil neist teha käivitatava binaari.

1) Esmalt peame navigeerima lähtekoodi kausta:

>cd C:\Projects\NASM\helloworld\

2) Lähtekoodi kompileerimiseks käivitame käsurealt NASM kompilaatori:

>nasm -f win32 helloworld.asm -o helloworld.obj

Argument -f – valime objektfaili formaadi, "win32" valib COFF, "elf" valib linuxi ELF formaadi.
Argument helloworld.asm – määrab sisendfailiks "helloworld.asm".
Argument -o helloworld.obj – määrab väljundfailiks "helloworld.obj".
Resultaadiks on objektfail ‘helloworld.obj’.

3) Linkimiseks kasutame MinGW linkerit:

>gcc -O3 helloworld.obj -o helloworld.exe

Argument -O3 – optimeeri koodi kui võimalik.
Argument helloworld.obj – määrame "helloworld.obj" sisendiks.
Argument -o helloworld.exe – määrame "helloworld.exe" väljundiks.
Resultaadiks on käivitatav binaarfail ‘helloworld.exe’. Antud programmi käivitamisel käsurealt, trükitakse konsooli "Hello world!":

>helloworld.exe
Hello, world!

5. Hello World – Makefile

Et iga kord ei peaks käsureal eraldi käivitama NASM-i ja GCC käsud, võime ka lihtsalt teha Makefile'i, mis automatiseerib kompileerimise ja linkimise ühte faili. Kõige lihtsam Makefile näeb välja selline:

# compile and link helloworld.exe
all:
	nasm -f win32 helloworld.asm -o helloworld.obj
	gcc -O3 helloworld.obj -o helloworld.exe

# delete the generated .obj and .exe 
clean:
	rm -rf helloworld.obj helloworld.exe

Makefile'i salvestame oma helloworld kausta "C:\NASM\helloworld\Makefile". Makefile on ilma igasuguse laiendita tekstifail. Käsurealt peame navigeerima helloworld kausta ja saame Makefile'i käivitada lihtsa käsuga make:

>cd C:\NASM\helloworld\
>make
nasm -f win32 helloworld.asm -o helloworld.obj
gcc -O3 helloworld.obj -o helloworld.exe

6. Kokkuvõte

Selles praktikumis õppisime kuidas kirjutada Netwide Assembleris kõige lihtsam "Hello world" programm. Võrreldes eelnevate aastate MASM-iga, on NASM palju kergem ning vajab vähem detailide seletamist. Sellegipoolest vajab Asmis programmeerimine palju tähelepanu, sest terve arvuti mälu ja raud on meie kasutada.
Lisaks tuleb mainida, et antud juhul pääsesime väga kergelt kasutades C stdlib printf funktsiooni, mis teeb meie eest suure osa tööd ära. Seda ülesannet on ka võimalik lahendada näiteks Windows API funktsiooniga WriteFile, mis tähendaks automaatselt, et Linuxi peal ei ole võimalik programmi linkida.

See on ka põhjus miks on pea võimatu portida Assembleris kirjutatud programm ühelt OS-ilt teisele. C keel leiutati just selleks, et kiirendada Unix kerneli portimist PDP-7 pealt PDP-11 peale.

NASM Lehele | 2. Praktikum >>