
Kode-værkstedet 2: GCC og kode-dækning
Som tidligere lovet vil jeg sætte mere fokus på nogle af de værktøjer på Linux (eller andre UNIX-varianter såsom FreeBSD/OpenBSD), som jeg har fået øjnene op for det sidste halve år. Et af de mere ukendte er C/C++ kode-dækning, som kan analyseres med GCC selv. Ideen er at oversætte al koden med nogle ekstra parametre, og ved kørsel skrives automatisk hvilke linier af koden, som eksekveres. Det har især relevant, hvis man har mange (f.eks. 100) test-scenarier, hvor man ikke kan overkomme f.eks. at debugge sig igennem dem alle. En god start er "man gcov", som indeholder en let læselig gennemgang af "code coverage" analyse med GCC.
Til eksempel har jeg lavet følgende kode "v2.c". Bemærk at jeg har indsat linienumre som bedre at kunne skrive om hvad der sker i koden. I kan selv hente koden fra Pastebin, hvis I vil have den rå kode.
1:#include <stdio.h>
2: 3:int main(int argc, char **argv) 4:{ 5:
6: if (argc<2) 7: printf("ingen argumenter\n");
8: else
9: if (argc%2==1) 10: printf("et lige antal argumenter\n");
11: else 12: printf("et ulige antal argumenter\n");
13: 14: if (argc<=3) 15: printf("Højst to argumenter\n");
16: else 17: printf("Mere end to argumenter\n");
18:
19: return 0;
20:}
Normalt vil man oversætte koden med $ gcc -o v2 v2.c som giver den binære fil "v2" ud fra kildekoden "v2.c". Jeg laver med vilje ikke optimering af koden (-O2 eller -O3) for jeg vil netop ikke miste dele af koden.
Koden oversættes i stedet nu med $ gcc -o v2*** -fprofile-arcs -ftest-coverage*** v2.c
Eksempel er i den lette ende og det er let at se at programmet hopper ned i de forskellige løkker, alt efter antallet af argumenter til "v2" programmet. Første kørsel - uden argumenter. $ ./v2 ingen argumenter Højst to argumenter Som forventet kommer vi ned i linie 7 og linie 15 - vi kan se det på udskriften ovenfor.
Bemærk at allerede ved oversættelsen af programmet dannes en ny fil "v2.gcno", og når koden udføres skrives den egentlige database-fil "v2.gcda" med oversigt over hvad der har været udført.
Fordi koden blev oversat med "-fprofile-arcs -ftest-coverage", kan vi nu analysere kodedækningen (ud fra den ene kørsel af programmet). $ gcov v2.c File 'v2.c' Lines executed:60.00% of 10 v2.c:creating 'v2.c.gcov'
Ud kom "v2.c.gcov" - hvor 60% af de 10 effektive kodelinier har været udført. Lad os se på denne fil:
-: 0:Source:v2.c
-: 0:Graph:v2.gcno
-: 0:Data:v2.gcda
-: 0:Runs:1
-: 0:Programs:1
-: 1:#include <stdio.h>
-: 2:
-: 3:int main(int argc, char **argv)
1: 4:{
-: 5:
1: 6: if (argc<2)
1: 7: printf("ingen argumenter\n");
-: 8: else
#####: 9: if (argc%2==1)
#####: 10: printf("et lige antal argumenter\n");
-: 11: else
#####: 12: printf("et ulige antal argumenter\n");
-: 13:
1: 14: if (argc<=3)
1: 15: printf("Højst to argumenter\n");
-: 16: else
#####: 17: printf("Mere end to argumenter\n");
-: 18:
1: 19: return 0;
-: 20:}
<br/>
<br/>
<br/>
Det interessante er især linierne med hash-tegn foran - dvs. linie 10, 12 og 17 som endnu ikke er blevet udført. Vi kan med 1-tallerne foran bla. linie 7 se at de øvrige linier har været udført en gang. Det ser meget fint ud.
<br/>
Lad os lige køre programmet et par gange mere (her dræber jeg lige tekst-output).<br/><br/>
$ v2 1 > /dev/null<br/>
$ v2 1 1 > /dev/null<br/>
Og igen køres gcov:<br/>
$ gcov v2.c<br/>
-: 0:Source:v2.c
-: 0:Graph:v2.gcno
-: 0:Data:v2.gcda
-: 0:Runs:3
-: 0:Programs:1
-: 1:#include <stdio.h>
-: 2:
-: 3:int main(int argc, char **argv)
3: 4:{
-: 5:
3: 6: if (argc<2)
1: 7: printf("ingen argumenter\n");
-: 8: else
2: 9: if (argc%2==1)
1: 10: printf("et lige antal argumenter\n");
-: 11: else
1: 12: printf("et ulige antal argumenter\n");
-: 13:
3: 14: if (argc<=3)
3: 15: printf("Højst to argumenter\n");
-: 16: else
#####: 17: printf("Mere end to argumenter\n");
-: 18:
3: 19: return 0;
-: 20:}
Nu er det kun linie 17 som ikke udført endnu. Det er vigtigt at forstå at den resulterende kode-dækning er en sum af alle gennemløb af koden. <br/>
Vil man slette historikken så skal "v2.gcda" filen slettes (har man mange C/C++ filer skal alle .gcda filer sletttes).
<br/>
Jeg har rigtig mange test-gennemløb af min "rigtige kode" og har haft glæde af at se mønstre i koden som ikke eksekveres - netop med de hash-tegn foran de enkelte linier i ".gcov" filerne. Man kan udvide "gcov" kaldet med f.eks. "gcov -b", hvor man ydermere får udvidet fokus på alle branches (især if-else konstruktionerne). En anden rar udvidelse er at anvende programmet "ggcov", som læser output-filerne af "gcov" og præsenterer dette (lidt) grafisk. Et par eksempler kan findes her.
<br/>
Faren i det her er nemt vises ved at slette "v2.gcda" og lave tre gennemløb.<br/>
<br/>
$ v2 > /dev/null<br/>
$ v2 1 > /dev/null<br/>
$ v2 a b c d > /dev/null
<br/>
<br/>Og igen køres gcov:<br/>
<br/>
$ gcov v2.c<br/>
File 'v2.c'<br/>
Lines executed:100.00% of 10<br/>
v2.c:creating 'v2.c.gcov'
<br/>
<br/>
Jubi - alt er udført.... NEJ! Alle kombinationer er ikke udført - men alle linier er mindst udført én gang.<br/><br/>
Derfor er dette ikke en mirakelkur - men nærmere et værktøj, der kan give nogen hjælp, især med massivt gennemløb af koden.
<br/>
Er der nogen af jer som anvender dette og har andre tips?
<br/>
<br/>
/pto
Peter Toft er senior specialist hos Renesas Mobile og har blogget om open source og Linux siden Version2's begyndelse. Blogger også jævnligt om andre sjove teknologi-områder.
Follow @petertoftKommentarer (3)
Jeg bruger gcov ret konsekvent.
Jeg tror nok jeg skrev om det i DKUUG-nyt første gang i 1988 eller noget i den stil, dengang post-processede jeg selv C-compilerens assembler output for at indsætte de nødvendige tællere.
Ideen fik jeg vist nok fra en Bell Labs Journal artikel.
pt. har jeg 81.94% dækning af Varnish kildeteksten fra mine automatiske test-cases.
Det er generelt svært at nå højere end ca. 80-90% uden at spilde en masse tid på at trigge fejlbetingelser eller provokere asserts.
Poul-Henning
Man får ikke fuld coverage - klart. Jeg har også en del linier kode til at håndtere mere eller mindre sygelige fejl - som jeg pt. ikke har set. Men PHK, hvad bruger du gcov til - at se steder i koden, der bruges mest - eller netop kode-dele, der ikke bruges?
Det er faktisk svært at svare på Peter, for spørgsmålet er lige så meningsløst som "hvad bruger du en skruetrækker til ?"
For mig er gcov et universalværktøj der fortæller mig hvad min kode foretager sig.
F.eks kan man se om de automatiske test-cases kommer ordentligt ud i hjørnerne, eller sikrer sig at en bestemt test-case faktisk tester det man forventer.
Man kan også køre en reproduceret bug igennem og se hvilken vej den kom.
Specielt hvis det er en fejl der tager lidt tid at provokere, så er kode der kun er exekveret
en enkelt gang er et godt sted at stirre mistroisk.
Så er der "hvor tit sker det her egentlig ?" analyser.
Hvor tit læses en HTTP request i et read(2) kald og hvor tit skal der mere end et kald til ? Ville det hjælpe
at have en større buffer ?
Hvis et bestemt kodepath vælges en gang ud af 10 mio gør det måske ikke noget hvis det er lidt kluntet, bruges det
en ud af tusinde gange, bør det optimeres.
Man kan naturligvis selv indsætte tællere i koden for at besvare den slags spørgsmål, men det er nemmere at bruge gcov og få tællere "overalt".
Der er også den mere målsøgende performance vurdering.
Er lister sorteret på det rigtige kriterie ? burde de være hashes eller træer istedet ? Bør en funktion skilles i 99.99% tilfældet og undtagelserne flyttes til en anden funktion ? osv.
Men jeg kunne godt savne en komplementær facilitet til gcov, der istedet for at tælle alle basic blocks, istedet skrev i en cirkulær buffer hvor den har været.
Mange coredumps ville være nemmere at gennemskue hvis man kunne få at vide hvad CPUen (thread'en) har lavet de sidste par milli- eller blot micro-sekunder.
Jeg kunne også godt ønske mig lidt bedre presentation af resultatet, jeg hack'er selv output om til HTML:
http://phk.freebsd.dk/misc/cache_fetch.c.html
Men det kan helt sikkert gøres bedre af folk med mere forstand på den slags...
Poul-Henning
