PRINCIPLES OF PROGRAMMING LANGUAGES - DEBUGGER Group 16 Ville Laatu Henri Myllyoja -
i SISÄLLYSLUETTELO 1. DEBUGGERI YLEISESTI... II 1.1 Debuggerin käyttämien... ii 1.2 Debuggerin käynnistäminen... ii 1.2.1 Debuggerin liittäminen ohjelmaan... ii 1.2.2 Debuggeri käynnistää ohjelman...iii 2. PTRACE SYSTEEMIKUTSU... IV 2.1 Komennon käyttäminen... iv 3. BREAKPOINTIT... V 3.1 Breakpointin asettaminen debuggerista... v 4. DEBUGGAUSINFORMAATIO... VI 4.1 Debugging Information Entry... vi 4.2 Funktiotietue... vi 4.3 Muuttujatietue ja muuttujan löytäminen muistista... vi 4.4 Koodirivien mappaus muistiosoitteisiin... vi 5. LÄHTEET... VIII
ii 1. DEBUGGERI YLEISESTI Debuggeri on ohjelma, jota käytetään ohjelmien testaukseen ja debuggaukseen. Jos tiedetään, että ohjelmassa on olemassa jokin virhe, debuggerin avulla on mahdollista jäljittää se kohtalaisen helposti. 1.1 Debuggerin käyttämien Yksi yleisimmistä tavoista käyttää debuggeria on asettaa ns. break point tiettyyn pisteeseen ohjelmakoodia, jolloin ohjelman suoritus halutaan pysäyttää. Tämän pysäytyksen seurauksena ohjelman suoritus siirtyy debuggerille, joka kerää kaikki tiedot ohjelman sen hetkisistä muuttujista, muistiviittauksista. Debuggerilta saatujen tietojen avulla voidaan tarkastaa ohjelman toimintaa, että onko kaikki arvot sellaisia kuin niiden kuuluisikin olla ja täten jäljittää ja etsiä jotain ennalta tuntematonta virhettä. Jos ohjelmassa ilmenee jokin virhe tai bugi ja tämä on jo paikallistettu, niin tällöin on mahdollista asettaa breakpoint juuri ennen tätä virhettä ja tämän jälkeen edetä askel askeleelta ohjelmakoodia eteenpäin. Debuggerin avulla on myös mahdollista muuttaa ohjelmassa olevien muuttujien ja muistiviittausten arvoja käsin ennen kuin ohjelman suorituksessa edetään eteenpäin. 1.2 Debuggerin käynnistäminen Debuggeria voidaan käyttää kahdella eri tavalla. Joko debuggeri käynnistää debugattavan prosessin itse ja alkaa debuggaamaan sitä ohjelmoijan määrittämällä tavalla. Toinen tapa käyttää debuggeria on se, että debuggeri liittää itsensä jo olemassa olevaan prosessiin seuraten sen suoritusta. Ennen debuggauksen käynnistämistä debuggeri kuitenkin kysyy käyttöjärjestelmältä lupaa, että voiko se aloittaa prosessin debuggauksen. Lupa tarvitaan, koska nykypäivänä ei missään nimessä saisi lukea tai pystyä kirjoittamaan toisen prosessin muistiavaruudessa. Täten kernelin tulee tarjota debuggerille ja debugattavalle ohjelmalle mahdollisuus kommunikoida keskenään. Kommunikointiin kernel tarjoaa signaalit, joita lähetetään edestakas debuggerin ja prosessin kesken. 1.2.1 Debuggerin liittäminen ohjelmaan Liitettäessä debuggeri ohjelman suoritukseen, käytetään sen tunnistamiseen prosessi ID:tä tai nimeä. Liittäminen linuxilla tapahtuu ptrace():n PTRACE_ATTACH systeemikutsulla. Tällöin debuggeri liitetään ajettavaan ohejlmaan, jolloin se menee ns. tapahtuma-looppiin odottamaan käyttöjärjestelmän lähettämiä signaaleja. Käyttöjärjestelmän lähettämien signaalien avulla debuggeri saa tietoa prosessin
iii tapahtumista ja sen etenemisestä. Erilaisia tapahtumia voivat olla esimerkiksi jo yllämainitut muistiviittaukset ja muuttujien arvomuutokset. 1.2.2 Debuggeri käynnistää ohjelman Toinen tapa jolla debuggerin voi käynnistää, on linuxin PTRACE_TRACEME komento. Kun tämä komento suoritetaan, luo debuggeri ensin fork() systeemikutsulla kopion ohjelmasta, jota sen on tarkoitus debugata. Tämän jälkeen debuggeri lähettää vielä exec() tai execve() systeemikutsun, jolloin juuri luotu kopio aloittaa ohjelmasuorituksen, jota se sitten seuraa isäntä prosessina. Tällöin debuggeri on siis oma irrallinen prosessinsa ja sen tarkoituksena on debugata tätä juuri luotua lapsi prosessia. Debuggaus tapahtuu vähän vastaavanlaisesti, kuin liittämisessäkin, eli otetaan vastaan käyttöjärjestelmän lähettämiä signaaleja ja kerätään tietoa tätä kautta debuggerille. Signaalityypit vain hieman poikkeavat toisistaan näiden kahden eri debuggaustavan kesken.
iv 2. PTRACE SYSTEEMIKUTSU Ptrace on linuxin systeemikutsu, joka tarjoaa keinon kuinka jokin prosessi voi jäljittää toista prosessia tutkien sen muistissa ja rekistereissä olevia arvoja. 2.1 Komennon käyttäminen Kun ptrace() komentoa käytetään, ottaa se vastaan neljä parametriä, joista kaksi ensimmäistä ovat pakollisia. Ensimmäinen kertoo, että millainen operaatio halutaan tehdä, esimerkiksi luku- vaiko kirjoitus-operaatio. Toisessa parametrissä lähetetään prosessin id tai nimi, johon tämä operaatio halutaan suorittaa. Kolmannessa voidaan antaa jokin tietty tarkkaan määritelty muistiosoite, johon operaatio suoritetaan, esimerkiksi jonkin muuttujan muistiosoite. Viimeisenä annetaan osoite moniin tietorakenteisiin, joita joko luetaan tai kirjoitetaan prosessiin tai prosessista ulos.
v 3. BREAKPOINTIT Breakpoint on paikka ohjelmassa jonka kohdalla debuggaus halutaan keskeyttää esimerkiksi muistiarvojen tarkastelemista varten. 3.1 Breakpointin asettaminen debuggerista Tapa jolla breakpoint asetetaan vaihtelee käyttöjärjestelmän ja arkkitehtuurin mukaan. Selkeyden vuoksi tässä keskitytään vain i386 ja x86_64 arkkitehtuureihin linuxilla. Breakpointin asettamiseen käytetään näissä arkkitehtuureissa int 3 - ohjelmistokeskeytystä. Int 3 on yhden tavun mittainen keskeytys, jonka vastaantullessa lähetetään SIGTRAP-signaali. Debuggeri korvaa halutusta kohdasta prosessia käskyn ensimmäisen tavun int 3:lla ja muistaa siinä olleen alkuperäisen tavun. Kun debugattavaa prosessia suoritetaan ja päädytään breakpointiin, debuggeri vastaanottaa prosessilta SIGTRAP-signaalin. Debuggeri korvaa nyt tähän asetetun int 3:n alkuperäisellä käskyllä ja vierittää ohjelmalaskuria taaksepäin oikeeaan kohtaan. Tässä vaiheessa ennen ohjelman suorituksen jatkamista debuggeri voi esimerkiksi tarkastella arvoja ohjelman muistista käyttäen yllä esiteltyä ptrace-kutsua. Breakpointien periaate on yksinkertainen, mutta käytännössä asiasta tulee ongelmallisempaa, koska normaalisti debuggereissa breakpointit asetetaan tiedetyn muistiosoitteen sijasta esimerkiksi tietylle koodiriville. Koodirivien mappaukseen muistiosoitteiden kanssa tarvitaan aputyökaluksi debuggausinformaatiota, jota käsitellään seuraavassa luvussa.
vi 4. DEBUGGAUSINFORMAATIO Ohjelmaa käännettäessä kääntäjä voi lisätä objektitiedostoon debuggausinformaatiota debuggauksen avuksi. Unixin kaltaisissa käyttöjärjestelmissä tähän käytetään yleensä DWARF-formaattia. GCC:llä käännettäessä debuggausinformaatiot lisätään -g vivulla. Tämä lisää objektitiedostoon joukon.debug_ -alkuisia osioita. 4.1 Debugging Information Entry DWARF:ssa jokaista proseduuria, muuttujaa, tietotyyppiä yms. varten luodaan DIEtietorakenne (Debugging Information Entry) osioon.debug_info. Jokaisella DIE:llä on tagi ja joukko avain-arvopareja. DIE voi sisältää myös toisen DIE:n, luoden näin puurakenteen. 4.2 Funktiotietue Jokaista funktiota varten luodaan DIE tagilla DW_TAG_subprogram. Tämä DIE sisältää muun muassa funktion nimen, tiedoston ja rivinumeron josta se löytyy, sekä ohjelmalaskurin alku- ja loppuosoitteet tätä funktiota varten. Se sisältää myös frame basen jonka avulla esimerkiksi funktion sisältämät muuttujat etsitään muistista. Tämä frame base on tyypillisesti pino-osoitin +/- offset, mutta monimutkaisemmissa järjestelmissä se voi myös olla lista tällaisia osoitin-offset pareja joista valitaan oikea sen mukaan missä kohdassa suoritusta sillä hetkellä ollaan. 4.3 Muuttujatietue ja muuttujan löytäminen muistista Jokaiseen funktio-die:n luodaan ali-die jokaista sen sisältämää muuttujaa varten tagilla DW_TAG_variable. Kuten funktioidenkin kohdalla, muuttuja-die sisältää tiedon muuttujan nimestä, tiedostosta ja rivinumerosta jossa se on esitelty sekä linkin sen tyyppiä varten luotuun DIE:n. Se sisältää myös tiedon muuttujan sijainnista offsetina kyseisen funktion frame baseen. Kun muuttuja halutaan ajon aikana etsiä muistista, otetaan siis ylös sen offset ja katsotaan funktio-die:stä frame basen sijainti. Jos vaikkapa offset on -20, ja frame base on pinoosoitin + 4, niin muuttuja löytyy sijainnista pino-osoitin - 16. 4.4 Koodirivien mappaus muistiosoitteisiin DWARF sisältää.debug_line -osassa mappauksen koodirivistä muistiosoitteeseen. Tämän tiedon avulla breakpoint voidaan asettaa halutulle koodiriville. Tätä voidaan
käyttää myös käänteisesti: ohjelman kaatuessa voidaan selvittää, millä koodirivillä kaatuminen tapahtui. vii
viii 5. LÄHTEET https://eli.thegreenplace.net/2011/01/27/how-debuggers-work-part-2-breakpoints https://en.wikipedia.org/wiki/dwarf https://eli.thegreenplace.net/2011/02/07/how-debuggers-work-part-3-debugginginformation http://www.dwarfstd.org/doc/dwarf-2.0.0.pdfhttp://www.alexonlinux.com/howdebugger-works https://eli.thegreenplace.net/2011/01/23/how-debuggers-work-part-1 https://en.wikipedia.org/wiki/ptrace https://en.wikipedia.org/wiki/debugger https://stackoverflow.com/questions/216819/how-does-a-debugger-work http://man7.org/linux/man-pages/man2/ptrace.2.html https://mikecvet.wordpress.com/2010/08/14/ptrace-tutorial/