Aliohjelma

ohjelman osa

Aliohjelma (kutsutaan eri yhteyksissä myös termeillä proseduuri, funktio, metodi tai rutiini) on ohjelmoinnissa itsenäinen ohjelman osa, joka suorittaa tietyn toiminnon ja jota voidaan kutsua eri puolilta pääohjelmaa tai muista aliohjelmista. Aliohjelman suorituksen jälkeen ohjelman suoritus jatkuu kutsuvassa ohjelmassa aliohjelmakutsua seuraavasta lauseesta.

Proseduraalisessa ohjelmointikielessä aliohjelmia kutsutaan funktioiksi tai proseduureiksi. Olio-ohjelmoinnissa olion funktioita nimitetään jäsenfunktioiksi tai metodeiksi.

Ohjelmoinnissa esiintyy usein tilanne, että samanlaista toimintoa tarvitaan ohjelman useassa eri kohdassa. Kirjoittamalla toiminto aliohjelmaksi voidaan se suorittaa useasta eri kohdasta. Näin säästetään työtä. Jos sama toiminto kirjoitettaisiin ohjelmaan yhä uudelleen, ohjelmakoodi pitenisi ja tulisi epäselväksi ja virheiden mahdollisuus kasvaisi. Mahdolliset korjaukset ohjelmakoodiin pitäisi tehdä useaan paikkaan. Aliohjelmat myös lisäävät ohjelman modulaarisuutta, koska ne mahdollistavat ohjelmakoodin jakamisen pienempiin itsenäisiin osiin ja parantavat siten ohjelmiston luettavuutta, testattavuutta ja ylläpidettävyyttä.

Aliohjelmaa voidaan kutsua useassa kohdassa ohjelmaa. Aliohjelman määrittelyn yhteydessä esitellään joukko aliohjelman parametreja tai argumentteja. Aliohjelmalla voi olla parametrien lisäksi paikallisia muuttujia, jotka näkyvät ja ovat käytettävissä vain aliohjelman alueella (engl. scope). Aliohjelman parametrit voivat olla muuttujia tai viittauksia muuttujiin.

Aliohjelman kutsuun on eräissä suorittimissa tuki käskytasolla: konekielinen käsky tallentaa senhetkisen ohjelmalaskurin arvon pinoon ja hyppää toiseen kohtaan ohjelmaa. Kun aliohjelma päättyy, paluuosoite haetaan pinon päältä. Kaikki ohjelmointikielet eivät tue pinoa: FORTRAN 77 ei tukenut pinoa vaan funktioilla oli oma muistialueensa argumenteille ja datalle.[1]

Järjestelmäkutsu ei ole suora funktiokutsu kuten sovelluksissa, vaan kutsun on ylitettävä ytimen ja käyttäjäavaruuden välinen jako.[2]

Tilaa jonne osoite tallennetaan kutsutaan aliohjelman aktivaatiotietueeksi. Siihen tallennetaan lisäksi aliohjelman käyttämät paikalliset muuttujat. Jos aliohjelma kutsuu itseään useaan kertaan, eli toimii rekursiivisesti, aktivaatiotietueita on useampi pinon päällä.

Historia muokkaa

Varhainen kuvaus alirutiinikirjastolle on esitetty vuonna 1948 John von Neumannin ja Herman H. Goldstinen teoksessa Planning and Coding of Problems for an Electronic Computing Instrument.[3] Maurice V. Wilkes David Wheelerin ja Stanley Gillin kanssa esitteli käytännössä toimivan ratkaisun vuonna 1951 teoksessa Preparation of Programs for Electronic Digital Computers.[4] Vuonna 1977 keskusteltiin proseduurikutsujen haitoista verrattuna goto-käskyjen käyttöön: PDP-11 oli yksi ensimmäisiä tietokoneita, joissa oli käskytuki pinoon työntämiseen alirutiinikutsussa.[5]

Toimintaperiaate muokkaa

Suoritin käyttää hyppykäskyä (jmp), kutsukäskyä (call, jsr) tai tarkoitukseen varattua haarautumiskäskyä (esim. bl, bsr) siirtyäkseen suoritettavaan ohjelmakohtaan.[6][7][8][9] Tätä varten suorittimelle on kerrottava suoritettavan aliohjelman osoite.[10][9] Kun suoritin siirtyy aliohjelmaan, paluuosoite sijoitetaan pinoon jotta suoritin palaa oikeaan kohtaan aliohjelman lopussa. Pinoon voidaan lisätä useampia paluuosoitteita sisäkkäisiä kutsuja varten.[7] Paluuosoite riippuu paikasta, jossa aliohjelmaa kutsutaan, esimerkiksi jos sitä kutsutaan kahdesti peräkkäin eri parametreilla.[11] Seuraavan suoritettavan käskyn osoite voidaan sijoittaa pinoon ennen aliohjelman kutsua, jolloin se palautetaan aliohjelmasta paluun jälkeen ja suoritus jatkuu seuraavasta käskystä.[10] Suorittimen arkkitehtuurista riippuu käskyjen toteutustapa ja käskyt voivat yhdistää suorituspaikan työntämisen pinoon sekä ehdottoman hyppykäskyn suorittamisen.[8][9] Kohde annetaan kääntäjälle aliohjelman nimenä, josta kääntäjä päättelee varsinaisen osoitteen: osoite voidaan laskea myös suorituksen aikana, jolloin voidaan käyttää uudelleensijoitettavia aliohjelmia.[9]

Aliohjelmakutsun yhteydessä suorittimen rekisterien tilat on talletettava pinoon jotta aliohjelma ei ylikirjoita säilytettävää tietoa sen suorituksen aikana. Suorittimessa on rajattu määrä rekistereitä, joita kaikki aliohjelmat voivat käyttää.[11] Vastuu rekisterin tilan säilömisessä voi olla aliohjelman kutsujalla tai kutsuttavalla aliohjelmalla, joissa on molemmissa tapauksissa etunsa ja haittansa.[11][12] Kutsukäytäntö määrittää tavan aliohjelmien kutsumiseen.[12] Suoritin voi sisältää erillisen käskyn rekisterien säilömistä varten ("push" ja "pop") tai ne voivat jäädä ohjelmoijan vastuulle: esimerkiksi MIPS-arkkitehtuurissa ei ole käskyjä tätä varten.[11] SPARC käyttää suurta määrää rekistereitä liukuvan ikkunan periaatteella, jolloin aliohjelma ei näe kaikkia rekistereitä.[13]

Suorittimella ei ole tietoa aliohjelmalle välitettävien muuttujien tyypeistä vaan se on ohjelmallisesti käsiteltävää tietoa joko käännösvaiheessa tai ajonaikaisella tyyppitarkistuksella.[11]

Esimerkki muokkaa

Esimerkki aliohjelmasta, joka saa parametriksi numeroarvon (int eli integer) nimeltä ika ja joka palauttaa boolean-arvon (true/false) sen mukaan, onko ika alle 18 vai ei.

 public Boolean onkoHenkiloAlaikainen(int ika) {
   if(ika<18) {
       return true;
    } else {
       return false;
    }
 }

Tämän jälkeen aliohjelmaa voitaisiin kutsua esimerkiksi seuraavasti:

 if(onkoHenkiloAlaikainen(12) == true) {
    tulosta "Henkilö on alaikäinen!";
 }

Olio-ohjelmoinnissa eräissä kieli aliohjelman (metodin) eteen kirjoitetaan halutun olion nimi seuraavasti:

 if(henkilo.onkoHenkiloAlaikainen(12) == true) {
    tulosta "Henkilö on alaikäinen!";
 }

Esittely ja määrittely muokkaa

Eräät ohjelmointikielet kuten C-kieli erottavat esittelyn (engl. declaration) ja määrittelyn tai toteutuksen (engl. definition). Esittely kertoo miten funktiota kutsutaan ilman tarvetta tietää toteutusta toisessa osassa ohjelmaa.[14][15][16][17]

void swap(int *a, int *b); /* esittely */

void swap(int *a, int *b) /* määrittely */
{
   int t = *a;
   *a = *b;
   *b = t;
}

Muuttujat muokkaa

Aliohjelma voi käyttää paikallisia muuttujia, jotka ovat rekisterissä tai pinossa vain aliohjelman suorituksen aikana.

int laske(int a, int b)
{
    int d; /* <- d voidaan sijoittaa rekisteriin tai pinoon */
    d = a + b; 

    return d;
}

Aliohjelmat voivat myös nähdä globaaleja muuttujia ja muuttaa niitä, tai olio-ohjelmoinnissa metodit voivat käsitellä luokan jäsenmuuttujia. Näiden muuttujien saatavuus ja elinaika on eri kuin aliohjelman paikallisen muuttujan.

int d; /* globaali, säilytettävä ohjelman suorituksen ajan, 
        * näkyvissä kaikkialle ohjelmassa */
void laske(int a, int b)
{
    d = a + b;
}

Jäsenmuuttujan elinkaari on sama kuin olion ja sen näkyvyys voi olla rajattu, jolloin sitä voi käyttää vain jäsenmetodien avulla.

class Laske
{
private:
    int d; /* jäsenmuuttuja */
public:
    void laske(int a, int b)
    {
        d = a + b;
    }
    int arvo() { return d; }
    Laske() { d = 0; } /* konstruktori, joka vain alustaa jäsenmuuttujan arvon */
};

Paluuarvo muokkaa

Yleinen tapa palauttaa aliohjelmasta tietoa on sen paluuarvon avulla. Esimerkiksi aliohjelma voi palauttaa int -tyyppisen kokonaisluvun ja sen arvona luvun 42 seuraavasti:

int anna_luku()
{
    return 42;
}

Oletusarvot muokkaa

Eräissä kielissä voidaan antaa oletusarvot argumenteille, jolloin kutsuessa ei välttämättä tarvitse antaa arvoa jokaiselle argumentille. Esimerkki, joka antaa kahden muuttujan tulon:

int kertolasku(int a, int b = 1) { return a * b; }

void main()
{
  int tulo;
  tulo = kertolasku(2); /* 2 * 1 = 2*/
  tulo = kertolasku(2, 1); /* 2 * 1 = 2 */
  tulo = kertolasku(2, 2); /* 2 * 2 = 4 */
}

Parametrien välitys muokkaa

Parametreja voidaan välittää kielestä riippuen kolmella tavalla aliohjelmalle:

Osoittimen ja viitteen tapauksessa aliohjelma voi palauttaa tietoa annettuihin muuttujiin tai olioihin. Esimerkiksi funktio, joka laskee kaksi arvoa yhteen, palauttaa summan sekä merkitsee muuttujan todeksi mikäli jompikumpi parametri ylittää maksimin.

void summa(int a, int *b, bool &maksimi)
{
    maksimi = false;
    if (a > INT_MAX || *b > INT_MAX)
    {
        maksimi = true;
    }
    *b = a + *b;
}

/* kutsuminen */
int main()
{
    bool maks = false;
    int a = 1;
    int b = 2;

    /* ensimmäiselle parametrille välitetään arvo, toiselle muuttujan osoite ja kolmannelle viite */ 
    summa(a, &b, maks);
}

Parametrien määrä muokkaa

C-kieli ja sen johdannaiset tukevat muuttuvaa määrää parametreja ellipsis (...) määrittelyllä. Tällöin aliohjelmassa itsessään on oltava tuki muuttujaluettelon käsittelyyn. Tyypillinen tapaus on printf() -funktio, jonka määrittely on muotoa:

int printf(const char *format, ...)

Käyttö:

printf("Hei maailma\n");
printf("Numero: %d\n", 42);

Ylikuormitus ja ylikirjoittaminen muokkaa

Eräät ohjelmointikielet tukevat ylikuormittamista (vaihtoehtoisia kutsutapoja ja toteutuksia) sekä ylikirjoittamista (ks. virtuaalimetodi). Ylikuormittamisessa samalle aliohjelmalle voi olla rinnakkaisia toteutuksia eri tietotyypeillä kuten neliöjuuren laskentaan:

float sqrt(float val);
double sqrt(double val);

Hyökkäykset muokkaa

Tietoturvan kiertävät haittaohjelmat pyrkivät muuttamaan kutsuttavaa aliohjelmaa suorittaakseen oman rutiininsa tai ne voivat muuttaa paluuosoitetta, jolloin aliohjelman paluussa suoritus siirtyy hyökkääjän omaan rutiiniin (engl. return oriented programming).[18]ASLR muuttaa osoitteita, jotta haavoittuvuuksien hyödyntäminen on vaikeampaa.

Funktiokutsujen optimointi muokkaa

Funktionkutsumisen nopeuteen vaikuttaa suoritusaikainen yleiskustannus (engl. overhead), joka sisältää argumenttien välittämisen, haarautumisen aliohjelmiin ja palaaminen takaisin kutsujaan. Yleiskustannuksella tarkoitetaan tässä yhteydessä ylimääräistä taakkaa ja suorittamisen aikaista viivettä, joka aiheutuu huonosti optimoitujen funktiokutsujen takia. olio-orientoituneissa kielissä dynaaminen sidonta (engl. dynamic dispatch) on aina ohjelman suoritusta hidastava.[16] Usein yleiskustannus sisältää myös prosessorin rekisterien tallentamisen ja palauttamisen, sekä kutsurungon tallennus- ja vapautustoimintojen varauksen. Ohjelmointikielestä riippuen voi funktiokutsu sisältää myös automaattisen funktion testauksen paluukoodista tai poikkeusten käsittelystä, joka voi vaikuttaa ohjelman yleiskustannuksiin.

Inline-funktio muokkaa

engl. Inline expansion or inlining[5] tarkoittaa funktiokutsun korvaamista suoraan sen toteuttamisella ohjelmakoodin kutsukohdassa. Tämä tarkoittaa sitä, että funktiota ei kutsuta erikseen, vaan funktion sisältämä koodi lisätään suoraan kutsun tilalle, jolloin vältetään ylimääräinen funktiokutsu ja siihen liittyvä suoritusaikaviive. Inline-funktioita käytetään yleensä silloin, kun funktiota kutsutaan usein ja sen suoritusaikaviive vaikuttaa merkittävästi ohjelman suorituskykyyn. Inline-funktio voi kuitenkin johtaa suurempaan koodikokoon, mikä voi vaikuttaa negatiivisesti kääntämiseen, käännöksen kokoon ja suoritusaikaiseen muistinkäyttöön.

Katso myös muokkaa

Lähteet muokkaa

  1. Understanding the Stack cs.umd.edu. Arkistoitu . Viitattu 29.9.2017. (englanniksi)
  2. M. Jones: Kernel command using Linux system calls 21.3.2007. IBM developerWorks. Arkistoitu 11.2.2017. Viitattu 5.11.2017.
  3. https://www.ias.edu/sites/default/files/library/pdfs/ecp/planningcodingof0103inst.pdf
  4. Maurice V. Wilkes amturing.acm.org. Viitattu 3.9.2019. (englanniksi)
  5. a b http://dspace.mit.edu/bitstream/handle/1721.1/5753/AIM-443.pdf?sequence=2
  6. Function Calls ece353.engr.wisc.edu. Viitattu 10.9.2022. (englanniksi)
  7. a b 68HC11 Assembly Language Programming clear.rice.edu. Viitattu 10.9.2022. (englanniksi)
  8. a b x86 Assembly Guide cs.virginia.edu. Viitattu 12.9.2022. (englanniksi)
  9. a b c d 14.1 Subroutine Invocation in m68k Assembler cs.mcgill.ca. Viitattu 12.9.2022. (englanniksi)
  10. a b Jennifer Rexford: Assembly Language: Function Calls (PDF) cs.princeton.edu. Viitattu 10.9.2022. (englanniksi)
  11. a b c d e Lecture 5 (PDF) courses.cs.washington.edu. Viitattu 10.9.2022. (englanniksi)
  12. a b Calling Conventions cs.cornell.edu. Viitattu 12.9.2022. (englanniksi)
  13. Part II: SPARC, an extreme windowed RISC (1987) . . cpushack.com. Viitattu 18.7.2020. (englanniksi)
  14. Oppaat: C++-ohjelmointi: Osa 6 - Esittelyt, määrittelyt ja elinajat ohjelmointiputka.net.
  15. Johdatus ohjelmointiin -luentomoniste: 2.4.1 Yleistä funktioista mit.jyu.fi.
  16. a b Smed, Jouni & Hakonen, Harri & Raita, Timo: Sopimuspohjainen olio-ohjelmointi Java-kielellä, s. 126,138. Turun yliopisto. ISBN 9789529217762. Teoksen verkkoversio.
  17. Helsingin yliopisto & Niklander, Tiina: C-ohjelmointi (luentomateriaali)
  18. Return Oriented Programming ctf101.org. Viitattu 10.9.2022. (englanniksi)