Kirjoittaja: Päärynämies
Kirjoitettu: 25.01.2008 – 25.01.2008
Tagit: ohjelmointitavat, koodi näytille, vinkki
Vinkki esittelee miten x86-assemblyllä kirjoitetusta funktiosta voidaan palauttaa C-kielen tietue. Vinkki on tavallaan jatkoa aiemmille vinkeilleni, jotka esittelevät assembly -funktioiden kutsumista C-koodista ja tietueen välittämistä parametrinä assembly -koodille.
Kuten jo tiedämme aiemmista vinkeistä, niin (32-bittisessä) järjestelmässä kaikki funktiot palauttavat arvonsa %eax -rekisteriä ja joskus %edx -rekisteriä hyödyntäen. Vinkissä esittelenkin miten tietueita, jotka usein ovat liian suuria mahtumaan rekistereihin, voidaan palauttaa assembly -funktiosta.
Tässä kohtaa mainittakoon, että mitä olen lukenut, niin tässä voi olla hieman kääntäjäkohtaisia eroja. Jotkut kääntäjät kuulemma saattavat palauttaa 4:n tavun kokoiset ja sitä pienemmät tietueet rekistereissä. Tosin ainakin gcc:n ei näytä itselläni niin tekevän, ainakaan ilman optimointivipujen käyttöä.
Huom! siis. Koodi ei välttämättä toimi sellaiseisaan muilla kääntäjillä. Tutustu omaan kääntäjääsi. Yksi hyvä keino (jota itsekin käytin) on kirjoittaa vastaava funktio C:llä ja antaa sitten kääntäjän kääntää se assemblyksi ja tutkia syntynyttä koodia.
Perusidea kuitenkin tietueiden palauttamisessa on melko selkeä. Kun funktiota, joka palauttaa tietueen, kutsutaan, niin välitetään sille parametrinä myös muistiosoite, johon palautettava tietue tallennetaan. Muistiosoite välitetään siis pinon kautta, kuten funktion kuvauksessa määritellyt parametrit. Pinoon se laitetaan varsinaisten parametrien jälkeen. Funktion palautusarvona on tuo samainen muistiosoite.
Kun assembly -funktiossa luomme tietueen, niin meidän täytyy myös tietää, miten kääntäjä sijoittaa tietueen muuttujat muistiin. Tästä mainitsinkin jo aiemmassa vinkissäni hieman. Hieman kertauksena ja lisäyksenäkin ehkä asiasta seuraavaksi.
/* typedef -määritettä olen tottunut käyttämään, koska silloin säästytään struct -sanan kirjoittamiselta monissa paikoissa. */ typedef struct{ char taulukko[3]; unsigned int numero; } Tietue;
Taulukolle siis varataan 3 tavua tilaa ja unsigned int vie 32-bittisillä järjestelmillä sen 4 tavua. Kääntäjä ei kuitenkka sijoita näitä välttämättä täysin peräkkäin muistiin eli:
&numero == &taulukko[0]+3
ei pidäkään paikkaansa. Sen sijaan (kehittyneempi) kääntäjä sijoittaa yleensä unsigned int -tyyppiset muuttujat neljällä jaollisiin osoitteisin, koska niillä työskentely on tehokkaampaa. Eli esimerkissämme voisikin olla, että:
&numero == &taulukko[0]+4
Tämä tuleekin ottaa huomioon, kun sijoitamme assembly -funktiossa muuttujille arvoja tietueeseen. Tästä aiheesta ei kuitenkaan enempää tällä erää, koska aiheesta saisi kirjoitettua vaikka kokonaisen oppaan. Materiaalia aiheesta kylläkin löytyy helposti lisää aina niin ihmeellisestä Internetistä, tosin hyvin vähän suomenkielisenä. Myös kääntäjien omista manuaaleista löytyy lisäinformaatiota aiheesta. Mainittakoon vielä, että on mahdollista määritellä myös tietue niin, että kaikki muuttujat ovat täysin peräkkäin muistissa. Lue oman kääntäjäsi manuaalia, jos olet kiinnostunut siitä. Gcc:llä esimerkiksi tämä saavutettaisiiin kirjoittamalla __attribute__ ((packed)) tietueen perään.
(Ohjeet Linuxille)
Kääntäminen:
gcc main.c -o main.o
gcc asm.s -o asm.o
gcc main.o asm.o -o main
Ajaminen:
./main
main.c
#include <stdio.h> typedef struct{ unsigned int x; unsigned int y; unsigned int z; } Cube; /* Luo Cube -tietueen ilmentymän, jossa x=y=z=xyz */ extern Cube create_cube(unsigned int xyz); void print_cube(Cube c) { printf("Cube:\nX: %i\nY: %i\nZ: %i\nVolume: %i\n", c.x, c.y, c.z, c.x*c.y*c.z); } int main(int argc, char *argv[]) { Cube c = create_cube(4); print_cube(c); return 0; }
asm.s
.section .text #Määritellään globaaliksi .global create_cube #8(%ebp) = Osoite, johon tietue tallennetaan #12(%ebp) = Funktion create_cube parametri create_cube: pushl %ebp movl %esp, %ebp movl 8(%ebp), %edx #Osoite rekisteriin %edx movl 12(%ebp), %eax #Arvo rekisteriin %eax movl %eax, (%edx) #x movl %eax, 4(%edx) #y movl %eax, 8(%edx) #z movl %edx, %eax #Palautetaan osoite leave ret
No sehän meni näppärästi, en tähän hätään keksi kummempaa sanottavaa. Muutama (O_o) kirjoitusvirhe näyttäisi olevan tuossa kuvauksessa. Selkeyden vuoksi voisi vielä olla hyvä käyttää C:n vertailumerkkiä (==) sijoitusoperaattorin sijaan noissa esimerkeissä siitä, miten jäsenten osoitteet suhtautuvat toisiinsa. Jälkimmäisen koodin kommentitkin voisi sisentää vähän nätimmin.
Onko jokin syy sille, että osoite sijoitetaan edx:ään ja arvo eax:ään? Toisin päin säästyisi viimeiseltä movl-käskyltä... ;) Aivan kuin sillä olisi oikeasti jotain väliä.
Kiva vinkki. Alussa voisi vielä kuitenkin mainita, että kyse on nimenomaan GCC:n tavasta, jos ei ole tietoa, toimiiko sama muilla. Koodin selkeydestä tälläkin kertaa plussaa, se puoli ainakin on vinkeissäsi kunnossa.
Tosiaan kirjoitusvirheitä tahtoo tulla, kun ei aina itse niitä huomaa vaikka ihan väärin kirjoittaa.
Tosiaan tuota assemblyfunktiota voisi optimoida myös. Syy tuohon, että osoitteen laitan %edx:ään ja %eax:ään on osakseen se, että gcc tekee asian niin. Ja muutenkin olen tottunut sijoittamaan arvoja %eax rekisterin avulla ja muissa sitten pidän osoitteita joihin sijoitetaan tai muita tietoja.
Tosiaan tuosta pitääkin vielä mainita enemmän, että gcc:n tapa on tämä. Muut kääntäjät saattavat tosiaan erota tässä, mutta ainakin mitä tietoa netistä löysin aiheesta, niin noiden rekistereihin mahtumattomien tietueiden palautus näyttäisi toimivan samalla tapaa useimmilla kääntäjillä. Tästä tietenkään ei takuuseen voi mennä. Lisään tuohon maininnan siitä ja siistin tuota hieman vielä.