tiistai 17. maaliskuuta 2009

C - kieli, vikojen etsintä.





Debuggerit (erityisesti gdb)

Ensimmäistä ohjelmapätkää C:llä koodatessani vietin tunteja etsien indexivirhettä for loopeistani. (Ensimmäinen ohjelmani oli muuten koodinpätkä, joka korvasi parametrina annettuja stringejä, toisilla - tehty silloisten kotisivujeni linkkiosion päivitykseen). Vian etsimiseen käytin printtejä. Tulostin muuttujan arvon toisensa jälkeen, ja yritin löytää kohdan jossa ohjelma kaatui... Printit ovat toki edelleen tehokas keino virheiden etsintään, mutta vuosien aikana olen tutustunut moniin tehokkaisiin ohjelmiin jotka helpottavat vikojen löytämistä.

Debuggerit:


Toivotko joskus että voisit nähdä muuttujien arvot ohjelman suorituksen aikana? Haluaisitko seurata ohjelman ajoa rivi riviltä, nähden miten suoritus hyppii kutsusta toiseen? Tämä on mahdollista debuggerin avulla. Muutamien windows koodaajien olen kuullut kehuvan MS visual studio 2005 mukana tulevaa debuggeria. Itse en ole kyseiseen vempeleeseen tutustunut, sillä windows on kaiken kaikkiaan aika outo alusta minulle. Linuxilla taas tunnetuin debuggeri on gdb, joka yleensä löytyy kaikkien distrojen pakettienhallinnasta. Puhdas gdb voi olla aika hankala käyttää ensi alkuun, sillä se on puhtaasti komentorivipohjainen. Onneksi gdb:n mukana tulee hyvä "helppi", jota voit käyttää myös kesken debuggauksen kirjoittamalla yksinkertaisen komennon "help" tai "help <gdbn_komento>". Lisäksi gdb:tä käyttämään on tehty useita graafisia ohjelmia. Itse olen käyttänyt Eclipse IDE:ä, joka siis sisältää debuggerin lisäksi koodieditorin, käännösnapit jne. Eclipse ei kuitenkaan ole maailman käyttäjäystävällisin mitä tulee projektin luontiin. (ainakaan vanhemmat versiot eivät olleet). Toinen graafinen käyttöliittymä gdb:lle jota olen käyttänyt on ddd. Ddd on oikeastaan vain käyttöliittymä gdb:lle, ilman mitään ylimääräisiä lisäosia.

Jotta saat täyden hyödyn debuggeristasi, tulee sinun käännösvaiheessa sisällyttä "debuggi-infot" kääntämääsi binääriin. Tällöin debuggeri osaa näyttää kutsuttujen funktioiden nimet, ja kooditiedostot + rivinumerot ajon aikana. Gcc kääntäjällä, debuggi-infot saat binääriin käyttämällä käännöksessä -g (tai -ggdb) flagia. Esim:

gcc -ggdb -o testiohjelma testiohjelmakoodi.c

Ja ihan peruskomennot, joilla gdb:n kanssa pääsee alkuun:

Gdb käynnistetään kertomalla sille ajettavan binäärin nimi, tai vaihtoehtoisesti suoritettvan ohjelman ajokomento parametreineen --args vivun keralla.

gdb testiohjelma
tai
gdb --args ./testiohjelma parametri1 parametr2 parametri3...

Tämä käynnistää gdbn, joka lukee ajettavan binäärin, ja lataa tarvittavat (ja saatavilla olevat) debuggitiedot. Muista, että C ohjelmasi on tehty standardikirjastojen päälle. Mikäli siis olet asentanut debuggiversiot standardikirjastoista ja linkannut ohjelmasi niitä vasten, voit sujuvasti kurkistella myös standardikirjastojen toteutuksen sisään.

Tässä vaiheessa ohjelma on siis ladattuna gdb:n sisään, mutta ei ole vielä ajossa. Ohjelman ajon aikana et voi kskyttää gdb:tä, joten nyt on hyvä aika asettaa mahdolliset breakpointit (joihin tultaessa ohjelma pysähtyy), signaalien käsittelyt jne. Sen jälkeen kun ajat ohjelman gdb:n sisällä, voit pysäyttää gdb:n antamalla näppäimistöltä interrupt signaalin SIG_INT (ctrl+c). Tämä tosin pysäyttää ohjelman ajon satunnaiseen paikkaan, mikä harvoin on kannattavaa, ellet halua muuttaa joitain gdb:n asetuksia, kuten breakpointteja tms. (listaan komentoja jäljempänä)

Tyypillisin ja helpoin gdb:n käyttötapa on seuraava:

Sinulla on ohjelma foo joka kaatuu segmentation Fault ilmoituksen kanssa. (lue aiemmat tekstini muistinkäytöstä mikäli segmentation fault on hämärä käsite).

Ladataan ohjelma gdb:hen

gdb --args ./foo fooparameters
tai
gdb foo

ajetaan ohjelma:

run
tai
run fooparameters

odotetaan että ohjelma saavuttaa kaatumispisteensä...

Kun MMU (memory management unit) huomaa userspace ohjelman (joita kaikki normaali applikaatiot ovat) pyrkivän accessoimaan muistiosoitteeseen jota sille ei ole "mapattu" käyttöön (esim. kirjoittamalla alustamattomaan osoittimeen, tai taulukon rajojen yli), se herättää kernelin, joka (tavallisesti) lähettää SIGSEGV signaalin ohjelmalle. Normaalisti ohjelman saadessa SIGSEGV signaalin, se lopettaa suorituksensa Segmentation Fault ilmoituksen kera (ellei jotain handleria ole rekisteröity - mihin en nyt puutu). Debuggerilla ajettaessa debuggeri kuitenkin "poimii" signaalin, ja pysäyttää ohjelman suorituksen ongelmakohtaan (kohtaan jossa varsinainen laiton muistiaccessi tapahtui - todellinen ongelma tosin on voinut sattua jo aiemmin). gdb myös näyttää koodirivin jolla kirjoitus/luku tapahtui.



Mikäli kyseisessä kohdassa ei näytä olevan mitään erikoista, tai mikäli ollaan jonkin standardikirjaston sisäisessa kutsussa, käyttäjä yleensä antaa komennon
bt
joka näyttää pinosta funktiot, joiden kautta kaatumispisteeseen on tultu. Kuljetut funktiot on numeroitu siten, että tuoreempi funktiokutsu saa pienemmän numeron. Näitä kutsukohtia voi sitten pomppia eteen ja taaksepäin komennolla
frame
kunnes pääsee kohtaan jossa epäilee ongelman mahdollisesti majailevan. Nyt esim osoittimien osoittaman muistin tilan (tai tavallisen muuttujan sisältämän datan) voit tarkistaa komentamalla
inspect muuttujannimi
tai
inspect muistiosoite.
Mikäli sinulla on osoitin johonkin datarakenteeseen, voit tutkia koko rakenteen datan helposti komennolla
inspect *osoitinmuuttujannimi
tai tietyn jäsenen sisältämä datan
inspect osoitinmuuttujannimi->jäsen.

Mutta ennen kuin tämä osio venyy kohtuuttoman kauas, lopetan yksityiskohtaisen kerronnan, ja sanon pari varoituksen sanaa + listaan liudan käteviä komentoja:

VaRoItUs! Erityisesti useampisäikeisissä ohjelmissa on tyypillistä, että koodarit tekevät ikäviä synkronointibugeja, eli sallivat useamman säikeen käpistelevän jotain jaettua resurssia samanaikaisesti. Ikäviä näistä bugeista tekee se, että niiden laukeaminen on hyvin satunnaista, ja erityisen ajoituskriittistä. Kun ohjelmaa ajetaan debuggerissa, ohjelman suoritus hidastuu debuggerin pitäessä kirjaa tapahtumista => usein ajoituskriittiset viat jotka tulevat esiin ilman debuggeria ajettaessa, eivät tule esiin debuggerin kanssa - ja päinvastoin...

sitten se lupaamani
lista gdb:n peruskomennoista:


ohjelman ajo:
run

pysäytyskohdan asettaminen:
break funktionnimi
tai
break tiedostonnimi:rivinumero

ehdollinen pysäyttäminen:
aseta pysäytyskohta kuten edellä, ja pane merkille pysäytyskohdan numero
tee ehto:
condition
esim:
condition 1 muuttuja == 5

signaalien käsittely:
handle
tai
handle
(kertoo tämänhetkisen signaalin käsittelytavan)
tämän jälkeen voit muuttaa kyseisiä käsittelytapoja määrittämällä pysäytetäänkö ohjelman suoritus signaalin saapuessa, printataanko signaalin tulo ja päästetänkö signaali ajettavalle ohjelmalle.
handle stop print pass
esim.
handle SIGILL
handle SIGILL nostop noprint pass

Säikeet saat listattua (ja tiedon siitä mitä säikeet ovat tekemässä) käyttämällä komentoa:
info threads

ja säikeestä toiseen voit hypätä komennolla
thread numero

paikalliset muuttujat saat esille komennolla
info locals

Muuttujien sisältämää dataa pääsee tutkimaan paitsi inspect komennolla, myös komennolla
x
x-komento sallii sinun näyttävän myös haluamasi määrän dataa tietystä muistiosoitteesta (tai osoitinmuuttujan osoittamasta osoitteesta). Erityisen kätevä x komento on kaivettaessa dataa suuresta void muuttujasta, esim muistipoolin alusta. koko syntaksi on

x/ osoite/osoitinmuuttuja

jossa näyttöformaatti voi olla o=octaali, x=heksadesimaali, d=desimaali, u=unsigned desimaali, t=binari, f=liukuluku, a=osoite, i=käsky, c=merkki s=merkkijono.
datatyyppi (eli datan koko) voi olla b=tavu, h=2 tavua(short), w=4 tavua(int), g=8 tavua (64 bittiä)

eli esimerkkinä. Minulla on taulukko int foo[100], jonka alkuun osoittaa osoitin void *bar. Voin siis käskeä esim:
x/100xw bar
tämän pitäisi tulostaa 100 kappaletta (w) 4:n tavun kokoisia datakönttejä (x)heksadesimaalimuodossa.

Ohjelmasta voit irroittautua komennolla
detach
jolloin debuggeri jättää ohjelman ajelemaan yksikseen.

tiedostolistauksen saat komennolla
list
joka näyttää 10 riviä kohdasta johon olet juuri pysähtynyt, tai edellisen listauksen loppukohdasta. Voit myös määrittää tiedosto ja rivin jolta alkaen haluat nähdä 10 riviä, tai funktion jonka alusta haluat nähdä 10 riviä komennolla:
list funktionnimi
tai
list tiedostonnimi:rivinumero

Watchpoint muuttujan int foo sisällölle nimen ja osoitteen perusteella:

watch foo [threadinnumero mikäli rautawatchpointti]
watch (int)0xB00BBABE [threadinnumero mikäli rautawatchpointti]

Eli jälkimmäisessä siis oletetaan, että muuttuja foo on talletettu osoitteeseen 0xB00BBABE. Oikean osoitteen saat selville käskyllä

print &foo

Lisää watchpointeista

Valgrind:

Debuggerin kaverina on hyvä olla jokin muistinanalysointityökalu, jolla voit helposti paikantaa luvut/kirjoitukset yli varatun tilan, alustamattomien muuttujien käytön, muistivuodot jne. Jälleen windows maailma on minulle hieman outo, mutta linuxilla olen käyttänyt mm. valgrind nimistä työkalua.

valgrind --log-file=foo --leak-check=full -v ./ajettavaohjelma

ajaa ohjelman valgrind nimisessä "hiekkalaatikossa", memcheck nimisen muistinanalysointiohjelman alla, tehden foo.prosessiId nimisen logitiedoston, johon liitetään myös täysi muistivuotoanalyysi, ja vielä verbose (mahdollisimman paljon tietoa) moodissa.
Pienen esimerkkilogin ja sen analyysin koetan räpeltää tänne lähipäivinä, kunhan vain ennättäisin...

To Be Continued... ...later :)

Ei kommentteja: