Suoritinarkkitehtuureista¶
Osaamistavoitteet: Tämän materiaalin luettuaan opiskelija tuntee kurssilla esimerkkisuorittimena käytettävän y86-64-prosessorin ohjelmoijalle näkyvän arkkitehtuurin, assembly-kielen syntaksin sekä osaa laatia sille pienimuotoisia assembly-kielisiä ohjelmia.
Kurssilla käytämme opetustarkoituksiin kehitettyä suoritinta y86-64. Tämä prosessori on (todella paljon) riisuttu versio PC-koneissa nykyään yleisestä x86-prosessoriarkkitehtuurista, mutta sen käskykanta ja sisäinen toiminta ovat hyvin samankaltaisia. Modernina prosessoriarkkitehtuurina x86-perhe on varsin pitkälle kehitetty vuosikymmenien työn tulos, jota ei lyhyen peruskurssin tuntimäärillä läpikäydä.
Kurssilla opetellaan ensin y86-assembly- ja konekieli, joita sitten käytetään havainnollistamaan käskyn suoritusta prosessorissa ja käydään yksityiskohtaisesti läpi suorittimen sisäinen rakenne siltäosin kun se liittyy käskyn suorittamiseen. Lisäksi y86-assembly-kieltä käytetään kurssin harjoitustehtävien ja -työn tekemisessä assembly-kääntäjän (engl. assembler) ja -simulaattorin avulla.
Mutta, lähdetään liikkeelle laajentamalla hiukan Johdatus-kurssilla esittelemämme tietokoneen perusarkkitehtuurin kuvausta.
Suorittimen sisäisessä arkkitehtuurissa on erotettavissa viisi osaa:
1. Laskentayksikkö ALU (Aritmeettis-looginen yksikkö), joka suorittaa aritmeettiset ja loogiset operaatiot rekistereissä olevalle datalle.
2. Kontrolliyksikkö/ohjausosa, joka ajaa suoritinta: synkronoi suorttimen eri komponenttien toimintaa, hakee käskyt ja datan muistista sekä ohjaa järjestelmä- ja I/O-väyliä.
3. Rekisterit, jotka ovat suorittimen sisäisen muistin sisältäen käskyt, niiden vaatiman datan sekä suorittimen tilan. Kuvassa alla 8086-suorittimen rekisterikuvaus. Nähdään, että
1. Laskentayksikkö ALU (Aritmeettis-looginen yksikkö), joka suorittaa aritmeettiset ja loogiset operaatiot rekistereissä olevalle datalle.
2. Kontrolliyksikkö/ohjausosa, joka ajaa suoritinta: synkronoi suorttimen eri komponenttien toimintaa, hakee käskyt ja datan muistista sekä ohjaa järjestelmä- ja I/O-väyliä.
3. Rekisterit, jotka ovat suorittimen sisäisen muistin sisältäen käskyt, niiden vaatiman datan sekä suorittimen tilan. Kuvassa alla 8086-suorittimen rekisterikuvaus. Nähdään, että
- Osa rekistereistä on yleiskäyttöisiä (GPR, general purpose register), joiden käyttötarkoitus on ohjelmoijan/kääntäjän valittavissa (tiettyjen sääntöjen mukaan).
- Osalle on määritelty tarkoitus liittyen ohjelman suoritukseen. Esimerkiksi käskyrekisteri (PC, program counter/instruction pointer), muistinosoitusrekistereitä (Segment registers) ja pino-osoitin (Stack segment).
- Osa pitää sisällään suorittimen tilan tilalippuina (Condition codes).
4. Keskus/käyttömuisti (Randon Access Memory, RAM), joka pitää sisällään ohjelmankoodin ja datan.
- I/O-väylään kytketyt laitteet voidaan myös ajatella osaksi tätä muistia muistiinkuvatun I/O:n kautta. Esimerkki I/O-laiteajurit ja niiden rekisterit.
5. Suorittimen sisäiset väylät: Erilliset data-, käsky- ja osoiteväylät.
- Nämä väylät voivat olla jaettuja ulos suorittimesta osaksi järjestelmäväylää.
Esimerkki. "Alkuperäisen" IBM PC-tietokoneen 8088-prosessorin sisäinen arkkitehtuuri.
Esimerkki. Modernin PC:n I/O-väyläratkaisu. Hierarkiset I/O-sillat (north ja south bridge) kytkevät toisiinsa erinopeuksiset, -tyyppiset ja -kokoiset väylät.
y86-64 Suoritinarkkitehtuuri¶
y86-64 prosessoriarkkitehtuuri yleisesti:
- Arkkitehtuuri on 64-bittinen, josta seuraa että muistipaikat ja rekisterit ovat 64-bittisiä.
- Näin ollen sanan pituus on 64 bittiä.
- Lukuesitys on 2-komplementti ja MSB-bitti on merkkibitti.
- Tavujärjestys on little endian, eli vähiten merkitsevä tavu ensin.
- Bittijärjestys on big endian eli eniten merkitsevä bitti ensin.
Kuvassa alla yksinkertaistettu y86-64 prosessorin arkkitehtuuri. Yleisesti y86-64 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. Ohjausosan logiikkaa käymme myös materiaalissa läpi siltä osin kun se liittyy ohjelmien suoritukseen.
Kuvassa alla ohjelmoijalle näkyvä prosessorin ja muistin tila.
y86-64 Rekisterit¶
Kun siis ohjelmoimme assemby-kielellä, yleiskäyttöiset rekisterit ovat konekielen 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
- Tilarekisteri STAT: Ohjelman suorituksen tila, kts alla.
- Hox! Tämä rekisteri on eri asia kuin suorittimen tilabitit.
- Ohjelmalaskuri (Program counter, PC) sisältää seuraavan suoritettavan käskyn muistiosoitteen.
Hox! Assembly-kielessä
%
-merkki ilmaisee, että kyse on ohjelmallisesti käytettävästä rekisteristä. Rekisterien nimet noudattavat x86-prosessoriperheen nimeämiskäytäntöä.Suorittimen ja ohjelman tila¶
Assebmly/konekielisen ohjelman toiminta perustuu prosessorin tilaa osoittavien tilalippujen/bittien arvon tarkasteluun. Tilabittejä, joista käytetään yleisesti nimitystä tilaliput, varten on varattu erillinen tilarekisteri:
- Tilalippu asetetaan, kun suoritin saa keskeytyksen.
- Konekielisen ohjelman ehdollinen toiminta (vertailuoperaattorit, hyppykäskyt, jne) perustuvat tilarekisterin bittien arvon vertailuun.
- Käskyn seurauksena aiheutuva virheellinen toiminto merkitään asettamalla tilalippu.
Yleisesti, konekielten käskyjen suorituksen seurauksena prosessorin tilaliput muuttuvat ja sitten seuraavat käskyt reagoivat tilalippuihin, Tällä tavoin saadaan konekieliseen ohjelmaan luotua sen logiikka käyttäen tilalippuja.
y86:sessa on kolme tilalippua/bittiä (engl. condition codes, CC):
1.
1.
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
2.
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
3.
OF
(Overflow flag): Ilmaisee tapahtuiko operaatiossa ylivuoto, joka tarkoittaa sitä, että laskutoimitus ei mahdu tulosrekisterin lukualueeseen eli on isompi kuin sanan pituus.- 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
Tilarekisterissä (status code,
STAT
) on y86:n ohjelman suorituksen tila, 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. Usein törmää oikeissa suorittimissa "muistinumerona" (Carry-lippu) käytettävälle "ylimääräiselle" bitille, jonka avulla voidaan suorittaa sanan pituuden ylittäviä operaatioita oikein.
Esimerkiksi. 8-bittinen lukualue + carry-lippu ikäänkuin antaa ohjelmalle käyttöön 9 bittiä. Mutta, carry-lipun tilaa voi ainoastaan lukea, ei itse muuttaa. Konekielissä on usein myös erillisiä käskyjä, jotka reagoivat carry:n tilaan.
Datan osoitusmuodot¶
Assembly/kone-kielessä voidaan käskyn operandeja esitellä ja niillä osoittaa dataa / muistia eri tavoin. Käskyn tulkitsemisen yhteydessä prosessori laskee osoitusmuodosta, mistä käskyn tarvitsema data löytyy.
y86-64:sessa on kolme osoitusmuotoa:
- Suora osoitus (Immediate) kun käskyn operandi on vakioarvo:
$numero
. Esimerkiksi luvut$1
,$-13
tai$0x1F
. - Esimerkki: talletetaan kymmenjärjestelmän luku -13 rekisteriin %rsi käskyllä
irmovq $-13,%rsi
. - Rekisteriosoitus (Register) jossa käskyn operandi on rekisterin kulloinenkin sisältö:
%rekisteri
. - Yleensä ALU voi tehdä operaatioita vain rekistereissä olevalle datalle, joten se on ensin talletettava johonkin rekisteriin.
- Esimerkiksi talletetaan rekisterin %rax arvo rekisteriin %rbx käskyllä
rrmovq %rax,%rbx
. - Epäsuora osoitus (Memory) jossa operandi haetaan jonkun rekisterin arvon osoittamasta muistipaikasta. Tässä rekisteri merkitään sulkeilla:
(%rekisteri)
. - Sulkeiden edessä voi vielä olla numero
n(%rekisteri)
(base + displacement), jossa tapauksessa osoitetaan muistipaikkaarekisterin arvo +- n
, jossa on voi olla positiivinen tai negatiivinen. (Tätä ominaisuutta käytetään tyypillisesti pinomuistin lukemisessa.) - Esimerkiksi haetaan muistista data-arvo muistipaikasta %rax:n arvo ja tallennetaan haettu arvo rekisteriin %rbx käskyllä
mrmovq (%rax),%rbx
. - Esimerkiksi haetaan muistista data-arvo laskemalla %rax:n arvo-16 ja tallennetaan haettu arvo rekisteriin %rbx käskyllä
mrmovq -16(%rax),%rbx
. - Tätä osoitusmuotoa vastaa C-kielen osoitin eli (%rekisteri) voisi olla C-kielessä uint64_t *rekisteri.
Keskusmuisti¶
y86-prosessorissa on keskusmuistia noin neljä kilotavua. Muistipaikan koko on tietenkin sanan pituus, eli 64 bittiä, eli 8 tavua.
Muistetaan tässä bitti- ja erityisti tavujärjestys, joka on vähiten merkitsevä tavu ensin. Tämä siis poikkeaa C-kielen osuudessa käytetystä määrittelystä!
Pinomuisti¶
Pino (engl. stack) on keskusyksikölle/ohjelman suoritukseen keskusmuistista varattu muistialue, johon varastoidaan tietoa LIFO-periaatteella (Last in -> First out). Toisinsanoen, pinoon viedyt arvot luetaan sieltä käänteisessä järjestyksessä, josta nimitys pinomuisti / pino. Pinomuistia käytetään yleisesti tiedon varastointipaikkana, esimerkiksi taulukkoja tms isompia muistialueita käyttäessä tai aliohjelmia suorittaessa (ja joskun rekisterit muuten loppuvat..).
Nyt, assembly/konekielisen ohjelman alussa tarvitsee asettaa pinomuistille paikka, asettamalla sen alkupään ja päällimmäisen muistipaikan osoite. Tyypillisesti suorittimissa on rekistereitä, joihin ohjelmallisesti nämä arvot voidaan asettaa.
Nyt, hämäävästi muistissa pino kasvaa alaspäin, eli vaikka aina konseptina asetamme arvon pinon päälle, niin päällimmäisellä arvolla on muistissa aina pienin muistiosoite.
Pinossa olevan datan käsittelyä varten on suorittimissa kaksi assembly/konekielen-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-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.) Kurssilla käytämme ohjelmissa aina pinoa, koska ilman pinoa aliohjelmat eivät toimi (kts. alla).
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ä. Käsittelykäskyt päivittävät itse pino-osoitinta, joten sitä ei tarvitse koodissa tehdä.
- 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. - Lisäksi muistin varaaminen omaan käyttöön pinosta menee yksinkertaisesti siten, että siirrämme pino-osoitina tarvittavan tavumäärän eteenpäin.
Esimerkki pinon käyttäytymisestä.
# Pinon alustus irmovq $100,%rbp # Pinon alkuosoite muistissa irmovq $100,%rsp # Pinon nykyisen (päällimäisen) muistipaikan osoite # Nyt rekisterit muuttuvat seuraavasti %rbp = 100 %rsp = 100 irmovq $2,%rax # Sijoitetaan rekisteriin %rax lukuarvo 2 pushq %rax # Viedään pinoon rax:n arvo # Nyt rekisterit muuttuvat seuraavasti %rax = 2 %rbp = 100 %rsp = 92 # Vietiin muistia sanan pituuden (8 tavua) verran: 100-8 = 92 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: 92-8 = 84 popq %rcx # Luetaan pinosta päällimmäinen arvo # Nyt rekisterit muuttuvat seuraavasti %rcx = 3 %rbp = 100 %rsp = 92 # Luettu sana poistui pinosta, päviitetään osoitin: 84+8 = 92
Esimerkki muistin varaamisesta pinossa.
irmovq $32,%rax # Varataan tilaa: 4 sanaa x 8 tavua = 32 tavua
subq $32, %rsp # Pino-osoitin rekisterinä, arvosta vähennetään 32 tavua
# Nyt kun pino-osoitin siirtyi, pinossa on välissä "tyhjää tilaa"
Hox! Kurssilla tyypillinen virhe on aluksi jättää pino alustamatta ja sitten ihmetellä miksi aliohjelmakutsut eivät toimi.
Hox2! Koska pino sijaitsee samassa muistissa kuin koodi ja data, pitää olla tarkkana ettei pinoon viedyt arvot sotkeudu koodiin tai muuhun dataan!
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 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
subq %rax,%rbx # Testataan a == b ? 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 apumuuttujana toista rekisteriä
rdx
, vaan silmukkamuuttujan koodi voisi olla rcx--
, mutta nyt y86-assembly-kielessä ei ole vastavaa aritmeettista unaarista operaattoria. Operandien käyttö rekistereiden kautta.)..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:
- Ennen aliohjelman suoritusta, pinoon tallennetaan nykyinen ohjelman tila (rekisterit + PC). Tämä on tärkeää kahdesta syystä:
- Voimme palata aliohjelmasta samaan suorittimen tilaan mistä aliohjelmaa lähdettiin suorittamaan.
- Voimme käyttää samoja rekistereitä pää- ja aliohjelmassa. Pääohjelmassa rekisterien arvot ovat tallessa pinossa ja sillä aikaa ne ovat vapaasti aliohjelman käytössä. Kun pääohjelmaan palataan takaisin, ladataan "vanhat" arvot takaisin rekistereihin
- Tässä osa rekistereistä on myös määritelty siten, että niissä olevan arvon ylläpitämisestä on vastuussa joko aliohjelman kutsuja tai aliohjelma itse.
- Aliohjelman argumentit voidaan välittää joko rekisterien tai pinon kautta. Koska rekistereitä on rajallinen määrä, on mahdollista välittää vain muutamia arvoja. Tähän tarjoaa pino apua, eli voimme viedä parametrit pinoon.
- Jos käytetään rekistereitä, niille on sovittu järjestys jota tulisi käyttää.
- Jos argumentit ovat pinossa, ne luetaan sieltä epäsuoraa osoitusta käyttäen.
- Aliohjelman paikallisille muuttujille voidaan ennen aliohjelmakutsua varata muistia siirtämällä pino-osoitinta eteenpäin tarvittava tavumäärä. Luemme arvot sitten pinosta käyttäen epäsuoraa osoitusta.
- Aliohjelmiin hypätään käskyllä
call
, jolloin operandiksi annetaan aliohjelman nimi, kts. alla. - Paluuosoite, mihin aliohjelman suorituksen jälkeen palataan, tallentuu automaattisesti pinoon käskyn yhteydessä.
- Aliohjelmassa ja sitä kutsuvassa pitää huolehtia, että pinosta haetaan täsmälleen sama määrä dataa, kun mitä sinne tallennettiin.
- Aliohjelmasta poistumiskäsky
ret
hakee pinosta kutsuneen ohjelman seuraavan koodirivin osoitteen ja palaa suorittamaan ohjelma siitä kohti. Tässä yhteydessä myös muistin tila tulee palauttaa entiselleen, jotta paluuosoite haetaan oikeasta paikasta.
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 0x100
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 0x100
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. Tällä tavoin voidaan siis merkitä muistiosoitteita. 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?