Kokoonpanokielen oppiminen

Kokoonpanokieli on ihmiselle luettava vastine tietokoneohjelmoinnin alimmalle ohjelmistotasolle eli konekoodille.

Kaikki ohjelmat tietokone ymmärtää numeroina, joissa erilaiset numerot käskevät tietokonetta tekemään erilaisia operaatioita, mutta tämä on liian työlästä ihmisen käyttöön (saati sitten kirjoittamiseen). Siksi ihmiset ohjelmoivat assembler-kielellä, joka vastaa lähes 1:1 konekoodia.

Opi C-ohjelmointikieli. Jos osaat C:n (tai C++:n), assembly on helpompaa.

C# ja Java ovat hyviä kieliä, mutta on vain vähän tai ei lainkaan työkaluja, jotka näyttävät, miltä C#- ja Java-koodi näyttää assembly-kielellä. (Itse asiassa Javalla ja C#:lla on oma tavukoodi-kokoonpanokielensä, joka on melko erilainen ja kaukana todellisista laitteistoprosessorin käskykanta-arkkitehtuureista.)

C- (ja C++)-koodista voimme käyttää kääntäjiä näyttämään, miltä kokoonpanokieli näyttää. C on matalan tason kieli, ja jos tiedät, miten jokainen C:n konstruktio käännetään assembly-kielelle, osaat assembly-kielen! Esimerkkejä C-konstruktioista, jotka haluamme tuntea kokoonpanokieltä varten, ovat:

C-konstruktio Assembly-kielen vastine
paikallinen muuttuja CPU-rekisterin tai pinomuistipaikan sijainti
globaalimuuttuja käyttökohde globaalin tai const-muistipaikka
array-viittaus yhteenlaskun ja muistin käytön kuvio
goto ehdoton haarautumiskäsky
if-käsky kuvio if-käskyyngoto: ehdollinen testi & haarautumisohjeet
while-silmukka kuvio if-lausekkeessagoto
siirto rekisterin tai muistin muokkaus
aritmeettinen aritmeettinen käsky
param. siirto rekisterin tai pinomuistin muokkaus
parametrin vastaanotto rekisterin tai pinomuistin käyttö
funktio call kutsuohje
funktio entry prologikuvio
funktio exit epilogikuvio
paluuarvo rekisterin muutos ja epilogi

Miten assemblerikieli eroaa muista ohjelmointikielistä

Assemblerikielessä, olet alttiina taustalla olevan suorittimen laitteiston paljaille resursseille. (Lisäksi asiat, jotka voimme kirjoittaa C:ssä yhdellä rivillä, vievät assembly-kielessä useita rivejä.)

  • Olet tietoinen sellaisista asioista kuin CPU-rekisterit ja kutsupino – näitä resursseja on hallittava, jotta ohjelma toimisi (ja muissa kielissä näitä resursseja hallitaan automaattisesti puolestamme.) Näiden resurssien tietoisuus on kriittistä funktioiden kutsumiselle ja palauttamiselle. (CPU-rekisterit ovat niukkoja verrattuna muistiin, mutta ne ovat kuitenkin supernopeita ja myös suorituskyvyltään johdonmukaisia.)

  • Assemblerikielessä ei ole strukturoituja lauseita – joten kaikki kontrollin virtausrakenteet on ohjelmoitava ”If-Goto”-tyylillä (muissa kielissä kääntäjä kääntää strukturoidut lausekkeet If-Goto-rakenteiksi). If-Goto-tyylissä käytetään kohteina merkintöjä. (Vaikka C tukee goto:n ja labeleiden käyttöä, ohjelmoijat suosivat useimmiten strukturoituja lausekkeita, joten emme usein näe goto:ta ja labeleita C:ssä.)

Assembly-kieli vs. konekoodi

Assembly-kieli on tarkoitettu ihmisille sekä luettavaksi että kirjoitettavaksi.

Labels

Assembly-kielessä käytämme paljon labeleita. Merkkien käyttö käännetään konekoodissa numeroiksi ja merkit häviävät konekoodissa. Merkinnät (vs. numeroiden käyttäminen suoraan) mahdollistavat pienten muutosten tekemisen ohjelmiin – ilman niitä pienikin muutos olisi itse asiassa valtava muutos.

Prosessorin on tiedettävä, missä operandi on, ehkä dataoperandi tai ehkä koodipaikka. Esimerkiksi koodipaikkojen osalta prosessori haluaa tyypillisesti tietää, kuinka kaukana kyseinen kohde on siitä koodipaikasta, jota se parhaillaan suorittaa – tätä kutsutaan pc-relatiiviseksi osoitteistukseksi. Jos suoritamme if-then-lauseen, voisimme siis sanoa prosessorille: tietyissä olosuhteissa hyppää eteenpäin N ohjetta, jossa N edustaa ohitettavien (tai ohittamatta jätettävien) ohjeiden kokoa tai lukumäärää, eli N on then-osan koko – tämä olisi konekoodia. Jos then-osaan lisätään ohje, ohitettavien ohjeiden määrä kasvaa (ja jos then-osasta poistetaan ohje, vähenee).

Tarrojen käyttäminen ottaa automaattisesti huomioon muutokset (esim. ohjeiden lisääminen, poistaminen) – haarautumiskäsky, joka käyttää kohdeosana etikettiä, ottaa huomioon muutokset sen laskennassa, kuinka monta ohjetta ehdollisesti ohitetaan.

Pseudokäskyt

Useimmat assembler-kielen käskyt kääntyvät 1:1 konekoodin käskyiksi. Toisinaan meidän on kuitenkin kerrottava assemblerille jotain, joka ei käänny suoraan konekoodiksi. Tyypillisiä esimerkkejä ovat kertominen assemblerille, että seuraavat ohjeet ovat koodia (yleensä .text-direktiivin avulla) vs. dataa (yleensä .data-direktiivin avulla). Kuten tavallisetkin ohjeet, nämä ohjeet kirjoitetaan tyypillisesti omalle rivilleen, mutta ne ilmoittavat assemblerille sen sijaan, että ne tuottaisivat suoraan konekoodin ohjeita.

Assemblykielen kirjoittaminen

C:ssä kirjoitetaan lausekkeita; nämä lausekkeet suoritetaan peräkkäin peräkkäin, oletusarvoisesti peräkkäin, kunnes jokin kontrollin kulkua kuvaava konstruktio (silmukka, if-rakenne) muuttaa kulkua. Assembly-kielessä sama pätee.

C:ssä lausekkeilla on vaikutuksia muuttujiin – voimme ajatella, että erilliset lausekkeet kommunikoivat keskenään muuttujien kautta. Sama pätee assembler-kielessä: käskyt kommunikoivat keskenään niiden muuttujiin kohdistuvien vaikutusten kautta, joskin kun käskyjä on enemmän (väistämättä, koska käskyt ovat yksinkertaisempia), on väistämättä myös enemmän muuttujia, joista monet ovat hyvin lyhytikäisiä väliaikaisia muuttujia, joita käytetään kommunikaatioon käskystä seuraavaan.

Assembler-ohjelmoinnin perusteet

Vuorottelemme rekisterinvalinnan ja käskynvalinnan välillä. Koska assembly-kielen käskyt ovat melko yksinkertaisia, joudumme usein käyttämään useita käskyjä, kyllä, toisiinsa kytkettyinä cpu-rekisterien muuttujina olevien arvojen kautta. Kun näin tapahtuu, meidän on valittava rekisteri pitämään välituloksia.

Rekisterin valinta

Rekisterin valintaa varten tarvitsemme henkisen mallin siitä, mitkä rekisterit ovat varattuja ja mitkä vapaita. Tämän tiedon avulla voimme valita vapaan rekisterin, jota käytämme pitämään väliaikaista (lyhytaikaista) tulosta. Kyseinen rekisteri pysyy varattuna, kunnes käytämme sitä viimeisen kerran, minkä jälkeen se palaa vapaaksi – kun se on vapaa, se voidaan käyttää uudelleen johonkin aivan muuhun. (Jossain mielessä prosessori ei oikeastaan tiedä, että teemme tätä jatkuvaa uudelleenkäyttöä, koska se ei tiedä ohjelman tarkoitusta.)

Käskyn valinta

Kun olemme määritelleet käytettävät resurssit, voimme valita tarvitsemamme tietyn käskyn. Usein emme löydä sellaista ohjetta, joka tekee juuri sen, mitä haluamme, joten joudumme tekemään työn kahdella tai useammalla ohjeella, mikä tarkoittaa, jep, lisää rekisterien valintaa tilapäisoperaatioita varten.

Esimerkiksi MIPS ja RISC V ovat samankaltaisia arkkitehtuureja, jotka tarjoavat compare & haarautumiskäskyn, mutta ne voivat verrata vain kahta cpu:n rekisteriä. Jos siis haluamme nähdä, onko merkkijonon tavu tietty merkki (kuten rivinvaihto), meidän on annettava (ladattava) tuo rivinvaihtoarvo (numeerinen vakio) johonkin cpu-rekisteriin, ennen kuin voimme käyttää compare & -haarakäskyä.

Funktioiden kutsuminen & Palauttaminen

Ohjelma koostuu ehkä tuhansista funktioista! Vaikka nämä funktiot joutuvat jakamaan muistia, muistia on valtavasti, joten ne voivat tulla toimeen keskenään. Yhdellä suorittimella on kuitenkin rajallinen ja kiinteä määrä rekistereitä (esimerkiksi 32 tai 16), ja, kaikkien funktioiden on jaettava nämä rekisterit!!!

Jotta tämä toimisi, ohjelmistosuunnittelijat määrittelevät konventiot – joita voidaan ajatella sääntöinä – näiden rajallisten kiinteiden resurssien jakamiselle. Lisäksi nämä konventiot ovat välttämättömiä, jotta yksi funktio (kutsuja) voi kutsua toista funktiota (kutsuja). Sääntöjä kutsutaan kutsusopimukseksi, joka määrittelee kutsujalle ja kutsutulle, mihin väitteet ja paluuarvot sijoitetaan/löydetään. Kutsukäytäntö on osa Application Binary Interface -rajapintaa, joka kertoo yleisesti, miten funktion tulisi kommunikoida ulkomaailman (muiden funktioiden) kanssa konekoodissa. Kutsukäytäntö määritellään ohjelmistokäyttöä varten (laitteisto ei välitä).

Nämä dokumentit kertovat, mitä pitää tehdä parametrien välityksessä, rekisterien käytössä, pinon allokoinnissa/deallokoinnissa ja paluuarvoissa. Nämä dokumentit julkaisee käskysarjan tarjoaja tai kääntäjän tarjoaja, ja ne ovat tunnettuja, jotta eri paikoista tuleva koodi voi toimia kunnolla yhteen. Julkaistuja kutsukäytäntöjä tulisi pitää itsestäänselvyyksinä, vaikka laitteisto ei tietäisikään niistä.

Ilman tällaista dokumentaatiota emme tietäisi, miten nämä asiat (parametrien välitys jne…) tehdään. Meidän ei tarvitse käyttää vakiokutsukäytäntöjä, mutta on epäkäytännöllistä keksiä omia muuhun kuin simulaattorilla ajettavaan yksinkertaiseen leluohjelmaan. Omien kutsukäytäntöjen keksiminen ei tarkoita yhteentoimivuutta muun koodin kanssa, mitä meidän on tehtävä missä tahansa realistisessa ohjelmassa, kuten järjestelmä- ja kirjastokutsujen tekeminen.

Laitteisto ei tiedä tai välitä ohjelmistojen käyttökäytännöistä. Se vain suorittaa sokeasti käskyä toisensa jälkeen (todella nopeasti). Esimerkiksi laitteistolle suurin osa rekistereistä on samanarvoisia, vaikka rekisterit on osioitu tai niille on annettu ohjelmistokäytön konventioilla erityisiä käyttötarkoituksia.

Dedikoidut rekisterit

Jotkut suorittimen rekisterit on omistettu tiettyihin käyttötarkoituksiin. Esimerkiksi useimmissa suorittimissa on nykyään jonkinlainen pino-osoitin. MIPS:ssä ja RISC V:ssä pino-osoitin on tavallinen rekisteri, joka ei eroa muista, paitsi ohjelmistokäytäntöjen määrittelyn perusteella – valinta, mikä rekisteri on mielivaltainen, mutta se on todellakin valittu, ja tämä valinta julkaistaan, jotta me kaikki tiedämme, mikä rekisteri se on. Sen sijaan x86:ssa pino-osoitinta tukevat omat push & pop -käskyt (samoin kuin kutsu- ja paluu-käskyt), joten pino-osoitinrekisterille on vain yksi looginen valinta.

Rekisterien jakaminen

Jotta muut rekisterit voidaan jakaa tuhansien funktioiden kesken, ne on ohjelmiston konvention mukaan jaettu tyypillisesti neljään joukkoon:

  • Rekisterit parametrien välittämiseen
  • Rekisterit paluuarvojen vastaanottamiseen
  • Rekisterit, jotka ovat ”haihtuvia”
  • Rekisterit, jotka ovat ”ei-haihtuvia”

Parametri-& Palautusrekisterit

Parametrirekistereitä käytetään parametrin välittämiseen! Noudattamalla kutsukäytäntöä kutsujat tietävät, mistä löytyvät argumentit, ja kutsujat tietävät, mihin argumenttien arvot sijoitetaan. Parametrirekistereitä käytetään parametrien välittämiseen kutsujakonventiossa asetettujen sääntöjen mukaisesti, ja kun parametreja välitetään enemmän kuin CPU:n konventio sallii, loput parametrit välitetään käyttämällä pinomuistia.

Paluuarvorekisterit pitävät sisällään funktioiden paluuarvoja, joten kun funktio valmistuu, sen tehtävänä on laittaa paluuarvonsa oikeaan rekisteriin ennen kuin se palauttaa kontrollin virran kutsujalle. Noudattamalla kutsukäytäntöä sekä kutsuja että kutsuttu tietävät, mistä funktion paluuarvo löytyy.

Volatile & Non-Volatile Registers

Volatile-rekistereitä voi vapaasti käyttää mikä tahansa funktio välittämättä rekisterin aiemmasta käytöstä tai sen arvoista. Ne ovat hyviä tilapäisrekistereitä, joita tarvitaan käskysekvenssin yhdistämiseen, ja niitä voidaan käyttää myös paikallisina muuttujina. Koska nämä rekisterit ovat kuitenkin vapaasti minkä tahansa funktion käytössä, emme voi luottaa siihen, että ne säilyttävät arvonsa funktiokutsun aikana – (ohjelmistokäytännön) määritelmän mukaan! Näin ollen sen jälkeen, kun funktio palaa, sen kutsuja ei voi tietää, mitä arvoja näissä rekistereissä olisi – mikä tahansa funktio voi vapaasti käyttää niitä uudelleen, tosin tietäen, että emme voi luottaa siihen, että niiden arvot säilyvät sen jälkeen, kun kutsuttu funktio palaa. Näin ollen näitä rekistereitä käytetään muuttujille ja väliaikaisille muuttujille, joiden ei tarvitse kestää funktiokutsua.

Muuttujille, joiden täytyy kestää funktiokutsua, on olemassa haihtumattomat rekisterit (ja myös pinomuisti). Nämä rekisterit säilyvät koko funktiokutsun ajan (ohjelmisto, joka noudattaa kutsukäytäntöä). Jos funktio siis haluaa muuttujan nopeaan CPU-rekisteriin, mutta muuttuja on käytössä koko kutsun ajan (se asetetaan ennen kutsua ja sitä käytetään kutsun jälkeen), voimme valita ei-haihtuvan rekisterin (kutsukäytännön määrittelemän) vaatimuksen mukaisesti, että rekisterin edellinen arvo palautetaan, kun se palaa kutsujalleen. Tämä edellyttää ei-haihtuvan rekisterin nykyisen arvon tallentamista (pinomuistiin) ennen rekisterin uudelleenkäytön aloittamista ja sen palauttamista (pinomuistin edellisen arvon avulla) paluun yhteydessä.

Vastaa

Sähköpostiosoitettasi ei julkaista.