Kirjoittaja: Päärynämies
Kirjoitettu: 21.01.2008 – 19.04.2013
Tagit: koodi näytille, vinkki
Vinkki esittelee, miten x86-assembly funktiolle voidaan parametrina antaa C-kielen tietue (struct). Vinkki on periaatteessa jatkoa vinkille numero 1828: Assembly-funktioiden kutsuminen C-koodista (https://www.ohjelmointiputka.net/koodivinkit/25102-assembly-assembly-funktioiden-kutsuminen-c-koodista).
Huom! Koodi ei välttämättä toimi kaikilla kääntäjillä, mutta ainakin gcc:llä sen pitäisi toimia. Itse en muilla ole kokeillut. Ilmoitelkaa, jos ei toimi.
Kuten vinkissi 1828 opimme, niin C:ssä parametrit välitetään funktioille pinon avulla. Määrittelyssä viimeinen parametri laitetaan pinoon aina ensimmäisenä, niin myös tietueet. Tietue kumminkin voi sisältää useampia erityyppissiä muuttujia. Kun koko tietue välitetään parametrina (ei siis vain osoitetta), niin tietueen sisältämät eri muuttujat laitetaan myös pinoon. Siinäkin pätee sama käänteinen järjestys. Viimeisenä määritelty muuttuja laitetaan pinoon ensimmäisenä.
Kun tietueen muuttujiin halutaan päästä käsiksi assembly-koodissa, niin käytämme apuna taas pino-osoitinta %esp. Tietueet kuitenkin voivat sisältää muuttujia, jotka varaavat eri suuruisia määriä tilaa muistista. Char vaatii yhden tavun, unsigned int yleensä neljä 32-bittisillä järjestelmillä jne. Kääntäjä kuitenkin yleensä varaa muuttujille tilaa niin, että jokaisen muuttujan osoite on kahdella tai neljällä jaollinen (riippuen muuttujan koosta), koska sellaisten osoitteiden kanssa työskentely on tehokkaampaa. Tätä on ehkä yksinkertaisinta selventää koodin avulla.
Esimerkissämme määritellään Maatila -tietue seuraavasti:
typedef struct { char farmari[10]; unsigned int pituus; unsigned int leveys; } Maatila;
Merkkitaulukko vie tilaa siis kymmenen tavua ja pituus- ja leveys -muuttujat neljä tavua. Kun mietimme mihin osoitteisiin kääntäjä sijoittaa nämä, niin ainakin gcc usein optimoi niin, että muuttujalle pituus varattu osoite onkin 12:sta tavun päässä merkkitaulukon alusta, vaikka taulukolle tarvitaan vain kymmenen tavua. Näin kuitenkin saadaan pituus-muuttuja neljällä jaolliseen osoitteeseen.
Kun sitten assembly-funktiossa haluamme päästä käsiksi pituus muuttujan, meidän pitää ottaa tuo muuttujien sijoittelu huomioon. Lisäksi pinossa on jo etukäteen paluuosoite ja funktion alussa sinne laitetaan myös %ebp. Kumpikin vievät neljä tavua. Merkkitaulukko vie 12 tavua, vaikka sen koko onkin vain 10 tavua. Siis muuttuja pituus on osoitteeessa %esp+(4+4+12)=%esp+20.
Gcc:ssä on olemass myös tapa käskeä kääntäjää laittamaan kaikki tietueen muuttujat muistiin peräkkäin. Tietyissä tapauksissa tämä on kovin hyödyllinen ominaisuus. Se saadaan lisäämällä tietueen perään __attribute__((packed)). Lisää tietoa löytyy gcc:n manuaalista.
Koodia ei ole paljoa kommentoitu, koska mielestäni se on melko selkeää. Tärkein asia mielestäni vinkissä on kerta tuo muuttujille varattu tila.
Kääntäminen:
as asm.s -o asm.o
gcc main.c -c -o main.o
gcc main.o asm.o -o farmi
Ajaminen:
./farmi
main.c
/* Koodi on yksinkertaista ja siksi hyvin kommentoimatonta */ #include <stdio.h> typedef struct { char farmari[10]; unsigned int pituus; unsigned int leveys; } Maatila; /* Funktio, joka laskee palauttaa arvon (pituus*leveys) */ extern int laske_ala(Maatila tila); int main(int argc, char *argv[]){ Maatila farmi = {"Perunasto"}; farmi.pituus = 3; farmi.leveys = 4; printf("Tilan omistaja: %s\n", farmi.farmari); printf("Pinta-ala: %i\n", laske_ala(farmi)); }
asm.s
#.text sisältää suoritettavaa koodia .section .text #Määritellään funktio globaaliksi eli se näkyy muuallekin .globl laske_ala #Funktio saa parametrinään tietueen Maatila laske_ala: pushl %ebp movl %esp, %ebp movl 20(%ebp), %eax #pituus movl 24(%ebp), %ecx #leveys imull %ecx, %eax #Kerrotaan, tulos on rekisterissä %eax movl %ebp, %esp popl %ebp ret
Pitää muistaa kehuakin: "Ihan kiva." ;)
Sijoitat 10 merkin taulukkoon 10 merkkiä. Tällöin taulukkoon ei mahdu nollamerkkiä ja tekstin tulostaminen ei välttämättä toimi. Se, että GCC tietyillä asetuksilla varaa taulukolle 12 tavua, ei ole mikään peruste käyttää taulukkoa väärin. Eihän mallocilla varatusta muististakaan saa sohia ohi, vaikka tietääkin, että prosessille varataan x86-koneessa muistia sivu (4k) kerrallaan tai että standardikirjaston kirjanpito toimii esimerkiksi kahdeksan tavun tarkkuudella.
Miksi nimi asetetaan muuttujan määrittelyssä ja muut tiedot omilla riveillään? Epäloogista.
Yleensä pinokehystä käytettäessä on tapana käyttää sekä parametrien että paikallisten muuttujien osoitteissa ebp:tä, joka on nimensäkin perusteella loogisempi tähän tarkoitukseen. Tuossa koodissahan eroa ei näy, mutta kun paikallisia muuttujia alkaa olla, koodi pysyy paremmin kasassa niin.
Kaipa tuosta kuitenkin ajatus selviää, jos joku asiaa pohtiva vain sattuu löytämään tänne. :)
Kiitos taas Metabolixille hyvistä huomautuksista. Kaikkea ei itse muista aina ajatella. Muokkaankin tuota vinkkiä hieman, että se noudattaa hyvää ohjelmointitapaa. Onneksi täällä on muitakin, jotka tietävät näistä asioista jotain, niin saadaan vinkkien laatu ja oikeellisuus varmistettua.
Tosiaan tuota %ebp:tä on loogisempi käyttää, koska jos esimerksiksi laitamme pinoon tavaraa, niin joudutaan uusiksi miettimään, missä mikin muuttuja on.
Toivottavasti kuitenkin vinkistä on apua jollekin ja perusajatus selviää.
Vielä yksi aika paha virhe, jota en ensimmäisellä syynäyksellä huomannut. Toivottavasti tiedät, että ebx on varattu rekisteri, jota ei saisi muokata funktiossa (tai siis jonka arvo pitäisi ottaa talteen). Vain eax, ecx ja edx ovat vapaasti muokattavissa. Käytät sitä kuitenkin tallentamatta minnekään. Tuo virhe on erittäin paha, koska se voi aiheuttaa ohjelmassa aivan mitä vain alkaen pienistä kummallisuuksista arvoissa ja kirjaimellisesti päättyen virheellisiin muistiosoituksiin. Lisäksi virhettä on äärimmäisen vaikea löytää, koska kaatuminen voi tapahtua aivan eri kohdassa ohjelmaa.
Tosiaan kaikkia rekistereitä ei pidä mennä tallentamatta muuttelemaan, koska ne voivat sisältää suorituksen kannalta tärkeitä tietoja, jotka sitten meneteään. Tänään myöhemmin korjaan vielä tuota vinkkiä ja lisäänkin edelliseen vinkkiini maininnan noiden rekistereiden arvojen muuttamisesta.
Ei pitäisi kiirreellä vinkkejä vääntää, koska tulee tälläisia hölmöjä virheitä. Ottakaa muut oppia tästä.
Asmi ounaa. Ainakin vaikeudessa..;-D