OSA II. TIETOKONEJÄRJESTELMÄT¶
Tässä osassa kurssissa tutustumme suorittimen (mikroprosessori, keskusyksikkö, CPU) arkkitehtuuriin ja toimintaperiaatteisiin osana tietokonetta. Kurssin suorituksesta 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. 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. Modernina prosessoriarkkitehtuurina x86-perhe on varsin pitkälle kehitetty vuosikymmenien työn tulos, jota ei peruskurssin tuntimäärillä läpikäydä.
Suoritinarkkitehtuureista¶
Alla Johdatus-kurssilla esittelemämme tietokoneen perusarkkitehtuurin kuvaus, jota kurssin tässä osassa syvennetään suorittimen (CPU) osalta.
Suorittimen sisäisessä arkkitehtuurissa on erotettavissa neljä osaa:
- Laskentayksikkö ALU (Aritmeettis-looginen yksikkö), joka suorittaa aritmeettiset ja loogiset operaatiot rekistereissä olevalle datalle.
- Kontrolliyksikkö, joka ajaa suoritinta: synkronoi 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.
Historiaa¶
Ihan ensimmäiset mekaaniset ja elektroniset 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 minkä tahansa ohjelmoitavan tietokoneen toiminta ja sen pohjalta on kehitetty erilaisia 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 ohjelma voitiin tallentaa ja sitä suorittaa sähköisestä muistista.
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 järjestelmäväyläratkaisu. Nykyiset suoritinarkkitehtuurit ovat välimuotoja molemmista ratkaisuista.
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. Nykyään Harvard-arkkitehtuuria käytetään yleisesti sulautetuissa järjestelmissä, joissa kuten tiedämme, ohjelma on tallessa haihtumattomassa Flash-ohjelmamuistissa ja data taas RAM-muistissa.
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. Nykyisin von Neumann-arkkitehtuuri on yleisesti käytössä mm. työasemissa.
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 nopeammin 18 kuukauden ajassa. Nykyisin arvellaan, että Mooren laki ei toteudu enää 2020-luvulla. Mutta nähtäväksi jää, lakia on yritetty kaataa aiemminkin..
Transistorien määrän kasvu ei suoraan kerro tietokoneen laskentatehon kasvusta. Koska samassa mittakaavassa voidaan lisätä logiikkaa mikropiirillä, seurauksena on mikropiirin monipuolisempi toiminta ja kasvanut kapasiteetti. Kuvissa alla vasemmalla Intelin suorittimien käskykannan muutosnopeus ja oikealla DRAM-muistipiirien kasvanut kapasiteetti.
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 joutuu / pääsee ohjelmoimaan konekielellä, auttaa Konekielen ja suorittimen toiminnan ymmärtäminen tuottamaan tehokkaampaa koodia, kun ohjelmoija tietää miten suoritin käyttäytyy. Tämä tietämys on tietotekniikan insinöörin ammattitaitoa. Ja, sulautettujen ohjelmoinnissa voi kyllä tulla eteen tilanteita, jossa esimerkiksi aikakriittinen osa koodia täytyy toteuttaa assembly-kielellä.
Aikoinaan konekielinen (tai assembly-kielinen) ohjelmointi oli ainoa vaihtoehto..
C-kielen käännösprosessi¶
Koska keskusyksikkötoteutuksia on erilaisia, konekieli on laitteisto/suoritinriippuvaista siinä missä C-kielelle on yleinen toteutus. C-kieli tarvitsee siis kääntää kunkin suorittimen konekielelle erikseen kääntäjän (engl. compiler) avulla (kääntäjäkin on siis tietokoneohjelma..). Tällä kurssilla emme mene kääntäjien toteutukseen, mutta yleinen käännösprosessi on syytä tuntea.
Ensimmäisessä vaiheessa siis C-kielinen ohjelma käännetään assembly-kieliseksi. Seuraavaksi assembler-ohjelma kääntää assembly-kielisen toteutuksen itse konekielelle objekti-tiedostomuotoon (näille termeille ei ole oikein hyvää suomennosta). Sitten linkkeri yhdistää objektitiedostot ajettavaksi konekieliseksi ohjelmaksi. Usein ohjelmamme käyttää valmiina järjestelmäkirjastoja, joiden objektimuotoiset toteutukset linkkeri liittää ajettavaan ohjelmaan. Lopputuloksena saadaan konekielinen ohjelma, jonka käyttöjärjestelmän komponentti loader lataa RAM-muistiin, kun ohjelma käynnistetään.
Assembly- ja konekieli¶
Koska suorittimen komponentit ovat digitaalipiirejä (joita käsiteltiin aiemmilla kursseilla), ne osaavat käsitellä vain bittejä kombinaatiologiikassaan Boolen algebran avulla. Tietokoneessa, samoin kuin digitaalipiirissä, jokaisella bitillä on tietty tarkoitus ja näin konekielen käskyt esitetään binäärilukuina. Käskyä kuvaavassa binääriluvussa osa biteistä kertoo käskyn operaatiokoodin ja osa sen operandit. No, tästä tarkemmin seuraavassa materiaalissa.
Nyt, konekielen ongelma ohjelmoijan näkökulmasta on binäärilukumuotoinen numeraalinen esitys, jota on vaikea hallita ja joka pakottaa ohjelmoijan ajattelemaan kuin tietokone. Tästä voikin jo aavistella, että konekielen käskyt ovat todella yksinkertaisia, verrattuna korkean tason kielten käskyihin, ja tekevät varsin pieniä juttuja kerrallaan. Seuraus tästä on, että jonkin meille yksinkertaisen ohjelman toiminnallisuuden toteuttaminen vaatii riveittäin konekieltä. 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.
Konekielisen ohjelmoinnin helpottamiseksi konekielelle tarjotaan symbolinen esitystapa (ts. ihmisen luettava) eli assembly-kieli. Assembly-kielen käskyt on laadittu siten, että ne kuvautuvat suoraan (vaikka päässälaskien) varsinaiselle konekielelle. Assembly-kieleen menemme tarkemmin seuraavassa luentomateriaalissa, mutta esitetään alla esimerkkejä.
Esimerkki. Varsin yksikertainen C-ohjelma (nimeltään mielikuvituksellisesti)
silmukka.c
:int main() {
int i=0,a=0;
for (i=0; i<10; i++) {
a += i;
}
}
..käännetään se x86:n assembly-kielelle käskyllä
gcc -S -Og silmukka.c
, jolloin kääntäjä antaa ulos tiedoston silmukka.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) # sijoitukset muistipaikkoihin: i=0 movl $0, -8(%rbp) # a=0 movl $0, -4(%rbp) # 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) # i < 10 jle .L3 # jos totta, hyppy suoritusosaan .L3 popq %rbp .cfi_def_cfa 7, 8 ret .cfi_endproc
Varsin kryptisen näköistä tavaraa. Assembly-kielen käskyt esitetään omilla riveillään, esimerkiksi
movl $0, -4(%rbp)
. Itse käsky on movl
ja sen perässä ovat operandit (joita C-kielessä vastaisi muuttujat) eli assembly-kielessä rekisterit tai muistiosoitteet. Esimerkkikäskyssä operandit ovat lukuarvo 0, merkitään $0
ja rekisteri rbp
ja muistiosoitus -4
.(
.
-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:
. No, palataan asiaan, mutta mitä ilmeisimmin main-lohko voisi vastata ohjelmamme main-funktiota.)Seuraavaksi käännetään assembly-koodi konekielelle käskyllä
gcc -c silmukka.c
. Tuloksena saadaan objektitiedosto silmukka.o
, jossa on seuraavanlaista tavaraa. (Objektitiedostoja voi tulkita ohjelmalla objdump -d silmukka.o
.)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ä (tässä heksalukuina). Oikealla puolen näkyy vastaavat assembly-kielen käskyt. Voimme jo tulkita esimerkkiä ja huomata, että movl-käskyä näyttäisi vastaavan luku
0xc7
. Nyt, koska assembly-kielen käskyt vastaavat yksi yhteen konekielen käskyjä, voidaan ne tulkita takaisin assembly-kielelle disassembler-ohjemalla. 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?
Lopuksi¶
Tässä lyhyessä esityksessä kerrottiin yleistietoa suoritinarkkitehtuureista, tietokonetekniikan historiasta ja näytettiin, miten C-kielisestä ohjelmasta päästään konekieleen.
Esimerkeissä annetuilla
gcc
-kääntäjän käskyllä voitte itse kokeilla 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 optimointeja O1-O3
tai Os
. Anna palautetta
Kommentteja materiaalista?