y86-64 Arkkitehtuuri¶
Osaamistavoitteet: Tämän materiaalin luettuaan opiskelija tuntee y86-64-prosessorin ohjelmoijalle näkyvän arkkitehtuurin ja sen assembly-kielen syntaksin sekä osaa laatia pienimuotoisia assembly-kielisiä ohjelmia.
Kurssilla y86-64 prosessorin assembly- ja konekieltä käytetään havainnollistamaan käskyn suoritusta prosessorissa. Lisäksi assemblyä (tuttavallisemmin assya) käytetään harjoitustehtävien ja -työn tekemisessä y86-kääntäjän ja -simulaattorin avulla.
y86-64 prosessoriarkkitehtuuri on 64-bittinen, josta seuraa että muistipaikat ja rekisterit ovat 64-bittisiä. Näin ollen sanan pituus on 64 bittiä eli 8 tavua. Lukuesitys on 2-komplementti ja bittijärjestys on big endian jossa MSB-bitti on merkkibitti. Tavujärjestys taas on little endian, eli vähiten merkitsevä tavu tulee ensin.
Kuvassa alla y86-prosessorin arkkitehtuuri. Yleisesti y86-prosessorissa on siis ALU, yleiskäyttöisiä ja myös käyttötarkoitukselta määrättyjä rekistereitä, kolme prosessorin tilabittiä ja keskusmuisti. Muisti on (von Neumann-arkkitehtuurin mukaisesti) jaettu ohjelmien ja datan kesken ja lisäksi osa siitä jaetaan pinomuistille. Kontrolliosan logiikkaa käymme myös materiaalissa läpi siltä osin kun se liittyy ohjelmien suoritukseen.
Rekisterit¶
Kun siis ohjelmoimme assemby-kielellä, yleiskäyttöiset rekisterit ovat muuttujien muistipaikkoja. Lisäksi suorittimen toiminta tarvitsee joukon omia rekistereitään.
y86-prosessorissa on useita rekistereitä, joista osalle on määrätty käyttötarkoitus:
- Yleiskäyttöisiä (GPR):
%rax
,%rcx
,%rdx
,%rbx
,%rsi
,%rdi
,%r8
-%r14
- Pinon alkupään osoite:
%rbp
- Pinon osoitinrekisteri:
%rsp
- Suorittimen tilarekisteri
- Ohjelmalaskuri (Program counter, PC) sisältää seuraavan suoritettavan käskyn muistiosoitteen.
Assembly-kielessä
%
-merkki ilmaisee, että kyse on ohjelmallisesti käytettävästä rekisteristä. Rekisterien nimet noudattavat x86-prosessoriperheen nimeämiskäytäntöä.Prosessorin tila¶
Assebmly/konekielisen ohjelman toiminta perustuu prosessorin tilaa osoittavien tilabittien arvon tarkasteluun. Tilabittejä varten on varattu erillinen tilarekisteri. Yleisesti konekielen käskyt muuttavat ja reagoivat prosessorin tilaan:
- Tilabitti asetetaan, kun suoritin saa keskeytyksen.
- Ohjelman ehdollinen toiminta (vertailuoperaattorit, hyppykäskyt, jne) perustuvat tilarekisterin bittien arvon vertailuun.
- Käskyn seurauksena aiheutuva virheellinen toiminto merkitään asettamalla tilabitti.
y86:sessa on kolme tilabittiä/lippua (engl. condition codes, CC):
ZF
(zero flag): Ilmaisee oliko edellisen ALUn suorittaman operaation tulos 0. Usein tätä lippua käytetään yhtäsuuruuden toteamisessa.- ZF=0, tulos erisuuri kuin 0
- ZF=1, kun tulos on 0
Esimerkiksi positiivisen ja positiivisen luvun vähennyslasku 01111111 - 01111111 -------- 00000000 ZF=1, koska tulos on 0
SF
(Sign flag): Oliko edellinen ALUn operaation tulos negatiivinen?- SF=0, tulos > 0
- SF=1, tulos < 0
Esimerkiksi positiivisen ja negatiivisen luvun yhteenlasku 00001111 10000001 -------- 10010000 Nyt merkkibitti MSB on 1, joten tulos negatiivinen -> SF=1
OF
(Overflow flag): Ilmaisee tapahtuiko operaatiossa ylivuoto, joka tarkoittaa sitä, että laskutoimitus ei mahdu tulosrekisterin lukualueeseen.- OF=0, ei ylivuotoa
- OF=1, ylivuoto tapahtui
Lasketaan yhteen kaksi positiivista lukua: 01111111 01111111 -------- 11111110 Tulos ei mahdu positiiviseen lukualueeseen, MSB = 1 -> OF=1 Lisäksi -> SF=1 Lasketaan yhteen kaksi negatiivistiä lukua: 10000001 + 10000001 -------- 100000010 Nyt MSB =1, mutta se leikkautuu pois 00000010 Tulos on positiivinen luku -> OF=1
- Osana tilarekisteria on y86:n ohjelman suorituksen tila (status code,
STAT
), joka voi olla: AOK
: Kaikki okHLT
: Prosessori pysähdyksissäADR
: Osoitettiin väärään muistiosoitteeseenINS
: Käskyssä virhe / väärä käsky
Oikeissa prosessoreissa tilabittejä on useita muitakin, esimerkkinä 8086:n 16-bittinen tilarekisteri. Oikeissa suorittimissa on "muistinumerona" Carry-lippu "ylimääräiselle" bitille, jonka avulla voidaan suorittaa lukualueen ylittäviä operaatioita oikein.
Osoitusmuodot¶
Assembly-kielessä voidaan käskyn operandeja käyttää ja niillä osoittaa muistia eri tavoin. Käskyn tulkitsemisen yhteydessä prosessori tietää osoitusmuodosta, mistä käskyn tarvitsema data löytyy.
y86:sessa on kolme osoitusmuotoa:
- Suora osoitus (Immediate) vakioarvoille:
$numero
. Esimerkiksi$1
,$-13
tai$0x1F
. - Esimerkki: talletetaan luku 1 rekisteriin rsi
irmovq $1,%rsi
- Rekisteriosoitus (Register) jossa operandi on rekisterin sisältö:
%rekisteri
. - Esimerkiksi talletetaan rekisteri rax arvo rekisteriin rbx
rrmovq %rax,%rbx
- Epäsuora osoitus (Memory) jossa operandi haetaan rekisterin osoittamasta muistipaikasta, merkitään sulkeilla:
(%rekisteri)
. - Sulkeiden edessä voi vielä olla numero
n(%rekisteri)
(base + displacement), jossa tapauksessa osoitetaan muistipaikkaarekisterin arvo + n
. Tätä käytetään tyypillisesti pinomuistin lukemisessa. - Esimerkiksi haetaan muistista arvo muistipaikasta %rax:n arvo ja tallennetaan se rekisteriin %rbx:
mrmovq (%rax),%rbx
- Esimerkiksi haetaan muistista arvo muistipaikasta %rax:n arvo -16 ja tallennetaan se rekisteriin %rbx:
mrmovq -16(%rax),%rbx
- Tätä osoitusmuotoa vastaa C-kielen osoitin eli (%rekisteri) voisi olla C-kielessä *rekisteri
Keskusmuisti¶
y86-prosessorissa on keskusmuistia noin neljä kilotavua. Muistipaikan koko on tietenkin sanan pituus, eli 64 bittiä eli 8 tavua.
Pinomuisti¶
Pino (engl. stack) on keskusyksikölle keskusmuistista varattu muistialue, johon varastoidaan tietoa LIFO-periaatteella (Last in -> First out). Toisinsanoen, pinoon viedyt arvot luetaan sieltä käänteisessä järjestyksessä, josta nimitys pino. Nyt muistissa pino kasvaa alaspäin, eli aina päällimmäisellä arvolla on aina pienin muistiosoite. Pinomuistia käytetään yleisesti tiedon varastointipaikkana, esimerkiksi taulukkoja tms isompia muistialueita käyttäessä tai aliohjelmia suorittaessa (ja joskun rekisterit muuten loppuvat..).
Pinoa varten on suorittimissa kaksi käskyä.
push
-käskyllä viedyt arvot päätyvät aina sen päällimäiseen muistiosoitteeseen. Pinosta luetaan pop
-käskyllä arvoja aina päällimmäinen arvo edellä, jolloin se ikäänkuin häviää pinosta. Arvo ei välttämättä itseasiassa häviä (=nollaannu) muistista, mutta vain pino-osoitin liikkuu. y86:sessa ohjelmoija voi päättää assembly-kielisen ohjelman pinon koon. Pinomuistin osoittamiseen y86:sessa on varattu kaksi rekisteriä
%rbp
ja %rsp
. Ohjelman alussa asetetaan pinomuistin alkupään ja päällimmäisen muistipaikan osoite. Pinomuistin päällimmäisen muistipaikan osoittimen arvo muuttuu käskyn suoritusten mukaan. Esimerkki pinon käyttäymisestä.
# Pinon alustus irmovq $100,%rbp # Pinon alkuosoite muistissa irmovq $100,%rsp # Pinon nykyisen (päällimäisen) muistipaikan osoite irmovq $2,%rax pushq %rax # Viedään pinoon rax:n arvo # Nyt rekisterit muuttuvat seuraavasti %rax = 2 %rbp = 100 %rsp = 92 # Vietiin muistia sanan pituuden verran irmovq $3,%rbx pushq %rbx # Viedään pinoon rbx:n arvo # Nyt rekisterit muuttuvat seuraavasti %rbx = 3 %rbp = 100 %rsp = 84 # Vietiin muistia sanan pituuden verran popq %rcx # Luetaan pinosta päällimmäinen arvo # Nyt rekisterit muuttuvat seuraavasti %rcx = 3 %rbp = 100 %rsp = 92 # Luettu sana poistui pinosta
Hox! Koska pino sijaitsee samassa muistissa kuin koodi ja data, pitää olla tarkkana ettei pinoon viedyt arvot sotkeudu koodiin tai muuhun dataan!
y86-64 Assembly-kieli¶
y86-64:sen assemblyn syntaksi ja käskykanta ovat perusteiltaan hyvin samankaltaisia kuin x86. Pienillä muutoksilla ohjelmia voi käyttää molemmissa suorittimissa! Toki on huomattava, ettei läheskään kaikkia x86-suoritinperheen assembly-käskyjä ole toteutettu y86-64:seen.
y86-assemblyn käskyt päättyvät usedin kirjaimeen
q
, joka tarkoittaa sitä että käskyn käsittelemä operandi on 64-bittinen (quad). 32-bittistä lukua voisi merkitä kirjain l, jossain toisessa arkkitehtuurissa (lue x86).Pino¶
Jokaisessa ohjelmassa meidän tulee ensin määrittää pinomuistin paikka. Ok, kirjaimellisesti ihan välttämätön se ei ole, pystymme toteuttamaan pieniä ohjelmia ilmankin. Opettelemme pinon käytön, koska ilman sitä aliohjelmat eivät toimisi. (Kurssilla tyypillinen virhe assy-koodissa on aluksi jättää pino alustamatta ja sitten ihmetellä miksi aliohjelmakutsut eivät toimi.)
Pinon hallintaan tarvitaan avuksi rekistereitä, jotka osoittavat mistä pino muistissa alkaa
%rbp
, sekä osoitinrekisteri %rsp
, johon tallentuu pinon päällimäisen arvon osoite. Pinon alustus ohjelman alussa tapahtuu seuraavasti:
.pos 0
irmovq Pino,%rbp # Pinon alkuosoite rekistereihin
irmovq Pino,%rsp # Pinon nykyisen muistipaikan osoite
.pos 0x100
Pino:
Ohjelman tarkempi toiminta selviää materiaalista alla, mutta ohjelmassa olemme nimenneet muistiosoitteen
0x100
nimellä Pino
. Ikäänkuin siis C-kielen #define-esikääntäjäkäsky. Ohjelman ensimmäisissä käskyissä sitten viemme tämän muistiosoitteen pinon rekistereihin. Nyt siis pinomuistin ensimmäinen osoite on 0x100. Muistetaan että pino kasvaa alaspäin, jolloin sen muistiosoite siis pienenee. Pinoa käsitellään kahdella käskyllä:
- Pinoon viemme tavaraa
pushq
käskyllä, joka ottaa operandikseen sen rekisterin arvon, mistä arvo haetaan. Pino-osoittimen osoittamasta kohtaan muistia tallennetaan rekisterin arvo ja päivitetään pino-osoitinta vastaavasti. - Pinosta haetaan tavaraa
popq
-käskyllä, joka tallentaa pino-osoittimen osoittamasta kohdasta arvon operandina annettuun rekisteriin.
Nämä pinon käsittelykäskyt päivittävät itse pino-osoitinta, joten sitä ei tarvitse koodissa tehdä.
Muistin varaaminen pinosta menee yksinkertaisesti siten, että siirrämme pino-osoitina tarvittavan tavumäärän eteenpäin.
irmovq $32,%rax # Varataan tilaa neljälle sanalle x 8 tavua = 32 tavua
subq $32, %rsp # Pino-osoitin siirtyy alaspäin 32 tavua
Aritmeettiset operaatiot¶
Aritmeettisiä operaatiota on vain! neljä erilaista.. Nämä käskyt ottavat kaksi operandia, joiden tulee olla rekistereitä. Vakioarvoja (=numeroita) ei voi operandeina käyttää koska y86:sessa ALU operoi aina rekisterien kanssa.
addq %r1,%r2
: yhteenlaskur2 = r2 + r1
subq %r1,%r2
: vähennyslaskur2 = r2 - r1
andq %r1,%r2
: JA-operaatior2 = r2 & r1
xorq %r1,%r2
: ERI-operaatior2 = r2 ^ r1
Nyt prosessorin tilabitit asettuvat aritmeettisen operaation tuloksen mukaan. Ts. tilabittien mukaan voimme tarkastella operaation tuloksia.
Esimerkkejä.
addq %r10,%rsi # %rsi = %rsi + %r10
subq %r9,%rsi # %rsi = %rsi - %r9
andq %rax,%rbx # %rbx = %rax & %rbx
xorq %rsp,%rbp # %rbp = %rsp ^ %rbp
Siirto-operaatiot¶
Sijoitusoperaatiota, esimerkiksi C-kielessä arvon sijoittamista muuttujaan, assembly:ssä vastaa siirto-operaatio.
Dataa voidaan siirtää kahdella tavalla: siirtokäskyillä ja ehdollisilla siirtokäskyillä. Siirtokäskyjen operandien tyyppi riippuu käskystä, kuten alla. Kaikki osoitusmuodot ovat käytössä.
Huomioitavaa on, ettei siirtoja voi tehdä muistiosoitteesta toiseen tai vakioarvoa ei voi viedä suoraan muistiin. Aina mennään jonkin rekisterin kautta.
Siirtokäskyjä on neljä erilaista. Käskyn
__movq
nimessä edessä olevat kaksi tyhjää kirjainta __
ilmaisevat käskyn operandin tyypin ao. mukaisesti. irmovq
: vakioarvo (i=immediate) rekisteriin (r=register):i->r
rrmovq
: rekisteristä rekisteriinr->r
mrmovq
muistista (m=memory) rekisteriinm->r
rmmovq
: rekisteristä muistiinr->m
Nämä käskyt ottavat kaksi operandia, eli mistä siirretään (source) ja minne siirretään (destination).
Esimerkkejä.
Esimerkkejä.
irmovq $4 , %rsi # Sijoitetaan numeroarvo 4 rekisteriin %rsi
rrmovq %rax , %rsp # Sijoitetaan %rax:n arvo %rsp:hen
mrmovq (%rdi), %r10 # Haetaan arvo rekisterin %rdi osoittamasta paikasta ja sijoitetaan %r10:n
rmmovq %rcx , 8(%rdx) # Sijoitetaan %rcx:n arvo osoitteeseen %rdx:n arvo + 8
Ehdollisia siirtokäskyjen toiminta riippuu prosessorin tilabittien arvoista, siitä siis ehdollisuus. Eli, edellisen käskyn suorituksen jälkeisestä tilasta päätellään tehdäänkö siirot vai ei. Ehdolliset siirtokäskyt ottavat operandeikseen rekistereitä ja siirtävät sen toiseen operandiin (rekisteriin) vain jos haluttu ehto on totta tilabittien mukaan.
Ehdollisia siirtokäskyjä ovat.
cmove
(equal): toteutuu kunZF=1
cmovne
(not equal): toteutuu kun~ZF
, eli ZF=0cmovl
(less): toteutuu kunSF^OF
cmovle
(less or equal): toteutuu kun(SF^OF) | ZF
cmovge
(greater or equal): toteutuu kun~(SF^OF)
cmovg
(greater): toteutuu kun~(SF^OF) & ~ZF
Kuten huomataan, ehdollisten siirtokäskyjen logiikka on mielenkiintoinen. Esimerkiksi, yhtäsuuruus todetaan aritmeettisellä operaatiolla, jonka lopputulos on 0. Takana tässä on ajatus, että näin ALUn digitaalilogiikan toteutus olisi mahdollisimman yksinkertainen. Seurauksena sitten assembly-ohjelmoijan täytyykin miettiä esim. vertailuoperaatiot tilabittien käytön näkökulmasta eikä totutusti helppolukuisina korkean tason ohjelmointikielen vertailuoperaatioina..
Esimerkki.
.pos 0
main:
irmovq $3,%rax # a=3
irmovq $2,%rbx # b=2
# Testataan a == b ?
subq %rax,%rbx # Vähennyslaskulla rbx = rbx - rax
# Jos rbx > rax, tilaliput eivät muutu
# Jos rbx < rax, SF=1 ja OF=1
# Jos rbx = rax, ZF=1
cmove %rax,%rcx # Nyt jos ZF=1, sijoitetaan rcx = rax
# muutoin käsky ei tee mitään
(Tämä toteutus voidaan tehdä myös pinoa käyttäen niin, että a:n arvo pistetään talteen pinoon.)
Ehdolliset siirtokäskyt voivat optimoida prosessorin toimintaa, josta lisää myöhemmin..
Hyppykäskyt¶
Hyppykäskyjä on seitsemän erilaista. Hyppykäskyssä ei itsessään vertailla mitään, vaan ne tekevät hyppypäätöksen edellisen käskyn asettamien tilabittien perusteella. Operandiksi hyppykäskylle annetaan muistiosoitteen nimi, mihin se ehdon toteutuessa hyppää.
jmp
: Hyppää ehdoittaje
yhtäsuuri (equal) toteutuu kunZF=1
jne
erisuuri (not equal) toteutuu kun~ZF eli ZF=0
jle
pienempi tai yhtäsuuri (less or equal) toteutuu kun(SF^OF) | ZF
jl
pienempi (less) toteutuu kunSF^OF
jge
suurempi tai yhtäsuuri (greater or equal) toteutuu kun~(SF^OF)
jg
suurempi (greater) toteutuu kun~(SF^OF) & ~ZF
Esimerkki. C-kielen silmukkarakenne:
int64_t rcx = 10; // Silmukkamuuttuja
int64_t rdx = 1; // apumuuttuja (jolla vähennetään 1)
while (rcx !=0) { // Testaus rcx != 0
tee_jotain(); // Aliohjelmakutsu
rcx = rcx - rdx; // Vähennyslasku rcx = rcx - rdx
}
(Tietenkään C-kielessä ei tarvittaisi apumuuttujaa rdx, vaan silmukkamuuttujan koodi voisi olla
rcx--
, mutta nyt y86-assembly-kielessä ei ole vastavaa aritmeettista unaarista operaattoria.)..ja sama y86-assemblyllä:
irmovq $0x10,%rcx # Silmukkamuuttuja
irmovq $0x1,%rdx # apumuuttuja (jolla vähennetään 1)
loop:
call tee_jotain # Aliohjelmakutsu, kts alla.
subq %rdx,%rcx # Vähennyslasku rcx = rcx - rdx: jos rcx = 0 -> ZF=1
jne loop # Testaus rcx != 0: Jos erisuuri, eli ZF=0, hyppää loop
halt # Muutoin lopeta ohjelma
Aliohjelmat¶
Assembly-kielessä voidaan toki kutsua ohjelmasta myös aliohjelmia (ts. funktioita). Voimme sijoittaa aliohjelman koodin haluamaamme paikkaan muistissa, mutta meidän pitää lisäksi huomioida seuraavat asiat:
- Aliohjelman suoritus tehdään hyppykäskyllä sen muistiosoitteeseen, josta paluu takaisin kutsuvaan koodiin. Ohjelman suoritus aliohjelman"hypyn" jälkeen jatkuu siis siitä mihin se jäi..
- Miten tehdään muuttujien (ts. parametrien) välitys aliohjelmalle ja aliohjelman paluuarvo?
- Miten varataan muistia aliohjelman paikallisille muuttujille?
Assembly- ja konekielessä rekistereitä ja/tai pinomuistia käytetään näiden kolmen asian toteutuksessa.
Yleensä on sovittu mihin rekistereihin kutsuparametrit talletetaan. Tällöin ei joka ohjelmassa tarvitse miettiä asiaa uudelleen ja lisäksi koodin uudelleenkäytettävyys paranee. x86:ssä (kyllä, x86) on sovittu, että parametrit tallennetaan rekistereihin seuraavassa järjestyksessä:
%rdi, %rsi, %rdx, %rcx, %r8, %r9
. Eli maksimissaan kuusi parametriä voitaisiin välittää rekisterien kautta. Tätä järjestystä on syytä noudattaa kurssilla. Aliohjelman suoritus¶
Aliohjelman suoritus y86-prosessorissa menee seuraavasti:
- Ensiksi, ennen hyppyä aliohjelmaan, tallennetaan nykyinen ohjelman tila (rekisterit + PC) pinoon. Tämä on tärkeää siksi, että voimme palata aliohjelmasta samaan suorittimen tilaan mistä aliohjelmaa lähdettiin suorittamaan. Joskun käytämme samoja rekistereitä, niin aliohjelma muuttaisi niiden arvoja, eikä kutsuvan ohjelman suoritus enää jatkuisi siitä tilasta mihin jäätiin!
- Osa rekistereistä on myös määritelty siten, että niissä olevan arvon ylläpitämisestä on vastuussa joko aliohjelman kutsuja tai aliohjelma itse.
- Aliohjelman hyppykutsu tallentaa pinoon paluuosoitteen, ts. osoitteen josta kutsuvan ohjelman suoritus jatkuu kun aliohjelman suoritus on loppunut.
- Aliohjelman parametrit voidaan välittää joko rekisterien tai pinon kautta. Jos käytetään rekistereitä, niille on sovittu järjestys jota tulisi käyttää. Eli ensimmäinen parametri menee rekisteriin rdi ja toinen parametri rekisteriin rsi, jne. Koska rekistereitä on rajallinen määrä, on mahdollista välittää vain muutamia arvoja. Tähän tarjoaa pino apua, eli voimme viedä parametrit pinoon, josta aliohjelma ne lukee.
- Funktion paikallisille muuttujille voidaan ennen funktion kutsua varata muistia siirtämällä pino-osoitinta eteenpäin tarvittava tavumäärä. Luemme arvot sitten pinosta käyttäen epäsuoraa osoitusta.
- Aliohjelmassa ja sitä kutsuvassa pitää huolehtia, että pinosta haetaan täsmälleen sama joukko tietoa, kun sinne tallennettiin. Aliohjelmakutsussa pino on siis tilapäinen tietovarasto.
- Aliohjelmasta poistumiskäsky lopuksi hakee pinosta kutsuneen ohjelman seuraavan koodirivin osoitteen ja palaa suorittamaan sitä. Tässä yhteydessä myös muistin tila tulee palauttaa entiselleen, jotta paluuosoite haetaan oikeasta paikasta.
Aliohjelmiin assembly-kielessä hypätään käskyllä
call
, jolloin operandiksi annetaan aliohjelman nimi, kts. alla. Paluuosoite, mihin aliohjelman suorituksen jälkeen palataan, tallentuu automaattisesti pinoon call-käskyn yhteydessä. Aliohjelmista palataan
ret
-käskyllä, joka kaivaa paluuosoitteen pinosta.Esimerkki yksinkertaisesta aliohjelmasta
laske
:.pos 0
init:
irmovq pino,%rsp # Pinon alustus (muistiosoitteeseen 0x100)
irmovq pino,%rbp
main: # Pääohjelma
irmovq $0x11,%rdi # Argumentit rekistereihin
irmovq $0x11,%rsi
call laske
halt
.pos 0x40
laske:
addq %rsi,%rdi # Argumentit aliohjelmassa
ret
.pos 0x400
pino:
Pinon käyttäminen parametrien välitykseen on hieman monimutkaisempaa.. Tyypillisesti pino jaetaan tässä osiin, joilla on oma käyttötarkoituksensa.
Kuvassa alla pinomuistin osittaminen aliohjelmakutsun yhteydessä. Muistetaan, että pino kasvaa alaspäin muistissa. Alussa pino-osoitin
%rsp=0x100
.Esimerkki.
.pos 0
irmovq Pino,%rsp # Alustetaan pino
irmovq Pino,%rbp
main:
irmovq $0x11,%rax
pushq %rax # vie 1. argumentti pinoon
irmovq $0x22,%rax
pushq %rax # vie 2. argumentti pinoon
pushq %rax # varataan tilaa funktion paluuarvolle
call laske # aliohjelmakutsu
popq %rax # haetaan paluuarvo
popq %rax # tyhjennetään pino
popq %rax
halt
.pos 0x40
laske:
mrmovq 24(%rsp),%rdx # 1. argumentti pinosta (kolme muistipaikkaa x 8 tavua = 24)
mrmovq 16(%rsp),%rcx # 2. argumentti pinosta (kaksi muistipaikkaa x 8 tavua = 16)
addq %rcx,%rdx
rmmovq %rdx,8(%rsp) # paluuarvo pinoon sovittuun paikkaan
ret
.pos 0x400
Pino:
Kutsuva ohjelma on sitten vastuussa argumenttien ja funktion paluaarvon käsittelystä, eli sen pitää poistaa ne pinosta.
Muita käskyjä¶
nop
-käsky ei tee mitään, paitsi kasvattaa PC-rekisteriä. Oikeissa prosessoreissa käskyllä on silti useita käyttötarkoituksia, mm. ajan mittaaminen ja koodin ryhmitys muistiin. Tästä lisää myöhemmin, mutta sanotaan jo teaserina, että suorittimen toiminnan optimoinnissa käskyllä, joka ei siis tee mitään, on äärimmäisen tärkeä rooli! halt
-käsky pysäyttää prosessorin toiminnan tähän käskyyn. Ohjelma tila STAT
asettuu tilaan HLT.Kääntämistä ohjaavat käskyt¶
Kääntämistä ohjaavat käskyt ovat seuraavat:
nimi:
Symbolinen nimi tätä seuraavalle koodilohkolle. Esimerkiksi aliohjelmalle voidaan antaa symbolinen nimi. Nimi häviää koodista käännösvaiheessa ja korvautuu varsinaisella muistiosoitteella.
Nimet eivät ole millään tavoin pakollisia, niitä voidaan käyttää auttamaan koodin jäsentämisessä. Ohjelmoijalla on tässä vapaat kädet. Yhtähyvin alla voisi lukea
oa_juttu
eikä main
. Esimerkissä alla varataan muistiosoitteet main:lle ja kahdelle funktiolle:main:
...
funktio1:
...
funktio2:
...
.pos
Asettaa tätä seuraavan koodin / koodilohkon alkavan mistä tahansa annetusta muistiosoitteesta.
.pos 0 # Koodi (tässä itse nimetty main-funktio) alkaa muistiosoitteesta 0
main:
...
.pos 0x100 # funktio1:sen koodi alkaa muistiosoitteesta 0x100
funktio1:
...
Ohjelman muistiosoitteiden ei tarvitse olla peräkkäin, välissä voi olla "tyhjää" tilaa.. tämä on varsin hyödyllistä, jos halutaan myöhemmin esim. päivittää aliohjelmaa, niin ei tarvitse siirtää koko koodia tai sen muistiosoitteita.
.align
tasaa muistiosoitteen annettuun sanan pituuteen. Jos muistiin asetattavan arvon koko on pienempi kuin sana, täytetään sen perään nollia niin kauan että se vastaa sanan pituutta. Tämä on hyödyllinen ominaisuus esimerkiksi taulukkojen yhteydessä, mutta sitä ei yleensä tarvita.
.quad
-käskyllä voidaan muistiin asettaa tietoa. Asetus tapahtuu ennen koodin ajamista, eli tämä toimii ikäänkuin massamuistina, josta voimme lukea dataa.
Ensin
.pos
-käskyllä voidaan asettaa haluttu muistipaikka, .align
käskyllä saadaan haluttu tasaus ja .quad
-käskyllä viedään muistipaikkaan dataa..pos 0x80
.align 8 # ryhmitellään muisti 8:n tavun mittaisiin osoitteisiin
.quad 0x1234 # 2-tavuinen luku
.quad 0x5678 # 2-tavuinen luku
Muisti näyttää seuraavalta, koska luku tasataan sanan pituuteen.
... 0x80: 0x3412000000000000 0x88: 0x7856000000000000 ...
Lopuksi¶
Assembly-ohjelmointiavarten y86-64:selle löytyy netistä vapaasti käytettävä simulaattori (Vasemmalta menusta Student site ja Chapter 4: Processor Architecture), jolle kurssilla laadimme pieniä assembly-kielisiä ohjelmia. Tämän simulaattorin voitte asentaa omalle koneella ja se onkin jo asennettu työasemaluokissa kurssin virtuaalikoneeseen TKJ_harjoitukset. Tulemme seuraavilla luennoilla käyttämään simulaattoria esimerkeissä.
Myös 64-bittinen verkkoversio ja 32-bittinen verkkoversio löytyy. Jälkimmäisessä rekisterit ovat 32-bittisiä, nimeltään: %eax, %ebx, jne ja käskyt hieman eri nimisiä: addq -> addl. Tämä nimeämismuutos johtuu siitä, että arkkitehtuurin vaihdon myötä muistipaikan koko vaihtuu quad:stä long:iin (64 -> 32-bit).
Anna palautetta
Kommentteja materiaalista?