Luku 5

Rakenteisen tiedon toteutus ja siihen viittaaminen

Rakenteinen tieto tarkoittaa yhtä tietoalkiota suurempia tietokokonaisuuksia, jotka loogisesti liittyvät toisiinsa. Tyypillisiä tällaisia tietoja ovat erilaiset taulukot, tietueet ja näiden yhdistelmät.

Kuten Tietokoneen toiminnan perusteet -kurssilla jo kävimme läpi, rakenteiseen tietoon viittaaminen tapahtuu yleisesti ottaen tietorakenteen alkuosoitteen ja indeksirekisterin avulla. Joissakin tapauksissa viittaaminen on helppoa ja voidaan suorittaa yhden konekäskyn avulla hyödyntäen jotain sitä varten toteutettua tiedonosoitusmoodia, kun taas monimutkaisimmissa tapauksissa viitatun tiedon osoitteen laskenta vaatii usean konekäskyn suorituksen.

1-ulotteiset taulukot ja tietueet

Yleisimmät tapaukset ovat peräkkäistalletetut 1-ulotteinen taulukko ja tietue. Peräkkäistalletus tarkoittaa sitä, että koko taulukko tai tietue on talletettu muistissa peräkkäisiin muistipaikkoihin. 1-ulotteisen taulukon alkion osoite on yksinkertaisesti taulukon alkuosoite lisättynä indeksin osoittaman määrällä. Tässä perustapauksessa oletamme, että taulukon alkion koko on yksi sana. Useimmissa konekielissä on tätä perustapausta varten indeksoitu tiedonosoitusmuoto, jolloin taulukon alkioon viittaaminen voidaan tehdä yhdellä konekäskyllä. Siinä taulukon alkuosoite on käskyn vakio-osassa ja viitatun alkion indeksi indeksirekisterissä.

      load r1, =0       ;  muuntelumuuttujan i alustus, arvo r1:ssä
loop  comp r1, =30      ; silmukan lopetustesti
      jequ done
      load r2, r1       ; silmukan runko
      mul  r2, =4
      store r2, A(r1)   ; taulukon A viitatun alkion indeksi on r1:ssä
      add r1, =1        ; lisätään muuntelumuuttujan arvoa yhdellä
      jump loop
done  nop               ; poistu silmukasta

Toinen tyypillinen ohjelmissa käytetty tietorakenne on tietue, jossa samaan tietoon liittyvät eri kentät on talletettu peräkkäisiin muistipaikkoihin. Tällä kertaa viitatun tiedon (tietueen jokin kenttä) osoite saadaan laskemalla yhteen tietueen alkuosoite ja viitatun kentän suhteellinen sijainti tietueen sisällä. Vähän yllättävästi, tällaiseenkin tietoon viittaaminen voidaan toteuttaa indeksoidulla tiedonosoitusmoodilla yhdessä konekäskyssä. Tällä kertaa kuitenkin tietueen alkuosoite annetaan indeksirekisterissä ja viitatun kentän suhteellinen sijainti on käskyn vakiokentässä.

Id      equ 0        ; tietueen Person kenttien suhteelliset sijainnit
Age     equ 1
Nr      equ 2

Per0    ds 3         ; kolme kappaletta Person-tyyppistä tietuetta
Per1    ds 3
Per2    ds 3

...               ; tietueiden alustus ja muuta koodia

   load r2, =Per1      ; r2 osoittaa tietueeseen Per1
   load  r1, Nr(r2)    ; hae rekisteriin r1 rekisterin r2
                       ; osoittaman tietueen kentän Nr arvo

2-ulotteiset taulukot

Moniulotteiset taulukot ovat jo vaikeampi tapaus. Useimmissa konekielissä ei ole niitä tukevia tiedonosoitusmoodeja, joten tietoon viittaaminen tapahtuu kahdessa vaiheessa. Ensin ohjelmassa lasketaan viitatun tiedon suhteellinen sijainti rakenteisen tiedon sisällä ja sitten indeksoitua tiedonosoitusmoodia käyttäen tehdään varsinainen tiedonosoitus.

Talletus riveittäin

2-ulotteiset taulukot voidaan tallettaa muistiin ainakin kolmella tavalla. Ne voidaan tallettaa yhtenäiselle alueelle riveittäin, jolloin esimerkiksi osoitteeseen 300 talletettu taulukko T[2,3]

25  88  2
11  66  3

talletetaan rivi kerrallaan muistiin peräkkäisiin muistipaikkoihin, muistipaikasta 300 alkaen.

300: 25  88  2 11  66  3

Nyt taulukon alkion T[i,j], missä i ilmaisee rivin ja j sarakkeen, osoite on T + 3*i + j ja alkioon T[i,j] viittaaminen tapahtuu seuraavasti (huomioi, että rivin ja sarakkeen indeksointi alkaa nollasta):

     load r1, i
     mul r1, =3
     add r1, j
     load r2, T(r1)  ; lataa r2:een alkion T[i,j] arvo

Talletus sarakkeittain

Jos taulukko T on talletettu sarakkeittain yhtenäiselle muistialueelle, niin se on talletettu järjestyksessä

300: 25  11 88  66  2  3

Alkion T[i,j] osoite on T + i + 2*j ja alkioon T[i,j] viittaaminen tapahtuu esimerkiksi näin:

     load r1, j
     mul r1, =2
     add r1, i
     load r2, T(r1)  ; lataa r2:een alkion T[i,j] arvo

Talletus linkitettynä rakenteena

Kolmas vaihtoehto on tallettaa (esim.) kukin rivi kerrallaan omalle yhtenäiselle muistialueelleen ja rivien alkuosoitteet 1-ulotteiseen taulukkoon. Taulukko T voisi nyt olla talletettu muistiin esim. seuraavasti:

400: 25  88  2            (rivi 0)
430: 11  66  3            (rivi 1)
300: 400 430              (Taulukon T rivien osoitteet)

Huomaa, että taulukko T voi olla nyt talletettuna epäyhtenäisille alueille keskusmuistiin. Alkioon T[i,j] viittaaminen tapahtuu nyt kaksivaiheisesti. Ensin haetaan rivin i osoite taulukosta T ja sitten tehdään varsinainen viite kyseisen rivin alkioon j.

     load r1, i
     load r1, T(r1)    ; rivin T[i] osoite
     add r1, j         ; alkion T[i,j] osoite
     load r2, 0(r1)    ; lataa r2:een alkion T[i,j] arvo

Kuten edellä olevista esimerkeistä ilmenee, kaikki kolme tapaa ovat yhtä helposti käytettävissä. Yhtenäisessä talletusmuodossa etuna on selkeys. Linkitetyn rakenteen hyvänä puolena on se, että varsinkin suurten taulukoiden yhteydessä niitä ei tarvitse tallettaa yhtenäiselle muistilohkolle. Lisäksi linkitetyssä rakenteessa voi optimoida tilankäyttöä esim. siten, että tällä hetkellä vielä alustamattomille riveille ei ole vielä varattu muistitilaa. Alustamaton rivi merkitään esim. riviosoitteena 0 ja riville varataan muistitila vasta siihen kohdistuvan ensimmäisen viittauksen yhteydessä.

Moniulotteiset taulukot

Moniulotteiset taulukot talletetaan aivan vastaavasti. Käytetään esimerkkinä osoitteeseen 600 talletettua 3-ulotteista taulukkoa S[2,3,4]. Siinä on siis 2 tasoa (tasot 0 ja 1), kullakin tasolla 3 riviä ja kullakin rivillä 4 alkiota. Tasolla 0 on alkiot

000 001 002 003
010 011 012 013
020 021 022 023

ja tasolla 1 alkiot

100 101 102 103
110 111 112 113
120 121 122 123

Alkioiden arvot on tässä esimerkissä valittu tahallaan siten, että arvot vastaavat alkioiden indeksejä taulukossa S. Esimerkiksi alkion S[1,2,1] arvo on 121. Huomaa, että taulukkoon viitattaessa kunkin indeksin täytyy olla omalla nollasta alkavalla arvoalueellaan, mikä on kullekin indeksille annettu taulukon määrittelyvaiheessa.

Jos S on talletettu "riveittäin", niin alkiot ovat muistissa riveittäin taso kerrallaan järjestyksessä

600: 000 001 002 003  010 011 012 013  020 021 022 023  (taso i=0)
612: 100 101 102 103  110 111 112 113  120 121 122 123  (taso i=1)

ja niihin voisi viitata esimerkiksi seuraavalla tavalla.

     ; laske tason i (suhteellinen) osoite (taulukossa S)
     load r1, i
     mul r1, =12   ; joka tasossa 12 sanaa

     ; laske rivin j osoite tasolla i
     load r2, j
     mul r2, =4    ; joka rivillä 4 sanaa
     add r1, r2

     ; laske alkion k osoite rivillä j tasolla i
     add r1, k

     ; tee varsinainen muistiviite
     load r2, S(r1)  ; lataa r2:een alkion S[i,j,k] arvo

Jos S on talletettu sarakkeittain, niin tasoja on viimeisen indeksin mukaisesti neljä ja alkiot ovat muistissa järjestyksessä

600: 000 100 010 110 020 120     (taso k=0)
606: 001 101 011 111 021 121     (taso k=1)
612: 002 102 012 112 022 122     (taso k=2)
618: 003 103 013 113 023 123     (taso k=3)

Viite (r2 = S[i,j,k]) toteutuu nyt käskyillä

     load r1, k
     mul r1, =6   ; joka tasossa 6 sanaa
     load r2, j
     mul r2, =2    ; joka sarakkeella 2 sanaa
     add r1, r2
     add r1, i
     load r2, S(r1)  ; lataa r2:een alkion S[i,j,k] arvo

Jos S on talletettu (riveittäin) linkitettynä rakenteena, niin tallennus voisi olla

700:  000 001 002 003  (rivi S[0,0,*])
301:  010 011 012 013  (rivi S[0,1,*])
802:  020 021 022 023  (rivi S[0,2,*])
510:  100 101 102 103  (rivi S[1,0,*])
611:  110 111 112 113  (rivi S[1,1,*])
712:  120 121 122 123  (rivi S[1,2,*])

630:  700  301  802    (tason S[0,*,*] rivien osoitteet)
541:  510  611  712    (tason S[1,*,*] rivien osoitteet)

600:  630   541       (taulukon S tasojen osoitteet)

Viite (r2 = S[i,j,k]) toteutuu nyt käskyillä

     load r1, i
     load r2, S(r1)    ; tason i osoite
     add r2, j         ; rivin j osoitteen osoite
     load r1, 0(r2)    ; rivin j osoite tasolla i
     add r1, k         ; alkio k osoite rivillä j tasolla i
     load r2, S(r1)    ; lataa r2:een alkion S[i,j,k] arvo

Nämä kaikki moniulotteisten taulukoiden talletusmuodot ovat käytössä. Useimmiten korkean tason kielellä ohjelmoitaessa niillä ei ole kovin paljon väliä, koska talletusmuoto ei näy korkean tason ohjelmointikielellä ohjelmoitaessa. Joissakin tapauksissa ohjelmien suoritusnopeutta voidaan kuitenkin huomattavasti nopeuttaa, jos talletusjärjestys otetaan huomioon koodissa. Jos esimerkiksi käydään läpi 2-ulotteisen taulukon kaikki alkiot, niin välimuisti tekee niiden läpikäynnin muistiintallennusjärjestyksessä huomattavasti nopeammaksi kuin jossakin muussa järjestyksessä. Valitettavasti 2-ulotteisia taulukoita tarvitsee usein käydä läpi niin sarakkeittain kuin riveittäin, joten kumpi tahansa talletusmuoto on aina joskus huono.

Monimutkaiset rakenteiset tietorakenteet

Monimutkaisemmat tietorakenteet talletetaan muistiin vastaavilla tavoilla. Usein talletusmuoto määräytyy suoraan käytettävän ohjelmointikielen perusteella, mutta joissakin tilanteissa ohjelmoija voi päästä vaikuttamaan talletusmuotoon itsekin. Kulloinkin käytössä oleva talletusmuoto pitää sitten tietenkin huomioida hyvin huolella jokaisen dataviitteen kohdalla.

Esimerkiksi, jos R[20,30] on riveittäin talletettu 2-ulotteinen taulukko, jonka kukin alkio on 14-sanainen tietue, jonka kentät ovat id, lkm ja 12-alkioinen taulukko pisteet, niin viite alkioon R[i,j].pisteet[kk] voisi olla toteutettuna

     ; rivin i osoite R:ssä
     load r1, i
     mul  r1, =420   ; rivillä 30 tietuetta, kussakin 14 sanaa

     ; alkion j osoite rivillä i
     load r2, j
     mul r2, 14      ; kussakin alkiossa 14 sanaa
     add r1, r2      ; alkion R[i,j] suhteellinen osoite R:n sisällä

     ; kentän pisteet osoite alkiossa R[i,j]
     add r1, =2      ; taulukon R[i,j] kentän pisteet suhteellinen osoite

     ; alkion kk osoite taulukossa pisteet
     add r1, kk      ; alkion R[i,j].pisteet[kk] suhteellinen osoite

     ; tee muistiviite
     load r2, R(r1)  ; lataa r2:een alkion R[i,j].pisteet[kk] arvo

Toteutus on monimutkaisellakin rakenteella siis hyvin suoraviivainen. Kääntäjät generoivat tällaista koodia hyvin helposti ja luotettavasti.

Indeksitarkistukset

Indeksitarkistusten avulla pyritään suojaamaan järjestelmää tietynlaisilta ohjelmointivirheiltä ja tietosuojahyökkäyksiltä. Ajatellaanpa esimerkiksi tilannetta, jossa osoitteeseen 200 talletetulle taulukolle T[20] on varattu tilaa 20 sanaa, ja siihen kohdistuu viittaus "X = T[N]", kun N:n arvo on 73. Nyt X:n arvoksi tulee muistipaikan 93 arvo, vaikka kyseinen muistipaikka ei edes kuulu taulukolle T. Vastaavasti viitteellä "T[-187] = Z" voidaan asettaa muistipaikan 13 arvoksi muuttujan Z arvo. Jos muuttujan Z arvo oli esimerkiksi 287309911 eli binäärilukuna 0b0001_0001_001_00_000_0000_0000_0101_0111, niin muistipaikassa 13 ollut konekäsky olisi näin vaihdettu konekäskyyn "add r1, =87".

Usein tällaiset taulukon ulkopuolelle tapahtuvat puskurin ylivuotovirheet ovat tavallisia ohjelmointivirheitä, jossa esimerkiksi silmukan päättymisehdon toteutus sallii silmukan suorittamisen yhden kerran liikaa tai yhden kerran liian vähän. Joissakin tapauksissa virhe on kuitenkin sellainen, että indeksin arvoa ei tarkisteta ennen taulukkoviitteen käyttöä ja pahantahtoinen hyökkääjä voi silloin ehkä käyttää tilannetta hyödykseen puskurin ylivuotohyökkäyksen tekemiseen. Tällöin taulukon T kautta hyökkääjä voi muuttaa järjestelmän kriittisiä tietokenttiä tai sijoittaa haittaohjelman järjestelmän suoritettavaksi.

Yksinkertainen tapa torjua tällaiset ongelmat on joka kerta taulukkoviitteen yhteydessä tarkistaa indeksin (indeksien) laillisuus. Esimerkiksi aikaisempi taulukkoon T[2,3] kohdistuva viite "r2 = T[i,j]"

     load r1, i
     mul r1, =3
     add r1, j
     load r2, T(r1)  ; lataa r2:een alkion T[i,j] arvo

olisi nyt muotoa

     load r1, i           ; tarkista I
     jneg r1, trouble
     comp r1, =2
     jnles trouble

     load r2, j           ; tarkista j
     jneg r2, trouble
     comp r2, =3
     jnles trouble

     mul r1, =3
     add r1, j
     load r2, T(r1)  ; lataa r2:een alkion T[i,j] arvo

     jump jatka
trouble svc sp, =BADINDEX  ; käsittele virhetilanne
jatka nop

Kuten tästä esimerkistä huomataan, tarkistusten hinta voi olla korkea suoritusnopeuden hidastuessa ylimääräisten suoritettavien konekäskyjen vuoksi. Toisaalta, haavoittuvaan järjestelmään kohdistuneen puskurin ylivuotohyökkäyksen kustannukset voivat olla valtaisat. On myös muita tapoja tehdä ja välttää indeksitarkistuksia sekä torjua puskurin ylivuotohyökkäyksiä, mutta ne eivät sisälly tämän kurssin oppimistavoitteisiin.

Pääsit aliluvun loppuun! Jatka tästä seuraavaan osaan:

Muistathan tarkistaa pistetilanteesi materiaalin oikeassa alareunassa olevasta pallosta!