ITKA203 Käyttöjärjestelmät -kurssin Demo 7a keväällä 2014. "Debugger-miniharkka"
Paavo Nieminen, paavo.j.nieminen@jyu.fi
Contents
Päätavoite:
- Näet esimerkin kautta, kuinka konekielinen ohjelma saa prosessorin suorittamaan aliohjelma-aktivaation. Sisäistät suorituspinon toimintaperiaatteen ja roolin prosessin (tai säikeen) suorituksessa.
Toissijainen tavoite:
- samalla näet hieman yleiskuvaa konekielisestä ohjelmoinnista.
- samalla syvennyt viitteen (tai muistiosoittimen) rooliin ohjelmoinnissa, mikä parantaa yleistä kykyäsi ymmärtää ohjelmia.
Historiallisia huomautuksia: Aliohjelma-aktivaation toimintaa käsittelevä harjoitustyö on ollut mukana käyttöjärjestelmäkurssilla IT-tiedekunnan perustamisesta alkaen, ja vuosia sitäkin ennen. Tarkoitus on, että tämä harjoitus jossain määrin vastaisi aiempien kurssikertojen perinteistä harjoitustyötä.
Käytämme vuonna 2014 relevantteja työkaluja eli x86-64 -arkkitehtuurin mukaisissa Red Hat Enterprise Linux -virtuaalipalvelimissamme toimivaa, jo tutuksi tullutta, kääntäjää gcc sekä uutena tuttavuutena debuggeria gdb. Molempia on nähty käytännön esimerkeissä kurssin luennoilla. Toki tällainen harjoitus voitaisiin tehdä yksinkertaisella "lasten tietokonesimulaattorilla" tai jonkin muinoisen (ja sitä kautta nykyisiä yksinkertaisemman) prosessorin emulaattorilla, mutta koen että nykyisen tuotantovälineistön käyttö tarjoaa jonkinlaisen "oikotien onneen", vaikkakin mahdollisesti "vaikeuksien kautta voittoon". Yhteisen palvelinkoneiston käyttö mahdollistaa yhtenäisen ohjeistuksen laitteistoon ja ohjelmistoon, mikä ei mikroluokkia ja kotikoneita käytettäessä välttämättä olisi järin helppoa.
Kesällä 2011 tehtiin vastaava harjoitus. Silloin ajattelin, että tämä tuottaisi vain vähän lisätyötä, jos on taustalla aiemmat demot ja luennot käytynä / materiaalit ymmärrettynä. Jälkikäteen oli silloin todettava, että joillakin opiskelijoilla vaati vielä kovastikin työtä päästä tästä kärryille. Lisäksi jotkut sivuaineopiskelijat kokivat aihepiirin tarpeettomaksi omaan osaamiskenttäänsä nähden. Keväällä 2014 näitä ilmiötä on koetettu korjata seuraavin tavoin:
- Harjoitus on vapaaehtoinen, jotta kellekään ei tulisi "pakkopullan" tuntua. Tentissäkään ei tulla enää kysymään perinteistä kysymystä "selvitä aliohjelmakutsun toiminta konekielen tasolla". Tekemistä kuitenkin motivoidaan jopa kahdella bonuspisteellä, ja ainakin tietotekniikan pääaineopiskelijan kannattaisi ajatella tätä oman osaamispohjan kannalta hyvinkin välttämättömänä jumppatuokiona.
- Harjoituksen muotoa on yritetty säätää yksinkertaisemmaksi edelliseen versioon nähden.
- Komentokehotteesta, C-kielestä ja tekstipohjaisesta tiedonkäsittelystä on tehty pohjaharjoituksia jonkin verran aiempaa enemmän.
Vielä lisämotivaatioksi: Mainitsen tässä erityisesti kaksi IT-tiedekunnan syksyllä 2014 starttaavaa maisterikoulutusta, joissa tämän harjoituksen käsittelemistä asioista on suoria yhtymäkohtia:
Ensin käydään malliharkka.c -ohjelman kautta läpi harjoitustyön työkalut ja idea. Koodi ja muu välineistö on saatavilla kurssin nettisivulta:
http://users.jyu.fi/~nieminen/kj14/demo7a.zip
Ensinnäkin ohjelmaa on syytä silmäillä lähdekoodin tasolla, kääntää ja ajaa sitä ja todeta, että se toimii odotusten mukaisesti:
gcc malliharkka.c ./a.out
Kokeile huvikseen seuraavaa:
gcc -E malliharkka.c | less
Em. komennolla ajoit vain C-käännöksen ensimmäisen vaiheen, jossa ns. esikääntäjä lisäilee omia rivejään lähdekoodiin ja käsittelee mm. #include -rivit. (Tämä oli vain erillinen käytännön demonstraatio siitä, mikä on C-kielen esikääntäjä ja mitä se tekee. Näet, että varsinaista C-kieltä kertyy tässä tapauksessa moninkertainen määrä itse kirjoitettuun nähden.)
Nyt itse asiaan. Anna seuraava komento:
gcc -O0 -S malliharkka.c
Optio -O0 (eli iso oo ja nolla) tarkoitti, että kääntäjä ei optimoisi koodia. Koodin optimointi oletettavasti tekisi käännöksestä vaikeamman ymmärtää, kun tuotettu konekieli koettaisi oikoa mutkia suoriksi nopeamman suoritettavuuden ja/tai lyhyemmän binääritiedoston toivossa. Optio -S (eli iso äs) tarkoitti, että ei käännetä valmiiksi konekieleksi asti, vaan tuotetaan vastaava assembler-koodi. Nyt pitäisi olla syntynyt tiedosto malliharkka.s. Sen pitäisi alkaa jotakuinkin näin:
. .file "malliharkka.c" .text .globl kaanna_taulukko .type kaanna_taulukko, @function kaanna_taulukko: .LFB2: pushq %rbp .LCFI0: movq %rsp, %rbp .LCFI1: movq %rdi, -40(%rbp) movq %rsi, -48(%rbp) ...
Pisteellä alkavat tekstit ovat assemblerin ohjauskomentoja. Esim. .file "malliharkka.c" kertoo, että tämä on syntynyt tuon nimisestä C-koodista kääntämällä. .text kertoo, että seuraavat asiat pitää sijoittaa koodialueelle (jota sanotaan jostain syystä tekstialueeksi Unix-maailmassa...) .globl kaanna_taulukko tarkoittanee että meillä on globaali symboli nimeltä kaanna_taulukko ja .type kaanna_taulukko, @function kertonee että kyseinen symboli tarkoittaa funktiota eli aliohjelmaa.
Kaksoispisteeseen päättyvät rivit ovat symbolisia nimiä muistiosoitteille. Eli sekä kaanna_taulukko että .LFB2 ovat nimiä sille muistipaikalle, jossa sijaitsee käskyn pushq %rbp konekielikoodin ensimmäinen tavu. Assembler-ohjelmoijan ei tarvitse tietää muistipaikan numeroarvoa; itse asiassa voi olla, että kukaan ei tiedä lopullisia osoitteita ennen kuin käyttöjärjestelmä lataa konekielisen ohjelman kovalevyltä muistiin suoritusta varten.
Tutustu lyhyesti assembler-käännökseen, pyri alustavasti ymmärtämään, miten se toimii. Luentomateriaalissa on x86-64 -assemblerin esimerkkikäskyjä. Sieltä pitäisi tutunnäköisiä komentoja löytyä. Kohta katsotaan asiaa vielä tarkemmin.
Käännä ensin ohjelma komennolla:
gcc -g -O0 -o malliharkka malliharkka.c
Argumentti -g tuottaa debug-tiedot; niitä tarvitaan kohta. Jälleen -O0 kieltää optimoimasta käännöstä ja -o malliharkka määrää nimen käännöksen tulostiedostolle. Pitäisi syntyä ajettava konekielinen tiedosto malliharkka.
Käynnistä komentorividebuggeri seuraavasti:
gdb malliharkka
Mitäs nyt? Tilanne näyttää seuraavalta ja vaatii hieman selitystä:
[nieminen@jalava harkka]$ gdb malliharkka GNU gdb (GDB) Red Hat Enterprise Linux (7.0.1-32.el5_6.2) Copyright (C) 2009 Free Software Foundation, Inc. ... (gdb) [ja vilkkuva kursori ...]
Käynnistit debuggerin. Ohjelmointi 1:ltä saattaa olla tuttu asia jokin (esim. 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. Tarkoitushan on käydä läpi ohjelmistojen hierarkioita ja askeltaa vähän aikaa näillä matalammilla ja vähemmän graafisilla tasoilla, joille ensinnäkin saattaa joskus syystä tai toisesta pakosti joutua ja joiden olemassaolo on joka tapauksessa hyvä ymmärtää. Joskus voi olla ihan hyvä idea eriyttää graafinen käyttöliittymä ja taustalla oleva koneisto löyhästi vaikkapa tekstimuotoisen kommunikointiprotokollan avulla. Debuggeri odottaa nyt siis tekstimuotoisia komentoja, joiden avulla voit tutkia argumenttina annetun ohjelman toimintaa. Komenna debuggeria:
run
Debuggeri käynnisti malliharjoitustyön käännetyn ohjelman, antoi sen mennä niin pitkälle kuin se ilman virheitä etenee, tässä tapauksessa onnistuneeseen loppuun saakka. Tulostui toivon mukaan seuraavaa:
Starting program: /autohome/home3/363/nieminen/kj07kesa/harkka/malliharkka -1 2 3 -16 2 8 8 2 -16 3 2 -1 Samoja lukuja oli 1 kpl. Program exited normally.
Jos ohjelma olisi kaatunut, debuggerin avulla voisi alkaa selvittelemään, miksi niin kävi... Debuggeri on siis arvokas ohjelmantekijän työkalu...
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ä.
- Komennoissa on automaattitäydennys tabulaattorinäppäimellä, ja niille on lyhyitä "alias"-nimiä, jotka on nopea kirjoittaa (ks. helpit).
- Komentohistoria on käytettävissä nuolinäppäimillä.
- Edellisen komennon voi toistaa painamalla pelkkää enteriä.
Debuggerin käyttöliittymä on paljolti samanlainen kuin tehokkaan interaktiivisen shellin -- tai minkä tahansa hyvin tehdyn tekstipohjaisen ohjelman. Mieleen tulee itselle esimerkiksi laskentaohjelmisto Matlabin "Command Window", joka on samalla tavoin kätevä. Tässä vaiheessa lienee käynyt selväksi, että tekstikomennoilla toimiva käyttöliittymä tukee luonnostaan 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. Seuraavaksi tutustutaan debuggeriin vielä tarkemmin.
Käynnistä debuggeri uudelleen:
gdb malliharkka
Komenna gdb:tä seuraavasti:
display /3i $rip display /t $eflags display /x $rbp display /x $rsp display /x $rip
Voit copy-pastata suoraan tästä harkkaohjeesta. Komennot voi halutessaan lukea tiedostosta gdb:n käynnistyksen yhteydessä optiolla -x aloituskomentoja.esim. Ylläolevien komentojen merkitys on, että tästedes gdb näyttää aina automaattisesti seuraavat tiedot (viimeksi komennettu ensimmäisenä):
/x $rip käskyosoiterekisterin arvo heksalukuna ("x = hex"), seuraavaksi suoritettava konekielikomento tullaan noutamaan (fetch) tästä muistiosoitteesta /x $rsp pino-osoittimen arvo heksalukuna /x $rbp pinokehyksen kantaosoittimen arvo heksana /t $eflags lippurekisterin lippubitit ("t = base two l. bitit") /3i $rip kolme konekielikäskyä alkaen muistipaikasta, johon RIP osoittaa ("3i = three instructions")
Jos tuli kirjoitus- tai kopiointivirhe, voit komentaa undisplay, mikä poistaa aiemmat näyttövalinnat, ja sen jälkeen voi antaa uudelleen ylläolevat (tai muut sopivat) display -komennot. Ne jäävät voimaan seuraavaan undisplayhin asti.
Debuggeri on nyt ladannut pyydetyn ohjelmatiedoston, ja se on valmis avaamaan konepellin oikein kunnolla. Katsotaan ensin yleiskuvaa ohjelmasta debuggerin disassembly-ominaisuudella. Komenna seuraavasti:
disassemble /m main
Pitäisi näkyä pääohjelman assembler-koodi alkaen jotenkin seuraavasti:
Dump of assembler code for function main: 68 int main(int argc, char *argv[]){ 0x00000000004005f7 <main+0>: push %rbp 0x00000000004005f8 <main+1>: mov %rsp,%rbp 0x00000000004005fb <main+4>: sub $0x50,%rsp 0x00000000004005ff <main+8>: mov %edi,-0x44(%rbp) 0x0000000000400602 <main+11>: mov %rsi,-0x50(%rbp) 69 long long int testitaulu[] = {-1, 2, 3, -16, 2, 8}; 0x0000000000400606 <main+15>: movq $0xffffffffffffffff,-0x40(%rbp) ...
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. Esim. ohjelman alussa on aliohjelman esittelyn jälkeen pinokehyksen alustava koodin osuus. Nähtävästi rekistereissä siirretyt parametrit (mainin tapauksessa siis ohjelman käynnistyksessä saamat argumentit) siirretään rekistereistä pinokehykseen ikään kuin ne olisivat paikallisia muuttujia.
Selitys asioille, jotka debuggeri näyttää:
- Vasemmassa reunassa on 64-bittinen heksaluku. Tämä on virtuaalimuistiosoite, 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 mm. siitä, onko mukana vakioarvoja, ja siitä ovatko käskyt alkuperäisiä 8086-käskyjä vai myöhempiä laajennoksia). Lyhimmät käskyt (kuten push BP) ovat pisimpään mukana olleita.
- Sitten on käsky AT&T-syntaksilla eli mnemonic ja operandit.
- Esimerkkejä käskyistä ja osoitusmuodoista on kurssimateriaalissa. Prosessorimanuaalit ovat useampituhatsivuisia. Niistä joitakin satoja on käskyjen toiminnan kuvailua. Suurin osa ohjelmien tarvitsemista rakenteista hoituvat kuitenkin muutamilla peruskäskyillä. Valtaosa nykyprosessorin käskyistä liittyy erityistehtäviin kuten liukulukulaskentaan. Myös prosessoriytimien keskinäinen synkronointi edellyttää runsaasti selvitystä nykyprosessorien manuaaleissa.
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). Katso vastaavat myös muista aliohjelmista, eli komenna:
disassemble /m tulosta_taulukko
ja:
disassemble /m kanna_taulukko
Jos koko listaus ei mahdu yhteen ruutuun, gdb sivuttaa sen; katso loppuun asti painamalla tarvittaessa enter.
Jotta et putoaisi kartalta, otetaan tähän kohtaan yleiskuva siitä, mitä seuraavaksi tapahtuu:
- Debugger-ohjelma gdb tulee "hallitsemaan" tarkasteltavana olevaa konekielistä ohjelmaa pyytämilläsi tavoin.
- Debugger antaa suorittaa ohjelmaa pala kerrallaan (opit kohta määrittelemään erilaisia tapoja suorittaa palanen: konekielikäsky kerrallaan, nykyinen aliohjelma loppuun, tietylle C-koodiriville saakka tahi määriteltyyn pysähdyspisteeseen eli breakpointtiin saakka).
- Kun debugger on pysäyttänyt ohjelman, se antaa tutkia pysäytettyä prosessia "jäädytetyssä" tilanteessa, tärkeimpinä rekisterien arvot ja muistin sisältö.
Seuraavan läpikävelyn idea on, että muutama gdb:n komento tulee tutuksi ja että voit soveltaa niitä jatkossa. Komennoista saa paljon lisätietoa komentamalla help sekä tietysti Internetistä.
[TODO: Tämän luvun teksti on OK 2014, mutta esimerkit on vielä vuoden 2011 ajosta, joten rivinumerot luultavasti eri kuin nyt.]
Ohjelma pitää tietysti ensin käynnistää, jotta sitä päästään debuggaamaan. Nyt ei ajeta sitä kokonaan läpi run:illa vaan aloitetaan käsky kerrallaan suorittaminen. Komenna:
start
Tutki debuggerin tulostetta, joka tuli välittömästi. Pitäisi olla jotakuinkin seuraavaa:
Temporary breakpoint 1 at 0x400606: file malliharkka.c, line 69. Starting program: /autohome/home3/363/nieminen/files/kjesim/malliharkka/malliharkka warning: no loadable sections found in added symbol-file system-supplied DSO at 0x2aaaaaaab000 Temporary breakpoint 1, main (argc=1, argv=0x7fffffffe208) at malliharkka.c:69 69 long long int testitaulu[] = {-1, 2, 3, -16, 2, 8}; 5: /x $rip = 0x400606 4: /x $rsp = 0x7fffffffe0d0 3: /x $rbp = 0x7fffffffe120 2: /t $eflags = 1000000010 1: x/3i $rip 0x400606 <main+15>: movq $0xffffffffffffffff,-0x40(%rbp) 0x40060e <main+23>: movq $0x2,-0x38(%rbp) 0x400616 <main+31>: movq $0x3,-0x30(%rbp)
Suoritus pysähtyi nyt main() -aliohjelman alussa ennen sitä konekielikäskyä, joka on syntynyt sovellusohjelman ensimmäisen suoritettavan C-kielisen rivin kääntämisestä (siis rivin numero 69, jossa on taulukon esittely ja alustus). Pinokehyksen alustus, joka aiemmassa disassemblyssä nähtiin, on nyt siis jo suoritettu. Koska käännöksessä oli mukana debug-tiedon tallennus ja lähdekoodi on saatavilla, debuggeri kykenee näyttämään C-kielisen koodirivin, jota ollaan suorittamassa. Sitten näkyvät edellisissä display-komennoissa pyydetyt asiat.
Pysähdy fiilistelemään: Olet nyt sillä äärilaidalla, johon sovellusohjelmoija syvimmillään pääsee tietokonetta käyttämään. Ohjelma on käännetty tietylle prosessorille (ja tietylle käyttöjärjestelmälle). Näet ohjelman pysäytettynä debuggerissa, ja ymmärrät (toivottavasti) tarkoin, mistä on kyse: Seuraavassa hetkessä prosessori noutaisi käskyn. Se olisi virtuaalimuistiosoitteesta 0x400606, jonka prosessori muuntaisi fyysiseksi osoitteeksi käyttäen 48-bittisen virtuaaliosoitteen osia osoitteenmuodostuksessa:
+----------------------------------+-----------+-----------------+ |Hierarkkinen indeksointi | Sivun | Muistipaikan | |sivutaulun hakemiseen | indeksi | indeksi sivulla | | | taulussa | | | | | | |000000000 000000000 000000010 | 000000000 | 011000000110 | | | | | +----------------------------------+-----------+-----------------+ | Korvautuu osoitteenmuodostuksessa | pysyy samana | | sivutaulun tietojen mukaan | | +----------------------------------------------+-----------------+
Ohjelman käynnistymisestä on jo aikaa, ja standardikirjasto on ehtinyt suorittaa alustustoimenpiteitä. Periaatteessa on täysin mahdollista, että jo tässä vaiheessa tulisi kellokeskeytys ja prosessi joutuisi ready-jonoon odottelemaan seuraavaa vuoroaan. Ja nythän se on joka tapauksessa pysäytettynä debuggerin toimesta. Pelit on seis, mutta se ei ohjelman kannalta vaikuta. Kun se seuraavan kerran jatkaa suorittamista, tilanne ei ole suoritettavan prosessin kannalta mitenkään erilainen.
Käsky, joka tuosta kohtaa muistia löytyy, on malliharjoitustyön osalta seuraavanlainen:
0x400606 <main+15>: movq $0xffffffffffffffff,-0x40(%rbp)
Tämän käskyn suoritus tulee siirtämään lukuarvon -1 (kaikki bitit ykkösiä, heksaesitys 0xffffffffffffffff) virtuaalimuistiosoitteeseen, joka saadaan vähentämällä RBP:n arvosta 64 (lisätään etumerkillinen heksaluku -0x40). Kyseinen muistiosoite, ja siis siirron kohde, tulee olemaan:
RBP-rekisterin arvo | | plus | | | | Käskyn binäärikoodiin sisällytetty vakiosiirros | | | | | | 64-bittiseen rekisteriin | | | jää tässä ynnäyksessä | | | tulokseksi tämä luku | | | | 0x7fffffffe120 + -0x40 == 0x7fffffffe0e0 Eli bitteinä: +----------------------------------+-----------+-----------------+ |Hierarkkinen indeksointi | Sivun | Muistipaikan | |sivutaulun hakemiseen | indeksi | indeksi sivulla | | | taulussa | | | | | | |011111111 111111111 111111111 | 111111110 | 000011100000 | | | | | +----------------------------------+-----------+-----------------+ | Korvautuu osoitteenmuodostuksessa | pysyy samana | +----------------------------------------------+-----------------+
Käskyn suorituksessa:
- prosessori tekee kohdeoperandin osoitteenmuodostuksen fyysiseksi osoitteeksi
- väylä siirtää luvun keskusmuistiin (varsinainen toimenpide).
- RIP:n arvo päivittyy seuraavan käskyn osoitteksi. Tässä tapauksessa lukuarvoksi 0x40060e.
Tai siltä ainakin näyttää. (Varsinaisesti pelissä on L1- ja L2 -välimuistit, muistinhallintayksikkö MMU ja sen TLB-puskurit; seuraava käskykin voi olla jo pitkään ollut suorituksessa tai suoritettuna johtuen liukuhihnoista ja ennakoivasta suorituksesta. Tai sitten suoritus on tehty emulaattorilla, simulaattorilla, virtuaalikoneella tai ainoastaan ohjelmaansa debuggaavan kehittäjän tai harjoitustyötä tekevän opiskelijan pään sisäisessä mielikuvamaailmassa. Millään näillä teknisillä suunnittelun ja optimoinnin urotöillä ei ole vaikutusta siihen, miten ohjelman suoritus näyttäytyy prosessoriarkkitehtuurirajapinnassa eli konekielikoodina.)
Totea, että näin käy, niinkuin ounasteltiin, eli komenna yhden käskyn suorittaminen ("step instruction"):
stepi
Tulostus vahvistaa, että ainakin RIP päivittyi juuri niin kuin pitikin; seuraava tilanne on debuggerin mukaan tällainen:
5: /x $rip = 0x40060e 4: /x $rsp = 0x7fffffffe0d0 3: /x $rbp = 0x7fffffffe120 2: /t $eflags = 1000000010 1: x/3i $rip 0x40060e <main+23>: movq $0x2,-0x38(%rbp) 0x400616 <main+31>: movq $0x3,-0x30(%rbp) 0x40061e <main+39>: movq $0xfffffffffffffff0,-0x28(%rbp)
Suoritetaan kohta ohjelmaa käsky kerrallaan eteenpäin. Jos nyt tekisit disassemblyn, se näyttäisi suorituksessa meneillään olevan aliohjelman koodin. Voit halutessasi kokeilla:
disassemble /m
Tutkitaanpa muistia kantaosoitteen RBP ympäriltä, esim:
x /32xg $rbp-128
Muistutetaan itseämme vielä rekisterien arvoista tässä vaiheessa ohjelman suoritusta:
display
Tuloste oli seuraavanlainen (huomioita lisätty jälkeenpäin):
*1*: Pinon huippu on osoitteessa $rsp *2*: Äskeinen sijoitus muutti osoitteen -64($rbp) sisällöksi -1:n *3*: Main-aliohjelman pinokehyksen kanta on $rbp. Tässä osoitteessa on yleensä aiemman pinokehyksen kantaosoite; se on nyt 0x00, eli mainia kutsuneella koodilla ilmeisesti ei ole pinokehystä ihan samassa mielessä kuin normaalilla aliohjelmalla... main on siis ohjelman "uloin/alimmainen kehys". *4*: Tässä on paluuosoite, eli main-aliohjelmaa kutsuneen call-käskyn jälkeisen käskyn osoite. Tämä on dynaamisesti ladatun C-kirjaston (tiedostosta /lib64/libc.so.6) koodia, joka jatkuu siis osoitteessa 0x0000003bbec1d994 siten että siellä kutsutaan käyttöjärjestelmän exit()-palvelua antaen parametriksi sovellusohjelman mainista palauttama kokonaisluku (gdb) x /32xg $rbp-128 0x7fffffffe0a0: 0x00000000000000bf 0x00000000005657f0 0x7fffffffe0b0: 0x00007fffffffe0f6 0x00007fffffffe0f7 0x7fffffffe0c0: 0x0000000000000000 0x0000003bbea1bbc0 0x7fffffffe0d0: 0x00007fffffffe208 *1* 0x00000001004003c3 0x7fffffffe0e0: 0xffffffffffffffff *2* 0x00000000004006d7 0x7fffffffe0f0: 0x0000000000000000 0x0000003bbea1bbc0 0x7fffffffe100: 0x00000000004006a0 0x0000000000000000 0x7fffffffe110: 0x00007fffffffe200 0x0000000000000000 0x7fffffffe120: 0x0000000000000000 *3* 0x0000003bbec1d994 *4* 0x7fffffffe130: 0x0000000000400410 0x00007fffffffe208 0x7fffffffe140: 0x0000000100000000 0x00000000004005f7 0x7fffffffe150: 0x0000003bbea1bbc0 0x01cc81a23c8695ae 0x7fffffffe160: 0x0000000000000000 0x00007fffffffe200 0x7fffffffe170: 0x0000000000000000 0x0000000000000000 0x7fffffffe180: 0x01ccfe5dc379749e 0x01cc819982474ce1 0x7fffffffe190: 0x0000000000000000 0x0000000000000000 (gdb) display 5: /x $rip = 0x40060e 4: /x $rsp = 0x7fffffffe0d0 3: /x $rbp = 0x7fffffffe120 2: /t $eflags = 1000000010 1: x/3i $rip 0x40060e <main+23>: movq $0x2,-0x38(%rbp) 0x400616 <main+31>: movq $0x3,-0x30(%rbp) 0x40061e <main+39>: movq $0xfffffffffffffff0,-0x28(%rbp)
Muistin tulostus tehdään myöhemminkin, jotta huomataan, miten pinomuisti muuttuu.
Sitten ajetaan ohjelmaa konekäsky kerrallaan:
stepi
Huomioi, miten rekisterit muuttuvat aina käskyn jälkeen. Edellisen komennon saat toistettua painamalla pelkästään enteriä. Jossain vaiheessa tulee vastaan malliharjoitustyön ensimmäinen aliohjelmakutsu:
0x400646 <main+79>: callq 0x4005a4 <tulosta_taulukko>
Mennään tähän aliohjelmaan, eli suoritetaan vielä kerran:
stepi
Katso, että suoritus on tosiaan tullut aliohjelmaan tulosta_taulukko eli gdb näyttää jotakin seuraavanlaista:
55 void tulosta_taulukko(long long int taulu[], long long int koko){
Eli suoritus päätyi C-koodiriville, josta alkaa aliohjelmalohko. Mutta tämä tulostusaliohjelma ei nyt ole se, mikä meitä kiinnostaa, vaan kiinnostaa se varsinainen algoritmitoteutus, johon suoritus päätyy vasta myöhemmin. Tämä on debuggauksessa tyypillinen tilanne: kiinnostava asia tulee vasta myöhemmin. Annetaan nyt koko aliohjelman mennä loppuun, eli komenna:
finish
Tällä tavoin voi gdb:llä suorittaa loppuun aliohjelman. (Tämä vastaa "step out" -komentoa joissakin debuggereissa).
Malliharjoitustyössä on seuraavaksi vuorossa kiinnostuksemme kohde, eli varsinaisen taulukonkääntöalgoritmin yksi suorituskerta. Debuggeri kertoo, millä rivillä suoritus on menossa:
74 samoja = kaanna_taulukko(testitaulu, koko); ...
Komenna esim. gdb:tä taas tulostamaan muistin sisältöä RBP:n ympäriltä ja muistuta itseäsi rekisterien arvoista (eli tehdään sama kuin aiemmassa kohdassa):
x /32xg $rbp-128 display
Huomannet muistin sisällöstä, missä kohtaa siellä sijaitsee taulukon luvut, missä kohtaa taulukon koko. Aja nyt käsky kerrallaan ja tarkkaile, mitä ohjelma tekee... miten ja mitä kautta parametrit siirretään (rekisterit RDI ja SDI), ja miten suoritus siirretään aliohjelman koodiin:
stepi
Askella ohjelmaa tarkkaavaisesti, kunnes seuraavaksi tulisi vuoroon call-käsky, jolla ohjelma siirtyy aliohjelmaan kaanna_taulukko. Ja sitten vielä se call-käsky. Pysyithän kärryillä siinä, miten rekisterit muuttuivat. Vuorossa pitäisi olla ensimmäinen käsky aliohjelmasta kaanna_taulukko.
Jos haluat, voit tietysti missä tahansa vaiheessa tehdä meneillään olevasta aliohjelmasta disassemblyn:
disassemble
tai koodirivien kanssa:
disassemble /m
Joka tapauksessa tutki, mitä kertoo komento:
info registers
Se näyttää prosessin koko kontekstin, eli käyttäjän näkemät rekisterit. Huomataan esimerkiksi, että rekisterissä RSI on lukuarvo kuusi ja rekisterissä RDI on muistiosoite johonkin kohtaan muistia. Katsotaan, mitä siellä kohtaa muistia on:
x /12xg $rdi
Siellähän on testiohjelman taulukon lukuarvot. Taulukko on siis välitetty aliohjelmalle muistiosoitteena rekisterissä RDI. Ihan niinkuin ABI-dokumentaation mukaan pitikin. Varsinaiset taulukon luvut sijaitsevat pinomuistissa kutsuvan ohjelman pinokehyksen alueella. Mainin alussahan oli sijoituskäskyjä, jotka laittoivat nuo luvut pinomuistiin. C-ohjelmassa taulukko alustettiin esittelyn yhteydessä, mutta konekielikäännöksessä se nähtävästi tehdään erillisillä peräkkäin suoritettavilla operaatioilla.
Nyt ilmeisesti ohjelmalla on tarkoitus luoda juuri kutsutulle aliohjelmalle oma kehys. Katso käsky käskyltä, miten se tapahtuu:
stepi
Katso tulosteesta, miten RSP ja RBP toimivat. Aliohjelman alussa myös siirretään parametrit rekistereistä uuden pinokehyksen sisään, ikään kuin ne olisivat lokaaleja muuttujia. Jatkossa niitä käytetään pinomuistin kautta noista osoitteista eli -0x28(%rbp) ja -0x30(%rbp).
Tutki vielä tässäkin kohtaa muistin sisältö seuraavasti:
x /32xg $rbp-128 display
Löydätkö pinomuistista edellisen pinokehyksen kantaosoitteen? Entä osoitteen käskyyn, johon aliohjelmasta tulee palata? Pitäisi olla asiat niissä muistipaikoissa, joissa pinokehysmallin mukaan olettaisitkin.
[TODO: (Varmista...) Tämän pätkän pitäisi olla jossain määrin tarpeeton 2014, kun tutkittavaan aliohjelmaan on lisätty turha_kutsu(): Jos aliohjelma ei kutsu mitään muita aliohjelmia, pinon huippua ei luultavasti ole päivitetty lokaalien muuttujien mukaan, koska uutta aktivaatiota ei tarvitse luoda. Tämä on siis "lehtialiohjelma", jonka lokaalit muuttujat mahtuvat kutsuvan ohjelman kehyksen "red zoneen" (ks. kurssimateriaali). Siksi suorituksen ajan RBP==RSP. Sen sijaan main() -aliohjelman suorituksesta nähdään normaali tilanne, jossa SP:n täytyy olla valmiina uuden kehyksen luontiin. 128 tavun "Red zone" on siis x86-64:n ABIssa sovittu asia, joka on x86-64:n GNU-C-kääntäjässä näin; muissa toteutuksissa voi olla sovittu ihan millä tavoin vaan!]
Käy sitten aktivaatiota läpi eteenpäin käsky käskyltä. Silmukan ja ehtolauseiden toteutustavasta ei olla kauhean kiinnostuneita, mutta suoritetaan sitä kuitenkin askel askeleelta läpi ja katsotaan päällisin puolin, millaisista käskyistä se muodostuu:
stepi
Ja toista tätä. Huomaa, miten debugger näyttää aina C-kielisen koodirivin, jota ollaan suorittamassa, ja voit käydä läpi komento komennolta sen, millaiseksi konekielikäskyjen ryppääksi kukin lähdekoodirivi on käännetty. Sijoituksista näet mm. missä muistiosoitteissa (suhteessa kantaosoiterekisteriin) säilytetään mitäkin muuttujia. Siitä näkee myös, miten for-silmukka toteutuu siten että yhdestä lähdekoodirivistä muodostaan käännöksessä käskyjä ennen ja jälkeen silmukkalohkon sisällön: Silmukka alkaa itseasiassa hyppykäskyllä sen loppuun, jossa testataan lopetusehto. Jos ehto ei vielä päde, hypätään silmukkalohkon koodin alkuun. Näin voidaan assembler-koodista havaita, että for-silmukkaa ei suoriteta kertaakaan, jos sen ehto ei ole alussa tosi. Ohjelmoinnista ehkä tiesit tämän sopimuksen. Nyt sille on myös konkreettinen näkökulma ja toteutus prosessorikomentoina. Mielestäni tämän pitäisi olla valaisevaa... Ehdolliset hypyt toimivat EFLAGS-rekisterin bittien mukaan. Observoi, että EFLAGSin bitit muuttuvat aika usein käskyjen seurauksena.
Huomaa, että osoitteiden muodostaminen kokonaisluvulla indeksoituun taulukkoon koostuu monesta vaiheesta: Indeksimuuttuja otetaan ensin pinomuistista rekisteriin, kerrotaan rekisterissä sitten kahdeksalla (bittien siirto kolmella pykälällä vasemmalle, shl) koska indeksoidaan kahdeksan tavun mittaisia alkioita, ja osoitteethan puolestaan osoittavat yhden tavun mittaisia peräkkäisiä muistilokeroita. Sitten lisätään tulokseen ensimmäisen alkion muistiosoite. Lopulta laskemisen jälkeen rekisterissä on sen muistipaikan numero, jota voidaan käyttää halutun alkion osoittamisessa. [Koodin optimointi luultavasti (toivottavasti) aiheuttaisi tehokkaamman (ja luultavasti sekavamman) koodin muodostumisen. Parhaimmillaan optimointi ehkä kopioisi lyhyen aliohjelman koodin suoraan kutsuvaan ohjelmaan, siis ilman aliohjelmakutsua, ja lyhyt tunnetun mittainen silmukka saatettaisiin tehdä tuottamalla silmukan sisällön koodi peräkkäin ilman varsinaisia ehtoja ja hyppykäskyjä, peräkkäiset indeksit ehkä käytäisiin läpi vain lisäämällä edelliseen indeksiin sopiva luku (tässä 8)... ym... ]
Okei, koko algoritmia ei tarvitse askeltaa läpi... Kun mielestäsi kykenet hyvin uskomaan sen asian, että C-koodi on kääntynyt käsky kerrallaan useaksi konekielikäskyksi, joita prosessori suorittaa yksi kerrallaan, ja että muuttujat ilmenevät tiettyinä muistipaikkoina suorituspinossa, voit siirtyä tämän aliohjelman loppuun. Tee se esim. seuraavasti. Aseta breakpoint return-lauseeseen, eli komenna:
break 51
(tai mikä nyt omassa lähdekoodissasi onkaan aliohjelman return-lauseen rivinumero). Sitten komenna:
continue
Nyt ohjelman suoritus jatkui breakpointtiin asti, siis muistiosoitteeseen, johon on sijoitettu ensimmäinen breakpoint-rivillä sijaitsevan return-lauseen suoritukseen kuuluva konekielikäsky. (Tästedes suoritus pysähtyisi aina tuohon kohtaan, kunnes breakpoint otettaisiin pois päältä. Breakpointit helpottavat debuggausta; sellainen laitetaan paikkaan, joka on juuri ennen sitä kohtaa, jossa ohjelma esimerkiksi tuntuu kaatuvan. Sitten tarkkaillaan käsky käskyltä, miten koodi esim. sen kaatumisen saa aikaan: mitä on missäkin rekisterissä, vastaako tilanne ennakko-oletuksia vielä breakpointin kohdalla... jos vastaa, niin mistä sitten oikein tulee lopulta se väärä muistiosoite, "null pointer" tai muu ongelma.)
Nyt taas ole hyvin tarkkana: Mitä tekee leaveq ja retq. Mitä on pinon päällä milläkin hetkellä, miten kontrolli palaa kutsuneeseen ohjelmaan. Mitä kutsuva ohjelma vielä tekee ennen kuin koko aliohjelmaa kutsuva C-koodirivi on suoritettu loppuun.
Muita aliohjelmia ei tarvitse enää askeltaa. Katsotaan kuriositeettina, mitä tapahtuu C-ohjelman lopuksi. Suorita ohjelmaa riville, jossa on malliharjoitustyön loppu eli main-ohjelman return:
until 78
Aja tästä eteenpäin käsky käskyltä kunnes kontrolli päätyy jonnekin aivan muualle kuin itse ohjelmoituun koodiin. Viimeinen retq -komento vie johonkin tällaiseen paikkaan:
0x0000003bbec1d994 in __libc_start_main () from /lib64/libc.so.6
Eli prosessin virtuaalimuistiin on tuohon kohtaan käynnistyksen yhteydessä dynaamisesti linkitetty kaikkien C-ohjelmien yhteinen apukirjasto libc, jonka tehtävä on mm. tehdä alustuksia ja lopetteluja käyttöjärjestelmäkutsujen avulla, ja siinä välissä kutsua sovellusohjelmoijan kirjoittamaa tietynlaista main -nimistä aliohjelmaa. Näin siis C-ohjelma päätyy sen sovittuun aloituspisteeseen, ja näin se jatkaa vielä sovitun päätepisteen jälkeenkin libc:ssä toteutetuilla lopputoimilla. Nyt tiedät senkin; se on jo aika paljon enemmän kuin Ohjelmointi 1:llä kerrottiin ohjelman suorituksesta... ja vasta alkua siitä, mistä todellisessa maailmassa olisi kyse, jos asia vietäisiin "oikeasti tekniseksi" (mitä edelleenkään mielestäni tällä kurssilla ei oikeasti tehty missään vaiheessa...).
Ohjattu läpikävely on nyt tehty. Mennään itse asiaan.
Tekstimuotoisia komentoja vastaanottavat ohjelmat ovat siitä kivoja, että niitä voi skriptata, eli komentoja voi tallentaa tiedostoksi, jotka luetaan ja suoritetaan. Niin myös gdb -debuggerille voidaan tehdä, tosin ei kovin monipuolisella tai helpolla tavalla. Koodipaketissa on mukana gdb:n ymmärtämä komentotiedosto gdbkomennot_malli, joka ajaa debuggeria automaattisesti, tarkoituksena tallentaa eräästä ohjelman osasta "puolikattava" logitieto aivan konekäsky kerrallaan. Toinen, melkein samanlainen komentotiedosto gdbkomennot_harkka tuottaa varsinaisessa harjoitustyössä tutkittavan login.
Kokeillaan kuitenkin ensin vielä malliharkan kanssa. Käännä ohjelma seuraavasti:
gcc -g -O0 -DHILJAA -o malliharkka malliharkka.c
Optiot ovat edelleen -g debug-tiedoille ja -O0 optimointien estämiselle, -o malliharkka tulostiedoston nimelle. Lisäksi on optio -DHILJAA, jotta C-koodista jää pois tulostukset. Ne on kirjoitettu rivien #ifndef HILJAA ja #endif väliin. Optio -DHILJAA määrittelee ("D" niinkuin "define", ehkä...) kyseisen makron, ja esikääntäjä yksinkertaisesti poistaa tulostuskutsut ennen kääntämistä. Emme ole enää kiinnostuneita tulosteista, vaan konekielisestä laskennasta. Tulostusten poisto yksinkertaistaa osaltaan tutkittavaa assembler-koodia.
Nyt ajetaan debuggeria komentojonotilassa, ja tuloste ohjataan talteen logitiedostoon:
gdb -batch -x gdbkomennot_malli malliharkka > logi_malli.txt
Optiolla -batch kerrotaan gdb:lle, että sen ei pidä odottaa tai kysellä mitään interaktiota käyttäjältä, vaan kaikki tarvittava on komentotiedostossa. Optio -x gdbkomennot_malli pyytää gdb:tä ottamaan vastaan tiedostoon gdbkomennot_malli kirjoitetut toimintaohjeet. Loppu lienee selvää. Toki saat mielellään olla hiukan kiinnostunut siitä, mitä gdbkomennot_malli sisältää.. Voin kertoa, että sen laatiminen tapahtui taas perinteisen hataralla pohjalla netistä ohjeita ja manuaaleja etsiskelleen. Komentotiedoston sisältö on kuitenkin sivuseikka... logitiedostoon ilmaantuvien tulosteiden pitäisi olla suomen kielellä kohtalaisen selvästi annotoituja.
Tutki tulostetta. Harjoitustyö perustuu samanlaisen login tutkimiseen eri ohjelmasta (miniharkka.c). Esimerkiksi näin saat tulostettua ihan vaan "jäljen" eli kiinnostavassa pätkässä suoritettujen konekäskyjen osoitteet ja suoritusjärjestyksen:
grep "=>" logi_malli.txt
Varo vaaraa jälleen: Huomaa lainausmerkit grepin ensimmäisessä argumentissa "=>" sillä jos niitä ei olisi, niin shell kuvittelisi että haluat väkäsellä ohjata tavaraa tiedoston logi_malli.txt päälle, ja uusiksihan se silloin menisi, kun tiedosto tyhjenisi...
Seuraavalla komennolla voisi tarkkailla, minkälaiset konekäskyt ovat muuttaneet lippurekisterin bittejä:
grep -E "FLAGS|=>" logi_malli.txt
Palataan alkuperäiseen aiheeseen, eli pinokehyksen käyttöön aliohjelma-aktivaation toteutuksessa ja siihen, kuinka parametrit välitetään aliohjelmalle ja paluuarvo sieltä takaisin kutsuvaan ohjelmaan...
Malliharjoitustyö; "ohjattu login tutkiminen".
Edellä tehty logi on normaali tekstitiedosto, joten sitä voi tutkia millä tahansa tekstieditorilla (joka osaa tulkita ASCII-koodin 0x0a eli "line feed" rivinvaihdoksi).
Höpsöhkönä esimerkkinä tulosteesta voisi poimia tilanteita vaikkapa grepillä (vaikkei tässä välttämättä järkeä olekaan, mutta jos nyt tilanne mahdollistaa, niin miksipä ei...). Koodipaketissa on mukana shell-skripti, jolla voi näyttää logista tietyn käskyn suorituksen ja askeltaa eteen- ja taaksepäin:
./selaa_tulostetta.sh logi_malli.txt
Tulosteen voi ottaa näkyviin myös vaikka nettiselaimeen, jossa voi kelata askel kerrallaan taakse ja eteenpäin selaimen "Find next" ja "Find previous" -toiminnoilla.
Varsinaisessa logissa on tilannekuva jokaisen konekielikäskyn suorituksen jälkeen siitä asti kun pääohjelma alkoi, siihen asti kun se loppuu. Välissä tapahtuu luonnollisesti se yksi aliohjelmakutsu. Tilannekuva on seuraavanlaisessa formaatissa:
--- Logitettu jälki tähän asti: 31 suoritettua konekäskyä --- Muutaman olennaisimman rekisterin tilanne: RIP (käsky) 0x000000000040066b RSP (huippu) 0x00007fffffffde30 RBP (kanta) 0x00007fffffffde60 RAX (yleis) 0x0000000000000005 RBX (yleis) 0x0000000000000000 FLAGS (liput) 1000000110 --- Pinomuistia: 0x7fffffffdec0: 0x0000000000000000 0x7fffffffdeb8: 0x0000000000000006 0x7fffffffdeb0: 0x00007fffffffdfa0 0x7fffffffdea8: 0x0000000000000008 0x7fffffffdea0: 0x0000000000000002 0x7fffffffde98: 0xfffffffffffffff0 0x7fffffffde90: 0x0000000000000003 0x7fffffffde88: 0x0000000000000002 0x7fffffffde80: 0xffffffffffffffff 0x7fffffffde78: 0x0000000100000000 0x7fffffffde70: 0x00007fffffffdfa8 0x7fffffffde68: 0x0000000000400748 kehyksen kanta --> 0x7fffffffde60: 0x00007fffffffdec0 0x7fffffffde58: 0x0000000000000005 0x7fffffffde50: 0x0000000000000000 0x7fffffffde48: 0x0000000000000000 0x7fffffffde40: 0x00000000f63d4e2e 0x7fffffffde38: 0x00007fffffffde80 pinon huippu --> 0x7fffffffde30: 0x0000000000000006 --- Seuraavaksi "noudettava ja suoritettava" käsky: => 0x40066b <kaanna_taulukko+229>: jg 0x4005c1 <kaanna_taulukko+59> --- Suoritetaan 32. käsky nyt... 68 if (taulu[ivasen] == taulu[ioikea]){ --- Pling. suoritettu
Alussa on tieto siitä, montako käskyä on tähän mennessä logitettu. Sitten on pinoon liittyvien rekisterien sisältö. Sitten on varsinaisen virtuaalimuistin sisältöä muodossa osoite_heksana: sisältö_heksana. Kantaosoite ja pinon huipun osoite on annotoitu muistitulosteeseen. Näissä esimerkkiohjelmissa kaikki käsiteltävät luvut, muistiosoitteet mukaanlukien, ovat tarkoituksella saman mittaisia (64 bittiä), joten osoitteiden tulostaminen 8 tavupaikan välein antaa ymmärrettävän tulosteen.
Tässä esimerkissä aliohjelman suoritus on hyvässä vauhdissa ja sillä on jo luotuna oma alue pinon päällä. Taulukon sisältö ei ole käytettävissä muuten kuin kehyksen sisälle viedyn muistiosoitteen (0x00007fffffffde80) kautta. Tässä siis perusesimerkki "olion tilan muuttamisesta viitteen kautta". Lopussa logiin on vielä laitettu seuraavana suoritettavan käskyn disassembly. Esimerkin tapauksessa tehdään ilmeisesti ehdollinen hyppykäsky jg ("jump if greater"), joka liittyy silmukan loppuehdon tarkistamiseen koodirivillä for(ioikea=koko-1; ioikea>ivasen; ioikea--){ - ikävä kyllä en ehtinyt selvittää, saisiko tulosteessa oikaistua rivien tulostusta. Debuggeri nimittäin tulostaa käskyn suorituksen yhteydessä (stepi) automaattisesti seuraavan koodirivin, jolle suoritus siirtyy. Tämä on interaktiivisessa käytössä luonnollista ja mukavata, mutta tällaisen login tekemisessä olisi kiva, jos tuossa olisi vielä lukenut se koodirivi, johon nimenomaan näytetty käsky liittyi.
Tarkoitus on logia selaamalla selvittää, miten paikallisia muuttujia, parametreja ja yksittäistä paluuarvoa konkreettisesti pidetään muistissa missäkin vaiheessa suoritusta, ja kuinka tämä hoituu kahta osoiterekisteriä käyttäen. Tarkoitus on tutkia, pohtia ja ymmärtää sekä kysyä apuja, mikäli tarvitsee.
Koodipaketissa on mukana toinenkin C-koodi nimeltään miniharkka.c - tutustu siihen, kunnes ymmärrät sen ikään kuin se olisi itse tekemäsi. Eli lue, käännä, aja jne... Seuraavaksi tehdään käännökset ja logi gdb:llä samalla periaatteella kuin edellä:
Idea:
gcc -g -O0 -DHILJAA -o miniharkka miniharkka.c gdb -batch -x gdbkomennot_harkka miniharkka > harkka_logi.txt
Ajo on nyt tallessa tekstitiedostossa harkka_logi.txt. Ota syntynyt tekstitiedosto auki lempparitekstieditoriisi tai muuhun, millä sitä mielestäsi on hyvä tutkia.
Vastauksesi on täsmälleen yksi tekstitiedosto, nimeltään vaikkapa miniharkka.txt, johon tulee vastauksia kysymyksiin ja valikoituja otteita edellä tehdystä logista harkka_logi.txt kommentoituna suomen kielellä.
Vastaa seuraaviin kysymyksiin login perusteella:
Montako konekäskyä ohjelman suorittaminen vaati pääohjelman alusta loppuun?
Montako konekäskyä ohjelman suorituksesta oli tutkitun aliohjelman sisällä?
Montako aktivaatiota kutsupinossa oli enimmillään?
Mitkä olivat aktivaatioiden pinokehysten kantaosoitteet sillä hetkellä?
Missä osoitteissa sijaitsivat aktivaatioiden paluuosoitteet?
Missä aliohjelman summaa_ja_nollaa_valilta paluuarvo sijaitsi siinä vaiheessa, kun aliohjelman viimeinen käsky ret oli juuri suoritettu?
Tulosteita käyttäen hahmota, missä kohtaa muistia (tai prosessoria) mikäkin asia sijaitsi ajon aikana. Kirjoita seuraavan muotoiset rivit, joissa kerrot jokaisen kiinnostavassa aliohjelmassa summaa_ja_nollaa_valilta olevan muuttujan sekä asian muistiosoitteet juuri kyseisen aliohjelma-aktivaation aikana.
Käytä osoitteille heksalukuja kuten gdb. Käytä nyt absoluuttisia virtuaalimuistiosoitteita, älä suhteellisia rekistereihin nähden (eli joudut ihan vähän laskemaan heksaluvuilla, tai tekemään tulkinnan muistissa olevien lukuarvojen perusteella):
Muuttuja indA oli muistiosoitteessa 0x287562858 Muuttuja indB oli muistiosoitteessa ... Muuttuja i oli muistiosoitteessa ... ... [Käsittele kaikki C-lähdekoodissa olevat muuttujat, niin lokaalit muuttujat kuin parametritkin.] Taulukkoparametrin osalta selvennä seuraavin tavoin: HARKKA: Aliohjelman taulukkoparametrin ``taulu`` sisältämät numerot sijaitsevat muistiosoitteissa ALKUOSOITE - LOPPUOSOITE
Login alussa on disassemblyt pääohjelmasta main sekä aliohjelmasta summaa_ja_nollaa_valilta. Kopioi nämä vastaustiedostoosi ja osoita tallentuneiden disassembly-rivien perään kirjoitetulla tekstillä, että olet ymmärtänyt miten aliohjelmaa kutsutaan ja miten sieltä palataan. Eli kohdenna vastaus aliohjelman disassemblyn alkuun ja loppuun sekä main-aliohjelman disassemblyssä niiden käskyjen kohdalle, jotka kävit kutsun osalta läpi konekäsky kerrallaan. Jokaisen aktivaatioon liittyvän konekielisen käskyrivin perään (muistele siis, mitkä rivit liittyivät aktivaatioon, so. parametrien välitys, RIP:n päivitys, paluuarvon välitys, paluuarvon sijoittaminen paikalliseen muuttujaan, tilanvaraukset pinosta) kirjoita seuraavan muotoinen kommentti:
0x000000000040066d <main+92>: callq 0x4004f8 <kaanna_taulukko> HARKKA: Yllä olevan käskyn rooli aliohjelmakutsun toteutuksessa on ... [tai vastaava aloitus, jolla aloitat selityksen, mikä on käskyn merkitys kokonaisuuden kannalta]Aliohjelman disassemblyssä tee samoin jokaisen aktivaatioon liittyvän koodirivin perään, siis aliohjelman alussa ja lopussa tiettyyn määrään rivejä. Muita kuin aliohjelmamekanismiin liittyviä rivejä ei tarvitse kommentoida mitenkään! Esimerkki:
0x00000000004004f9 <kaanna_taulukko+1>: mov %rsp,%rbp HARKKA: Yllä olevan käskyn rooli aliohjelmakutsun toteutuksessa on ... [tai vastaava aloitus, jolla aloitat selityksen mikä on käskyn merkitys kokonaisuuden kannalta] 0x00000000004004fc <kaanna_taulukko+4>: mov %rdi,-0x40(%rbp) HARKKA: ...[Tämän rivin merkitys ei ole niin kriittinen, mutta saa nämäkin toki huvikseen kommentoida; liittyväthän nämä pinokehyksen käyttöön... ] ... sitten kun tulee algoritmin toteutuksellisten rivien käännös, ei tarvitse kommentoida ollenkaan...