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.

Aliohjelmien tarkoitus on yksinkertaistaa ohjelmointia: useimmat tietokonetta vaativat tehtävät ovat liian monimutkaisia kerralla ratkaistavaksi ja aliohjelmien käytöllä ongelmat voidaan pilkkoa pienempiin yksittäin ratkaistaviin palasiin.[1]

Proseduraalisessa ohjelmointikielessä aliohjelmia kutsutaan funktioiksi tai proseduureiksi. Olio-ohjelmoinnissa olion funktioita nimitetään jäsenfunktioiksi tai metodeiksi. Assemblyssä aliohjelmia kutsutaan alirutiineiksi. Joissakin yhteyksissä aliohjelmasta käytetään nimitystä kutsuttava yksikkö (engl. callable unit) usein eri nimitysten sijaan.[2]

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.[3]

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

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.[5] 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.[6] 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. Eräiden testitulosten mukaan tuon ajan hitaus aliohjelmakutsuissa johtui PL/I-kielen kääntäjän toteutuksesta, joka johti virheellisiin käytäntöihin.[7]

Toimintaperiaate

muokkaa

Suoritin käyttää hyppykäskyä (jmp), kutsukäskyä (call, jsr) tai tarkoitukseen varattua haarautumiskäskyä (esim. bl, bsr) siirtyäkseen suoritettavaan ohjelmakohtaan.[8][9][10][11] Tätä varten suorittimelle on kerrottava suoritettavan aliohjelman osoite.[12][11] 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.[9] Paluuosoite riippuu paikasta, jossa aliohjelmaa kutsutaan, esimerkiksi jos sitä kutsutaan kahdesti peräkkäin eri parametreilla.[13] Seuraavan suoritettavan käskyn osoite voidaan sijoittaa pinoon ennen aliohjelman kutsua, jolloin se palautetaan aliohjelmasta paluun jälkeen ja suoritus jatkuu seuraavasta käskystä.[12] Suorittimen arkkitehtuurista riippuu käskyjen toteutustapa ja käskyt voivat yhdistää suorituspaikan työntämisen pinoon sekä ehdottoman hyppykäskyn suorittamisen.[10][11] 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.[11]

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ää.[13] Vastuu rekisterin tilan säilömisessä voi olla aliohjelman kutsujalla tai kutsuttavalla aliohjelmalla, joissa on molemmissa tapauksissa etunsa ja haittansa.[13][14] Kutsukäytäntö määrittää tavan aliohjelmien kutsumiseen.[14] 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.[13] SPARC käyttää suurta määrää rekistereitä liukuvan ikkunan periaatteella, jolloin aliohjelma ei näe kaikkia rekistereitä.[15]

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.[13]

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!";
 }

Assembly-tasolla aliohjelman kutsu:

main:    bsr foo
         ... paluun jälkeen tehtävät toiminnot

foo:     ... aliohjelmassa tehtävät asiat
         rts

... jossa bsr (branch-to-subroutine) siirtää suorituksen aliohjelmaan ja rts (return-from-subroutine) palaa aliohjelmasta kutsun tehneeseen paikkaan. Esimerkissä ei ole mukana pinon tai parametrien käsittelyä.[16] Vastaavasti bl (branch-and-link) käytetään ARM-suorittimella aliohjelman kutsuun, mutta paluu tapahtuu asettamalla paluuosoite ohjelmalaskuriin (PC).[17]

Kahden numeron yhteenlasku aliohjelmassa ARM-suorittimella:[18]

        AREA    subrout, CODE, READONLY     ; koodilohkon nimi
        ENTRY                     ; ensimmäisenä suoritettavan käskyn merkki
start   MOV     r0, #10           ; parametrien asetus rekistereihin
        MOV     r1, #3
        BL      doadd             ; kutsutaan alirutiinia "doadd"
stop    MOV     r0, #0x18         ; angel_SWIreason_ReportException
        LDR     r1, =0x20026      ; ADP_Stopped_ApplicationExit
        SVC     #0x123456         ; ARM semihosting (formerly SWI)
doadd   ADD     r0, r0, r1        ; alirutiinin koodi (laske yhteen rekisterien r1 ja r0 arvot, sijoita lopputulos rekisteriin r0)
        BX      lr                ; paluu alirutiinista (katso BL)
        END                       ; tiedoston loppu

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.[19][20][21][22]

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;
}

Rust-kielessä funktion viimeinen lause on oletuksena myös funktion paluuarvo:[23]

fn five() -> i32 {
    5
}

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 */
}

Oletusarvot lisättiin Java-kieleen versiossa 8.[24]

Parametrien välitys

muokkaa

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

  • arvo (pass by value)
  • osoitin (pass by pointer)
  • viite (viittaus, referenssi) (pass by reference)

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);
}

Arvona välitetyn argumentin (by value) muuttaminen aliohjelmassa ei muuta kutsujan käyttämää arvoa, mutta viitteellä välitettyä kutsujan käyttämää arvoa voidaan muuttaa kutsutussa aliohjelmassa. Viitteen välittäminen on tehokkaampaa kuin arvon välittäminen koska tällöin ei tehdä kopiota.[25] Osoittimen välittämien (by pointer) tekee kopion osoittimen arvosta aliohjelmalle. Osoittimen arvon muuttaminen aliohjelmassa muuttaa vain aliohjelman käyttämää osoitinta.[26] Arvon välittämisessä (by value) kääntäjä tekee aliohjelmalle kopion, jonka muuttaminen aliohjelmassa näkyy vain aliohjelmalle eikä vaikuta kutsujan käyttämään arvoon.[27]

Ada-kielessä käytetään in ja out määrityksiä parametreille. in out sisältää kutsujan antaman alkuarvon, jota aliohjelma voi muuttaa.[28]

Esimerkki Adan kutsutavoista:[28]

procedure Proc
 (Var1 : Integer;
  Var2 : out Integer;
  Var3 : in out Integer)
is
begin
   Var2 := Func (Var1);
   Var3 := Var3 + 1;
end Proc;

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

muokkaa

Eräät ohjelmointikielet tukevat ylikuormittamista (engl. overloading) (vaihtoehtoisia kutsutapoja ja toteutuksia).

Ylikuormittaminen on tarkoitettu käyttöön, kun sama operaatio tehdään eri tyyppisille olioille. Saman nimen käyttöä eri tyyppien kanssa kutsutaan ylikuormittamiseksi.[29]

Ylikuormittamisessa samalle aliohjelmalle voi olla rinnakkaisia toteutuksia eri tietotyypeillä kuten neliöjuuren laskentaan:

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

Metodin ylikirjoittaminen (engl. overriding tai engl. overwriting) tarkoittaa kantaluokassa olevan toteutuksen korvaamista toisella samalla nimellä ja samoilla argumenteilla perityssä luokassa.[30][31]

Katso myös: virtuaalimetodi

Inline-funktiot

muokkaa

Inline-funktiolla tarkoitetaan funktion, jonka kutsun kääntäjä korvaa funktion sisällöllä. Kääntäjän kyvykkyyttä optimointiin ei ole määritelty, joten eri kääntäjä voi korvata kutsun eri tavoin (esimerkiksi yksinkertaistamalla laskentaa tai korvaamalla se vakioarvolla käännösaikaisella laskennalla). Määrittäminen vakioilmaisuksi (constexpr) helpottaa käännösaikaista arviointia. inline-avainsana ei muuta funktion semantiikkaa.[32] Kääntäjän yleensä sallitaan olla välittämättä pyynnöstä tuottaa inline-koodia funktiokutsun sijaan. Inline-kutsu voi nopeuttaa, mutta se voi myös hidastaa ohjelman suoritusta kun ohjelman koko kasvaa liikaa, josta johtuen yksinkertaista vastausta nopeutukseen ei ole.[33]

Sisäänrakennetut funktiot

muokkaa

Sisäänrakennetut funktiot (engl. built-in tai engl. intrinsic) tarkoittavat funktioita, joille kääntäjä tuottaa inline-koodia käännöksen aikana. Tämä korvaa ajonaikaisen kutsun samannimiseen funktioon dynaamisesti ladattavassa kirjastossa. Tämä kasvattaa tuotetun ohjelmakoodin määrää, mutta nopeuttaa suoritusta kun kutsut jäävät pois.[34] Joissakin tapauksissa kyseessä on laitteiston tukema erikoistunut toiminto ja lopputulos voi olla yksi tai kaksi käskyä, mutta niitä kutsutaan funktioiksi kutsuun käytetyn syntaksin vuoksi.[35]

Tietoturvahaavoittuvuudet

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).[36] ASLR muuttaa osoitteita, jotta haavoittuvuuksien hyödyntäminen on vaikeampaa.

Katso myös

muokkaa

Lähteet

muokkaa
  1. Functions and Subroutines web.chem.ox.ac.uk. Viitattu 29.12.2023. (englanniksi)
  2. Definitions of Words with Special Meanings Arkistoitu . Viitattu 29.12.2023. (englanniksi)
  3. Understanding the Stack cs.umd.edu. Arkistoitu . Viitattu 29.9.2017. (englanniksi)
  4. M. Jones: Kernel command using Linux system calls 21.3.2007. IBM developerWorks. Arkistoitu 11.2.2017. Viitattu 5.11.2017.
  5. https://www.ias.edu/sites/default/files/library/pdfs/ecp/planningcodingof0103inst.pdf
  6. Maurice V. Wilkes amturing.acm.org. Viitattu 3.9.2019. (englanniksi)
  7. Guy Lewis Steele Jr.: Debunking the "expensive procedure call" myth or, procedure call implementations considered harmful or,lambda: the ultimate goto (PDF) dspace.mit.edu. lokakuu 1977. Viitattu 1.1.2024. (englanniksi)
  8. Function Calls ece353.engr.wisc.edu. Viitattu 10.9.2022. (englanniksi)
  9. a b 68HC11 Assembly Language Programming clear.rice.edu. Viitattu 10.9.2022. (englanniksi)
  10. a b x86 Assembly Guide cs.virginia.edu. Viitattu 12.9.2022. (englanniksi)
  11. a b c d 14.1 Subroutine Invocation in m68k Assembler cs.mcgill.ca. Viitattu 12.9.2022. (englanniksi)
  12. a b Jennifer Rexford: Assembly Language: Function Calls (PDF) cs.princeton.edu. Viitattu 10.9.2022. (englanniksi)
  13. a b c d e Lecture 5 (PDF) courses.cs.washington.edu. Viitattu 10.9.2022. (englanniksi)
  14. a b Calling Conventions cs.cornell.edu. Viitattu 12.9.2022. (englanniksi)
  15. Part II: SPARC, an extreme windowed RISC (1987) . . cpushack.com. Viitattu 18.7.2020. (englanniksi)
  16. Andreas Moshovos: Lecture 11 eecg.toronto.edu. kevät 2005. Viitattu 28.12.2023. (englanniksi)
  17. ARM Subroutine/procedure/function Calls cs.uregina.ca. Viitattu 28.12.2023. (englanniksi)
  18. Register usage in subroutine calls developer.arm.com. Viitattu 28.12.2023. (englanniksi)
  19. Oppaat: C++-ohjelmointi: Osa 6 - Esittelyt, määrittelyt ja elinajat ohjelmointiputka.net.
  20. Johdatus ohjelmointiin -luentomoniste: 2.4.1 Yleistä funktioista mit.jyu.fi.
  21. Smed, Jouni & Hakonen, Harri & Raita, Timo: Sopimuspohjainen olio-ohjelmointi Java-kielellä, s. 126,138. Turun yliopisto. ISBN 9789529217762. Teoksen verkkoversio.
  22. Helsingin yliopisto & Niklander, Tiina: C-ohjelmointi (luentomateriaali)
  23. Functions with Return Values doc.rust-lang.org. Viitattu 22.1.2024. (englanniksi)
  24. Default Parameter in Java javatpoint.com. Viitattu 22.1.2024. (englanniksi)
  25. Pass by reference (C++ only) ibm.com. Viitattu 22.1.2024. (englanniksi)
  26. Pass by pointer ibm.com. Viitattu 22.1.2024. (englanniksi)
  27. Pass by value ibm.com. Viitattu 22.1.2024. (englanniksi)
  28. a b Functions and Procedures learn.adacore.com. Viitattu 22.1.2024. (englanniksi)
  29. Stroustrup, Bjarne: The C++ Programming Language, 4th ed., s. 326. Addison-Wesley, 2015. ISBN 0-321-56384-0. (englanniksi)
  30. H.Mössenböck: Advanced C# (PDF) ssw.jku.at. Viitattu 22.1.2024. (englanniksi)
  31. Stroustrup, Bjarne: The C++ Programming Language, 4th ed., s. 587,589. Addison-Wesley, 2015. ISBN 0-321-56384-0. (englanniksi)
  32. Stroustrup, Bjarne: The C++ Programming Language, 4th ed., s. 311. Addison-Wesley, 2015. ISBN 0-321-56384-0. (englanniksi)
  33. Inline Functions isocpp.org. Viitattu 1.1.2024. (englanniksi)
  34. Built-in functions ibm.com. Viitattu 29.12.2023. (englanniksi)
  35. Intrinsic Functions hpe.com. Viitattu 29.12.2023. (englanniksi)
  36. Return Oriented Programming ctf101.org. Viitattu 10.9.2022. (englanniksi)