Kirjoittaja: Päärynämies
Kirjoitettu: 20.01.2008 – 20.01.2008
Tagit: koodi näytille, vinkki
Koodivinkki esittelee miten voidaan kutsua x86-assemblyllä kirjoitettuja funktioita C -koodista. Koodi toimii ainakin omassa Linux -järjestelmässäni.
Kun C- koodissa kutsutaan funktiota, niin funktiolle välitettävät arvot laitetaan pinoon käänteisessä järjestyksessä. Esimerkiksi kutsuttaessa funktiota summa ensiksi pinoon laitetaan muuttuja b ja sen jälkeen muuttuja a. Näiden jälkeen pinoon laitetaan seuraavan käskyn osoite, jotta osataan palata funktiosta oikeaan kohtaan koodia. Funktion paluuarvo on rekisterissä %eax. Funktioiden paluuarvot palautetaan yleensä %eax rekisterissä, mutta aina näin ei suinkaan ole. Joskus paluuarvo on liian suuri mahtuakseen 32-bittiseen rekisteriin ja tällöin käytetään myös rekisteriä %edx.
Funktiossa summa pinoon laitetaan yhteenlaskettavan luvut ja paluuosoite. Muuttuja a siis löytyy osoitteesta %esp+4 (Muista! Pino kasvaa alaspäin) ja muuttuja b osoitteesta %esp+8. Osoite %esp+0 sisältää paluuosoitteen.
Funktiossa kasvata pinoon laitetaan funktiolle annetun muuttujan osoite. Sitten vain lisäämme osoitteen osoittamaan muistipaikkaan yhden.
Jos katsot esimerkiksi gcc generoima funktioiden assembly koodia, niin ne alkavat aina:
pushl %ebp movl %esp, %ebp
ja päättyvät:
movl %ebp, %esp popl %ebp ret
(tai vaihtoehtoisesti: leave ja ret, ne tekevät samat asiat)
Näin tekemällä voidaan myös funktion laittaa ja noutaa pinosta arvoja eikä tarvitse huolehtia siitä, että funktion lopussa pino-osoitin osoittaa samaan kohtaan kuin alussa. Tuolloin kuitenkin pitää huomioida, että ensimmäinen funktiolle välitetty arvo on osoitteessa %esp+8.
Kääntäminen:
gcc main.c -c
as asm.s -o asm.o
gcc main.o asm.o -o summa
Ajaminen:
./summa
main.c
/* Kääntäminen: * gcc main.c -c */ #include <stdio.h> /* * Funktiot summa ja kasvata ovat määritelty tiedostossa asm.s. * summa: Palauttaa a+b * kasvata: Kasvattaa muutujan arvoa yhdellä */ extern int summa(int a, int b); extern void kasvata(int *a); int main(int argc, char *argv[]){ int a = 1; int b = 2; int c = summa(a,b); printf("%i+%i=%i\n%i+1=", a, b, c, c); kasvata(&c); printf("%i\n", c); return 0; }
ams.s
#Kääntäminen: #as asm.s -o asm.o #Osio, joka sisältää suoritettavaa koodia .section .text #Määritellään funktiot globaaleiksi, jotta ne näkyvät muuallakin .globl summa .globl kasvata #Laskee kaksi lukua yhteen summa: movl 4(%esp), %eax #Noudetaan 1. parametri movl 8(%esp), %ebx #Noudetaan 2. parametri addl %ebx, %eax #Lasketaan yhteen. Tulos on rekisterissä %eax ret #Palataan funktiosta #Kasvattaa muuttujan arvoa yhdellä kasvata: movl 4(%esp), %eax #Haetaan muuttujan osoite incl (%eax) #(%eax) = Sen muistipaikan sisältö, jonka osoite on %eax ret #incl kasvattaa lukua yhdellä
Ihan asiallinen vinkki. Olisit saman tien voinut kertoa toiminnan toiseenkin suuntaan.
Stack framella eli pinokehyksellä (jonka nimeä et maininnut) on muutakin merkitystä kuin selittämäsi. Se nimittäin on debuggerin keino selvittää esimerkiksi funktioiden kutsujärjestystä. Siksi sitä kannattaa käyttää isoissa funktioissa aina ja pienissäkin vähintään debug-vaiheessa, jos funktiossa on edes pieni kaatumisen vaara.
lainaus:
%esp sisältää paluuosoitteen.
Ei vaan (%esp). Itse kyllä ymmärrät tuon oikein mutta aloittelijat välttämättä eivät. "Osoite %esp+0" olisi minusta selkein ilmaus.
lainaus:
Funktion paluuarvo on aina rekisterissä %eax.
Tämähän ei tietenkään pidä paikkaansa. Näin toki on, jos arvo mahtuu eax-rekisteriin. Liukuluvut taas kuljetetaan liukulukupinossa. Ainakin GCC:n tuotoksissa 64-bittisen luvun (long long) yläosa kulkee edx:ssä, ja isommat rakenteet hoidetaan sillä, että funktiolle annetaan ylimääräisenä (ensimmäisenä) parametrina osoitin kelvolliseen tallennuspaikkaan. (Kiinnostavaa, ettei GCC ilmeisesti osaa optimoida tällaista rakenteen palauttavaa funktiokutsua kunnolla edes monissa aivan selkeissä tapauksissa...)
Voisit vielä lisätä kooditagit noihin pariin kuvauksessa olevaan koodiin, vaikka lyhyitä ovatkin.
Kiitos hyvistä korjauksista Metabolixille. Korjasinkin tuota vinkkiä hieman. Tosiaan noihin funktioidin en lisänny tuota pinokehyksen käyttöä selkeyden takia. Ajattelin, että se on aloittijaystävällisempi tapa vaikkakin niitä olisi tietysti hyvä käyttää ainakin isommissa funktioissa. Tietystu, jos kovasti olisi tarve optimoida, niin ne taas voi jättää pois. Ensisijaisesti ajattelin kuitenkin, että tämä on suunnattu lähinnä aloittelijoille, joten en halunnut liikaa asioita siihen laittaa.
C-funktioiden kutsumisesta assemblykoodista olen suunnitellut tehdä erillisen vinkin. Näin pysyy yksi asia yhdessä vinkissä, joka on mielestäni selkeämpi rakenne. Myös esim. C:n structien (mitä ovatkaan suomeksi) käyttämisestä funktion parametrinä voisin vinkin tehdä.
Lisäksi mietin, että pitäisikö koittaa vaikka pienen oppaan tekemistä tuosta gcc:n käyttämästä at&t syntaksista, kun se monelle tuntuu olevan se hankalampi tai tuntemattomampi. Itse kun vinkkini sillä tulen kirjoittamaan.
Päärynämies kirjoitti:
Kun C- koodissa kutsutaan funktiota, niin funktiolle välitettävät arvot laitetaan pinoon käänteisessä järjestyksessä.
Tämä riippuu seikasta nimeltä Calling Convention. Vapaasti suomennettuna kutsusopimus (en tiedä virallista termiä). Calling Conventioneja on erilaisia ja ne ovat riippuvaisia kielestä, kääntäjästä ja ympäristöstä (käyttis + cpu). x86 arkkitehtuurissakin se vaihtelee laitetaanko parametrit pinoon, rekisteriin (jos mahtuu) vai sekä että. Se mistä ne parametrit sitten löytyvät, täytyy ensin selvittää kyseiselle ympäristölle, eikä vain suoraan olettaa että pinostahan minä ne kaikki parametrit saan.
Totta on että usein c-kielellä x86-arkkitehtuurissa käytetään _oletusarvoisesti_ cdecl Calling Conventionia, missä parametrit pukataan pinoon, mutta ei aina. Vaihtoehtoja on mm. stdcall missä parametrien järjestys on päinvastainen, fastcall missä käytetään rekistereitä hyväksi jne.