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 kurssiosuuden suorittamisesta on kerrottu tarkemmin materiaalin etusivulla.
Mutta, ennenkuin sukelletaan syvemmälle suorittimiin ja tietokonejärjestelmiin, palataan hetkeksi ajassa taaksepäin,..
Ohjelmoitavan tietokoneen historiaa¶
Historiallisesti jo Aristoteleen ajoista lähtien filosofit ja matemaatikot (kts. Leibniz) ovat koettaneet formalisoida (ihmisen) loogísta päättelyä, ts. luoda yleistä symbolista logiikkaa, jolla pystyttäisiin esittämään ja ratkaisemaan mikä tahansa algoritmi..
"Ainoa tapa korjata päättelymme on tehdä niistä yhtä kouriintuntuvia kuin matemaatikkojen päättelyistä, niin että voimme löytää virheemme yhdellä vilkaisulla, ja kun henkilöiden välillä on erimielisyyksiä, voimme yksinkertaisesti sanoa: laskekaamme, pitemmittä puheitta, katsoaksemme kuka on oikeassa." - Tutkielma varmuuden metodista ja keksimisen taidosta, Leibniz 1685.
Tietokoneiden kannalta oleellinen formalisointi logiikan sääntöjen esittämiseksi algebrallisesti on Boolen algebra (joka sittemmin johti itseasiassa digitaalitekniikan syntyyn Claude Shannonin ajatusten pohjalta).
Näiden ajatusten pohjalta kehitetyt ensimmäiset mekaaniset (Leinbiz, Charles Babbage) ja elektroniset (John Atanasoff) tietokoneet eivät olleet (uudelleen)ohjelmoitavia eikä näinollen yleiskäyttöisiä.
Näiden ajatusten pohjalta kehitetyt ensimmäiset mekaaniset (Leinbiz, 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 haettuja käskyjä luetulle datalle ja tallentaen niiden tuloksia takaisin 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 ja sen data 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 (Elektroninen SarjaKomputaattori) jota rakennettiin vuosina 1954-60. Oulun yliopiston ensimmäinen tietokone oli Elliott 803 1960-luvulta.
Professori Matti Otala ja Elliott 803 Oulun yliopistossa vuonna 1970.
Ohjelmointi silloin joskus¶
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 suunnitteli Babbagen mekaanisen tietokoneen seuraajalle ohjelman mitä kone ajaisi, eli nykykielellä firmiksen..
Elliott 803:sta ohjelmointiin mm. ALGOL-ohjelmointikielellä.
Arkkitehtuurijako¶
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 soveltuu hyvin yleiskäyttöiselle tietokoneelle, mm. modernit PC-työasemat.
Tosin nykyään, modernit suoritin- ja tietokonejärjestelmä-arkkitehtuurit 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 suoraan tietokoneen laskentatehon kasvusta (kuten tulemme luentomateriaalissa näkemään). Kuitenkin, samassa fyysisessä mittakaavassa piirille saadaan prosessoreihin lisättyä sisäistä toimintalogiikkaa ja muistia, jonka seurauksena saadaan toiminta tehokkaammaksi, voidaan toteuttaa laajempia konekielen käskykantoja, jne.
Kuvissa alla vasemmalla Intelin suorittimien käskykannan muutosnopeus ja oikealla DRAM-muistipiirien kasvanut kapasiteetti. Intelin käskykannassa kantava periaate 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 Pythoniin. Tietokoneen keskusyksikölle C-kieli kuitenkin on aivan liiian korkealentoista/ilmaisuvoimaista ymmärrettävää.
Suorittimelle ohjelma pitää esittää sen digitaalilogiikan ymmärtämässä muodossa, eli konekielellä. Kuten tulemme näkemään, konekielinen ohjelma koostuu joukosta hyvin yksinkertaisia käskyjä, jotka tekevät operaatioita (pääasiassa) suorittiminen sisäisissä rekistereissä olevalle datalle.
Vaikka ohjelmoija nykyisin harvemmin pääsee (/joutuu) ohjelmoimaan konekielellä, auttaa konekielen ja suorittimen toiminnan ymmärtäminen tuottamaan tehokkainta mahdollista koodia, kun saadaan suorittimesta tehot irti. 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".. vaikkapa nyt avaruusraketteihin.
(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 digitaalisiin mikropiireihin (joita käsiteltiin aiemmilla kursseilla), jotka käsittelevät vain bittejä kombinaatio- sekvenssilogiikassaan, Boolen algebran avulla. Näin ollen suorittimen ymmärtämät käskytkin pitää esitellä bitteinä, ts. binäärilukuina, joiden jokaisella bitillä käskyssä ohjelmoijan ja suorittimen yhdessä tuntema merkitys. Konekieli on siis syvimmältä olemukseltaan pelkkiä bittejä.
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 niille tarjotaan symbolinen esitystapa (ts. ihmisen luettava) eli assembly-kieli. Kielen käskyt on laadittu siten, että ne on tekstimuotoisina ihmisen luettavaa koodia, mutta 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 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?