Assembleri kasutamine

Assemblerit kasutatakse sageli keerukate probleemide jaoks, mida ei saa kõrgemas programmeerimiskeeles lahendada. Selliste probleemide hulka kuuluvad tavaliselt katkestustega tegelevad funktsioonid jms. Ka suurimat töökiirust nõudvates funktsioonides on sageli vaja kasutada assemblerit.

Tavaliselt kirjutatakse vaid osa programmist assembleris. Assemblerkoodi sidumiseks C - keele programmiga luuakse siis üks terve funktsioon assembleris ja kasutatakse seda C - keele programmis nagu harilikku funktsiooni. Selleks tuleb teada, kuidas C - keeles funktsioonile parameetreid üle antakse. Borland C/C++ translaator lubab teil kasutada assemblerkoodi ka otse C - keele failis. Selleks on vaja kasutada vaid võtmesõna asm. Sellised assemblerkäsud võivad omada järgmist kuju:

asm <assembleri käsk>
asm <assembleri käsk> ;
asm <assembleri käsk> ; asm <assembleri käsk> 
asm
{
...
  <assembleri käsk>
  <assembleri käsk>
...
}

Näiteks:

asm  MOV  AX, 6
asm  mov  bx, ax;  int 21h
asm
{
  push ds
  mov ax, [bp+8]
  inc ax
}

Assembleris märgib semikoolon kommentaari algust. Kui te aga sisestate C - keele faili assemblerit, siis märgib semikoolon ühe assemblerkäsu lõppu ja võimaldab teil seega sisestada ühele reale mitu assemblerkäsku.

Looksulud koos võtmesõnaga asm määravad ühe assemblerkeele bloki. Sellise bloki sees ei ole võtmesõna asm kasutamine iga rea alguses enam vajalik.

C - keele faili sisestatud assemblerkäskudes võite kasutada kõiki antud blokis kehtivaid C - keele muutujaid.

Sellise faili transleerimiseks, mis sisaldab assemblerkäske C - keele seas, võite kasutada kas Turbo Assemblerit või Borland C/C++ programmeerimiskeskkonna (IDE) oma assemblerit. Esimesel juhul tuleb teil kasutada translaatori valikut -B või sisestada oma programmi teksti eeltranslaatori käsk #pragma inline. Vastasel juhul kasutatakse Borland C/C++ programmeerimiskeskkonna oma assemblerit. Viimane on ka tunduvalt kiirem meetod. Borland C/C++ programmeerimiskeskkonna oma assembler lubab teil kasutada enamikku assemblerkäskudest. Te võite isegi defineerida muutujaid assembleristiilis käskudega DB, DW ja DT, kuid te ei saa kasutada üldisi assemblerkäske nagu näiteks SEGMENT ja ASSUME. Kui te olete valinud menüüst Options | Compiler | Code Generation valiku 80186 või 80286 protsessori käsud, siis võite neid käske kasutada ka selle programmi sees. Te ei saa aga kasutada 80386 käske C - keele faili sisestatud assemblerkäskudes. Protsessori 80386 käskude kasutamiseks tuleb luua assembleris terve funktsioon ja transleerida ta eraldi näiteks Turbo Assembleriga ning seejärel kasutada seda funktsiooni oma C - keele programmis.

Kui te soovite C - keele faili salvestatud assemblerkäskude transleerimiseks kasutada Turbo Assemblerit või mingit muud assemblerit, siis tuleb ka peale translaatori käsku -B või eeltranslaatori käsku #pragma inline määrata, milist assembleri kasutada. Selleks valige menüüst Options käsk Transfer. Avanenud dialoogis määrake assembleri asukoht (kataloog, kuhu on installeeritud Turbo Assembler või mingi muu assembler). Peale normaalset installeerimist on need andmed tavaliselt korras ja ei vaja muutmist.

C - keele faili sisestatud assemblerkäsud peavad sobima antud programmiga. Te peate arvesse võtma kasutatud mälumudelit ja kasutama oma assemblerkäskudes sobivaid osuteid. Assemblerkäskudes võib kodeerida ka tsükleid ja hüppeid (JMP, JE, JB jne.), kuid tsükli või hüppe märgiks peab olema C - keele märgend (label). Selline märgend ei saa asuda assemblerkäskude bloki sees.

C - keeles kasutatakse programmi spetsiaalsete andmete salvestamiseks registreid. Segmendiregistreid võite te küll muuta, kuid nende sisu tuleb enne järgmiste C - keele lausete täitmist taastada. Üldisi registreid AX, BX, CX, DX, segmendiregistrit ES ja tähiseid võite muuta, kuidas soovite, ilma nende sisu taastamise üle muret tundmata. Registreid CS ja SS ei tohiks üldse otseselt muuta, neid registreid tohib muuta ainult kaudsete käskudega (JMP, JE, PUSH, POP jne.).

Kui Borland C/C++ programmeerimiskeskkonna assemblerist ei peaks jätkuma, siis tuleks luua eraldi *.ASM failid ja kasutada nende transleerimiseks Turbo Assemblerit. See nõuab põhjalikke teadmisi C - keele funktsioonide kasutamisest. Assemblerfunktsioonid peavad käituma täpselt nii nagu normaalsed C - keele funktsioonid, et neid saaks kasutada C - keele programmis.

Pprogrammeerimiskeeles C kasutatakse parameetrite üleandmiseks funktsioonidele pinu. Parameetrid salvestatakse pinusse alates kõige viimasest ja lõpetades esimesega. Seejärel salvestatakse pinusse registri IP sisu ehk koha aadress, kust programm peaks peale funktsiooni lõpetamist jätkama. Kui väljakutsutav funktsioon omas pikka viita, s.t. tuleb vahetada ka registri CS sisu, siis salvestatakse ka tolle registri vana sisu pinusse. Funktsiooni parameetrid ja tagasipöördumiseks vajalik aadress moodustavad nn. pinuraami (stack frame). Iga funktsioon omab oma pinuraami. Parameetrite ja kohalike muutujate pinult lugemiseks kasutatakse registrit BP (base pointer). Pinuraamis salvestatakse eelmise funktsiooni registri BP väärtus tagasipöörumiseks vajalikku aadressi järel. Sellele järgnevad antud funktsiooni kohalikud muutujad. Joonisel 9 näete tüüpilist pinuraami ehitust:


Joonis 9: Funktsiooni pinuraami ehitus

Väljakutsutav funktsioon peab kohe alguses salvestama registri BP pinusse. Seejärel võib ta reserveerida pinul ruumi oma kohalike muutujate jaoks. Kui te kasutate C - keele programmis asemblerkäske, siis võite lasta kõigi nende probleemidega tegelda translaatoril. Järgmises näites näete assemblerkäskude kasutamist ühes C - keele programmis.


ASMDEMO1.C

/*********************************************************/
/***                                                   ***/
/***	  AsmDemo1.C                                   ***/
/***                                                   ***/
/***    Näitab, kuidas kasutada  C - keele programmis  ***/
/***    assemblerkäske ilma eraldi *.ASM faili loomata.***/
/*********************************************************/

#include <stdio.h>

/*-------------------------------------------------------*/
/*---                                                 ---*/
/*---   SumArray()                                    ---*/
/*---                                                 ---*/
/*---   Liidab n täisarvu ja loovutab tulemuse.       ---*/
/*---   Kuna programm transleeritakse small mälumudeli---*/
/*---   abil, siis on parameetrid jaotatud järgmiselt:---*/
/*---     [BP+4]  -  nCount                           ---*/
/*---     [BP+6]  -  nFirst ehk esimene liidetav      ---*/
/*---     [BP+8]  -  teine liidetav                   ---*/
/*---     [BP+10] -  kolmas liidetav jne.             ---*/
/*-------------------------------------------------------*/
int SumArray(int nCount, int nFirst, ... )
{
  asm MOV AX, nFirst  /* tulemuseks on nFirst kui arve on vaid üks */
  asm MOV CX, nCount  /* täpselt nCount arvu tuleb liita */
  asm MOV BX, BP      /* registris BX salvestame järgmise 
                         liikme numbri */
  asm ADD BX, 8

sum: asm ADD AX, [BX] /* järgmisena liidame [BP+8], siis [BP+10] jne */ asm ADD BX, 2 /* suurendame BX väärtust */ asm LOOPNZ sum /* liidame niikaua, kuni CX on null */ /* tulemus on juba registris AX, mis on ka tema oige koht */ } /*=======================================================*/ /*=== ===*/ /*=== Main() ===*/ /*=== ===*/ /*=== Kasutab funktsiooni SumArray() abi n täisarvu ===*/ /*=== liitmiseks. ===*/ /*=======================================================*/ int main( void ) { /* liidame kuus ühte ja trükime nende väärtuse ekraanile */ printf("6 x 1 = %d", SumArray(6, 1, 1, 1, 1, 1, 1)); return 0; }

Sel kombel on kõige lihtsam assemblerit C - keele programmiga siduda. Nii võiks näiteks implementeerida funktsiooni, mis tegeleb väga suurt kiirust nõudvate töödega. Samas näete ka muutuva hulga parameetritega funktsioonide loomist. Selles assemblerfunktsioonis ei vaja me enam va_ makrosid, vaid kasutame parameetreid otse registri BP abil.

Funktsiooni tulemuseks on int, mis loovutatakse väljakutsujale registris AX. Kuna nimetatud arv summeerimise lõpus juba asub nimetatud registris, siis ei olegi vaja midagi muud teha.

Programmeerimiskeeles C on kindlad reeglid selle kohta, kuidas tagastada funktsiooni väärtus väljakutsujale. Erinevad väärtused antakse tagasi erineval viisil. Alljärgnevas tabelis näete lühikest ülevaadet funktsiooni väärtuse loovutamisest väljakutsujale


Tüüp Loovutamise koht
unsigned char AX
char AX
enum AX
unsigned short AX
short AX
unsigned int AX
int AX
unsigned long DX:AX (DX = viimased 2 bait, AX = esimesed 2 baiti)
long DX:AX
float Matemaatikaprotsessori register TOS
double Matemaatikaprotsessori register TOS
long double Matemaatikaprotsessori register TOS
near * AX
far * DX:AX
struct (1-2 baiti) AX (esimene bait asub AL -s)
struct (4 baiti) DX:AX<
struct (3 või enam kui 4 baiti) Kopeeritakse andmesegmenti ja loovutatakse viit sellele andmestruktuurile.

Tabel 10: Funktsiooni väärtuse väljakutsujale loovutamise viis

Kui te soovite kasutada täielikult assembleris programmeeritud funktsioone, siis tuleb ise tegelda ka pinuraami loomisega, vajalike registrite väärtuste säilitamisega ja muu sellisega, millega tavaliselt tegeleb translaator. Kui te peate muutma mingit programmile vajalikku registrit, siis säilitage tema väärtus kõigepealt PUSH käsuga pinus ja peale registri kasutamist taastage tema väärtus POP käsuga. Tähele tuleb panna, et kõik PUSH ja POP käsud täidetaks õiges järjekorras. See väärtus, mis viimasena pinusse salvestatati, tuleb taastada esimeses järjekorras.

C - keele translaator lisab iga funktsiooni nime ette sümboli "_". Seda tuleb assemblerprogrammis arvesse võtta ja seal ise funktsiooni nime ette too sümbol lisada. C - keele programmis saab siis kasutada selle funktsiooni nime ilma tolle sümbolita, kuna translaator selle talle hiljem lisab.

Loodud *.ASM faili võite samuti lisada oma projektfaili. Kontrollige kindlasti *:ASM failide valikuid projektfailis (klahvikombinatsioon <Ctrl>+<O>). Avanenud dialoogis peab olema *.ASM faili translaatoriks määratud Turbo Assembler või mingi muu sobiv assembler.

Näiteprogrammis ASMDEMO2.C näete puhta assemblerfunktsiooni kasutamist C - keele programmis. Kuna too *.ASM fail on kompileeritud LARGE mälumudeliga (ainult selleks, et teha asju natuke keerukamaks) ja *.C fail SMALL mälumudeliga, siis tuleb *.C faili lisada tolle assemblerfunktsiooni deklaratsioon, et translaator teaks, kuidas talle parameetreid üle anda.


ASMDEMO2.C

/*********************************************************/
/***                                                   ***/
/***   ASM2.C                                          ***/
/***                                                   ***/
/***   See programm näitab, kuidas kasutada täielikult ***/
/***   assemblerkoodis programmeeritud funktsioone.    ***/
/*********************************************************/

/*----------------< Päisefailid >------------------------*/
#include <stdio.h>

/*----------------< Funktsioonide prototüübid >----------*/

/* see funktsioon on defineeritud assemblerfailis RUUTJUUR.ASM */
extern long double far  ruutjuur(long double x);

/*=======================================================*/
/*===                                                 ===*/
/*===   Main()                                        ===*/
/*===                                                 ===*/
/*===   Selles programmis arvutame arvu Pi väärtuse   ===*/
/*===   suurima voimaliku täpsusega. Selleks kasutame ===*/
/*===   failis RUUTJUUR.ASM defineeritud samanimelist ===*/
/*===   funktsiooni.                                  ===*/
/*=======================================================*/
int main( void )
{
  printf("Arvu Pi väärtus on: %10.15Le", ruutjuur(2.0));
  return 0;
}

RUUTJUUR.ASM

;**********************************************************
;*                                                        *
;*   RUUTJUUR.ASM                                         *
;*                                                        *
;*   Selles failis defineerime assemblerfunktsiooni       *
;*   _ruutjuur. Pinul asub alates [BP+6] funktsiooni      *
;*   ainus argument, mis on 10-baidine kümnendmurd.       *
;*   Funktsioon arvutab oma argumendi ruutjuure, kasuta-  *
;*   des selleks kas matemaatikaprotsessorit voi          *
;*   Borland C/C++ teeki EMUL.LIB, mis jäljendab matemaa- *
;*   tiakaprotsessorit.                                   *
;*   Selle funktsiooni kuju C - keeles on:                *
;*      long double far _ruutjuur(long double x);         *
;*                                                        *
;**********************************************************

	EMUL
           ;kasuta Borland C/C++ teeki kui matemaatika-
           ;protsessorit ei ole
	DOSSEG
    	;kasuta DOS standardset segmentide järjestust
	.MODEL large        ;kasuta LARGE mälumudelit
	.CODE               ;siit algab funktsiooni kood
	PUBLIC  _ruutjuur   ;C - keeles lisatakse funktsiooni nimele
            ;peale linkimist sümbol "_"
_ruutjuur  PROC
	push
	bp                 ;loo korralik pinuraam
	mov	bp, sp
	fld	tbyte
ptr [bp+6]   ;laadi parameeter matemaatika-
       ;protsessori registrisse TOS
	fsqrt               ;arvuta ruutjuur
       ;tulemus jääb registrisse TOS
	pop	bp
	ret                 ;ja ongi koik
_ruutjuur  ENDP
	END

Kui te lingite C keele faili mingi *.ASM failiga, siis saab mõlemas failis kasutada teises failis defineeritud muutujaid, täpselt nii nagu ka mitme *.C faili puhul. C keele failis defineeritud muutujaid saab *.ASM failis kasutada, kui nad deklareerida seal võtmesõna extern abil. Samuti võib defineerida muutujaid ka *.ASM failis (võtmesõnaga public) ja neid hiljem *.C failis kasutada, kui nad deklareerida seal võtmesõnaga extern. Ainult HUGE mälumudeliga programmides võib andmetega natuke probleeme tekkida.

Nagu juba peatükis Mälujaotus öeldud, on HUGE ja LARGE mälumudeli põhiline erinevus selles, et HUGE mälumudeliga programm võib omada enam kui 64 KB initsialiseeritud ja initsialiseerimata globaalseid andmeid. Kõigi muude mälumudelite puhul osutab register DS ühele segmendile, mis sisaldab kõiki globaalseid andmeid (nn. DGROUP segment). Dünaamilisi andmesegmente võib rohkem olla. Järgmises tabelis näete registrite sisu erinevate mälumudelite puhul.

Mälumudel registri CS sisu registri DS sisu
TINY _TEXT DGROUP
SMALL _TEXT DGROUP
COMPACT _TEXT DGROUP
MEDIUM <faili nimi>_TEXT DGROUP
LARGE <faili nimi>_TEXT DGROUP
HUGE <faili nimi>_TEXT <faili nimi>_DATA

Tabel 11: Registrite CS ja DS sisu erinevate mälumudelite puhul

Nagu näete, ei ole mälumudelite TINY, SMALL, COMPACT, MEDIUM ja LARGE globaalsete andmetega puhul mingit probleemi. Te peate nad lihtsalt deklareerima*.ASM failis võtmesõnaga extern ja seejärel võite kohe tarvitada tolle muutuja nime tema sisu kasutamiseks, näiteks:


*.C failis:

...
int	i, j;
extern  char  *nimi, buf[20];
...
strcpy(buf, nimi);	/* kasutame *.ASM failis 
                           defineeritud muutujat */

*.ASM failis

...
	EXTRN _i : WORD
	EXTRN _j : WORD
	.DATA
	DB   nimi	"Jaanus", 0 ;nimi, mille lõppu lisame nulli 
                              ;C keele funktsioonide jaoks
...
     MOV	AX, i		;kasutame *.C failis defineeritud
     MOV	BX, j		;muutujaid

See kõik on võimalik just sellepärast, et DS osutab segmendile DGROUP. Mälumudeli HUGE puhul omab aga iga fail oma andmesegmenti, mis ka sisaldab tema globaalseid muutujaid. Iga faili jaoks on olemas segment nimega <faili nimi>_DATA, mis sisaldab selles failis defineeritud initsialiseeritud muutujaid ja segment nimega <faili nimi>_BSS selles failis defineeritud initsialiseerimata muutujate jaoks. Assemblerprogrammis tuleks siis oma andmesegmendi kasutamiseks kõigepealt laadida selle segmendi aadress registrisse DS, näiteks:

	.MODEL HUGE
	.FARDATA
	DW	number	235
...
	.CODE
	PUBLIC	_func1
_func1	PROC
	push  bp	;loome pinuraami
	mov   bp, sp
	push  ds	;säilitame registri DS endise väärtuse
	mov	ax, @fardata ;loeme oma andmesegmendi aadressi
	mov	ds, ax	 ;registrisse DS
	mov	ax, number   ;teeme midagi selle muutujaga
	...
	pop	ds
	pop	bp
	ret
_func1	ENDP
	END

Kui te aga HUGE mälumudeli puhul tahaksite *.ASM failis kasutada temaga lingitud *.C failis defineeritud muutujaid, siis tekib küsimus - millises segmendis need muutujad on salvestatud ja kust saada segmendi aadressi. Selle probleemi jaoks on vaid üks õige lahendus - kasutada SEG käsku koos muutujanimega ja lasta translaatoril selle probleemiga tegeleda. Kui ülemises näites defineeritud muutuja number oleks olnud defineeritud mingis muus failis, siis oleks tulnud teha järgmist:

	...
	push ds
	mov  ax, SEG _number
	mov  ds, ax
	mov  ax, _number
	...
	pop  ds
	...

Assemblerfailis kasutatavad C - keele funktsioonid tuleb samuti deklareerida võtmesõnaga EXTRN. Kui funktsiooni või muutuja deklaratsioonile või definitsioonile lisada sümbol "C", siis teab Turbo Assembler, et seda muutujat tuleb kasutada C - keele reeglite järgi ja lisab talle ise ka sümboli "_". Nüüd saab ka assemblerfailis kasutada C - keele funktsioone käsu CALL abil. Te võite kas ise salvestada vajalikud parameetrid pinule ja seejärel tolle funktsiooni välja kutsuda, või kasutada sümbolit "C" ja reastada lihtsalt funktsiooni parameetrid temast paremale.

Kohalike muutujate kasutamiseks tuleb nihutada pinuviita (SP) muutujatele ruumi tegemiseks allapoole. Nüüd saab registrit BP kasutada nende muutujate väärtuste lugemiseks ja muutumiseks. Enne funktsiooni lõppu tuleb need muutujad aga pinult eemaldada ja alles seejärel taastada endine BP. Järgmises programmis näete, kuidas kasutada assemblerprogrammis C - keele funktsioone ja kohalikke muutujaid.


ASMDEMO3.C

/*********************************************************/
/***                                                   ***/
/***   AsmDemo3.C                                      ***/
/***                                                   ***/
/***   Näitab globaalsete andmete vahetamist C - keele ***/
/***   ja assemblerfunktsioonide vahel ning C - keele  ***/
/***   funktsioonide kasutamist assemblerfunktsioonis. ***/
/*********************************************************/

/*----------------< Päisefailid >------------------------*/

#include	<stdio.h>

/*----------------< Globaalsed muutujad >----------------*/

struct MyData {
 char	name[20];
 int	amount;
} Data[] = {
 "Peeter", 4,
 "Jaanus", 5,
 "Toomas", 21,
 NULL, 0,
};

char	szFormat[] = "%s omab %d protsenti aktsiatest\n";

/*----------------< Funktsioonide prototüübid >----------*/

extern void ShowData(struct MyData *);
int CalcPercent(int, int);


/*=======================================================*/
/*===                                                 ===*/
/*===   main()                                        ===*/
/*===                                                 ===*/
/*===   Selles programmis kasutatakse failis          ===*/
/*===   ASMBAR.ASM defineeritud funktsioone           ===*/
/*===   soovitud andmete esitamiseks ekraanil         ===*/
/*===   graafilise ringdiagrammina.                   ===*/
/*=======================================================*/
int main( void )
{
  ShowData(&Data);           /* esita diagramm ekraanile */
  return 0;
}	//main





/*-------------------------------------------------------*/
/*---                                                 ---*/
/*---   CalcPercent()                                 ---*/
/*---                                                 ---*/
/*---   Arvutab, kuipalju protsente moodustab arv     ---*/
/*---   nNum arvust nWhole. Seda funktsiooni kasu-    ---*/
/*---   tatakse assemblerfunktsioonis ShowData().     ---*/
/*-------------------------------------------------------*/
int CalcPercent(int nNum, int nWhole)
{
  return (int)((double)nNum /  (double)nWhole * 100.0);
}	/* CalcPercent() */

SHOWDATA.ASM

;**********************************************************
;***                                                    ***
;***   ShowData.ASM                                     ***
;***                                                    ***
;***   Sisaldab assemblerfunktsiooni ShowData(), mis    ***
;***   esitab  ekraanile talle üle antud andmed.        ***
;***   Lisaks sellele näidatakse selles failis, kuidas  ***
;***   kasutada assemblerfunktsioonis kohalikke         ***
;***   muutujaid.                                       ***
;**********************************************************
	DOSSEG
	.MODEL SMALL
;koiki neid C funktsioone saab kasutada assembler funktsioonis
	EXTRN C printf:PROC          ;standardne C funktsioon
	EXTRN C CalcPercent:PROC     ;teises failis defineeritud
                                     ;funktsioon
	.DATA
	EXTRN C szFormat	     ;formaadispetsifikatsioon
	.CODE
	PUBLIC C ShowData
	;Pinuraamil on järgmised andmed:
	; [BP-4]	-	kohalik muutuja - viit struktuurile
	; [BP-2]	- 	kohalik muutuja - kogu summa
	; [BP+0]	-	vana BP
	; [BP+2]	-	tagasipöördumise aadress
	; [BP+4]	-	andmestruktuuri MyStrct aadress
ShowData	PROC	C
	push bp
	mov  bp, sp
	sub  sp, 4	;seda ruumi pinul on vaja kohalike muutujate jaoks
	mov  bx, [BP+4]            ;hangi andmestruktuuri aadress
	mov  word ptr [BP-2], 0	   ;initsialiseeri muutuja
getwhole:
	mov  ax, word ptr [BX+20]  ;hangi antud isiku aktsiate arv
	add  [BP-2], ax	           ;liida tema väärtus kohalikule 
                                   ;muutujale
	add  bx, 22	           ;nihuta viita edasi
	cmp  ax, 0                 ;kontrolli, kas see on viimane arv
	jnz  getwhole              ;kui see ei ole viimane arv, 
                                   ;siis hangi järgmine
	mov  ax, [bp+4]
	mov  [bp-4], ax            ;initsialiseeri viit
show:
	mov  bx, [bp-4]   ;BX voib muutuda, aga kohalik muutuja mitte
			  ;seepärast säilitame aadressi pinul
	mov  ax, [bx+20]
	cmp  ax, 0              ;kontrolli, kas see oli viimane isik
	jz   getout
	call
CalcPercent C, ax, [bp-2]      	;arvuta protsent
	call printf C, OFFSET szFormat, bx, ax  ;väljasta tulemused
	add [bp-4], 22    			;nihuta viita edasi
	jmp show
getout:
	add  sp, 4       ;enne sai SP -st 4 lahutatud, nüüd tuleb
                         ;see uuesti liita

pop bp ret ShowData ENDP END