OSA II. TIETOKONEJÄRJESTELMÄT¶
Kurssin tässä osassa tutustumme suorittimen (mikroprosessori, keskusyksikkö, CPU) arkkitehtuuriin ja sisäiseen toimintaan osana tietokonejärjestelmää. Tämän osan suorittamisesta on kerrottu tarkemmin materiaalin etusivulla.
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 peruskurssin tuntimäärillä läpikäydä.
Kurssilla opetellaan ensin y86-assembly-kieli ja sitten käydään yksityiskohtaisesti läpi y86-prosessorin sisäisen toiminta, eli miten käskyt itseasiassa suoritetaan.
Kurssilla opetellaan ensin y86-assembly-kieli ja sitten käydään yksityiskohtaisesti läpi y86-prosessorin sisäisen toiminta, eli miten käskyt itseasiassa suoritetaan.
Suoritinarkkitehtuureista¶
Alla Johdatus-kurssilla esittelemämme tietokoneen perusarkkitehtuurin kuvaus, jota kurssin tässä osassa syvennetään suorittimen (CPU) ja muistinkäytön osalta.
Suorittimen sisäisessä arkkitehtuurissa on erotettavissa neljä osaa:
- Laskentayksikkö ALU (Aritmeettis-looginen yksikkö), joka suorittaa aritmeettiset ja loogiset operaatiot rekistereissä olevalle datalle.
- 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ä.
- 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), osalle on määritelty tarkoitus liittyen ohjelman suoritukseen (esimerkiksi käskyrekisteri, PC ja pino-osoitin) ja osa pitää sisällään suorittimen tilan (Condition codes).
- Muisti, joka pitää sisällään ohjelmankoodin ja datan. I/O-väylään kytketyt laitteet ajatellaan myös osaksi muistia.
Esimerkki. "Alkuperäisen" IBM PC-tietokoneen 8088-prosessorin sisäinen arkkitehtuuri.
Esimerkki. Modernin PC:n väyläratkaisu.
Tietokoneen historiaa¶
Historiallisesti jo Aristoteleen ajoista lähtien filosofit ja matemaatikot ovat koettaneet formalisoida (ihmisen) loogísta päättelyä, ts. luoda yleistä symbolista logiikkaa, jolla pystyttäisiin esittämään ja ratkaisemaan mikä tahansa algoritmi. Mm. Boolen algebra on yksi matemaattinen toteutus, joka sittemmin johti digitaalitekniikan syntyyn Claude Shannonin ajatusten pohjalta.
Ihan ensimmäiset mekaaniset (Charles Babbage) ja elektroniset (John Atanasoff) tietokoneet eivät olleet (uudelleen)ohjelmoitavia eikä näinollen yleiskäyttöisiä.
Moderni yleiskäyttöinen tietokone perustuu matemaatikko Alan Turingin 1930-luvulla kehittämään malliin universaalista Turingin koneesta. Malli on teoreettinen, mutta sillä pystytään kuvaamaan ja suorittamaan mikä tahansa laskutoimitus äärettömän pitkällä nauhalla, jossa luku/kirjoituspää liikkuu suorittaen sieltä käskyjä luetulle datalle ja tallentaen niiden tuloksia nauhalle. Turingin mallin pohjalta on kehitetty mittareita tietokonejärjestelmien ja esimerkiksi ohjelmointikielten teoreettiselle kyvykkyydelle.
1930- ja 40-luvuilla Konrad Zuse ja John von Neumann kehittivät Turingin ajatusten pohjalta käsitteen ohjelmoitavasta tietokoneesta (engl. stored-program computer), jossa esitettiin sähköinen muisti, jonne ohjelma voitiin tallentaa ja sieltä sitä suorittaa.
Ensimmäisiä toteutettuja yleiskäyttöisiä elektronisia tietokoneita olivat mm. Harvard Mark I ja ENIAC. Suomen ensimmäinen oma tietokone oli ESKO, joka kehitystyö aloitettiin 1954. Oulun yliopiston ensimmäinen tietokone oli Elliott 803 1960-luvulta.
Näitä ensimmäisiä yleiskäyttöisiä tietokoneita ohjelmointiin tietysti suoraan konekielellä, kunnes Grace Hopper esitti ajatuksen laitteistoriippumattomasta ohjelmointikielestä, joka voitaisiin kääntää eri tietokoneille. Ensimmäisenä tietokoneohjelmoijana pidetään Ada Lovelacea, joka keksi yrittää ohjelmoida Babbagen mekaanisen tietokoneen seuraajaa.
Historiallisesti ohjelmoitavien tietokoneiden suoritinarkkitehtuurit on jaettu Harvard- ja von Neumann-arkkitehtuureihin, joiden periaatteet luotiin 1930- ja 40-luvuilla. Ratkaisut erottaa toisistaan se, miten data ja ohjelman käskyt ovat muistiin tallennettuja sekä tästä seuraava (nykyisin) järjestelmäväyläratkaisu.
1. Harvard-arkkitehtuurissa datalle ja ohjelmalle on suunniteltu erilliset muistit (ja välimuistit), kts kuva alla. Tästä seuraa, että järjestelmäväylä on jaettu kahteen osaan. Arkkitehtuurissa on myös erilliset osoiteavaruudet ohjelmamuistille ja datamuistille. Periatteessa Harvard-arkkitehtuuri on nopea ratkaisu, koska muistiosoituksia voidaan tehdä yhtäaikaa molempiin muisteihin erillisten väylien kautta.
Nykyesimerkki Harvard-arkkitehtuurista on sulautetut järjestelmät, joissa on erilliset muistit, ohjelmalle Flash-ohjelmamuistit ja datalle RAM-muisti. Harvard-arkkitehtuurilla saatiin siis resurssirajoitteiseen laitteeseen lisää tehoja.
2. von Neumann-arkkitehtuurissa data ja ohjelman käskyt ovat samassa fyysisessä muistissa ja samassa muistiavaruudessa. Tällöin käytetään yhtä samaa järjestelmäväylää, joten suorittimen toteutus on yksinkertaisempi. Mutta, nyt käskyä ja sen dataa ei voida noutaa samanaikaisesti, joka teki siitä hitaamman ratkaisun.
Nykyisin von Neumann-arkkitehtuuri on yleisesti käytössä mm. PC-työasemissa. Mutta, modernit suoritinarkkitehtuurit ovat välimuotoja, joihin on poimittu parhaat puolet molemmista ratkaisuista.
Mooren laki¶
Mooren laki on (mm. Intelin perustajan) Gordon Mooren tekemä havainto siitä kuinka tekniikan kehityksen myötä transistorien määrä mikropiirillä kaksinkertaistuu noin kahden vuoden välein.
Mooren laki on jatkuvan keskustelun alla, mutta tähän saakka se on pitänyt varsin hyvin paikkansa. Transistorien määrä on tuplaantunut jopa nopeamminkin, noin 18 kuukauden ajassa. Nykyisin arvellaan, että Mooren laki ei toteudu enää 2020-luvulla. Mutta nähtäväksi jää, lakia on yritetty kaataa aiemminkin..
Sinänsä transistorien määrän kasvu ei kerro tietokoneen laskentatehon kasvusta, kuten tulemme luentomateriaalissa näkemään. Nyt kuitenkin, samassa fyysisessä mittakaavassa voidaan prosessoreihin lisätä sisäistä toimintalogiikkaa, jonka seurauksena saadaan mikroprosessorien tehokkaampi toiminta, laajempia käskykantoja, jne.
Kuvissa alla vasemmalla Intelin suorittimien käskykannan muutosnopeus ja oikealla DRAM-muistipiirien kasvanut kapasiteetti. Intelin käskykannassa kantava periatee on ollut alaspäin yhteensopivuus eri prosessorisukupolvien kesken.
C-kielestä konekieleen¶
Kurssilla olemme jo tutustuneet yhteen korkean tason ohjelmointikieleen, eli C-kieleen. Ok ok, aiemmin puhuttiin C:stä matalan tason laiteläheisenä kielenä ja sitä se nykyään onkin verrattuna moniin uudempiin kieliin, kuten Ohjelmoinnin alkeet-kurssin Python. Tietokoneen keskusyksikölle C-kieli kuitenkin on aivan liiian korkealentoista ymmärrettävää.
Suorittimelle ohjelma pitää saada sen ymmärtämään muotoon, eli esittää ohjelma konekielellä. Vaikka ohjelmoija nykyisin harvemmin pääsee (/joutuu) ohjelmoimaan konekielellä, auttaa konekielen ja suorittimen toiminnan ymmärtäminen tuottamaan tehokkainta mahdollista koodia. Tämä tietämys on yleistä tietotekniikan insinöörin ammattitaitoa, joka heijastuu myös ohjelmointiin korkean tason kielillä. Sulautettujen ohjelmoinnissa työelämässä tulee kyllä eteen tilanteita, jossa ohjelmassa aikakriittinen osa koodia täytyy toteuttaa assembly-kielellä.
Aikoinaan konekielinen (tai assembly-kielinen) ohjelmointi oli ainoa vaihtoehto ylipäänsä koodata tai tuottaa tehokasta koodia silloisiin "sulautettuihin"..
(Hamilton ei toki itse kirjoittanut kaikkea tätä koodia. Lisäksi hän johti Apollo-projektien koodaustiimiä..)
C-kielen käännösprosessi¶
Koska keskusyksikkö-arkkitehtuureja on erilaisia, konekieli on laitteisto/suoritinriippuvaista siinä missä C-kielelle on standardoitu toteutus, joka toimii yleisesti järjestelmästä toiseen. C-kieli tarvitsee siis kääntää kunkin suorittimen konekielelle erikseen kääntäjän (engl. compiler) avulla. Kurssilla emme mene kääntäjien toteutukseen syvemmälle, mutta yleinen käännösprosessi on syytä tuntea. Ja juu, kääntäjäkin on siis tietokoneohjelma..
Ensimmäisessä vaiheessa siis C-kielinen ohjelma käännetään assembly-kieliseksi. Seuraavaksi assembler-ohjelma kääntää assembly-kielisen toteutuksen itse konekielelle objekti-tiedostomuotoon (pääte .o, .so tai .obj, tms). Sitten linkkeri yhdistää objektitiedostot ajettavaksi konekieliseksi ohjelmaksi. Usein ohjelmamme käyttää valmiina järjestelmäkirjastoja (C-kielessä stdio, RTOS:n kirjastot, jne), joiden objektimuotoiset valmiit toteutukset linkkeri liittää ajettavaan ohjelmaamme. Lopputuloksena saadaan konekielinen ohjelma, jonka käyttöjärjestelmän komponentti loader lataa RAM-muistiin, kun ohjelma käynnistetään.
Assembly- ja konekieli¶
Suorittimen toteutukset perustuvat digitaalipiireihin (joita käsiteltiin aiemmilla kursseilla), jotka käsittelevät vain bittejä kombinaatio- sekvenssilogiikassaan Boolen algebran avulla. Näinollen suorittimen ymmärtämät käskytkin pitää esitellä bitteinä, ts. binäärilukuina joissa jokaisella bitillä on määritelty tarkoitus. Tällöin puhutaan konekielestä.
Yleisesti käskyä kuvaavassa binääriluvussa osa biteistä identifioi käskyn ja osa kertoo millaisia ja mistä käskyn operandit saadaan. Tästä voikin jo aavistella, että konekielen käskyt ovat todella yksinkertaisia verrattuna korkean tason kielten käskyihin ja ohjelmalauseisiin ja tekevät varsin pieniä juttuja kerrallaan. Nyt, meille korkean tason kielellä ohjelmointiin tottuneille, yksinkertaisenkin toiminnallisuuden toteuttaminen vaatii riveittäin konekieltä.
Konekielen binäärilukuesitys on ohjelmoijan tietenkin vaikea hallita (vrt. Hamiltonin tiimi..), mutta sen logiikan ja toiminnan ymmärtäminen auttaa ohjelmoijaa ajattelemaan kuin tietokone ohjelmaa tehdessään. Tuloksena on tehokkaampaa koodia, kun tiedämme miten suoritin toimii ja miten koodimme suoritetaan keskusyksikössä. Konekielisen ohjelmoinnin helpottamiseksi kielelle tarjotaan symbolinen esitystapa (ts. ihmisen luettava) eli assembly-kieli. Assembly-kielen käskyt on laadittu siten, että ne kuvautuvat suoraan (vaikka päässälaskien) yksi yhteen varsinaiselle konekielelle. Assembly-kieleen menemme tarkemmin seuraavassa luentomateriaalissa, mutta esitetään alla esimerkkejä.
Esimerkki. C-ohjelma
mahtijuttu.c
:int main() {
int i=0,a=0;
for (i=0; i<10; i++) {
a += i;
}
}
..käännetään se x86:n käskyllä
gcc -S -Og mahtijuttu.c
, jolloin kääntäjä tuottaa assembly-kielisen totetuksen C-koodistamme mahtijuttu.s
. Katsotaanpa mitä siellä löytyy:main: .LFB0: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 movl $0, -4(%rbp) # muuttujan i esittely: i=0 movl $0, -8(%rbp) # muuttujan a esittely: a=0 movl $0, -4(%rbp) # sijoitus (loopin alku) i=0 jmp .L2 # hyppy ehtolauseeseen .L2 .L3: movl -4(%rbp), %eax # haetaan i muistipaikasta rekisteriin addl %eax, -8(%rbp) # a = a + i addl $1, -4(%rbp) # i++ .L2: cmpl $9, -4(%rbp) # vertailu, onko i < 10 jle .L3 # jos totta, hyppy suoritusosaan .L3 popq %rbp .cfi_def_cfa 7, 8 ret # paluu main:iin .cfi_endproc
Okei, varsin kryptisen näköistä tavaraa. Jokainen assembly-kielen käskyt esitetään omalla rivillään ja käskyissä on kaksi osaa, itse käskykoodi ja sen operandit. Esimerkiksi lause
movl $0, -4(%rbp)
, jossa käsky on movl
ja sen perässä ovat operandit (joita C-kielessä vastaisi muuttujat) eli assembly-kielessä rekisterit ja/tai muistiosoitteet. Esimerkkikäskyssä operandit ovat lukuarvo 0, merkitään $0
, rekisteri rbp
ja muistiosoitus -4
. No näihin palaamme vielä yksityiskohtaisemmin..(
.
-alkuiset komennot ovat ohjeita assembly-kääntäjälle, miten ohjelma käännetään konekielelle, ikäänkuin siis C-kielen esikääntäjäkäskyt. Ohjelmassa näkyy myös useita nimettyjä koodilohkoja, esimerkiksi main:
tai .L2:
. Mitä ilmeisimmin main-lohko voisi vastata ohjelmamme main-funktiota.)Seuraavaksi käännetään C-koodi suoraan konekielelle käskyllä
gcc -c mahtijuttu.c
. Tuloksena saadaan konekielinen objektitiedosto mahtijuttu.o
, jossa konekielen käskyt ovat suorittimen ymmärtämässä binääriluku-esityksessä. Tämän konekielisen tiedoston voi sitten tulkita takaisin assembly-kielelle ohjelmallisesti objdump -d mahtijuttu.o
, josta alla kuva. 0000000000000000 main: 0: 55 push %rbp 1: 48 89 e5 mov %rsp,%rbp 4: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp) b: c7 45 f8 00 00 00 00 movl $0x0,-0x8(%rbp) 12: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp) 19: eb 0a jmp 251b: 8b 45 fc mov -0x4(%rbp),%eax 1e: 01 45 f8 add %eax,-0x8(%rbp) 21: 83 45 fc 01 addl $0x1,-0x4(%rbp) 25: 83 7d fc 09 cmpl $0x9,-0x4(%rbp) 29: 7e f0 jle 1b 2b: 5d pop %rbp 2c: c3 retq
Nyt meillä on vasemmassa reunassa muistiosoitteet ja niihin tallennettua konekieltä heksalukuina . Huomataan, että yhtä assemblykielistä käskyä voi vastata 1-n tavua. Kuvasta katsoen, vaikka assemblykieli oli jo kohtalaisen kryptistä niin on se huomattavasti luettavampaa, kuin konekieli, eikös..
Esimerkissä rivi
0: 55
tarkoittaa sitä että muistiosoitteessa 0x0 on käsky 0x55. Oikealla näkyy vastaavat assembly-kielen käskyt ja todetaan että heksaluku 0x55
tarkoittaa käskyä push %rbp
. Vastaavasti voimme arvailla koodia lukemalla, että movl-käskyä näyttäisi vastaavan heksaluvut 0xc7
ja 0x45
. Nyt kun konekielen käskyjen rakenne on tarkkaan määritelty, ne voidaan ne tulkita takaisin assembly-kielelle disassembler-ohjemalla, esimerkiksi ylläoleva objdump-ohjelma. Allaolevassa tiedostossa on esimerkin vuoksi sama silmukka käännetty 8-bittiselle RISC-arkkitehtuurin (maltetaan vielä hetki) sulautetulle mikrokontrollerille ATtiny2313. Huomataan, että AVR-arkkitehtuurissa konekielen käskykanta on yksinkertaisempi, jolloin saman c-koodin konekieliseen esitykseen tarvitaan enemmän käskyjä. Lisäksi huomataan erilainen tavujärjestys verrattuna x86-arkkitehtuuriin.
Hox! Konekielen tai assembly-kielen kääntäminen takaisin C-kielelle ei enää onnistukaan, koska C-kielen käskyillä on huomattavasti parempi ilmaisuvoima. Oliko C-kielisessä ohjelmassa käytössä esimerkiksi for- vai while-silmukkarakenne? Oliko konekielen ehtolauseke C-kielessä
i==9
? Vai i < 10
?Lopuksi¶
Tässä lyhyessä esityksessä kerrottiin yleistietoa suoritinarkkitehtuureista, tietokonetekniikan historiasta ja esitettiin, miten C-kielisestä ohjelmasta päästään konekieleen. Itseasiassa prosessorikaan ei aina suorita konekieltä täsmälleen kuten se on kirjoitettu, vaan monimutkaisemmissa suoritintoteutuksissa on välissä mikro-ohjelma, joka vielä tulkitsee ja suorittaa konekielen käskyt.
Esimerkeissä annetuilla
gcc
-kääntäjän käskyillä voitte itse kokeilla vaikkapa millaiselta kurssin Johdanto-osan C-kieliset ohjelmat näyttävät assembly- ja konekielisenä. gcc:llä voi myös kokeilla, miten käännös konekielelle muuttuu, kun käytetään erilaisia sen tarjoamia koodioptimointeja O1-O3
tai Os
.Anna palautetta
Kommentteja materiaalista?