<< 2. Praktikum | NASM Lehele | 4. Praktikum >>

NASM – Praktikum3: Andmetüübid, Funktsioonitüübid ja Extern Sümbolid

Tänane praktikum koosneb kolmest alampeatükist ja on otseselt seotud 2. loengu materjalidega. Igas peatükis tuleb implementeerida funktsioone Assembleris, mille aluseks on C-keeles kirjutatud vastav algoritm.

1. Andmetüübid

Eesmärk on läbi testida erinevad Assembleri andmetüübid. Aluseks on meil alus.asm ja vastav Makefile. Antud juhul on tegemist tavapärasest erilisema Makefile-iga, mis võtab wildcard käsuga kõik *.asm failid aktiivses kaustas ja genereerib vastavalt OBJS ja TARGETS. Igast assembleri failist kompileeritakse üks binaar. See tuleb meil testimisel kasuks ja saame teha terve praktikumi ainult ühe Makefile-iga.

Praktikumi alusfailid: Prax3_alus.zip

:/NASM/Prax3/
  `- Makefile
  `- alus.asm

Makefile:

SRCS      = $(wildcard *.asm)
OBJS      = $(SRCS:%.asm=%.obj)
TARGETS   = $(SRCS:%.asm=%.exe)
NASMFLAGS = -Wall -g -f win32
GCCFLAGS  = -Wall -g -O3

# first compile OBJS with NASM
# then link TARGETS with GCC
all: $(OBJS) $(TARGETS)
clean:
	rm -rf $(OBJS) $(TARGETS)
	
# for each %.obj : %.asm, call NASM --> target.obj
%.obj: %.asm
	nasm $(NASMFLAGS) $*.asm -o $*.obj
	
# for each %.exe : %.obj, call GCC --> target.exe
%.exe: %.obj
	gcc $(GCCFLAGS) $*.obj -o $*.exe

alus.asm:

; ----------------------------------------------------
; alus.asm
; @brief	NASM Prax 3 alusfail
; @author	...
; @date		19.11.2013
; ----------------------------------------------------

section	.data
	
	
section .text
global 	_main
_main:

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

Funktsiooni PROLOGUE ja EPILOGUE spikker:

func:
	push	ebp
	mov		ebp, esp		; set new frame
	sub		esp, 0			; 0 local bytes
	; --------------
	
	; --------------
	mov		esp, ebp		; restore stack
	pop		ebp
	ret

1.1 Data DWORD - integers

Teeme alusfailist koopia: integers.asm ja asume tõlgime sinna järgneva C programmi:

static int index = 0;
static int array[6] = { 3, 7, 7, 0, 5, 0 };

int main()
{
	int i;
	
	printf("Enter an index: ");
	scanf("%d", &index);
	for (i = index; i < 6; ++i)
		printf("array[%d] = %d\n", i, array[i]);
		
	return 0;
}

Ülesanne: Kirjutada vastav programm Assembleris. Spikker:

	var dd ...    ; declare integer data (data-doubleword(s))
	var db ...    ; declare byte data (data-byte(s))
	extern ...    ; declare a symbol of external linkage
	.label:       ; function local label
	cmp eax, 0    ; compare values
	jge .label    ; jump if greater-equal
	section .data ; write assembly to .data RW section
	section .text ; write assembly to .text RO section

C keeles omab keyword static mitmeid erinevaid tähendusi. Täpsemalt võib lugeda siit: http://tigcc.ticalc.org/doc/keywords.html#static. Globaalsel tasandil tähendab static, et antud sümbol (global variable/function) ei ole linkerile nähtav. Assembleris on vaikimisi kõik sümbolid static ning linkerile nähtavaks tehakse sümbolid keywordiga global.

Et linker teaks otsida scanf ja printf funktsioone, on meil vaja extern käsku. Samuti peame olema tähelepanelikud, et kood läheks .text segmenti ja andmed .data segmenti.


1.2 Data BYTE - strings, byte arrays

Teeme alusfailist koopia: strings.asm ja asume jälle tõlkima C programmi:

static const char* string = "astrlen test";
static char array[] = { 65, 66, 67, 68 };

int astrlen(const char* str)
{
	int len = 0;
	while (str[len] != 0)
		++len;
	return len;
}

int main()
{
	int i;
	printf("string.length: %d\n", astrlen(string));

	for (i = 0; i < 4; ++i)
		printf("array[%d] = %c\n", i, array[i]);
	
	return 0;
}

Ülesanne: Kirjutada vastav programm Assembleris. Spikker:

	movzx eax, byte[...]  ; move zero extend byte
	db `string\n\0`       ; string with escape sequences like \n, \0
	db "string",10,0      ; define a byte array and add \n, \0 to the end as bytes

Stringi pikkuse mõõtmine on C-s nähtavasti kõige tihedamalt kasutatav funktsioon ning ühtlasi ka üks lihtsamaid algoritme. Strlen-i implementatsioone võib olla kümneid erinevaid, kuid üldiselt on lühemad unaligned byte-by-byte variandid kõige aeglasemad.

Baitide sisselugemisel peame teadma kas meil on vaja tulemust DWORD või BYTE kujul. Kui me anname printf-ile '%c' argumendi, peab see olema DWORD. Baidi lugemine mälust DWORD-iks saab teha käsuga movzx.


2. Funktsioonitüübid

Proovime järgnevalt läbi erinevaid funktsioonitüüpe: CDECL, STDCALL, FASTCALL ja THISCALL.
Siinkohal peame meelde tuletama üldised reeglid:
 1. Return Value - EAX registris
 2. Trashable Registers - EAX, ECX, EDX on alati rikutavad
 3. Protected Registers - EBX, ESI, EDI peab alati enne kasutamist kaitsma push/pop käsuga


2.1 CDECL - C calling convention

Kopeerime jälle alusfaili ja nimetame ümber: cdecl.asm. CDECL on kõige lihtsam funktsioonitüüp ja seni olemegi kasutanud just CDECL funktsioone. CDECL üldreeglid:
 1. Caller Cleanup - Funktsiooni kutsuja puhastab ka argumendid
 2. Dekoratsioon - _func

Algoritmiks on vastav väga lihtne jupp C koodi:

int __cdecl cfunc(int a, int b)
{
	return a + b;
}
int main()
{
	printf("%d\n", cfunc(10, 20) );
	return 0;
}

Ülesanne: Kirjutada vastav CDECL funktsioon Assembleris.

2.2 STDCALL - Win32 calling convention

Kopeerime eelneva cdecl.asm koodi ja salvestame: stdcall.asm. STDCALL funktsioonitüüpe kasutatakse Win32 API-s ja Watcom C++ kompilaatoris. STDCALL üldreeglid:
 1. Function Cleanup - Funktsiooni puhastab argumendid
 2. Dekoratsioon - _func@N, kus N = argumentide suurus baitides

Algoritm jääb täpselt samaks, kuid deklaratsioon C-s näeb välja selline:

int __stdcall stdfunc(int a, int b)
{
	return a + b;
}

Ülesanne: Kirjutada vastav STDCALL funktsioon Assembleris.


2.3 FASTCALL ECX:EDX - Fastcall through ECX and EDX (GCC)

Teeme jätkuvalt uue koopia: fastcall.asm. FASTCALL funktsioonitüüpe kasutatakse tänapäeval harva, kui Assembleris näeb selliseid funktsioone sagedamini. Et ka Assembleri maailm oleks ühtlustatud, lepiti kokku FASTCALL calling convention, kus 2 esimest argumenti tulevad vastavalt ECX ja EDX registritest ning ülejäänud läbi stack-i. FASTCALL üldreeglid:
 1. Caller Cleanup - Kutsuja puhastab argumendid
 2. Dekoratsioon - @fastfunc@N, kus N = argumentide suurus baitides
 3. Argumendid - arg1: ECX, arg2: EDX, ...rest: stack

Deklaratsioon C-s näeb välja selline:

int __fastcall fastfunc(int a, int b, int c)
{
	return a + b + c;
}

Ülesanne: Kirjutada vastav FASTCALL funktsioon Assembleris.


2.3 THISCALL ECX - Fastcall through ECX(GCC)

Teeme viimase koopia: thiscall.asm. THISCALL on objekt-orienteeritud programmeerimise funktsioonitüüp, enamlevinud C++ keeles. Kahjuks ei eksisteeri C-s vastavat calling conventioni, mis ühilduks standardselt mõne C++ THISCALL dialektiga, sest iga C++ kompilaator dekoreerib funktsioonid erinevalt.

MinGW G++ THISCALL:
 1. Calling Convention - CDECL
 2. THIS pointer - ECX registris
 3. Dekoreering - __ZN8ivec28addEv

Visual C++ THISCALL:
 1. Calling Convention - STDCALL
 2. THIS pointer - ECX registris
 3. Dekoreering - ?add@ivec2@@QAEHXZ

Nagu näha siis THISCALL ei ole päris ühtne standard, aga siiski on sellel alati 2 põhilist reeglit:
 1. THIS pointer - Alati funktsiooni "esimene" argument või läbi ECX registri
 2. Type Safety - Keeruline nimedekoreering võimaldab väga hea tüübikindluse

Mugavuse mõttes lepime kokku NASM Prax THISCALL-i, mis oleks ühilduv C-ga:
 1. Calling Convention - STDCALL
 2. THIS pointer - Esimene argument stackis
 3. Dekoreering - _ivec2_add@N

Deklaratsioon C-s näeb välja selline:

typedef struct 
{
	int a, b;
} 
ivec2;

int __stdcall ivec2_add(ivec2* this)
{
	return this->a + this->b;
}
int main()
{
	ivec2 vec = { 10, 20 };
	printf("%d\n", ivec2_add(&vec));
	return 0;
}

Ülesanne: Kirjutada vastav THISCALL funktsioon Assembleris.

	struc ivec2
		.a resd 1
		.b resd 1
	endstruc
	
	myVec  ivec2  0,0

3. Extern Sümbolid

Üldiselt kasutame keyword extern mõne välise funktsiooni puhul. Nagu varasemalt räägitud, extern sümbolid lahendatakse linkimise, mitte kompileerimise hetkel. Niimoodi saame funktsioone kompileerida ühes moodulis, aga linkida mitmes erinevas.
Hea näide on extern _printf, mis asub C standard library dünaamilises teegis (DLL). Väga palju programme kasutavad korraga printf funktsiooni, seega on mõistlik kui see eksisteerib ainult ühes kohas.

Muidugi ei ole extern piiratud ainult funktsioonide tasemele, vaid see kehtib kõikide globaalsete sümbolite puhul. Seega peaks extern töötama nii Assembleris kui ka C-s samamoodi. Sellegipoolest on üks erinevus mida mainisime ka eelpool:
1. Assembler - Ainult global sümbolid eksporditakse linkerile.
2. C - Sümbolid mis on static ei ekspordita linkerile.

Selle teadmisega võime alustada uue testprogrammiga. Vanast Makefile-ist ei ole enam siin kasu, seega peame iseseisvalt tegema uue. Tekitame 2 faili test.asm ja symbols.asm. Test moodulis on meie main funktsioon ja lähtekood; Symbols moodulis on ainult mingid sümbolid.

:/NASM/Prax3-extern/
  `- Makefile
  `- test.asm
  `- symbols.asm

Pseudokood C-s:

// symbols.c
int GlobalInt = 42;
// test.c

extern int GlobalInt;

int main()
{
	printf("%d", GlobalInt);
	return 0;
}

Ülesanne: Kirjutada vastav programm Assembleris.
Lisa 1: Kirjutada symbols.asm-i funktsioon memchr() ja kasutada seda test.asm-is
Lisa 2: Kirjutada C programm mis vastab Lisa 1 programmi struktuurile


4. Kokkuvõte

Selle praktikumiga õppisime kuidas Assembleris funktsioone kirjutada ning andmeid kasutada. Veelgi tähtsam osa oli õppida selgeks EXTERN, kuna tulevikus on just see osa kõige tähtsam C ja Assembleri programmide sidumiseks.

Selle praktikumi lahendused: Prax3_lahendused.zip

<< 2. Praktikum | NASM Lehele | 4. Praktikum >>