Sormet C:hen

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

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

Jyväskylän yliopiston tietotekniikan laitos.

STATUS: Tämä on nyt päivitetty vuodelle 2021. Jos aloitit aiemmalla versiolla, suosittelen tekemään palautustiedoston uudelleen nykyisellä tehtäväkoodilla, niin ei tarkastaessa tarvitse ihmetellä puuttuvaa tietoa lempikalasta..

Tehdään korjauksia aina, jos havaitaan tarpeelliseksi. Ilmoita etenemistä estävistä ongelmista vertaistukikanavalla, niin tieto menee saman tien kaikille, eikä monien tarvitse ihmetellä samaa juttua turhaan.

Sisällys

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 kuoressa pitäisi olla tuttua samoin kuin ohjelmien tulosteiden tulkitseminen päätteeltä ja interaktiivisten tekstipohjaisten ohjelmien käyttelyn periaate.

Huomioi seuraavaa:

  • Käännökset ja kokeilut on tarkoitus tehdä pääteyhteydellä yliopiston etäkoneessa jalava.cc.jyu.fi tai halava.cc.jyu.fi
  • Jomman kumman suorakäyttökoneille asennetun tehokkaan tekstieditorin (emacs tai vim) opettelu on erittäin vahvasti 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ä absoluuttisessa 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.
  • Tällä kurssilla ei tehdä kovin suuria ohjelmointisuorituksia. Enemmänkin katsellaan ja yritetään ymmärtää. Mikäli ohjelmointi tuntuu hankalalta ymmärtää, voi olla syytä kerrata kurssin Ohjelmointi 1 asioita. Muista myös kysyä apua ennen kuin menee liikaa aikaa pelkän turhautumisen parissa. Kohtuullinen määrä turhautumista ja tuskaa kuuluu asiaan, kun opitaan uutta.

Harjoituksen tavoitteet:

  • Teet omin käsin käytännön toimenpiteitä, joita luentomonisteessa ja mahdollisissa luentoesimerkeissä näytetään, 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 yhteensopivan järjestelmän "matalan tason" kirjastoissa.
  • Perinteisistä ohjelmien toiminnan mukauttamiseen vaikuttavista keinoista käydään tarkoin läpi komentoriviargumentit ja ympäristömuuttujat.

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

Alustavalla tasolla, C-kielen osalta, opiskelija demon sisäistettyään:

Lisäksi tähdätään jatkuvasti kohti useaa muutakin osaamistavoitetta, 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, muistele miten screeniä käytellään näppärästi, ja aseta sopivaksi katsomassasi screen-ikkunassa työhakemistoksesi tämän kolmannen demon hakemisto, nimeltään esimerkiksi ~/kj21/demo3/ tai muuta vastaavaa. (Muistele tarvittaessa aiempia demoja: miten hakemisto vaihdettiin, miten varmistettiin, mikä on nykyinen työhakemisto, jne. ... Tee vaikka itsellesi omaa muistilistaa olennaisimmista komennoista, kunnes ne alkavat löytyä selkäytimestä.)

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

wget https://gitlab.jyu.fi/itka203-kurssimateriaali/itka203-kurssimateriaali-avoin/-/raw/master/2015/esimerkit/l04_helloworld.c

Huomautus 1: Pitkä ja hankala URL varmaan kannattaa varovaisesti kopioida 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 kuoreen, 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.

Huomautus 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. Skripteissä näkee paljon myös samaan tarkoitukseen käytettävää komentoa curl, joka on sallivammin lisensoitu, kaiketi alustariippumattomampi ja virallisen nettisivun mukaan miljardien ihmisten käytössä päivittäin, konepeltien alla ( https://curl.haxx.se ). Laajemman yhteensopivuuden takaamiseksi kannattaa käyttää skripteissä curl -työkalua, jos tarkoitus ei ole sitoa skriptiä GNU-työkaluihin, jollainen wget on.

Hae vielä seuraava:

wget https://gitlab.jyu.fi/itka203-kurssimateriaali/itka203-kurssimateriaali-avoin/-/raw/master/2015/demot/mallikoodia/d03/d03_fiilikset.c

Ja mikäli kaikista suosituksistamme 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://gitlab.jyu.fi/itka203-kurssimateriaali/itka203-kurssimateriaali-avoin/-/raw/master/2015/demot/mallikoodia/d03/nanorc_patka

cat nanorc_patka >> ~/.nanorc

Huomautus 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!

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 (kuten 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 niin sanottuja otsikkotiedostoja (engl. 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.it.kurssit.munkurssi.Harpake voi olla aivan erilainen kuin joku.muu.organisaatio.kirjasto.Harpake. Tiedoston alussa voisin haluta kirjoittaa siis import fi.jyu.it.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 valmiiksi käännettyinä (linkitysjärjestelmän avulla, mikä on tyypillinen ja joustavampi tapa). Muista, että kaikkia asioita voidaan katsoa tarkemmin luennoilla, sikäli kuin kysymyksiä herää ja esitetään.

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 muiden muassa 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.

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 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 tyypiltään int ja toinen on tyypiltään char** eli osoitin merkkijonotaulukkoon (älä vielä huoli tietotyypeistä, niihin syvennytään vasta 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 kaikissa näissä kielissä täysin sama: lohko alkaa aaltosululla { ja päättyy vastaavaan käänteiseen sulkuun }. Muutenkin syntakseissa on paljon samaa. Esimerkiksi C-kielen 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 lisämääreet eivät kuitenkaan tarkoita samaa uudemmissa kielissä! Esimerkiksi static on C-kielessä ihan eri asia kuin C#:ssa tai Javassa. Päivän analogia: "pulma" on suomeksi ongelma ja viroksi häät. Pitää mielellään aina tietää, millä kielellä puhutaan.

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 konekielikäskyyn sen jälkeen, kun parametriksi on laitettu merkkijonon Hello world!\n ensimmäisen merkin eli kyseisen H-kirjaimen osoite tietokoneen muistissa.

Merkkijonon perässä oleva \n eli kenoviiva ja än-kirjain tarkoittavat rivinvaihtomerkkiä, joka tulostuu ihan kuin mikä tahansa merkki. printf ei tulosta rivinvaihtoa oletuksena, vaan se pitää erikseen laittaa mukaan tulostettaviin merkkeihin.

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 ja aliohjelmasta palaaminen on viimeinen asia, minkä ohjelmoijan määrittelemä C-ohjelma tekee. Automaattisesti liitetty standardikirjasto ja käyttöjärjestelmä hoitavat loppusiivouksen siitä eteenpäin.

Yleensä virhekoodi 0 tarkoittaa, että mitään virhettä ei tullut. Kokonaisluvun on tarkoitus olla etumerkitön (siis aina 0 tai positiivinen), ja esimerkiksi 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 kuoren kautta. 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ä kuori 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.

Miksi oletusnimi on juuri a.out eikä jotain muuta? Kevään 2019 luennolla seikkailtiin hieman Internetissä, ja löydettiin sellainen historiallinen etymologia, että a.out tarkoittaa "assembler output" eli "kokoonpanijan tuloste". Nykyisessä käännös- ja linkitysprosessissa oletusnimi on lähinnä historiallinen jäänne.

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ä! Nyt on hyvä hetki esimerkiksi juhlistaa tätä saavutusta hieman ja pitää pieni tauko, jonka aikana aiemmin opittu ehtii hiukan mennä kohti selkäydintä!

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

Kokeile itse, ajatuksen kanssa, luentomonisteen esimerkkejä, joilla voi tutustua tiedon esitystapoihin tietokoneessa. Samalla harjaantuu kuoren käyttötaidot ja omatoiminen kokeileminen. Olennaiset esimerkit on lueteltu alla ja ehkäpä luennoilla on muistettu näyttää, miltä omatoimisenkin kokeilemisen pitäisi näyttää. Kaikkea muutakin saa kokeilla, kunhan muistat demo 1:n perussäännöt ja selvität ennen enterin painamista, mitä komentosi tulee tekemään.

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

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 kuoressa:

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 esimerkiksi 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. Esimerkiksi gdb:n käyttäjäystävällisyys toteutuu 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 (se help kertoo näistä lisää).
  • Komentohistoria on käytettävissä samoilla tehokäyttönäppäimillä kuin bash-kuoressa.
  • Edellisen komennon voi toistaa painamalla pelkkää enteriä. Debuggerilla halutaan usein askeltaa ohjelmaa yksi rivi tai käsky kerrallaan useiden silmukkakierrosten ja muiden 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 kuoren -- 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 ohjelmaa 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.

Huomautus: tämä demo on vasta työkaluun tutustumista ja ennakoiva katsaus.. Hämmennys ja ihmetys ovat tässä vaiheessa normaali olotila. Kaikki ei pysty olemaan selvää vielä tänään, mutta onneksi olet tälläkin hetkellä tekemässä kovaa työtä kohti vankempaa ymmärrystä. Tässä nähtäviä asioita katsellaan loppukurssin ajan, kun niiden kautta selitetään laitteiston ja käyttöjärjestelmän yhteispeliin liittyviä ilmiöitä, tavoitteita ja menettelytapoja.

Selitystä 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 muistikas (mnemonic) ja operandit siten, että kohde on oikeanpuolimmainen operandi. Luentomonisteen konekieltä 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.

Huomautus: 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. Ohjeet siihen tässä seuraavaksi.

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

start

Katso ja tulkitse näkymää itse. Heitä uudelleen (gdb:n 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 nuolimaisen symbolin => 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

Eli seuraavaa suorituskohtaa ilmaiseva nuoli => pitäisi nyt olla myöhemmässä kohdassa kuin viimeksi. Steppaile sitten vielä, ja totea tilanne:

step

step

Viimeisen stepin jälkeen aliohjelman main() suoritus päättyy. Voit itse todeta saman, mikä luennollakin voidaan nähdä, jos porukka haluaa: 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.

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, syötetiedostojen ja muiden reaaliaikaisten keinojen lisäksi kahdella vakiintuneella tavalla, jotka POSIX-standardi edellyttää: komentoriviargumenteilla ja ympäristömuuttujilla.

Komentoriviargumentit

Jotta yliopittaisiin samalla myös tähän asti nähtyjen kuorikomentojen 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. Tässä käytetään nyt Javaa, koska sille löytyy kääntäjä suorakäyttökoneelta. Ohjelmointi 1 -kurssilta tutumpi C# on hyvin lähellä Javaa, joten tässä ei tapahdu kovinkaan isoa hyppäystä esitietokurssiin nähden.

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 kuoriskripteissä vastaavasti $0:n sisältö vaikuttaisi olevan.
  • Nykyinen Java-spesifikaatio ( https://docs.oracle.com/javase/specs/ ) ei näköjään tarkkaan ottaen määritä, mitkä merkkijonot taulukossa args tulee olla, mutta antaa esimerkkinä Unix-tyyppisen järjestelmän, jossa virtuaalikone käynnistyy juuri niin kuin näemme sen tekevän suorakäyttökoneissamme.

Alustavia huomioita syntaksista:

  • for-silmukan syntaksi on tässä tapauksessa täysin sama molemmissa kielissä
  • Java-kielen peruskirjastosta löytyy nykyään PrintStream-luokassa 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... Se on aina tavattoman kätevä tapa hoitaa tulostukset!
  • 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.
  • Eikä hätiä mitiä toisinkaan päin: Asia, joka nyt tuntuu uudelta ja ihmeelliseltä C:ssä, voikin olla tosi tarpeellista lisäkertausta juttuihin, jotka on Ohjelmointi 1:ssä jääneet vähemmälle huomiolle!

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 kielen tasolla olemassa 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 kuoressa, jolloin ne asettamisen jälkeen näkyvät kaikille ohjelmille, jotka käynnistetään samassa interaktiivisessa kuorisessiossa tai skriptissä. Yksi tyypillinen käyttötarkoitus kuoriskriptille 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.

Kevään 2019 luennolla nähtiin esimerkki bashin komentokehotteen muokkaamisesta vaihtamalla ympäristömuuttujan PS1 arvoa. Kaikkiin Bashin sessioihin haluamansa muuttuja-asetukset voi laittaa suorakäyttökoneen ~/.bashrc -tiedostoon tekstieditorilla.

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. Kääntäminen monelle kielelle on työlästä elikkäs kallista hommaa.

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 kaikissa ohjelmissa, joita kyseisestä kuorisessiosta 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 eivät vaikuta.

Tässä harjoituksessa varmistetaan vähintään, että osaat säätää oman kuorisession 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 tällä itsensä 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.. Linkki Jyväskylä ry:n muinoisessa IRC-ohjeessa taidettiin puhua em. asetuksen laittamisesta .bashrc -tiedostoon.. jos muistais joskus jutella linkkareiden kanssa, niin kyseistä kohtaa ohjeesta voisi kaikkien näiden vuosien jälkeen tarkistaa... jos sitä IRCiä nyt monikaan enää 2020-luvulla kaipailee...)

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, onnistunut asettamaan ympäristömuuttujia ja säätänyt kieliasetukset ainakin siinä kuorisessiossa, jossa ajat kääntämäsi ohjelman.

Vastaustiedosto tuotetaan kääntämällä ja ajamalla alussa mainitusta URLista haettu C-lähdekoodi nimeltään d03_fiilikset.c siten, että tuloste ohjautuu kuoressa 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/kj21esim/demo3
Ympäristömuuttuja LC_ALL == en_GB.utf8
Ympäristömuuttuja LANG == en_US.UTF-8
Ympäristömuuttuja LEMPIKALA == miekkakala
argv[0] == ./a.out
argv[1] == Ihan kivalta tuntuu: uskokaa tai älkää, niin aikataulu pitää aiempaa paremiin.
Kertauksena: käyttäjän 'nieminen' tunnelmat tässä vaiheessa kurssia:

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

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

Lempikala voi olla Pirkkala, Porkkala tai ihan mitä vaan - kunhan olet osannut asettaa sen ympäristömuuttujan arvoksi.

Demon palauttaminen hoidetaan teknisesti samalla tavalla kuin aiemmatkin.

Tästä demosta palautetaan tasan yksi tiedosto nimeltään "d3_vastaus.txt", joka on luotu ohjaamalla C-ohjelman tuloste kyseiseen tiedostoon.