gcov の使い方

concov のドキュメントを書こうと思ったけれど、何から書くか困ったので、とりあえずその前に gcov の使い方とはまりどころを書いてみます。

gcov とは

C 言語で書かれたプログラムのカバレッジを測定するツールです。gcc に付属しています。

基本的な使い方

こういうコードがあるとする。

/* test.c */
#include <stdio.h>

int foo(int x, int y) {
  return x + y;
}

int bar(int x, int y) {
  return x - y;
}

int main(void) {
  printf("%d\n", foo(2, 3));
  printf("%d\n", foo(3, 4));
  return 0;
}

コンパイルする。-coverage をつけると gcov 用のオブジェクトファイルが生成される *1

$ gcc -coverage -o test test.c

すると .gcno という拡張子のファイルが生成される。これには (たぶん) カバレッジのログとソースコードの行番号とかを対応させるための情報が入ってる。

$ ls
test  test.c  test.gcno

実行すると、

$ ./test
5
7

カバレッジのログを記録した .gcda というファイルができる。

$ ls
test  test.c  test.gcda  test.gcno

gcov を実行する。

$ gcov test.gcda
File 'test.c'
Lines executed:75.00% of 8
test.c:creating 'test.c.gcov'

カバレッジが 75.00% なんだなーとわかる。詳しい結果は test.c.gcov に保存されている。

        -:    0:Source:test.c
        -:    0:Graph:test.gcno
        -:    0:Data:test.gcda
        -:    0:Runs:1
        -:    0:Programs:1
        -:    1:/* test.c */
        -:    2:#include <stdio.h>
        -:    3:
        2:    4:int foo(int x, int y) {
        2:    5:  return x + y;
        -:    6:}
        -:    7:
    #####:    8:int bar(int x, int y) {
    #####:    9:  return x - y;
        -:   10:}
        -:   11:
        1:   12:int main(void) {
        1:   13:  printf("%d\n", foo(2, 3));
        1:   14:  printf("%d\n", foo(3, 4));
        1:   15:  return 0;
        -:   16:}

foo が 2 回呼ばれてるとか、bar が呼ばれてないとかわかる。

ソースコードとオブジェクトが別のディレクトリにある場合

gcov がファイルを見つけられなくてエラーになることがしばしばあります。
例えば、ソースは src/ に、オブジェクトは obj/ に置く場合。

/* src/foo.c */
int foo(int x, int y) {
  return x + y;
}
/* src/bar.c */
int bar(int x, int y) {
  return x - y;
}
/* src/main.c */
#include <stdio.h>

int foo(int, int);
int bar(int, int);

int main(void) {
  printf("%d\n", foo(2, 3));
  printf("%d\n", foo(3, 4));
  return 0;
}

.o を obj/ 以下に出力するようにコンパイルする。-coverage はコンパイル時にもリンク時にもつける。

$ gcc -coverage -c -o obj/foo.o src/foo.c
$ gcc -coverage -c -o obj/bar.o src/bar.c
$ gcc -coverage -c -o obj/main.o src/main.c
$ gcc -coverage -o test obj/foo.o obj/bar.o obj/main.o

.gcno も obj/ 以下にできる。

$ ls obj
bar.gcno  bar.o  foo.gcno  foo.o  main.gcno  main.o

実行すると、

$ ./test
5
7

.gcda が obj/ 以下にできる。

$ ls obj
bar.gcda  bar.o     foo.gcno  main.gcda  main.o
bar.gcno  foo.gcda  foo.o     main.gcno

gcov にかけるとグラフファイルが見えないといわれる。

$ gcov obj/foo.gcda
foo.gcno:cannot open graph file

ここで cd obj すると、foo.gcno は見つかるので一応動くけど foo.c が見つからないので foo.c.gcov の表示が変になるなど、泥沼にはまります。


どうすればいいかというと、

  • gcov は必ず gcc したのと同じディレクトリで実行する
  • .gcno の位置を -o オプションで教えてやる

とします。

$ gcov obj/foo.gcda -o obj
File 'src/foo.c'
Lines executed:100.00% of 2
src/foo.c:creating 'foo.c.gcov'

カレントディレクトリに foo.c.gcov が出来ます。

        -:    0:Source:src/foo.c
        -:    0:Graph:obj/foo.gcno
        -:    0:Data:obj/foo.gcda
        -:    0:Runs:1
        -:    0:Programs:1
        -:    1:/* src/foo.c */
        2:    2:int foo(int x, int y) {
        2:    3:  return x + y;
        -:    4:}

同じ名前のソースコードが別のディレクトリにある場合

例えば src/core-module/foo.c と src/sub-module/foo.c があった場合、どちらもカバレッジの出力が foo.c.gcov になって、知らないうちに上書きする可能性があります。
どうすればいいかというと、

  • gcov に -p オプションを渡す

と、src#core-module#foo.c.gcov だの src#sub-module#foo.c.gcov だのといった、ソースのパスをエンコードした怪しいファイル名にしてくれます。

同じソースファイルがいろんなソースファイルに #include されている場合

例えば bar.c と baz.c から #include "foo.h" している場合。

/* foo.h */
static int foo(int x, int y) {
  return x + y;
}
/* bar.c */
#include "foo.h"

int bar(int x, int y) {
  return foo(x, y);
}
/* baz.c */
#include "foo.h"

int baz(int x, int y) {
  return foo(x, y);
}

これで bar.c のカバレッジを見ようとすると

$ gcov bar.gcda
File 'foo.h'
Lines executed:100.00% of 2
foo.h:creating 'foo.h.gcov'

File 'bar.c'
Lines executed:100.00% of 2
bar.c:creating 'bar.c.gcov'

となり、その後で baz.c のカバレッジを見ようとすると

$ gcov baz.gcda
File 'foo.h'
Lines executed:100.00% of 2
foo.h:creating 'foo.h.gcov'

File 'baz.c'
Lines executed:100.00% of 2
baz.c:creating 'baz.c.gcov'

となります。foo.h.gcov が書きつぶされてます。これは -p オプションをつけても回避できません。


どうすればいいかというと、

  • gcov に -l オプションを渡す

とします。

$ gcov bar.gcda -l
File 'foo.h'
Lines executed:100.00% of 2
foo.h:creating 'bar.gcda##foo.h.gcov'

File 'bar.c'
Lines executed:100.00% of 2
bar.c:creating 'bar.gcda##bar.c.gcov'


$ gcov baz.gcda -l
File 'foo.h'
Lines executed:100.00% of 2
foo.h:creating 'baz.gcda##foo.h.gcov'

File 'baz.c'
Lines executed:100.00% of 2
baz.c:creating 'baz.gcda##baz.c.gcov'

というように、gcda ファイルの名前が最初につくので、上書きすることはなくなります。-l と -p を合わせて使うこともできます。

まとめ

gcov を使うときのポイント。

$ gcc -coverage -o test test.c
$ gcov foo.gcda
$ gcov foo.gcda -o obj
$ gcov foo.gcda -p
$ gcov foo.gcda -l

以上は concov と関係なく使える知識だと思います。ちなみに concov で使う場合は、全部の .gcda を処理して .gcov を生成して、それらを回収して集計してデータベースに突っ込むので、.gcov の上書きが発生するとデータの欠損につながります。よってとりあえず -p -l をつけるといいです。

*1:古い gcc だと -fprofile-arcs -ftest-coverage でないとダメかも。