Debugging Tore Berg Hansen, TISIP
Innhold Innledning... 1 Å kompilere og bygge et program for debugging... 1 Når debugger er i gang... 2 Symbolene i verktøylinjen... 3 Start på nytt... 3 Stopp debugging... 3 Avbryt... 3 Step over... 3 Step ut av... 3 Step inn i... 4 Vis neste setning... 4 QuickWatch... 4 Debugg vinduer... 4 Watch... 4 Variabler... 5 Registers... 5 Memory... 6 Call stack... 6 Dissassembly... 6 Andre menyvalg som berører debugger... 7 Et lite eksempel... 9 i
Innledning Debugging er prosessen med å lokalisere hvor i koden årsaken til at et program eventuelt feiler ligger. Deretter kan feil rettes. Debugging gjøres på programmer som ikke feiler under kompilering og lenking. Det vil si at programmer kjører, men leverer ikke forventede resultater. En debugger er et verktøy som kan hjelpe til med å lokalisere feilkildene. Med en debugger kan man kjøre et program linje for linje sette stopp-punkter (breakpoints) for så å kjøre programmet frem til disse overvåke innholdet i variabler sette nye verdier i varibler evaluere uttrykk endre kode For at man skal kunne gjøre dette i et program blir kode for debugging lagt til programmet. Dette gjør at programmer med debugger ikke vil være optimalisert verken for plass eller hastighet. Når programmer virker uten å feile vil man kompilere og bygge en frislippversjon som er optimalisert etter ønskede kriterier som kan være f.eks hastighet. Vi skal i dette notat se nærmere på debuggeren i Visual Studio. Den har de fleste muligheter vi kan ønska av en debugger. Å kompilere og bygge et program for debugging Visual Studio gir mulighet for å kompilere å bygge et program i to versjoner, enten for frislipp eller for debugging. Det bestemmer man ved å gå inn på menyvalget Build og deretter klikke på Set Active Configuration Da kommer denne dialogen opp som er selvforklarende. 1
Etter at konfigurasjon er valgt bruker man de samme kommandoer til å kompilere, bygge og kjøre. Skal man derimot debugge går man inn i Build menyen og klikker deretter på Start Debug. Da får man flere alternativer: Go programmet vil kjøre til slutt hvis det ikke er noen stopp-punkter. Step Into går til første linje i programmet og stopper. Run to Cursor kjører til der cursor står i kildekoden. Attach to Process - kobler debugger til en prosess som kjører. Prosessen kan så avbrytes og debugges. Det finnes også to andre muligheter. Man kan bruke spesielle funksjonstaster eller klikke på symboler på verktøylinjen. Funksjonstastene er F5 for Go, F11 for Step Into og CTRL-F10 for Run to Cursor. Symbolene i verktøylinjen er som vist på denne figuren: Bygg Stopp bygg Go Kompiler Kjør program Sett/fjern stopp-punkt Når debugger er i gang Når debugger er i gang vil menyvalget Debug overta for Build. Det kommer også opp en flytende verktøylinje for debugger som er et alternativ til menyvalget Debug. Verktøylinjen er vist i neste figur. 2
Bruk kode endring Avbryt Stopp debugging Vis neste setning Step inn i Step over Step ut av Start på nytt Run to Cursor QuickWatch Watch Registre Stakk Dissassembly Variabler Minne Åpning/lukking av vinduer Den flytende verktøylinjen kan dokkes. Vi skal se nærmere på hva som skjuler seg bak symbolene og dermed hva vi kan gjøre i debuggeren. Symbolene i verktøylinjen Her skal vi se på symbolene som ikke er behandlet tidligere. Start på nytt Begynner igjen på første linje i programmet hvor det stopper. Programmer lastes på nytt sli at alle verdier blir satt på nytt. Stopp-punkter beholdes. Stopp debugging Avslutter debugging. Programmet er klart til å starte på nytt. Avbryt Stopper programmet der det er i øyeblikket. Kan være nyttig hvis programmer kjører lenge. Step over Kjører en og en instruksjon i programmet. Når det treffes på en funksjon, blir den utført. Men man går ikke trinn for trinn i funksjonen. Step ut av Kjører ut av en funksjon. Stopper på instruksjonen rett etter kallet til funksjonen. Denne kan brukes for å hoppe kjapt ut av en funksjon etter å ha påvist at en feil ikke er inne i funksjonen. 3
Step inn i Går trinn for trinn gjennom programmet. Går også inn i en funksjon når den påtreffes og fortsetter trinn for trinn inne i funksjonen. Vis neste setning Viser neste setning i programkoden. Hvis kildekode ikke er tilgjengelig vises setningen i et dissassembly vindu. QuickWatch Viser QuickWatch vinduet hvor man kan legge inn utrykk. Dette er QuickWatch vinduet. Her har vi lagt inn uttrykket a*b og klikket på Recalculate. Hvis vi nå klikker på Add Watch vi utrykket bli lagt inn i Watch vinduet, hvor det kan overvåkes. Debugg vinduer Debuggeren har disse vinduene som man kan åpne eller lukke ved å klikke på symbolene i verktøylinjen: Watch Viser navn og verdier for variabler og utrykk. Man kan legge dette inn i fire forskjellige vinduer. På den måten kan man gruppere de data man ønsker å overvåke. Slik er det mulig å få bedre oversikt, f.eks hvis man skal overvåke mange data som kan være lokalisert til forskjellige moduler i programmet. Neste figur viser Watch vinduet. 4
Når en verdi skifter farge til rødt indikerer det en nylig endring i verdi. Vinduet har form av et regneark. Man kan sette inn på navn og verdier. Variabler Vinduet viser informasjon om variabler. Det er tre tagger. I Auto vises variabler brukt i nåværende og foregående setninger. Likeledes vises returverdier fra funksjoner. Locals har variabler som er lokale i den aktuelle funksjon man er inne i. This viser objektet pekt på av this. Her er et vindu fra en kjøring. Legg merke til hvordan tabeller vises. man kan ekspandere eller komprimere ved å klikke på + eller symbolene. En verdi angitt med rødt har nylig skiftet verdi. Registers Dette vinduet viser innholdet i CPU ens generelle registre og statusregistre. Derfor er innholdet avhengig av hvilken prosessor det kjøres på. Dette vinduer er interessant for de som driver med maskinnær programmering. Se figuren. 5
Memory Innholdet i datamaskinens lager (minne) vises i dette vinduet. Også dette vinduet er mest interessant ved maskinnær programmering. Call stack På norsk stakk. Det viser stakken med alle funksjonskall som er aktive. Sist kalte funksjon øverst. Legg også merke til at figuren viser at sist kalte funksjon inneholder et stopp-punkt og at programmet har stoppet der. Dissassembly. Her vises assembly kode for det kompilerte programmet. Kan være nyttig hvis kildekode ikke finnes. Det er også nyttig hvis det er flere kommandoer på samme linje i kildekode. I kildekoden kan man bare kjøre linje for linje, mens man i assemblerkoden kan kjøre kommando for kommando. 6
Figuren ovenfor viser et utsnitt av assembler koden for en funksjon hvor programmet er stoppet. Legg merke til at først kommer en linje fra kildekoden. Deretter følger assembly koden for denne linjen og så nest linje i kildekoden. I dette vinduet kan man gå kommando for kommando ved å klikke på Step inn i (Step Into). Andre menyvalg som berører debugger Edit og Breakpoints får frem denne dialogen: Den gir mulighet for å sette avanserte stopp-punkter i form av bl.a logiske uttrykk, som vist her 1. Man kan få programmet til å stoppe når 1 Under demonstrasjonen i klasserommet fikk vi ikke dette til å fungere. det skyldes at vi satte betingelsen før programmet var startet og ikke kvalifiserte uttrykket med aktuell fil. 7
en variabel endrer verdi når et uttrykk endrer verdi når et uttrykk er sant når første element i en tabell skifter verdi når første element i en tabell har en bestemt verdi når et bestemt element i en tabell skifter verdi når et hvilket som helst element i en tabell skifter verdi når et hvilket som av de n første elementene i en tabell skifter verdi når verdien i en peker sifter verdi når verdien i det det pekes på skifter verdi når en verdi i en bestemt lageradresse skifter verdi når e register endres når et registeruttrykk er sant Det er stort samme fremgangsmåte. Man klikker på Edit og deretter Breakpoints. Når dialogen spretter opp Data taggen. Uttrykk skrives inn i tekstboksen under Enter the expression to be evaluated. Klikk OK til slutt for å sette stopp-punktet. View og Debug Windows gir rask tilgang til de forskjellige vinduer. Tools, Options og Debug gir mulighet for å spesifisere forskjellige egenskaper for debuggeren. 8
Et lite eksempel Her er et veldig enkelt program. Det skal konvertere fra kilometer til engelske mile og nautiske mil. #include <iostream.h> int tilmile (double& kilometer); int tilnautiskmile (double& kilometer); void main( void ) { double antallkm; cout << "Sett inn antall km som skal konverteres --> "; cin>> antallkm; cout << antallkm << " km " << " er " << tilmile( antallkm ) << " engelske mile" << endl; cout << antallkm << " km " << " er " << tilnautiskmile( antallkm ) << " nautiske mil" << endl; } int tilmile( double& kilometer) { if (kilometer >= 0.0) { kilometer = kilometer / 1.609; return 1; } } return 0; int tilnautiskmile( double& kilometer) { if (kilometer >= 0.0) { kilometer = kilometer / 1.852; return 1; } } return 0; Konverteringen skjer i to forskjellige funksjoner. Vi kan tenke oss at disse funksjonene er skrevet av andre programmere og tilhører et bibliotek som vi har hentet de fra. Det vi tror er at resultatet av konverteringen returneres i funksjonen. Vi kjører programmet og får dette resultatet. 9
Dette er åpenbart feil. En inspeksjon av koden vil fort fortelle oss hvorfor, men la oss se hvordan vi kunne bruke debuggeren til å lokalisere feilkilden (buggen). Vi vil kjøre en test: Inndata 1km Forventet resultat 0.621504 mile og 0.539957 nautiske mil Setter stopp-punkter etter at data er lest inn og etter beregningene. I Watch vinduet vil vi overvåke verdien av konverteringene ed å se på uttrykkene antallkm / 1.609 og antallkm / 1.852 samt verdien av antallkm. Figuren viser situasjonen etter at programmer er stoppet ved første stopp-punkt. 10
Klikk her for å sette stopp-punkt Disse uttrykkene er skrevet direkte inn Så kjører vi frem til neste stopp-punkt og får denne situasjonen vist i et utsnitt. Det vi ser er at returverdien til funksjonen tilmile() er 1 og at antallkm har endret verdi. Det samme har uttrykkene. Så det er åpenbart at det er noe galt med funksjonene som gjør konverteringene. 11
Vi vil kikke nærmere på det som skjer i linjene som gjør utskriften og klikker på dissassemblervinduet. Neste utsnitt vier situasjonen etter at vi har gått trinn for trinn og stoppet etter kall til funksjonen tilnautiskmil(). Vi ser at uttrykkene igjen har skiftet verdi. Så det er i funksjonene det skjer. Dette får oss til å se nærmere på spesifikasjonene for funksjonene. De viser at den konverterte verdi returneres i argumentet ved referanseoverføring. Returverdien i funksjonen skal 12
indikere om det var en lovlig verdi eller ikke som ble overført. Det er den standard som er brukt. Vi kan altså ikke bruke funksjonene på den måten. Enten må vi lage nye funksjoner som foretar verdioverføring eller så må vi skrive om programmet slik at det bruker funksjonene riktig. I det første tilfelle må vi avvike en standard som gjelder og det bør vi antagelig ikke gjøre. Så i tillegg til å se på hvordan debuggeren kan brukes, ser vi også hvor viktig det er å sikre at man har forstått grensesnittet til funksjoner som skal gjenbrukes i nye sammenhenger. 13