Assembly-funktioiden suoritus C/CPP-koodista
Mureakuha
- Teknisistä rajoituksista johtuen artikkelin yllä näkyvä otsikko on virheellinen. Oikea otsikko on Assembly-funktioiden suoritus C/C++-koodista.
by Maniace
Sisällysluettelo |
Johdanto
Esimerkeissä käytetään NASM- ja GCC-yhteensopivaa kääntäjää. Esittelen oppaassa muutaman NASM-makron, jotka ovat olleet hyödyllisiä (ja toistaiseksi toimivia) omassa käyttöjärjestelmäprojektissani. Mikäli olet jo niin pitkällä projektissasi, että sinulla on jo siinä Assembly- ja C/C++ -kieltä linkattuna ja painit funktiokutsujen kanssa, sinua tuskin tarvitsee erikseen opastaa linkkerin ja komentorivin käytössä tai tiedostojen luomisesta.
Luonnollisesti en vastaa mistää suorasti tai epäsuorasti aiheutuneesta vahingosta, luonnonkatastrofeista tai mahdollisista ihmishenkien menetyksistä, joita kyseisten makrojen käyttö voi aiheuttaa.
Yleistä
Kutsuttavan assembly-funktion täytyy olla kutsuvan koodin kanssa yhteensopivaa, ts. C-koodista kutsuttavan funktion täytyy toimia kuten C-funktio, muuten C-koodi ei toimi odotetulla tavalla enää funktion suorituksen loputtua. Pinon kanssa riehuessa täytyy olla varovainen, koska paluuosoite on pinossa. Tätähän voi tietysti myös käyttää hyväksi, mikäli haluatkin palata eri kohtaan kuin kutsuttaessa (setjmp()).
C-funktio toimii seuraavalla tavalla:
- Tallennetaan pino-osoitin (ESP)
Mikäli pino syystä tai toisesta menee sekaisin, palautetaan oikea pino-osoittimen oikea arvo ja mitä suurimmalla todennäköisyydellä palaamme oikeaan paikkaan. - Varataan pinosta tilaa paikallisille muuttujille
Tämä tapahtuu pienentämällä pino-osoittimen arvoa varattavien tavujen verran. - Tallennetaan rekisterit
Funktiossa et voi käyttää rekistereitä EBX, EDI, ESI, EBP, DS, ES ja SS. Tämä rajoittaakin paljon, siksi suoritetaan nk. callee-save, eli kyseiset rekisterit tallennetaan pinoon. Rekistereitä EAX, ECX, EDX, FS, GS ja EFLAGS ei tarvitse tallentaa, eikä myöskään liukulukurekistereitä. - Suoritetaan itse koodi
- Siirretään paluuarvo EAX-rekisteriin
- Palautetaan rekisterien arvot
- Palautetaan pino-osoittimen arvo
- Palataan suorittamaan kutsunutta koodia
Meidän makromme tulevat suorittamaan täsmälleen samat toiminnot, lukuunottamatta paikallisten muuttujien tilanvarausta. Sen voit tehdä itse harjoitustyönä. Lisää yksi parametri enemmän makrolle ja pienennät pino-osoittimen arvoa.
Makrot
Ja sitten itse makrot. Ensimmäisenä makro CGLOBAL, jolla kääntäjälle ilmoitetaan, että kyseinen muuttuja tai nimiö on globaali, ts. näkyy myös C-koodille.
%macro CGLOBAL 1
GLOBAL _%1
%define %1 _%1
%endmacro
GCC:llä on tapana käyttää alaviivoja muuttujien edessä, mikäli tapauksessasi ei näin ole, ei elämääsi voi helpottaa makrolla vaan käytät vain komentoa GLOBAL muuttujan_nimi.
Seuraavakin makro koskettaa vain alaviivojen kanssa taistelevien kooderien sisintä, ainakin se kosketti mua. *niisk*
%macro CEXTERN 1
EXTERN _%1
%define %1 _%1
%endmacro
Makroja käytetään siis seuraavalla tavalla:
CGLOBAL-muuttuja
Ilmoitetaan ASM-kääntäjälle, että kyseessä on globaali muuttuja, jonka arvoa voidaan muuttaa myös C-koodista tai mikäli kyseessä on nimiö, kyseisen nimiötä voidaan käsitellä C-koodista(käyttää nimiön osoitetta, suorittaa sen kohdalla olevaa koodia, jne.).
CEXTERN-muuttuja
Ilmoitetaan ASM-kääntäjälle, että kyseessä on jossain muualla kuin kyseisessä tiedostossa oleva muuttuja. Käytännössä sama kuin CGLOBAL, mutta homma toimii toiseen suuntaan.
Sitten makro CFUNC, jolla aloitetaan funktio:
%macro CFUNC 1 GLOBAL _%1 _%1: push ebp ; Tallennetaan pino-osoitin mov ebp, esp push ebx ; Callee-save push edi push esi push ds push es push ss %endmacro
Ja makro ENDFUNC, jolla lopetetaan funktio:
%macro ENDFUNC 0 pop ss pop es pop ds pop esi pop edi pop ebx leave ret %endmacro
Kahta ylläolevaa makroa käytetään siis syntaksilla:
CFUNC crash_win_9x loop: cli jmp loop ENDFUNC
Tosin ylläolevassa esimerkissä funktion suoritus ei koskaan lopu, mutta ymmärsit varmaan idean.
Viimeisenä, mutta ei suinkaan vähäisimpänä makro CVAR, jota käytämme funktion parametrien käsittelyyn:
%macro CVAR 2 mov dword %1, [ebp + 8 + (%2 * 4)] %endmacro
Jos siis haluamme siirtää ensimmäisen parametrin EAX-rekisteriin, käyttö on seuraava:
CVAR EAX, 0
Kuten huomasit, ensimmäinen parametri on 0, toinen on 1, jne. Parametri 3 siirtyy rekisteriin EDX komennolla:
CVAR EDX, 2
Jos funktiolle vastaanottaa 64-bittisiä parametreja, ne täytyy ottaa kyseisellä makrolla kahtena parametrina, sillä makro käsittelee 32-bittisiä palikoita muistissa.
Esimerkkifunktio
Seuraavana vielä funktio, joka ei tee mitään järkevää ja jonka tarkoitus on vain demonstroida makrojen toimintaa:
CFUNC addnums ; Esitellään funktio nimeltä addnums CVAR eax, 0 ; Siirretään eax-rekisteriin ensimmäinen parametri CVAR ebx, 1 ; Siirretään ebx-rekisteriin toinen parametri add eax, ebx ; Lasketaan parametrien arvot yhteen ; Koska laskun tulos on eax-rekisterissä, funktio palauttaa yhteenlasketun arvon ENDFUNC
Kun kutsut sitä C/C++ -koodista, voit käyttää vaikkapa seuraavaa syntaksia:
extern unsigned long int addnums(unsigned long int num1, unsigned long int num2);
printf("1024 + 1024 = %i\n", addnums(1024, 1024));
Jostain minulle tuntemattomasta luonnonlaista johtuen funktion pitäisi toimia, vaikka parametreina unsigned long intien sijasta olisi unsigned short int.
Loppusanat
Toivottavasti tästä on hyötyä. Mikäli joku jaksaa valaista meikäläistä komennoista ENTER ja LEAVE, olisin kiitollinen. Käsittääkseni nämä ovat varta vasten C-kielen tarpeisiin suunniteltuja komentoja ja niissä voi säästää kalliita kellojaksoja. Valaistusta kaipaisin myös 64- ja 128-bittisistä paluuarvoista, eli mikäs otus se hidden pointer oikein on? Hämmästyksen kummastuksen aihe on myös se, että miksi esim. unsigned short (16-bittiä) kulkeutuu funktioilleni 32-bittisenä (toisin sanoen makro toimii myös 32-bittistä pienemmillä muuttujilla, vaikka käsitteleekin 32-bittisiä palikoita).
Ja kommentit virheistä Mureakuhaan tai osoitteeseen maniace@daug.net.
