<< 1. Praktikum | NASM Lehele | 3. Praktikum >>

NASM – Praktikum2: Makefile, Adresseerimine ja Põhikäsud

Eelmine praktikum keskendus arenduskeskkonna ülesseadmisele ja esimese hello world programmi kirjutamisele. Selles praktikumis vaatame lähemalt mis on Makefile-id ja õpime ära põhilised assembleri käsud ning adresseerimismeetodid. Samas õpime ka mitme assembleri lähtekoodi kompileerimist üheks binaariks.

1. Makefile alused

Eelmises praktikumis puutusime väga põgusalt kokku ühe väga lihtsa Makefile-iga. Sellel kohal peaks mainima, et tegu oli väga lihtsustatud näitega ning päris Makefile-iks seda ikkagi lugeda ei saanud.
Käivitades käsurealt käsu "make", käivitatakse fail nimega "Makefile" mis asub hetkel olevas kaustas. Helloworld näite puhul peame eelnevalt navigeerima helloworld kausta, mille puhul käivitatakse "C:\NASM\helloworld\Makefile":

>cd C:\NASM\helloworld\
>make

Makefile puhul on tegemist erilise scriptiga, mis otsustab kas ja kuidas kompileerida lähtekood väljundbinaariks. Kusjuures, "reeglid" (rules) peame me ise käsitsi defineerima. Vaikimisi reegel "all" käivitub siis kui käsurealt ei ole make saanud ühtegi argumenti. Kõik mis järgneb tabulatsiooniga (TAB) pärast reeglit, kuulub selle reegli käskude alla - paljuski sarnane Pythoniga:

# default 'rule'
all:
	nasm -f win32 helloworld.asm
	gcc helloworld.o -o helloworld.exe

Üldreeglid

Selleks, et toimuks automaatne kompileerimine (kompileeritakse failid mis on muutunud), peame defineerima muutuja OBJECTS milles on soovitud objektfailid ja ühe üldise reegli (pattern rule) nendele vastavate lähtefailide kompileerimiseks. OBJECTS märgime "all" reegli eelduseks. Sellisel juhul rakendatakse kõik reeglid mis kattuvad vastava objekti nimetusega. Antud juhul kasutame üldreeglit "%.o: %.asm", kuid võiksime ka kasutada reeglit "helloworld.o: helloworld.asm". Üldreeglid vähendavad märgatavalt Makefile suurust, seega on targem jääda üldreeglite juurde.

OBJECTS = helloworld.o
all: $(OBJECTS)
	gcc -O3 $(OBJECTS) -o helloworld.exe

# general rule for generating [.o], only [.asm] inputs are considered
%.o: %.asm
	nasm -f win32 $*.asm -o $*.o

Clean Reegel

Üldiselt on hea idee lisada Makefile lõppu ka "clean:" reegel. Clean peaks minimaalselt kustutama kõik ajutised objektfailid ja muidugi ka väljundbinaari. Alles peab jääma lähtekood ja kõik muu. Käsurealt käivitame clean reegli "make clean" käsuga.

OBJS = test01.o module02.o
OUT = test01.exe
all: $(OBJS)
	gcc -O3 $(OBJS) -o $(OUT)
	
# delete OBJS and OUT
clean:
	rm $(OBJS) $(OUT)

# general rule for [.asm] -> [.o]
%.o: %.asm
	nasm -f win32 $*.asm -o $*.o
>make clean
rm helloworld.o helloworld.exe

Makefile on üpriski keeruline script, aga siiski võimaldab see meil kõvasti lihtsustada käsurealt kompileerimist. Tegelikult on ka näha, et enamus Makefile-ist saab deklareerida väga üldiselt ning kõik olulise saab panna Makefile algusesse kirja.

Makroasendus

Makroasendus võimaldab deklareeritud makro sisu asendada mingi lihtsa reegli järgi. Kõige tavalisemalt kasutatakse seda selliselt:

SRCS = test01.asm module01.asm module02.asm
OBJS = $(SRCS:.asm=.o)

Antud juhul asendatakse SRCS makro kõik ".asm" laiendid ".o" laiendiga, saades seega uue makro OBJS, mille sisuks on "test01.o module01.o module02.o". Sellest pole üksikus Assembleri näiteks palju kasu, seega peab näiteks tooma Makefile-i, mis kombineerib Assembleri ja C programmi:

ASRCS = mystrlen.asm  mystrcmp.asm
CSRCS = main.c  helpers.c
AOBJS = $(ASRCS:.asm=.o)
COBJS = $(CSRCS:.c=.o)
OUT = mytest.exe

all: $(AOBJS) $(COBJS)
	gcc -O3 $(AOBJS) $(COBJS) -o $(OUT)

.PHONY: clean
clean:
	rm -rf $(AOBJS) $(COBJS) $(OUT)

%.o: %.asm
	nasm -f win32 $*.asm -o $*.o
%.o: %.c
	gcc -O3 -c $*.c -o $*.o

Käsk ".PHONY: clean" tähendab, et "clean:" reegel ei sõltu mingist sisendfailist nimega "clean". Väljundist näeme, et 1) kompileeritakse Assembler objektid, siis 2) C objektid ja 3) lingitakse kõik objektid binaariks:

>cd C:\NASM\AsmAndC\
>make
nasm -f win32 mystrlen.asm -o mystrlen.o
nasm -f win32 mystrcmp.asm -o mystrcmp.o
gcc -O3 -c main.c -o main.o
gcc -O3 -c helpers.c -o helpers.o
gcc -O3 mystrlen.o mystrcmp.o main.o helpers.o -o mytest.exe

>mytest.exe
helpsome() => void
mystrlen("abcd") => 4
mystrcmp("abcd", "abcd") => 0

Et näha miks Makefile on eriti kasulik, teeme väikese muudatuse "helpers.c" failis ja käivitame uuesti make käsu. Näeme, et seekord kompileeriti ainult "helpers.c" ja lingiti väljundbinaar. See säästab suuremates projektides kõvasti kompileerimisaega ja vaeva:

>make
gcc -O3 -c helpers.c -o helpers.o
gcc -O3 mystrlen.o mystrcmp.o main.o helpers.o -o mytest.exe

Täpsemalt saab Makefile detailide kohta lugeda vastavalt lingilt: A Simple Makefile Tutorial.


2. MOV Käsk ja Adresseerimine

mov – Move data: Andmete kopeerimine registrite ja mälu vahel. Inteli assembler süntaks järgib alati kuju:

mov DST, SRC	; DST <- SRC

Paar lihtsat näidet:
mov eax, edx	; EAX dword <- EDX dword
mov dx,  ax		; EDX word  <- EAX word
mov ah,  dl 	; EAX hi    <- EDX lo

movzx – Move with zero extend: Kopeerib andmed väiksema laiusega registrist suuremasse ja väärtustab ülejäänud bitid nullidega. Näiteks bait 0x28 (40) loetaks sisse kui 0x00000028:

mov		bl,	 40		; BL  := 0x28
movzx	eax, bl		; EAX := 0x00000028

movsx – Move with sign extend: Töötab nagu MOVZX, aga arvestab sign bitti. Näiteks negatiivne bait 0xE0 (-32) loetaks sisse kui 0xFFFFFFE0:

mov		bl,	 -32	; BL  := 0xE0
movsx	eax, bl		; EAX := 0xFFFFFFE0

Adresseerimine

Mälu adresseerimine x86 protsessoril on arhitektuuriliselt väga keerukas selleks, et teha programmeerija elu natukene kergemaks. Põhiliseks piiranguks osutub see, et kahe mäluaadressi vahel ei ole võimalik andmeid liigutada ja vahetulemus peab alati käima läbi registrite. Seega mälust saab andmeid lugeda ja kirjutada ainult läbi registrite.
Reegli erandiks on vahetud väärtused (immediate values), mida protsessor suudab oma sisemisest käsuinterpretaatorist välja kirjutada.

; Prax2/address.asm
; Testing for different addressing modes
section .data
	array	dd	0,1,2,3,4
	string	db	"Result: %d",10,0
section .text
global	_main
extern	_printf
_main:
	; ---- prologue ----
	push	ebp
	push	ebx			; save EBX
	mov		ebp, esp
	; ------------------
	
	
	; insert your code here:
	mov		eax, [array]
	
	
	; printf("Result: %d\n", EAX);
	push	eax
	push	string
	call	_printf
	add		esp, 8
	; return 0;
	xor		eax, eax	; EAX := 0
	
	; ---- epilogue ----
	mov		esp, ebp
	pop		ebx			; restore EBX
	pop		ebp
	ret
	; ------------------

Järgmiste näidete jaoks kasutame ülemist skelettprogrammi, mille abil saame uurida lähemalt erinevate adresseerimiste vahetulemusi. Iga assembleri näite juures on ka selgitav C alternatiiv.

x86 Adresseerimine

1. Register väärtustamine [register]
Liigutab väärtusi registrite vahel.

	; int a = b;
	mov		eax, ebx

2. Vahetu väärtustamine [immediate]
Liigutab vahetu väärtuse (konstandi) registrisse või mõnda mäluaadressi. Mäluaadressi puhul peab määrama andmetüübi laiuse "dword[...]".

	; int a = (int)array;
	; int a = 0x1000;
	; *array = 30;
	mov		eax, array
	mov		eax, 0x1000
	mov		dword[array], 30

3. Otsene adresseerimine [direct]
Liigutab väärtuse fikseeritud mäluaadressilt registrisse või vastupidi. Register määrab adresseeritava mälu laiuse. MOVZX/MOVSX käsuga peab määrama laiuse nt: "byte[...]".

	; int a = *array;
	; int a = *(int*)0x1000;
	; char c= *(char*)array;
	mov		eax, [array]
	movsx	ecx, byte[array]

4. Registrikaudne adresseerimine [base]
Liigutab väärtuse mälust registrisse ja mäluaadress (pointer) asub samuti registris.

	; int* ptr = array;
	; int a = *ptr;
	mov		ebx, array
	mov		eax, dword[ebx]

5. Baas+Nihe adresseerimine [base + offset]
Baasregistrile (base) lisandub vahetu nihe (immediate offset).

	; int* ptr = array;
	; int a = ptr[1];
	mov		ebx, array
	mov		eax, [ebx + 4]

6. Baas+Indeks adresseerimine [base + index]
Baasregistrile (base) lisandub indeksregister (index). Alternatiiv nihkele.

	; int* ptr = array;
	; int i = 1;
	; int a = ptr[i];
	mov		ebx, array
	mov		eax, 4
	mov		eax, dword[ebx + eax]

7. Baas+Indeks+Nihe adresseerimine [base + index + offset]
Baasregistrile (base) lisandub indeksregister (index) ja vahetu nihe (immediate offset).

	; int* ptr = array;
	; int i = 1;
	; int a = ptr[i + 1];
	mov		ebx, array
	mov		eax, 4
	mov		eax, dword[ebx + eax + 4]

8. Baas+SkalaarIndeks adresseerimine [base + index*scale]
Baasregistrile (base) lisandub skalaarindeks. Lubatud skalaarid on [1,2,4,8]. Lihtsustab massiivide adresseerimist.

	; int* ptr = array;
	; int i = 2;
	; int a = ptr[i];
	mov		ebx, array
	mov		eax, 2
	mov		eax, dword[ebx + eax*4]

9. Baas+SkalaarIndeks+Nihe adresseerimine [base + index*scale + offset]
Baasregistrile (base) ja skalaarindeksile lisandub vahetu nihe (immediate offset). Lihtsustab massiivide adresseerimisi.

	; int* ptr = array;
	; int i = 2;
	; int a = ptr[i + 1];
	mov		ebx, array
	mov		eax, 2
	mov		eax, dword[ebx + eax*4 + 4]

Üldiselt saab x86 adresseerimise kokku võtta järgmise visandiga. Kõiki aadresse saab lugeda kujul:
[base + index*scale + offset]

Sellega peaks olema kaetud kõik adresseerimismeetodid x86 protsessoril. Antud näidetes teostati ainult lugemistehteid, et vähendada näidete mahtu. Loomulikult saab kõik nähtud tehted teha ka tagurpidi: "mov dword[ebx + 12], eax".


3. Põhikäsud, Aritmeetika, Programmivoog

Põhikäsud

push – Push to stack: Lükkab väärtuse stack segmenti ja lahutab ESP registrist väärtuse laiuse.

push	eax      		; pushes eax onto the stack
push	dword[array] 	; pushes 4 bytes from address @array

pop – Pop from stack: Võtab väärtuse stack segmendist ja liidab ESP registrile väärtuse laiuse.

pop		dword[array]	; pops value to the specified address
pop		eax      		; pops the top element from the stack into eax

lea – Load effective address: Arvutab adresseeringu aadressi ning salvestab selle aadressi. Mälust ei loeta midagi.

lea		ebx, [array]		; address of array copied to ebx, same as mov ebx, array
lea 	ebx, [ebx + eax*4]	; calculated address into ebx

Aritmeetika ja Loogika käsud

add,sub – Integer Addition/Subtraction: liidab/lahutab kaks operandi ja salvestab tulemuse esimesse operandi.

add		eax, 4			; eax += 4
add		eax, ebx		; eax += ebx
add		[ebx], eax		; [ebx] += eax
sub		eax, 10			; eax -= 10

inc,dec – Integer Increment/Decrement: suurendab/vähendab operaatorit 1 võrra.

inc 	eax				; ++eax
dec 	ebx				; --ebx
inc 	dword[ebx]		; ++[ebx]

imul – Integer Signed Multiplication: omab kahte erinevat süntaksit. Esimene variant korrutab kaks operandi ja paigutab tulemuse esimesse operandi. Teine süntaks korrutab teise ja kolmanda operandi ning paigutab tulemuse esimesse operandi.

imul 	eax, edx  		; eax *= edx
imul 	eax, [ebx]		; eax *= [ebx]
imul 	eax, edx, 25	; eax = edx * 25
imul 	eax, [ebx], 10	; eax = [ebx]*10

idiv – Integer Signed Division: jagab 64-bitise täisarvu EDX:EAX määratud väärtusega läbi. EAX tuleb sign-extendida registrisse EDX käsuga CDQ - vastasel juhul ei toimu jagamine korrektselt. Tulem on alati EAX registris ja jagatise jääk on EDX registris. Operand ei saa olla EDX register või vahetu väärtus.

mov     eax, 10
cdq                     ; edx:eax = 00000000:0000000a
idiv 	ebx      		; eax /= ebx

mov     eax, -10
cdq                     ; edx:eax = ffffffff:ffffffec
idiv	eax     		; eax /= eax

and, or, xor – Bitwise logical and, or and exclusive or. Kasutame bitt-loogilisi tehteid kahe operandi vahel, paigutades tulemi esimesse operandi.

xor  	edx, edx		; edx ^= edx
and  	ecx, eax		; ecx &= eax
or   	ebx, edx		; ebx |= edx
and  	eax, 0x0F		; eax &= 0x0F

not – Bitwise logical not. Pöörab bitid ümber. Väärtus võib olla register või adresseeritud mälu.

not 	eax            	; eax = !eax
not 	dword[ebx] 		; *ebx = !*ebx

neg – Arithmetic negate. Negatiivselt väärtustab täisarvu i = -i.

neg 	eax 			; eax = -eax

shl,shr – Logical Shift Left/Right. Nihutab bitte loogiliselt vasakule/paremale, üle ääre nihutatud bitid kaovad ja lisatud bitid on nullid. Näiteks 0b10011100 << 2 = 0b01110000

shl 	eax, 1   		; eax = eax << 1   ; same as eax*2
shl 	ebx, eax 		; ebx = ebx << eax
shr 	eax, 2   		; eax = eax >> 2   ; sames as eax/4

sar,sal – Arithmetic Shift Left/Right. Nihutab bitte aritmeetiliselt vasakule/paremale. Arvestab, et tegu võib olla negatiivse arvuga ning säilitab vajadusel negatiivsusbiti.

movsx	eax, 0b10000000
sar		eax, 1			; eax = eax >> 1
						; AL (0b11000000)

Programmivoo käsud

Programmivoo käske kasutatakse programmi loogiliseks juhtimiseks. Madalamal tasemel implementeeritakse nende käskuda if/else/while käitumist.

jmp – Unconditional jump. Hüppab määratud sümbolile, muutes programmi käivitusasukohta. Otseselt muutub EIP register.

jmp 	begin   		; jumps to label 'begin'

jcondition – Conditional jump. Hüppab sümbolile ainul siis kui kindel tingimus on täidetud. Need käsud sõltuvad protsessori FLAGS registrist. Kõige tähtsamad bitid on Zero Flag (ZF) ja Sign Flag (SF). Kõik aritmeetilised käsud mõjutavad protsessori FLAGS registrit. Näiteks käsk xor eax, eax tõstab Zero Flagi (ZF). Täpsemalt saab lugeda FLAGS registrist Wikipedias.

je  	label			; jump if equal            (ZF=1)
jne 	label			; jump if not equal        (ZF=0)
jz  	label			; jump if zero             (ZF=1)
jnz 	label			; jump if not zero         (ZF=0)
jg  	label			; jump if greater          (signed)
jge 	label			; jump if greater or equal (signed)
jl  	label			; jump if less             (signed)
jle 	label			; jump if less or equal    (signed)
ja  	label			; jump if above          (unsigned)
jae 	label			; jump if above or equal (unsigned)
jb  	label			; jump if below          (unsigned)
jbe 	label			; jump if below or equal (unsigned)
jcxz 	label			; jump if CX register == 0
jecxz 	label			; jump if ECX register == 0

cmp – Signed compare. Teostab märgitundliku lahutustehte kahe operandi vahel ja uuendab protsessori FLAGS registrit. Märgatavalt ZF ja SF. Võrdluskäsku kasutame ainult siis kui on vaja võrrelda kahe operandi vahet (suurem/väiksem?).

cmp		[len], 0		; len ?? 0
jl   	.f_exit			; len < 0 ? .f_exit
cmp  	eax, edx		; eax ?? edx
jg   	.loop			; eax > edx ? .loop

test – Equality test. Teostab loogilise AND tehte kahe operandi vahel ja uuendab FLAGS registrit. Uuenevad ZF ja SF. Test käsku kasutame ainult siis kui on vaja testida kahe operandi võrdsust või juhul kui mõni register on väärtusega 0.

test 	eax, ecx  		; eax ?? ecx
je   	.f_exit   		; eax == ecx ? .f_exit
test 	eax, eax  		; eax ?? eax
jz   	.loop1    		; eax == 0 ? .loop1

loop – Looping instruction. Teostab tsüklihüppe juhul kui ECX != 0 ja vähendab ECX-i 1 võrra.

mov  eax, 0				; sum = 0
mov  ecx, 10			; n = 10
.loop1					; do {
	inc eax				; 	++sum
loop .loop1				; } while(n--);
						; sum == 10

4. Kokkuvõte

Selle praktikumiga õppisime täpsemalt tundma kuidas Makefile-id töötavad, mis on väga tähtis, et programmide kompileerimine oleks piisavalt mugav. Lisaks tunneme nüüd põhilisemaid adresseerimisviise x86 protsessoril ja õppisime ära põhilised käsud mille abil saame hakata järgmises praktikumis programmeerima.

Antud hetkeks oleme õppinud palju kompileerimise, adresseerimise ja käskude kohta. Siiski on veel vaja õppida erinevaid funktsioonitüüpe ning kuidas funktsioonid käituvad, enne kui jõuame reaalsete programmide kirjutamiseni. Samuti tuleb meil ära õppida kuidas toimub "debugimine".

<< 1. Praktikum | NASM Lehele | 3. Praktikum >>