Sormet C:hen

ITKA203 Käyttöjärjestelmät -kurssin Demo 3 keväällä 2015, 2016 ja 2017. "Lähespikaintro C-kielellä ohjelmointiin"

Paavo Nieminen, paavo.j.nieminen@jyu.fi

Jyväskylän yliopiston tietotekniikan laitos.

STATUS: LOPULLINEN KEVÄÄLLE 2017!. Lisätty yleisön pyynnöstä vapaaehtoinen lisätehtävä lukujärjestelmiin liittyen.

Contents

Mistä tässä harjoitteessa on kyse

Tämä harjoite perustuu demoissa 1 ja 2 esiteltyihin perustaitoihin, jotka oletetaan alustavasti tunnetuksi ja joita koko ajan harjoitellaan lisää. Erityisesti tulisi jo olla jollain tapaa miellyttävää toimia pääteyhteyden yli ja editoida lähdekoodia (=tekstitiedosto) yhdessä screen-ikkunassa ja käyttää komentoriviä toisessa. Komentojen ja argumenttien antaminen shellissä pitäisi olla tuttua samoin kuin ohjelmien tulosteiden tulkitseminen päätteeltä ja interaktiivisten tekstipohjaisten ohjelmien käyttely ym.

Huomioi seuraavaa:

  • Käännökset ja kokeilut on tarkoitus tehdä pääteyhteydellä yliopiston etäkoneessa jalava.cc.jyu.fi tai halava.cc.jyu.fi tai itka203-testi.it.jyu.fi.
  • Jomman kumman suorakäyttökoneille asennetun tehokkaan tekstieditorin (emacs tai vim) opettelu on suositeltavaa. Selviytymisohjeet suomeksi löytyvät demosta 2, mutta toki ohjelmien oman englanninkielisen tutoriaalin läpikäynti on aina paras vaihtoehto. Netistä löytyy myös paljon hyviä videotutoriaaleja molempiin editoreihin!
  • Tekstieditoinnin voi tehdä hätätilanteessa helppokäyttöisellä nano -editorilla... Syntaksin mukainen väritys auttaa elämässä sen verran, että myös karvahattu-Nano kannattaa laittaa värittämään sen minkä se pystyy. Kopioi siis demossa linkitetty nanorc_patka -niminen tiedosto kotihakemistoosi tiedoston ~/.nanorc jatkeeksi, jos se ei vielä tue C-kielen väritystä. Pyydä tähän apuja lähiohjauksessa, jos tuntuu vaikealta. (Viimeisenä, ei missään nimessä suositeltavana, keinona voi editoida tiedostoja mikroluokassa Windowsin Notepad++ -editorilla tai vastaavalla järkevällä editorilla; tämä toimintamalli on kuitenkin historiallisesti korreloinut arvosanatavoitteen 1 tai "hylätty" kanssa, joten edelleen suosittelen laajentamaan omaa teknistä repertuaaria, jos se vain on henkisesti mahdollista itsellesi).
  • Ei tarvitse pelätä että tällä kurssilla tehtäisiin kovin suuria ohjelmointisuorituksia. Enemmänkin katsellaan ja yritetään ymmärtää. Demotilaisuuksissa tai sähköpostitse tarjotaan apua, jos ei muuten aukene. Mikäli ohjelmointi tuntuu hankalalta ymmärtää, on syytä kerrata kurssin Ohjelmointi 1 asioita.

Harjoituksen tavoitteet:

  • Teet omin käsin luennoilla nähtyjä käytännön toimenpiteitä, jolloin niiden merkitys toivon mukaan konkretisoituu. Erityisesti käännät ja suoritat C-kielisen ohjelman.
  • Näet alustavasti, millaisia asioita POSIX-standardi määrittelee löytyväksi "matalan tason" kirjastoissa yhteensopivassa järjestelmässä.
  • Perinteisistä ohjelmien toiminnan mukauttamiseen vaikuttavista keinoista käydään tarkoin läpi komentoriviargumentit ja ympäristömuuttujat.
  • Lasket yksinkertaisia laskutoimituksia eri lukujärjestelmillä ja teet muunnoksia lukujärjestelmästä toiseen. Yleisön pyynnöstä on mukana vapaaehtoinen lisäharjoitus jotta on mahdollista harjaannuttaa tätä taitoa. Erityisesti kaksi- ja heksadesimaalijärjestelmä ovat käyttöjärjestelmien (ja lisäksi mm. tietoliikennetekniikan) parissa paljon käytettyjä lukujärjestelmiä kymmenjärjestelmän tilalla.

Keväällä 2015 määriteltyjen osaamistavoitteiden osalta demon tehtyään opiskelija:

Alustavalla tasolla C-koodin (ei vielä bashin) osalta opiskelija:

Lisäksi tähdätään edelleen useaan muuhun osaamistavoitteeseen, jotka viedään perille myöhemmissä demoissa.

Ohjeita

Aiemmissa demoissa teit oman kotihakemistosi alle hakemistot Käyttöjärjestelmät-kurssin ensimmäisille harjoitteille. Ota jälleen pääteyhteys IT-palveluiden suorakäyttökoneeseen ja aseta työhakemistoksesi tämän kolmannen demon hakemisto, nimeltään esim. ~/kj17/demo3/ tai muuta vastaavaa. (Muistele tarvittaessa aiempia demoja: miten hakemisto vaihdettiin, miten varmistettiin, mikä on nykyinen työhakemisto, jne. ...)

Kun olet demo 3:n hakemistossa, hae esimerkkikoodi kurssin materiaalitietovarastosta, eli komenna shellissä:

wget https://yousource.it.jyu.fi/itka203-kurssimateriaalikehitys/itka203-kurssimateriaali-avoin/blobs/raw/master/2015/esimerkit/l04_helloworld.c

Huom 1: Pitkä ja hankala URL varmaan kannattaa varovaisesti leikata ja liimata pääteyhteysohjelmaan.. Varmista, että olet varmasti kopioinut komennon tai URLin web-selaimesta leikepöydälle. Kun olet varma, että leikepöydällä on täsmälleen ylläoleva komento, uskallat liittää sen pääteyhteysikkunaan shelliin, joka odottaa komentoa. KiTTYssä ja PUTTYssä klikataan hiiren oikeanpuoleista nappia; Linux-käyttäjät klikkaavat keskimmäistä nappia ja Mac-käyttäjät tekevät mahdollisesti jotain muuta. Tavoitteena on tavalla tai toisella suorittaa yllämainittu komento, joka hakee Internet-yhteyden yli WWW:stä yhden tiedoston.

Huom 2: Free software foundationin ohjelma wget ei ole määritelty POSIX-standardissa, mutta se on niin näppärä, että käytämme sitä nyt. POSIX kyllä määrittelee samaan tarkoitukseen ohjelman curl, johon mahdollisesti palataan, kun shell ja skriptiohjelmointi saadaan hieman tukevammalle pohjalle.

Hae vielä seuraava:

wget https://yousource.it.jyu.fi/itka203-kurssimateriaalikehitys/itka203-kurssimateriaali-avoin/blobs/raw/master/2015/demot/mallikoodia/d03_fiilikset.c

Ja mikäli suosituksista huolimatta käytät tekstin editointiin nanoa, etkä vimiä tai emacsia, niin tee ihmeessä myös seuraavat operaatiot, jotta C-koodi näyttää mielekkäämmältä editorissa:

wget https://yousource.it.jyu.fi/itka203-kurssimateriaalikehitys/itka203-kurssimateriaali-avoin/blobs/raw/master/2015/demot/mallikoodia/nanorc_patka

cat nanorc_patka >> ~/.nanorc

Huom 3: Etäpalautuksen vuoksi emme täydellisesti pysty toteamaan, että olet tehnyt noin viisitoista minuuttia kestävän palautustehtävän lisäksi kaikki tehtävät aivan itse ja oppinut niistä -- jokainen on oman onnensa nojassa, ja "teknisesti sallittu" laiskuus on jokaisen oma häpeä. Tentti sen sitten kertoo, mitä itse kukin on oppinut. Näillä sanoin työn touhuun... Kaikki tämän demon vaiheet kuuluvat kurssin sisältöön.

Vapaaehtoinen, mutta suositeltava: Eri lukujärjestelmistä ja niiden aritmetiikasta

Luentomonisteesta tätä samaa asiaa löytyy tiiviisti luvusta 2.3.1. Lukujärjestelmät (s. 24, luku ja sivu tarkastettu 27.3.-17). Tässä osiossa toistetaan osa, mutta ei aivan kaikkea luentomonisteessa lukujärjestelmistä mainittuja asioita. Lue tarvittaessa kyseinen luku ennen kuin jatkat tästä eteenpäin.

Suorilta käsin saattaa olla mahdotonta tietää, mikä lukujärjestelmä milloinkin on kyseessä jos sitä ei erikseen sanota tai sitä ei voi kontekstista päätellä. Erilaisilla tekstinkäsittely- ja ladontajärjestelmillä tuotetuissa dokumenteissa saatetaan käyttää kantalukua ko. luvun alaindeksinä erottamaan eri lukujärjestelmät toisistaan. Tällä sivulla kirjoitetaan aina eksplisiittisesti, mikä järjestelmä on kyseessä. Lisäksi apuna käytetään seuraavaksi määriteltävää esitystapaa. Heksadesimaalijärjestelmän luvut on kirjoitettu alkavalla merkkiyhdistelmällä 0x, eli 0x1 ja 0xF2A, kuten C-kielessä ja sen perillisissä. Binäärijärjestelmän luvuilla on lopussa pieni b-kirjain, eli 1b ja 1010101b. Nämä merkinnät eivät liity luvuilla suoritettaviin laskutoimituksiin millään tavalla, vaan ne ovat ainoastaan havainnollistajia! Vastaavia merkintätapoja saatetaan käyttää maailmalla, joten ne on hyvä tietää. Kymmenjärjestelmän luvut esitetään 'normaalisti', eli 2, 101 ja 603. Lisäksi nyt mietitään lukuja vain ''matemaattisessa mielessä'' ja pysytään positiivissa kokonaisluvuissa, eli erilaiset tietokoneen sisäiset esitystavat ja negatiivisten lukujen esittämistavat jätetään huomiotta.

Selvyyden vuoksi mainittakoot, että tässä yhteydessä käytämme termiä numero kun tarkoitamme ''lukujärjestelmän mahdollisia kertoimia''. Esimerkiksi binäärijärjestelmä koostuu numeroista 0b ja 1b, kymmenjärjestelmä koostuu numeroista 0, 1, 2, 3, 4, 5, 6, 7, 8 ja 9 ja heksadesimaalijärjestelmässä on numerot 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE ja 0xF. Termiä luku käytämme kun nyt tarkoitamme ''numeroista tehtyä koostetta''. Lisäksi mainittakoon vielä se, että heksalukujen yhteydessä käytetään nyt ainoastaan isoja kirjaimia ja binääriluvuilla havainnolistamiseen pientä b-kirjainta.

Muunnoksia lukujärjestelmästä toiseen

Seuraavaksi on esimerkkejä ja harjoitteita lukujen muuttamisesta lukujärjestelmästä toiseen. Aluksi on (toivottavasti) riittävä määrä esimerkkejä, jonka jälkeen voi itsenäisesti kokeilla muunnoksia. Nämä harjoitteet eivät ole osa pakollista palautustehtävää, vaan ne on tarkoitettu oppimisen tueksi. Visuaalisten havainnolisteiden lisäksi erikseen mainitaan, mistä lukujärjestelmästä tehdään muunnos ja mihin lukujärjestelmään. Lisäksi mainittakkoon, että esitetyt menetelmät lukujärjestelmien vaihtoon eivät ole ainoita mahdollisia. Jos jo osaat jonkun toisen tavan mukaan muunnoksia, niin voit käyttää myös niitäkin. Muunnokset esitetään tässä niin, että lukujärjestelmästä, mistä muunnos tehdään on, aina vasemmalla ja lukujärjestelmä, mihin muunnetaan, on aina oikealla yhtäsuurusmerkistä. Esimerkeissä on myös kirjoitettu välivaihe auki. Kysymysmerkki tarkoittaa, että itse sopii harjoitella kyseistä suoritetta. Eksponentin merkkinä käytetään hattua (A.K.A. sirkumfleksiä) eli ^ -merkkiä.

Muunnos binäärijärjestelmästä kymmenjärjestelmään onnistuu helposti, kun lisäilee binääriluvun sijaintipaikan perusteella oikeat kakkosen potenssin yhteen:

110b = 1 * 2^2 + 1 * 2^1 + 0 * 2^0 = 4 + 2 + 0 = 6
10101b = 2^4 + 2^2 + 2^0 = 16 + 4 + 1 = 21
1b = ?
1001b = ?
1010b = ?
110101b = ?
111111b = ?

Muunnos heksadesimaalijärjestelmästä kymmenjärjestelmään toimii vastaavalla tavalla kuin binäärijärjestelmästä kymmenjärjestelmään. Erona vain on, että summaillaan luvun kuusitoista potensseja yhteen. Heksadesimaalijärjestelmästä pitää lisäksi muistaa, että numerot A, B, C, D, E ja F ovat myös pelissä mukana:

0x4 = 4 * 16^0 = 4
0xA = 10 * 16^0 = 10
0xF = 15 * 16^0 =15
0x10 = 1 * 16^1 + 0 * 16^0 = 16
0x12 = 1 * 16^1 + 2 * 16^0 = 18
0x110 = 16^2 + 16^1 = 272
0x23 = ?
0x16 = ?
0x2F = ?
0xBC = ?
0xEB = ?

Muunnos kymmenjärjestelmästä binäärijärjestelmään voi vaikuttaa aluksi haastavammalta kuin muunnos binäärijärjestelmästä kymmenjärjestelmään, mutta se vaatii vain vähän enemmän pähkäilyä ja mahdollisesti jonkun verran kokeilua. Esimerkiksi muunnettaessa kymmenjärjestelmän lukua 5 binäärijärjestelmään aloitamme miettimällä ''mikä kakkosen potenssi on suurempi kuin muunnettava luku, mutta sitä pienempi kakkosen potenssi on pienempi tai yhtäsuuri kuin muunnettava luku''. Tässä tapauksessa 2^3 eli luku 8 täyttää tämän ehdon. Tästä tiedämme, että muunnetussa luvussa tulee olemaan kolme numeroa, koska toinen, ensimmäinen ja ''nollas'' potenssi on mukana pelissä jollain tapaa. Tämän jälkeen mietimme, millä jäljelle jäävien kakkosten potenssien summalla tämän suurimman potenssin kanssa saadaan haluttu luku. Ainoa mahdollisuus on 2^2 + 2^0 eli alkuperäinen luku 5 kymmenjärjestelmässä:

1  = 2^0 = 1b
3  = 2^1 + 2^0 = 11b
12 = 2^3 + 2^2 = 1100b
13 = 2^3 + 2^2 + 2^0 = 1101b
110 = 2^6 + 2^5 + 2^3 + 2^2 + 2^1 = 110 1110b
1  = ?
8  = ?
10  = ?
53  = ?
302  = ?

Muunnos kymmenjärjestelmästä heksadesimaalijärjestelmään voidaan tehdä muutoin vastaavasti kuin binäärijärjestelmään. Esimerkiksi kymmenjärjestelmän lukua 53 muuttaessa heksadesimaalijärjestelmään huomataan ensin, että 16^2 = 256 menee yli eli muunnetussa heksadesimaaliluvussa on kaksi numeroa. Sitten kokeilun tai muun osaamisen kautta keksitään 3 * 16^1 + 5 * 16^0 = 53, eli muunnettu heksaluku on 0x35:

4 = 0x4
11 = 0xB
110 = 0x6E
15 = ?
33 = ?
53 = ?
117 = ?
256 = ?

Muunnos heksadesimaalijärjestelmästä binäärijärjestelmään pitäisi onnistua kivuttomasti kun huomaa, että 16 = 2^4. Tästä saadaan, että heksadesimaalijärjestelmän luvun numerot voi korvata vastaavilla binäärijärjestelmän luvuilla. Esimerkiksi heksadesimaalijärjestelmän luku CD voidaan ensin muuttaa numero kerrallaan vastaaviksi binäärijärjestelmän luvuiksi, eli 0xC = 12 = 1100b ja vastaavasti 0xD = 1101b. Tästä saamme suoraan binäärijärjestelmän luvun 11001101b:

0x2 = 10b
0x9 = 1001b
0x1E = 11110b
0xFF = ?
0x15 = ?
0xAD = ?
0x53 = ?
0x1A3 = ?

Binäärijärjestelmästä heksadesimaalijärjestelmään onnistuu käänteisesti vastaavalla tavalla kuin heksadesimaalijärjestelmästä binäärijärjestelmään. Esimerkiksi binäärijärjestelmän luku 10001010b saadaan numeroittain muuntaen 1000b = 8 = 0x8 ja 1010b = 10 = 0xA, joten muunnettu heksadesimaaliluku on 0x8A:

101b = 0x3
11011011b = 0xDB
1111b = ?
10001b = ?
11001100b = ?
10010010b = ?
111011010b = ?

Aritmetiikkaa eri lukujärjestelmillä

Seuraavaksi tarkastellaan plus- ja miinuslaskua eri lukujärjestelmissä. Mikäli päässälasku tuottaa hankaluuksia, niin kynällä ja paperilla suoritettu allekkainlasku toimii aivan samalla tavalla kuin kymmenjärjestelmässä. Laskiessa pitää vain muistaa käytetty kantaluku, eli esimerkiksi heksoina laskiessa ei ''pyöräytä'' numeroa ympäri ja laita muistinumeroa vielä numeron 9 jälkeen vaan jatkaa tilanteen vaatiessa sinne numeroon F asti.

Laske laskutoimitukset binäärijärjestelmän luvuilla:

1b + 1b = 10b
10b + 1b = 11b
111b + 11b = 1010b
10010b + 110010b = ?
11111b + 1b = ?
11001b + 110b = ?
10110b + 11001b = ?
1100111b + 100101b = ?
1001100b - 1000001b  = ?
11001111b - 101001b = ?
11101010b - 10110b = ?

Laske laskutoimitukset heksadesimaalijärjestelmän luvuilla:

0x4 + 0x3 = 0x7
0x6 + 0x6 = 0xC
0xD + 0x5 = 0x12
0x1B + 0x3A = ?
0xDD + 0x23 = ?
0x89 + 0xEB = ?
0x12 + 0x1E = ?
0x10+ 0x99 = ?
0xAAF - 0x4C1 = ?
0x1B8 - 0x73  = ?
0xDEF - 0xABC = ?

Kappalemäärien ja välien laskemisen erosta

Vielä tämän osion päätteeksi mainittakoon lukujärjestelmistä riippumaton ns. ''aidan välien'' ja ''aidan seipäiden'' laskemisen välinen ero. Tämä maininta saattaa tuntua irralliselta, mutta ohjelmoinnissa tämä ongelma tulee paikoitellen vastaan eri tilanteissa, luultavasti eri nimillä. Havainnollistetaan tätä ongelmaa esimerkiksi taulukkoindeksoinnilla. Olkoon meillä perustavanlaatuinen tietorakenteemme taulukko. Taulukossa on tässä tapauksessa vaikka kahdeksan alkiota, eli indeksit menevät nollasta seitsemään (kuten C:n sukuisissa kielissä esim. C#:ssa ja Javassa). Nyt itse ''ongelmaan'', eli montako taulukon alkiota on vaikka indeksien 3 ja 5 välillä? Nopeasti asiaa sen enempää ajattelmatta voisi laskea 5 - 3 = 2, mutta onhan siellä indeksit 3, 4 ja 5. Alkuperäinen laskumme laskee ''aidan välejä'', mutta nyt olimmekin kiinnostuneita ''aidan seipäistä''. Esimerkki on alkeellinen ja myöskin keinotekoinen, mutta vastaavanlainen tilanne tulee vastaa monimutkaisemmissakin tapauksissa (esim. pinomuistin osoitteita laskiessa). Emme mene tässä sen syvällisemmin tämän ongelman mihinkään matemaattiseen luonteeseen, mutta tämän(kin) kurssin materiaalissa tulee vastaan tilanteita, joissa kannattaa miettiä mitä oikeasti on laskemassa.

Reunahuomautus: Ainakaan tämän kurssin vastuuopettaja ei koskaan saa aikaan oikeata koodia kirjoittamatta paperille yksinkertaista esimerkkiä väleistä ja seipäistä. Todellinen välien määrä voi olla miljoonia, mutta riittää piirtää ihan muutama, että näkee vastauksen:

+------+------+------+------+   Ookke.. jos mulla on 4 alkiota, niin vika
| t[0] | t[1] | t[2] | t[3] |   on indeksissä 3 eli 4-1 ja jos laskisin
+------+------+------+------+   "aidanseipäitä", niin niitä olisi se 4 kpl.
                                Eli jos on 12345 alkiota, niin viimeinen
                                olis t[12344], jos olis N alkiota, niin
                                viimeinen olis N-1. OK.. sitten vasta
                                kajotaan koodiin, kun asia on selvä.

Ensimmäinen C-ohjelma

Katso tekstieditorilla ohjelmaa l04_helloworld.c -- siitä nähdään yksinkertaisen C-ohjelman muoto. Tällainenhan se on (rivinumerot lisätty tulosteeseen):

1  #include <stdio.h>
2  int main(int argc, char **argv){
3      printf("Hello World!\n");
4      return 0;
5  }

Selitetään merkitys rivi riviltä, ja peilataan olio-ohjelmoinnista (esim. C# tai Java) tuttuihin asioihin. Syntaksiin mennään tarkemmin myöhemmin; nyt mietitään vain rivien merkitystä ohjelman kannalta.

Rivi 1:

#include <stdio.h>

Tällä liitetään mukaan tiedosto stdio.h. Tällaiset .h -päätteiset tiedostot ovat ns. otsikkotiedostoja (Header file), joiden perusteella C-kielen kääntäjä tietää, millaisen rajapinnan jokin kirjasto (tai kirjaston osa) tarjoaa. Nimenomainen stdio (Standard input/output) tarkoittaa perusmallin syöttö- ja tulostusvälineistöä, jollainen aina löytyy ja jota vastaava kirjastokin liitetään ohjelmaan ilman erillistä pyyntöä. Tässä ohjelmassa tulostetaan printf() -aliohjelmalla, jonka otsikko on nimenomaan tiedostossa stdio.h. Lähin vastine esimerkiksi Javassa on import -avainsana, jolla lähdekooditiedoston alussa ilmoitetaan, minkä nimenomaisen kirjaston rajapintaa halutaan käyttää - esimerkiksi fi.jyu.mit.kurssit.munkurssi.Harpake voi olla aivan erilainen kuin joku.muu.organisaatio.kirjasto.Harpake. Tiedoston alussa voisin haluta kirjoittaa siis import fi.jyu.mit.kurssit.munkurssi, minkä jälkeen Harpake on se luokka, jota oikeasti tarkoitan. Samoin kuin oliokielisten lähdekoodien alussa on paljon import -rivejä, C-ohjelmien alussa on usein paljon #include -rivejä.

Otsikkotiedosto sisältää vain tiedon rajapinnan määrittelystä (eli kunkin aliohjelman nimen sekä sen parametrien ja paluuarvon tyypit); itse kirjastot on liitettävä vielä erikseen joko lähdekoodina (käännöksen yhteydessä) tai käännettyinä eli ns. binäärisinä kirjastoina (linkitysjärjestelmän avulla, mikä on tyypillinen ja joustavampi tapa). Kirjastot, linkittäminen ja ohjelman lataaminen käydään tarkoin läpi kevään 2015 luennoilla.

Konkretiaa halajaville todettakoon, että otsikkotiedosto stdio.h on suorakäyttökoneellamme sijainnissa /usr/include/stdio.h, mistä se löytyy automaattisesti, koska koneelle asennettu kääntäjä osaa oletusarvoisesti etsiä otsikoita mm. hakemistosta /usr/include. Otsikkotiedostossa luvatulla rajapinnalla varustetun aliohjelman printf() konekielinen koodi puolestaan on osa kirjastoa, joka löytyy suorakäyttökoneellamme sijainnissa /lib64/libc.so.6. Ohjelman käynnistysvaiheessa käyttöjärjestelmän ns. lataus- ja linkityskoodi osaa ladata ja liittää suoritettavan prosessin koodiin juuri tuon kirjaston, koska ELF-muotoiseen ohjelmatiedostoon on tallennettu tieto kaikista tarvittavista kirjastoista. Tämä libc sisältää kaikkein yleisimmät ja useimmin C-kielessä tarvitut, standardin määrittelemät kirjastoaliohjelmat, joten se liitetään ohjelmiin automaattisesti ilman erillistä pyyntöä. (Mikään pakko ei ole käyttää libc:tä, jos vaikkapa hullutellakseen tai muusta syystä pitää karsia ohjelmatiedoston koko absoluuttiseen minimiin - silloin pitää kuitenkin käännösvaiheessa suorastaan kieltää kääntäjää hyödyntämästä standardikirjastoa). Muut kirjastot täytyy erikseen ilmoittaa, kun suoritettava ohjelma luodaan kääntäjällä ja muilla apuohjelmilla. Mitään mystiikkaa näihinkään asioihin ei liity, mikä on hyvä tiedostaa heti aluksi. Aiheesta jatketaan seuraavassa demossa, jossa käännetään erikseen oma minikirjasto ja liitetään se vaiheittain osaksi kokonaista ohjelmaa.

Rivi 2:

int main(int argc, char **argv){

Tästä alkaa C-ohjelman suoritus. On sovittu C-kielen määrittelyssä sekä sitä kautta myös mm. POSIX-standardissa, että ohjelman suoritus tarkoittaa (kyseisessä laitteistossa ja käyttöjärjestelmässä tarvittavien, automaattisesti tapahtuvien alustusoperaatioiden jälkeen) sellaisen aliohjelman kutsumista, jossa:

  • nimi on main
  • paluuarvo on int eli kokonaisluku
  • on kaksi parametria, joista ensimmäinen on int-tyyppinen ja toinen on char * * -tyyppinen eli osoitin merkkijonotaulukkoon (älä vielä huoli tietotyypeistä, niihin syvennytään seuraavassa demossa).

Tämä on ihan samanlainen sopimusasia kuin se, että C#-luokan suoritus tarkoittaa seuraavanlaisen luokkametodin kutsumista:

public static void Main(string[] args)

Tai että Java-luokan suoritus tarkoittaa seuraavanlaisen luokkametodin kutsumista:

public static void main(String[] args)

Huomataan, että C, kuten C# tai Javakin, on lohkorakenteinen, ja lohkojen syntaksikin on sama: lohko alkaa kiharasululla { ja päättyy vastaavaan käänteiseen sulkuun }. Muutenkin syntaksissa on yhtäläisyyttä: C:n aliohjelmat aloitetaan muodossa:

PALUUARVON_TYYPPI aliohjelmanNimi(TYYPITETTY_PARAMETRILISTA)

joka on täysin sama kuin C#:n tai Javan metodimäärittelyn syntaksi (varmasti ainakin sen takia, että nämä myöhemmät kielet on tarkoituksella tehty samannäköiseksi kuin C). Myös C:ssä aliohjelmamäärityksen eteen laitetaan tarvittaessa lisämääreitä (kaikki määreet eivät kuitenkaan tarkoita samaa uudemmissa kielissä! Esim. static on C-kielessä ihan eri asia kuin C#:ssa tai Javassa).

Rivi 3:

printf("Hello World!\n");

Tällainen on C:ssä aliohjelmakutsun syntaksi. Eikö näytäkin samalta kuin C#:ssa tai Javassa metodin käyttäminen? Paitsi että usein oliokielissä kutsutaan pistenotaatiota käyttämällä jonkun olion instanssimetodia; esimerkiksi C#-kutsussa System.Console.WriteLine("juu") ja vastaavassa Java-kutsussa System.out.println("juu") kutsutaan System -luokan luokka-attribuuttina löytyvän Console tai out-nimisen viitteen osoittamana löytyvän luokan instanssille metodia WriteLine tai println, ja metodille annetaan parametriksi viite vakiopoolissa sijaitsevaan string tai String -luokan instanssiin.

C:n tulostus on vähän lyhyempi selittää täsmällisesti kuin oliokielten luokkahässäkät: esimerkin rivi 3 siirtää prosessorin suorituksen aliohjelman printf ensimmäiseen käskyyn, sen jälkeen kun parametriksi on laitettu merkkijonon Hello world!\n ensimmäisen merkin eli kyseisen H:n osoite muistissa.

Huomaa, että C:n lauseet tulee päättää puolipisteellä ; ihan niinkuin C#:ssa tai Javassakin. Ylipäätään syntaktiset erot ovat erittäin pieniä. Varsinaiset erot kielissä johtuvat C:n yksinkertaisuudesta olioita tukeviin seuraajiinsa nähden.

Rivi 4:

return 0;

Ohjelmat voivat kertoa operaation onnistumisesta tai epäonnistumisesta virhekoodilla. Se on kokonaisluku, jonka ohjelman käynnistäjä saa haltuunsa. C-kielessä koodi annetaan main() -aliohjelman paluuarvona, eli tuon arvon asettaminen on viimeinen asia, minkä käyttäjän ohjelma suorittaa. Yleensä 0 tarkoittaa, että mitään virhettä ei tullut. Kokonaisluvun on tarkoitus olla positiivinen, ja esim. POSIX-standardin mukaan siitä huomioidaan vain 8 alinta bittiä. Näin ollen mahdollisia paluuarvoja on 256 kappaletta. POSIX määrää niistä osan tiettyihin tarkoituksiin. Katso tarvittaessa, mitä standardi sanoo aiheesta "exit code".

Rivi 5:

}

Kun aliohjelman sisällön määrittelevä lohko avataan ohjelman alussa, niin toki se pitää lopuksi sulkea. Ihan samoin kuin C#:ssa, Javassa ja muissakin lohkorakenteisissa kielissä, joiden syntaksi on tarkoituksella johdettu C:stä.

Käännä ja testaa ohjelmaa suorakäyttökoneessa shell-yhteydellä. Komenna:

c99 l04_helloworld.c

Jos kaikki meni hyvin, ohjelma kääntyi oletusnimelle a.out, jonka voit nyt suorittaa komennolla:

./a.out

Toivottavasti tulostui se, mitä odotitkin. Huomaa, että shell ei etsi ajettavia tiedostoja automaattisesti nykyisestä työhakemistosta. Ajaaksesi jotakin työhakemistosta, pitää kertoa eksplisiittisesti, että haluat ajaa ohjelman tästä hakemistosta eli ./ eli piste ja kauttaviiva.

Välitehtävä:Muuta ohjelma tulostamaan vaikkapa "Moikka kaikki ihqt" tai mitä nyt haluatkaan. Tallenna muutettu lähdekoodi samalle nimelle eli l04_helloworld.c, käännä uudelleen ja testaa että toimii odotusten mukaisesti. Olet nyt onnistuneesti ohjelmoinut C-kielellä! Saa juhlia hetkeä!

Ohjelman ilmeneminen laitteistossa: lähdekoodi, binääritiedosto

Toista itse luennoilla näytetyt esimerkit, joilla tutustuttiin tiedon esitystapoihin tietokoneessa.

Näytä olennaiset tiedot työhakemiston tiedostoista, mukaanlukien lähdekooditiedosto:

ls -l

Näytä enemmän metatietoja liittyen yhteen tiedostoon:

stat l04_helloworld.c

Näytä tiedoston sisältö (mikä on eri asia kuin sen sijainti tai muut metatiedot) tavu tavulta niin kuin se on:

hexdump -C l04_helloworld.c

Näytä kääntäjän tuottaman suoritettavan ohjelmatiedoston sisältö, sivuttajaohjelmaa hyödyntäen:

hexdump -C a.out | less

Noin viidennen luennon paikkeilla käytäneen läpi, mitä osioita suoritettavan ohjelmakoodin sisällä on, ja mitä toimenpiteitä käyttöjärjestelmän tarvitsee tehdä, että tällainen bittipötkö saadaan ladattua tietokoneen muistiin ja käynnistettyä prosessiksi.

Ensimmäinen katsaus komentorividebuggeriin (gdb)

Otetaan tässä demossa ensikosketus debuggeriin, jotta se tulee tutuksi mahdollisimman varhaisessa vaiheessa. Käännä ohjelma seuraavalla komentorivillä:

c99 -g -O0 l04_helloworld.c

Nyt argumentti -g ilmoittaa kääntäjälle, että käännettyyn ohjelmaan pitää laittaa mukaan virheenkorjausta eli debuggausta helpottavia asioita, kuten tieto alkuperäisen lähdekoodin tiedostonimistä ja rivinumeroista. Argumentti -O0 eli viiva, iso O-kirjain ja nolla puolestaan kieltää kääntäjää tekemästä automaattista optimointia konekielikoodiin. Tällä kurssilla käytetään mielellään kaikissa esimerkeissä optiota -O0, jotta syntyvä konekieli vastaa mahdollisimman läheisesti alkuperäistä C-ohjelmaa. Oikeiden tuotantokoodien julkaisussa puolestaan käytettäisiin esimerkiksi optiota -O3 jolloin ohjelman konekielinen koodi toimisi paljon nopeammin. Nopeutus perustuisi kuitenkin mm. konekielikäskyjen suoritusjärjestyksen muuttamiseen, konekielikäskyjen käyttämiseen eri tarkoituksiin kuin ne manuaalin mukaan on suunniteltu, muuttujien pitämiseen rekistereissä muistin sijasta ja ylimääräisen koodin generointiin ynnä muuhun automaattisesti tehtävään kikkailuun. Kääntäjän automaattisesti optimoima koodi voi olla hyvin kinkkisen näköistä suhteessa alkuperäiseen lähdekoodiin, millä emme halua vaivata päätä, kun tarkoitus on ymmärtää perusteita. Optimoimattomassa koodissa jokaista C-kielistä koodiriviä kohden generoituu korkeintaan muutamia selkeästi ymmärrettävissä olevia konekielisiä käskyjä, jotka ovat mukavan selkeästi peräkkäin.

Nyt pitäisi olla syntynyt taas suoritettava a.out, jonka koko on hieman aiempaa isompi, johtuen mukana olevasta debuggaustiedosta. Normaali suorittaminen näyttänee samalta kuin aiemmin. Nyt ajetaan ohjelmaa kuitenkin debuggerissa. Komenna shellissä:

gdb a.out

Mitäs nyt? Tilanne näyttää seuraavalta ja vaatii hieman selitystä:

[nieminen@halava esimerkit]$ gdb a.out
GNU gdb (GDB) Red Hat Enterprise Linux (7.2-75.el6)
Copyright (C) 2010 Free Software Foundation, Inc.
...
(gdb)   [ja vilkkuva kursori ...]

Käynnistit debuggerin. Ohjelmointi 1:ltä pitäisi olla jossain määrin tuttu asia jokin (esim. Visual Studion tai Eclipse IDE:n) graafinen debuggeri, jolla voi opiskella ohjelman toimintaa, ja jota voi käyttää virheenetsinnässä (siitä niiden nimi "de-" "bugger"). Nyt käytettävä gdb on samanlainen, mutta komentorivikäyttöinen. Tällekin on olemassa graafisia julkisivuja eli front-endejä, mutta interaktiivisten komentoriviohjelmien käyttö on yksi tämän kurssin teemoja, joten noudatamme sitä loppuun asti.

Debuggeri odottaa nyt siis tekstimuotoisia komentoja, joiden avulla voit tutkia argumenttina annetun ohjelman toimintaa. Komenna debuggeria:

run

Debuggeri käynnisti käännetyn ohjelman, antoi sen mennä niin pitkälle kuin se ilman virheitä etenee, tässä tapauksessa onnistuneeseen loppuun saakka. Tulostui toivon mukaan seuraavankaltaista:

Starting program: /autohome/nashome3/nieminen/charragit/itka203-kurssimateriaali-avoin/2015/esimerkit/a.out
Hello world!

Program exited normally.
Missing separate debuginfos, use: debuginfo-install glibc-2.12-1.149.el6_6.5.x86_64

Viimeinen rivi tarkoittaa, että suorakäyttökoneillamme ei ole asennettuna debug-tietoja standardikirjastoille. Ei välitetä siitä. Itse käännettyjen ohjelmien debuggaukseen tämä ei vaikuta, koska debug-tiedot laitetaan mukaan käännösvaiheessa. Ohjelman ongelmattomasta loppumisesta saatiin tässä ilmoitus. Jos ohjelma olisi kaatunut, debuggerin avulla voisi alkaa selvittelemään, miksi niin kävi...

Tekstipohjainen ohjelma on erilainen kuin graafinen ohjelma, mutta se voi olla käyttäjäystävällinen omalla tavallaan. Esim. gdb:n käyttäjäystävällisyys toteutuu mm. seuraavin tavoin:

  • Komennoista saa ohjeita komentamalla help. Eli komentoja ei tarvitse muistaa, vaan ne saa esille aina ohjelman käytön yhteydessä ilman tarvetta tarkistaa ulkopuolisesta manuaalista tai netistä.
  • Komennoissa on automaattitäydennys tabulaattorinäppäimellä, ja niille on lyhyitä "alias"-nimiä, jotka on nopea kirjoittaa (ks. helpit).
  • Komentohistoria on käytettävissä samoilla tehokäyttönäppäimillä kuin bash-shellissä.
  • Edellisen komennon voi toistaa painamalla pelkkää enteriä. Debuggerissa on tyypillistä mm. askeltaa ohjelmaa yksi rivi tai käsky kerrallaan useiden silmukkakierrosten ym. pitkien "jälkien" läpi, joten tämä on tehty yhtä helpoksi kuin enterin painelu peräkkäin.

Debuggerin käyttöliittymä on paljolti samanlainen kuin tehokkaan interaktiivisen shellin -- tai minkä tahansa hyvin tehdyn tekstipohjaisen ohjelman. Mm. suositun laskentaohjelmisto Matlabin "Command Window" on samalla tavoin kätevä. Tässä vaiheessa lienee käynyt selväksi, että tekstikomennoilla toimiva käyttöliittymä tukee luonnostaan myös komentojen täsmällistä tallentamista, toistamista ja skriptiksi asettelemista.

Käväistään vähän aikaa pois debuggerista; siitä pääsee pois komennolla:

quit

Olet nyt käyttänyt komentorividebuggeria kerran ja lopettanut sen nätisti. Seuraavaksi tutustutaan debuggeriin askelta tarkemmin. Eli askelletaan ohjelman läpi rivi riviltä.

Eli uudelleen vaan käyntiin:

gdb a.out

Debuggeri on jälleen ladannut pyydetyn ohjelmatiedoston, ja se on valmis avaamaan konepellin oikein kunnolla. Katsotaan ensin yleiskuvaa ohjelmasta debuggerin disassembly-ominaisuudella. Komenna seuraavasti:

disassemble /mr main

Debuggerin ohjauskomento on tässä disassemble eli "pura konekieli symboliseen, ihmisen ymmärtämään, muotoon". Oletusarvoisesti komento näyttäisi vain käskyjen muistiosoitteet ja symbolit, mutta täydellisyyden vuoksi käytetään nyt lisukkeita /m ja /r joilla mukaan liitetään debug-tiedoista saatu alkuperäinen lähdekoodi sekä todellinen muistissa sijaitseva konekielinen tavujono.

Koska ohjelmaa ei vielä ole käynnistetty, pitää gdb:lle kertoa eksplisiittisesti, mistä aliohjelmasta disassembly halutaan tehdä (toki näin voi aina tehdä muutenkin; oletuksena disassembly näytetään siitä aliohjelmasta, jonka sisällä suorituskohta milloinkin on).

Pitäisi näkyä pääohjelman assembler-koodi alkaen jotenkin seuraavasti:

Dump of assembler code for function main:
2       int main(int argc, char **argv){
   0x00000000004004c4 <+0>:      55     push   %rbp
   0x00000000004004c5 <+1>:      48 89 e5       mov    %rsp,%rbp
   0x00000000004004c8 <+4>:      48 83 ec 10    sub    $0x10,%rsp
   0x00000000004004cc <+8>:      89 7d fc       mov    %edi,-0x4(%rbp)
   0x00000000004004cf <+11>:     48 89 75 f0    mov    %rsi,-0x10(%rbp)

3         printf("Hello world!\n");
   0x00000000004004d3 <+15>:     bf e8 05 40 00 mov    $0x4005e8,%edi
   0x00000000004004d8 <+20>:     e8 db fe ff ff callq  0x4003b8 <puts@plt>

4         return 0;
   0x00000000004004dd <+25>:     b8 00 00 00 00 mov    $0x0,%eax

5       }
   0x00000000004004e2 <+30>:     c9     leaveq
   0x00000000004004e3 <+31>:     c3     retq

End of assembler dump.

Nyt, kun ohjelma on käännetty debug-tietojen kanssa, osaa gdb liittää assembler-tulosteeseen C-koodirivit (rivinumero ja kyseisellä rivillä oleva koodi). Tämä on havainnollista, koska nähdään suoraan, mikä koodin osa kääntyy minkäkinlaiseksi konekieleksi. Huom: tämä demo on vasta työkaluun tutustumista ja ennakoiva katsaus.. tässä nähtävien käskyjen ja rekisterien roolista puhutaan kaikessa rauhassa luentojen 6 ja 7 paikkeilla ja mahdollisesti myöhemminkin, ja niistä jatketaan pienin askelin myöhemmissä demoissa. Tässä vaiheessa riittää oppia ja kokeilla itse, miten saat käännetyn ohjelman konekielen debuggerilla avattua konkreettisesti silmiesi eteen, ja tietää alustavasti minkä tyyppisiä asioita debuggeri silloin näyttää.

Selitys asioille, jotka debuggeri näyttää:

  • Vasemmassa reunassa on 64-bittinen heksaluku. Tämä on muistiosoite, josta käskyn konekielinen bittijono alkaa.
  • Seuraavaksi on suhteellinen osoite aliohjelman alusta lähtien. Siis esim. <main+4> tarkoittaa että kyseinen käsky alkaa muistipaikasta, joka saadaan lisäämällä main() -aliohjelman ensimmäisen käskyn osoitteeseen 4. Huomataan, että x86-64:ssä konekielikäskyjen bittijonot voivat olla eri mittaisia (riippuen siitä, onko mukana vakioarvoja, ja siitä ovatko käskyt alkuperäisiä 8086-käskyjä vai myöhempiä laajennoksia).
  • Seuraavaksi on konkreettinen käsky tavujonona siten kuin se on tietokoneen muistissa tallessa, peräkkäisissä osoitteissa.
  • Sitten on käsky ns. AT&T-syntaksilla eli mnemonic ja operandit. Luentomonisteen konekielitä esittelevä luku kertoo näistä esimerkkejä; totuus rajapintasopimuksista on prosessorivalmistajan julkaisemassa manuaalissa. Yksi assembly-kielinen rivi ja sen konekielinen tavujono vastaavat käytännössä toisiaan lähes yksi-yhteen.

Huom: Suurin osa ohjelmien tarvitsemista rakenteista hoituu muutamilla peruskäskyillä, joista kaikkein yleisimpiä kiteytetään luentomonisteessa tämän johdantokurssin esimerkeiksi. Nykyiset prosessorimanuaalit ovat kuitenkin laajuudeltaan useampituhatsivuisia, johtuen niiden monipuolisuudesta: Joitakin satoja sivuja on käskyjen toiminnan kuvailua. Valtaosa nykyprosessorin käskyistä liittyy erityistehtäviin kuten liukulukulaskentaan. Myös prosessoriytimien keskinäinen synkronointi edellyttää runsaasti sivuja nykyprosessorien manuaaleissa. Erityisesti x86-64:n manuaaleja pidentää myös se, että ne joutuvat kuvailemaan myös kaikkien aiempien x86-sarjan prosessorien toiminnan, koska yhteensopivuus aiemman binäärikoodin kanssa on säilytetty. Manuaalien alkupuolella on hyödyllistä ja selkeästi kirjoitettua johdantomaista selitystä laitteiston toiminnasta. (Vinkki iltalukemiseksi, mikäli kiinnostut aiheesta enemmän.)

Nyt kun ymmärrät näkemäsi, askella vielä omin käsin läpi "Hei maailma"-ohjelma C-koodirivi kerrallaan, samoin kuin luennolla näytettiin. Seuraavassa vielä uudelleen muistin virkistämiseksi:

Näin debugger antaa automaattisten alkutoimenpiteiden tapahtua ja pysäyttää suorituksen main-aliohjelman ensimmäisen koodirivin kohdalle:

start

Katso ja tulkitse näkymää itse. Heitä uudelleen (komentohistoriasta tietysti, nuolinäppäimellä!) edellinen komento:

disassemble /mr main

Katso ja tulkitse näkymää itse. Sehän on tietysti muuten sama kuin aiemmin, mutta nyt gdb näyttää vasemmassa laidassa symbolit => sen rivin alussa, jossa on seuraavaksi suoritettava konekielinen käsky. Ei vielä väliä, mitä käskyt tekevät. Totea nyt vaan, mitä tapahtuu, kun askellat yhden koodirivin eteenpäin:

step

Kaikki riviin liittyvät konekielikäskyt on nyt tehty ja debugger on pysäyttänyt suorituksen ennen seuraavaa lähdekoodiriviä. Totea tämä itse disassemblystä:

disassemble /mr main

Steppaile sitten vielä, ja totea tilanne:

step

step

Viimeisen stepin jälkeen aliohjelman main() suoritus päättyy. Voit itse todeta saman, mikä luennollakin nähtiin: päädytään alustakirjaston puolelle, josta järjestelmäämme ei nyt ole asennettu lähdekoodia tai debuggaustietoa, joten debuggeri ei voi paljoa enempää näyttää, vaan se antaa automaattisesti liitetyn alustus- ja lopetuskoodin hoitaa ohjelman lopputoimet. Sovellusohjelman ulkopuolista alustus- ja lopetuskoodia tarvitaan hoitamaan mukavuus- ja välttämättömyysseikkoja, jotka liittyvät tiettyyn käyttöjärjestelmätoteutukseen. Esimerkiksi C-kielen määrittelyssä ohjelman virhekoodiksi tulkitaan main-aliohjelman paluuarvo, mutta käyttöjärjestelmätoteutus odottaa saavansa sen käyttöjärjestelmän kutsurajapinnan kautta, johon alustariippumaton C-koodi ei edes pääse itse käsiksi. Normaalisti tarvitaan siis vielä jotakin pikkukoodia, joka tekee järjestelmäsidonnaiset alkutoimet, kutsuu mainia, ja suorittaa lopputoimet. Luennoilla (noin 5. tai 6. luento) nähdään myös, miten nämä pikkukoodit voitaisiin ohittaa, mutta jouduttaisiin tekemään rumia ja epästandardeja temppuja. Siis "mitä tapahtuu luennolla puhtaan assemblerin osalta, jääköön luennolle" (tekemisen tasolla, mutta niiden asioiden olemassaolo täytyy ymmärtää, ettei ohjelman käynnistyksen, suorituksen ja käyttöjärjestelmäkutsun roolin suhteen jää epäselvyyksiä).

Välitehtävä:Edellä kerrottuja ohjeita tai luentoesimerkkiä seuraten, lataa debug-tiedot sisältävä helloworld-ohjelma debuggeriin, ota sen aliohjelmasta main omin käsin disassembly, jossa näkyy alkuperäiset lähdekoodirivit ja syntynyt konekieli. Askella ohjelma läpi lähdekoodirivi kerrallaan step-käskyllä.

Komentoriviargumentit, ympäristömuuttujat

Ohjelman toimintaan voi perinteisesti vaikuttaa mahdollisen graafisen tai tekstimuotoisen käyttöliittymän ja syötetiedostojen ym. reaaliaikaisten keinojen lisäksi kahdella vakiintuneella (ja mm. POSIXin edellyttämällä) tavalla: komentoriviargumenteilla ja ympäristömuuttujilla.

Komentoriviargumentit

Jotta yliopittaisiin samalla myös tähän asti nähtyjen shell-komentojen argumenttien periaate, katsotaan päällisin puolin pääsyä argumentteihin C-kielessä ja Javassa. Samalla nähdään, miten tietyt syntaksit voivat olla identtisiä C:ssä ja Javassa. Ohjelmointi 1 -kurssilta tutumpi C# on varmasti erittäin lähellä Javaa.

Komentoriviargumentit Javassa:

public class Argumentit{
    public static void main(String[] args){
        int i;
        System.out.printf("Ohjelman saamat komentoriviargumentit ovat:\n");
        for(i=0; i<args.length; i++){
            System.out.printf("Argumentti %d: %s\n", i, args[i]);
        }
    }
}

Komentoriviargumentit C:ssä:

#include <stdio.h>
int main(int argc, char **argv){
    int i;
    printf("Ohjelman saamat komentoriviargumentit ovat:\n");
    for (i=0; i<argc; i++){
        printf("Argumentti %d: %s\n", i, argv[i]);
    }
}

Katselu ja ymmärtäminenkin riittää, mutta jos haluat kokeilla, niin nämä ohjelmat voi kirjoittaa (tai copy-pastata) tekstieditorilla pääteyhteydessä ja kääntää suorakäyttökoneella komentamalla:

javac Argumentit.java

c99 argumentit.c

Sitten voisit konkreettisesti todeta että syntyi käännetyt tiedostot Argumentit.class ja a.out Ne voi ajaa esim. komentamalla:

java Argumentit kissa koira

./a.out kissa koira

Tulosteita voisi tarkastella ja erilaisia argumentteja voisi kokeilla kissan ja koiran tilalle. Huomioita:

  • Tavukoodiksi käännetyn Java-ohjelman ajamista varten käynnistetään itse asiassa java -niminen virtuaalikoneohjelma, jonka ensimmäisen argumentin pitää olla ajettavan luokan nimi (ilman tarkennetta .class); kaikki loput argumentit välittyvät suoritettavalle Java-ohjelmalle main-metodin taulukkoparametriksi nimeltään args.
  • C-käännös on suoraan ajettavissa x86-64:ssä, joten Linux lataa ja käynnistää sen suoraan, ja tiedottaa kaikki argumentit komentoriviltä ohjelmalle.
  • C-käännös itse asiassa saa ensimmäisenä argumenttina (indeksi 0) ohjelman nimen siten kuin käyttäjä sen kirjoitti. Javassahan tuli vain argumentit eikä ohjelman nimeä. C-ohjelman osalta POSIX määrittelee kevyellä sanamuodolla ("should") että indeksillä nolla löytyvän argumentin "pitäisi olla nimi, joka liittyy käynnistetyn ohjelman sijaintiin". Käytännössä tänä päivänä tähän voi useimmissa järjestelmissä luottaa, eli ihan de facto standardi menettely tuo argv[0]:n ja shell-skripteissä vastaavasti $0:n sisältö vaikuttaisi olevan.
  • Java-dokumentaatio sanoo, että tällainen komentoriviargumenttien syöttäminen "ei ole 100% Javaa", vaikka pääohjelman parametrilistan sopimisesta voisi niin päätellä. Kumma juttu...

Alustavia huomioita syntaksista:

  • for-silmukan syntaksi on tässä tapauksessa täysin sama molemmissa kielissä
  • Java-kielen peruskirjastosta löytyy nykyään PrintStream-luokkassa formatoidun tulostuksen hoitava metodi printf(), joka tekee lähes samalla syntaksilla samat kätevät asiat kuin C-kielessä. Myös C# pystyy formatoituun tulostukseen vaikkapa ihan WriteLine() -metodilla. Formaattimerkkijonon syntaksi on kuitenkin C#:ssa erilainen kuin C:ssa (ja POSIXissa), joten siitä ei puhuta enempää nyt - selvitä itsellesi aina, miten formatoitu tulostus toimii siinä ympäristössä, jolla milloinkin ohjelmoit... Tavattoman kätevää on se nimittäin aina!
  • Käytännön erot syntaksissa olivat siis tässä tapauksessa pieniä, eikä suuria ole odotettavissakaan, vaikka havaittaneen, että C:n lähdekoodi on toisaalta rajoitetumpaa ja toisaalta ehkä osin "kryptisempää" kuin C#:ssa tai Javassa. Luonnollinen syy tälle tilanteelle on, että uudempien kielten määrittelyssä voi aina yrittää oppia virheistä tai epäselvyyksistä aiempien kielten kohdalla. C# on uudempi kuin Java, joka on uudempi kuin C (joka on uudempi kuin B, joka on uudempi kuin BCPL, ...). Vaikka C-kielen uusin versio on vain pari vuotta vanha, 1970-luvulla tehtyjä "virheliikkeitä" ei voi kumota, koska vanhan koodin pitää edelleen kääntyä ja toimia uudessa versiossa.

Tähän astisen tarkoitus oli saada mahdollinen "C-pelkokerroin" putoamaan välittömästi. Kyseessä on suurelta osin tuttu asia, jos osaat jonkin verran ohjelmoida esimerkiksi C#:lla tai Javalla, kuten esitietokurssin Ohjelmointi 1 pohjalta tulisi olla asia.

Kuitenkin yksi merkittävä ero on nähtävissä taulukon ja merkkijonojen käsittelyssä:

  • Javassa, C#:ssa ja muissa oliokielissä taulukko (esim. tässä args) on viite olioon, jonka rajapinnassa on attribuutti / ominaisuus, jonka kautta voi kysyä olioinstanssin pituutta eli elementtien määrää. Javassa attribuutti on nimeltään length ja C#:ssa ominaisuus on nimeltään Length.
  • C:ssä ei ole luokkia, eikä taulukon pituutta voi saada ominaisuuden arvona tai metodikutsulla. Siksi taulukon pituus tulee pääohjelman osalta erillisessä kokonaislukumuuttujassa, tyypillisesti nimeltään argc eli oletettavasti "argument count". Taulukko puolestaan on pötkö dataa, joka alkaa muistiosoitteesta nimeltään argv eli oletettavasti "argument vector".
  • Merkkijonot puolestaan ovat merkkitaulukoita eli pötköjä, joita käsitellään ensimmäisen merkin osoitteella. Tässä siis argv on osoite, josta löytyy peräkkäin lisää osoitteita. Tyyppi on tämän vuoksi char ** eli "merkin muistiosoitteen osoite". Asiaan palataan tarkemmin seuraavassa demossa ja myöhemmissä luentoesimerkeissä.

Ympäristömuuttujat

Argumenttien, eli komentoriviltä annettavien, välilyönneillä erotettujen, merkkijonojen lisäksi jokainen suoritettava ohjelma saa käyttöönsä ympäristön (engl. environment). POSIX määrittelee standardin muodon ympäristölle, mutta sama periaate toteutuu myös esimerkiksi Windowsissa. Winkkari-puolella ohjelmien toimintaan voi vaikuttaa myös asetuksilla järjestelmänlaajuisessa ns. "rekisterissä". Toimintaidealtaan kaikki nämä tavat ovat hyvin samankaltaisia.

POSIXin määrittelemä ympäristö koostuu ympäristömuuttujista (engl. environment variable). Jokainen ympäristömuuttuja on merkkijonopari: Toinen merkkijono määrittelee nimen (eli "avaimen"), jonka kautta sisältöä käsitellään. Toinen merkkijono puolestaan on ympäristömuuttujan arvo. POSIX ei määrittele kovin monia nimiä, joille yhteensopivan järjestelmän tulisi määritellä arvoja, mutta se mainitsee kevyesti tyypillisiä käytössä olevia nimiä, joita kannattaa käyttää lähinnä mainittuihin tarkoituksiin eikä mihinkään omiin "sooloiluihin".

Ympäristömuuttujia voi asettaa esimerkiksi shellissä, jolloin ne asettamisen jälkeen näkyvät kaikille ohjelmille, jotka käynnistetään samassa interaktiivisessa shell-sessiossa tai skriptissä. Yksi tyypillinen käyttötarkoitus shell-skriptille onkin hoitaa tietyn nimisiin ympäristömuuttujiin tietyt, paikallisen asennuksen tarpeiden mukaiset, arvot, ja käynnistää varsinainen ohjelma vasta sitten, kun sitä ohjaavat ympäristömuuttujat on asetettu. Myös etäkoneelle kirjautumisen yhteydessä yleensä suoritetaan tietty skripti, jossa voi asettaa ympäristömuuttujia jokaista käynnistyvää sessiota varten.

Esimerkki: kieliasetukset

Esimerkin vuoksi kokeillaan asettaa kansainvälisyysasetukset siten, että ei tarvitse kärsiä puoliksi käännetyistä suomenkielisistä teksteistä eikä suomenkielisistä virheilmoituksista, joiden perusteella on hyvin vaikea löytää Internetistä ohjeita. Olemme kohtalaisen pieni kielialue. Englanniksi löytyy apuja helpommin. Suomennoksetkin vaikuttaisivat olevan jossain määrin keskeneräisiä ja epätäydellisiä (tilanne 2016).

POSIX määrittelee, että ohjelmia voi pyytää toimimaan ympäristön paikallisuuden eli "lokaalin" (engl. locale) mukaisesti eri osa-alueiden osalta, jotka tyypillisesti ovat erilaisia eri kulttuureissa - mm. päivämäärät ja kellonajat, desimaaliluvut ja rahamäärät saatetaan kirjoittaa vaihtelevilla käytänteillä. Kuinka hyvin ohjelmat sitten tukevat erilaisia lokaaleja, on laatukysymys.

Virheilmoitusten saaminen englanniksi on helpointa saada aikaan asettamalla samalla kertaa kaikki kieliasetukset esimerkiksi yhdysvaltain muotoon. Käytännössä täytyy asettaa ympäristömuuttuja nimeltään LC_ALL arvoon en_US.utf8. Suorakäyttökoneillamme tämän voi hoitaa bash-sessiossa seuraavalla komennolla:

export LC_ALL="en_US.utf8"

(Konkretiaa haluaville: komento export on POSIXin määräämä, joten ''lähes yhteensopiva'' bash tuntee sen. Sillä julkaistaan ympäristömuuttuja näkymään kaikkissa ohjelmissa, joita kyseisestä shell-sessiosta tai skriptistä jatkossa käynnistetään. Myös ympäristömuuttujan nimi LC_ALL on POSIXin määräämä. Sisältö kuitenkin voi olla vain sellainen kielimäärittely, joka löytyy juuri kyseiseen järjestelmään asennettuna. POSIX ei edellytä minkään maan kieliasetusten olemassaoloa. Asennetut kielet saa listattua komennolla locale -a, jonka antamasta listasta tuo kyseinen arvo en_US.utf8 on poimittu, sen sijaan että hatusta vedetty tai tuulesta temmattu olisi tässä meidän tapauksessamme.)

Samalla ikävä kyllä ilmoitustekstien lisäksi myös muut säädöt alkavat toimia jenkkienglannin mukaisesti - päivämäärät näyttävät hassuilta suomalaiseen silmään ja ääkkösiä sisältävien merkkijonojen lajittelu ei välttämättä vastaa suomalaista aakkosjärjestystä (voi mahdollisesti olla, että ä lajitellaan tasa-arvoisesti a:n kanssa ja ö tasa-arvoisesti o:n kanssa).

POSIX-standardi kertoo esimerkkien kautta hienojakoisemmista kieliasetuksista, missä käytetään useampaa ympäristömuuttujaa, nimiltään LC_COLLATE, LC_CTYPE, LC_MESSAGES, LC_MONETARY, LC_NUMERIC ja LC_TIME. On esimerkiksi mahdollista pyytää viestit ranskaksi, merkkijonojen lajittelu saksaksi, desimaaliluvut suomeksi (eli pilkulla, ei pisteellä eroteltuna) ja päivämäärät yhdysvaltalaisittain, jos tällaisessa yhdistelmässä sattuisi olemaan käyttötarkoituksen kannalta jotakin järkeä. Jos tuo kaiken yliajava LC_ALL on asetettu, hienojakoisemmat säädöt jätetään huomioimatta.

Tässä harjoituksessa varmistetaan vähintään, että osaat säätää oman shell-session kaikki kieliasetukset englanniksi asettamalla LC_ALL -ympäristömuuttujan. Silloin osaat ylipäätään asettaa minkä tahansa ympäristömuuttujan sillä tavoin, että se näkyy kaikille suoritettaville ohjelmille.

Mikäli löydät itsellesi mieleisen kieliasetusten kombinaation, saat sen voimaan jokaiseen uuteen bashia käyttävään pääteyhteyteen editoimalla kotihakemiston tiedostoa .bash_profile lisäämällä sinne export-rivin. Kyseinen tiedosto suoritetaan automaattisesti silloin, kun interaktiivinen bash käynnistyy kirjautumisen yhteydessä. Tällöin ei tarvitse tehdä asetuksia aina uudelleen pääteyhteyssessioiden välillä.

Vastaavasti kotihakemiston tiedosto .bashrc näemmä suoritetaan jokaisen bash-käynnistyksen yhteydessä ja sitä kautta jokaisen skriptin alussa... Tälle voi olla tarkoituksensa, mutta täytyy huomata, että ympäristön normaali periytyminen lapsiprosessille menee silloin rikki noiden .bashrc:ssä tehtyjen muutosten kohdalla, jos käynnistyvä prosessi sattuu olemaan bash-skripti - ei kuitenkaan muunlaisten ohjelmien osalta. Voi tulla kummallisia ja odottamattomia ilmiöitä.

(Luennoitsija kampitti itsensä jollain vuosien takaisella "näppäryydellään" kevään 2015 kurssin viidennellä luennolla: Luennolla näytetty kieliasetuksen muutos bash-shellissä ei näyttänyt vaikuttavan suoritettavaan skriptiin, jossa date -komennon tuloste pysyi itsepintaisesti ja "maagisesti" suomalaisessa formaatissa, vaikka juuri oli asetettu jenkkienglannin mukaiset kieliasetukset LC_ALL -ympäristömuuttujalla. Magiikkaa näissä hommissa ei kuitenkaan koskaan ole, vaan syy oli joskus viimeisen 16 vuoden aikana lisätty rivi export LC_ALL=fi_FI.utf8 tiedostossa .bashrc. Ei mitään havaintoa, miksi tämä tuntui joskus hyvältä idealta lisätä. Nyt oli kuitenkin ihan hyvä aika poistaa se. Aa.. sylttytehdas alkaa löytyä jälkien perusteella.. Linkin IRC-ohjeessa taidetaan puhua em. asetuksen laittamisesta .bashrc -tiedostoon.. jos muistais joskus jutella linkkareiden kanssa, niin ehkä tätä kohtaa ohjeesta voisi kaikkien näiden vuosien jälkeen tarkistaa...)

Pakollinen palautustehtävä

Pakollisessa palautustehtävässä verifioidaan, että olet onnistuneesti kääntänyt ja käynnistänyt C-kielisen ohjelman, antanut sille yhden argumentin, joka sisältää välilyönnin, ja asettanut ympäristömuuttujan avulla kieliasetukset ainakin siinä shell-sessiossa, jossa ajat kääntämäsi ohjelman. Vapaaehtoisena lisäosiona katsastetaan, että olet osannut laskea ja muuntaa lukuja eri lukujärjestelmissä.

Vapaaehtoisen palautuksen kannalta oleelliset laskutoimitukset ovat seuraavaksi alla:

[luku1]: Muunna syntymävuotesi heksadesimaaliluvuksi


[luku2]: Laske käyttämällä edellisen muunnoksen tulosta
         [luku1] + 0xAB

[luku3]: Laske binäärijärjestelmän luvulla käyttäen edellisen kohdan tulosta. Ilmoita vastaus binäärilukuna.
         10101011b - [luku2]

[luku4]: Mikä luku tarvitsee lisätä tai poistaa edellisen kohdan
         vastauksesta [luku3], jotta saat taas syntymävuotesi? Jos jotain tarvitsee
         lisätä, laita vastaus muodossa +[luku4], jos taas vähentää laita vastaus
         muodossa -[luku4] ja kirjoita vastaus kymmenjärjestelmän lukuna

Laskutoimituksen osalta palautus tehdään siten, että kirjoitat alla olevan tekstin uuteen tiedostoon sanatarkasti (copy-paste ehkä rikkoo tai ehkä ei riko rivinvaihtoa riippuen toimintaympäristöstäsi, joten jos kopioit niin varmista että tiedosto varmasti näyttää oikean muotoiselta):

Muunnos heksadesimaalijärjestelmästä kymmenjärjestelmään: [luku1]
Laskutoimitusten tulokset: [luku2] [luku3] [luku4]

Korvaa saamasi vastaukset vastaavien ''muuttujien'' tilalle ja lukujärjestelmien osuus on palautusmuodossa.

Pakollisen C-ohjelman osuuden vastaus tuotetaan kääntämällä ja ajamalla alussa mainitusta URLista haettu C-lähdekoodi nimeltään d03_fiilikset.c siten, että tuloste ohjautuu shellissä vastaustiedostoon unix-rivinvaihtoineen aiemmissa demoissa opitulla syntaksilla, jossa käytetään väkästä >. Ei mitään copy-paste -kikkailuja tai "tarpeettomia echo-komentoja" edellenkään. Tulosteen pitäisi näyttää seuraavan malliselta (paitsi käyttäjätunnus on tietenkin omasi eikä nieminen, tuloste vastaa omia tunnelmiasi kurssilla jne..):

Ympäristömuuttuja USER   == nieminen
Ympäristömuuttuja HOME   == /nashome3/nieminen
Ympäristömuuttuja PWD    == /nashome3/nieminen/kj15_esimerkkidemot/demo3
Ympäristömuuttuja LANG   == en_US.utf8
Ympäristömuuttuja LC_ALL == en_US.utf8
argv[0] == ./a.out
argv[1] == Ihan kivalta tuntuu: uskokaa tai älkää, niin aikataulu pitää aiempaa paremmin.
Kertauksena: käyttäjän 'nieminen' tunnelmat tässä vaiheessa kurssia:

Ihan kivalta tuntuu: uskokaa tai älkää, niin aikataulu pitää aiempaa paremmin.

Kieliasetusten täytyy olla ohjelmaa ajettaessa joko britti- tai jenkkienglanti UTF8-merkistökoodauksella.

Keväällä 2017 kurssin demot ja niiden palautus hoidetaan Optimassa. Kullekin demolle on oma palautuslaatikko, johon tehtävässä tuotettu tiedosto palautetaan.

Tästä demosta palautetaan tasan yksi tiedosto nimeltään "d3_vastaus.txt", joka on luotu C-ohjelman osuuden vastaukset tuohon yhteen tiedostoon. Jos teet vapaaehtoisen osion, niin yhdistä saman tiedoston alkuun vapaaehtoisen lukujärjestelmäosuuden vastaus. Yhdistäminen kannattaa tuotaa cat ohjelmaa käyttäen (lue tarvittaessa manuaalista ohjelman käyttötapa). Palautustiedoston voi luoda myös muulla määritteet täyttävällä tavalla, mutta siinä tapauksessa pitää vain olla tarkkana että vastaustiedosto näyttää oikealta. Loppuviimeksi vapaaehtoisen palautustiedoston pitäisi siis näyttää jotenkin tältä:

Muunnos heksadesimaalijärjestelmästä kymmenjärjestelmään: [luku1]
Laskutoimitusten tulokset: [luku2] [luku3] [luku4]
Ympäristömuuttuja USER   == nieminen
Ympäristömuuttuja HOME   == /nashome3/nieminen
Ympäristömuuttuja PWD    == /nashome3/nieminen/kj15_esimerkkidemot/demo3
Ympäristömuuttuja LANG   == en_US.utf8
Ympäristömuuttuja LC_ALL == en_US.utf8
argv[0] == ./a.out
argv[1] == Ihan kivalta tuntuu: uskokaa tai älkää, niin aikataulu pitää aiempaa paremmin.
Kertauksena: käyttäjän 'nieminen' tunnelmat tässä vaiheessa kurssia:

Ihan kivalta tuntuu: uskokaa tai älkää, niin aikataulu pitää aiempaa paremmin.

Jos et tee vapaaehtoista osuutta, niin palautustiedostosta pitäsi löytyä pelkästään C-ohjelman tulosteen asiat.