Valgrindのエラーメッセージだけを出力したい

最近、プライベートではなぜかCを書くことが多くて、その過程で今更ながらValgrindを使うようになった。

で、使っているうちにValgrindのエラーメッセージだけを表示したくなったので、そのやり方をメモ。 環境は以下のとおり。

一応明記しておくと、Valgrindのバージョンは3.10.0。 シェルはbashなので、それ以外のシェルの場合は適宜読み替えるように。

ネタバレ

--log-fdオプションとリダイレクトを組み合わせればいい。

Valgrindの使い方

例えば以下のCコードsample.cを考える。

#include <stdio.h>
#include <stdlib.h>

int main(void) {
  puts("hello");
  malloc(42); // 特に意味もなく malloc して……
  fputs("goodby\n", stderr);
  return 0;   // free せずに終了
}

もちろんこのコードはコンパイルして実行することができる。

$ cc -Wall -g sample.c && ./a.out
hello  # 標準出力
goodby # 標準エラー出力

しかし、sample.cは6行目で(無意味に)mallocした領域を解放せずにプログラムを終了している。

Valgrindを介して実行すると、このようなメモリリークを検出できる。 --leak-check=fullオプションを付けると、メモリリーク箇所の詳細な情報もわかる

$ valgrind --leak-check=full ./a.out
==6865== Memcheck, a memory error detector
==6865== Copyright (C) 2002-2013, and GNU GPL'd, by Julian Seward et al.
==6865== Using Valgrind-3.10.0.SVN and LibVEX; rerun with -h for copyright info
==6865== Command: ./a.out
==6865==
hello
goodby
==6865==
==6865== HEAP SUMMARY:
==6865==     in use at exit: 42 bytes in 1 blocks
==6865==   total heap usage: 1 allocs, 0 frees, 42 bytes allocated
==6865==
==6865== 42 bytes in 1 blocks are definitely lost in loss record 1 of 1
==6865==    at 0x4C2AB80: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==6865==    by 0x400614: main (sample.c:6)
==6865==
==6865== LEAK SUMMARY:
==6865==    definitely lost: 42 bytes in 1 blocks
==6865==    indirectly lost: 0 bytes in 0 blocks
==6865==      possibly lost: 0 bytes in 0 blocks
==6865==    still reachable: 0 bytes in 0 blocks
==6865==         suppressed: 0 bytes in 0 blocks
==6865==
==6865== For counts of detected and suppressed errors, rerun with: -v
==6865== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

definitely lost≒メモリリークしていると思えばいい。 今回はデバッグフラグを付けてコンパイルしているので、mallocした場所(sample.c:6)まで出力されている。

-qオプションをつけるとエラーメッセージのみが出力される。

$ valgrind -q --leak-check=full ./a.out
hello
goodby
==7135== 42 bytes in 1 blocks are definitely lost in loss record 1 of 1
==7135==    at 0x4C2AB80: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==7135==    by 0x400614: main (sample.c:6)
==7135==

Valgrind自体は他にも色んな用途があるようだが、ここではとりあえずメモリリーク検出ツールとして扱う。

やりたいこと

makeスクリプトでまとめてチェックするような場合、「プログラム自体の出力を全て抑制して」「Valgrindの出すエラー報告だけを表示」したくなる。 つまり以下のsimple_leak_checkコマンドが欲しい。

$ simple_leak_check ./a.out # hello も goodby も出力しない
==7135== 42 bytes in 1 blocks are definitely lost in loss record 1 of 1
==7135==    at 0x4C2AB80: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==7135==    by 0x400614: main (sample.c:6)
==7135==

「Valgrindのエラー報告だけ」という部分は、先述のValgrindの-qオプションで解決済みなので、問題は前者である。

試した方法

ダメな方法: 単純なリダイレクト

Valgrindのメッセージは標準エラー出力に出力されるので、標準出力を/dev/nullにリダイレクトすればいい……とはいかない。 これだとsample.cの7行目のような、プログラム中の標準エラー出力への出力が混じってしまう。

$ valgrind -q --leak-check=full ./a.out >/dev/null
goodby   # <- ./a.out 中の stderr への出力
==7613== 42 bytes in 1 blocks are definitely lost in loss record 1 of 1
==7613==    at 0x4C2AB80: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==7613==    by 0x400614: main (sample.c:6)
==7613==

「メモリチェックしたいプログラムが出力するのがおかしい」と言いたくもなるが、この時チェックしていたのは自作のテストフレームワークという、本質的に出力も込みのプログラムだったので、なんとかしたいところ。

一応大丈夫な方法: --log-fileオプションの使用

Valgrindのメッセージをファイルに出す方法がある筈と思って調べたら、--log-fileオプションがあった。 標準出力と標準エラー出力を両方共/dev/nullへリダイレクトし、Valgrindのエラーメッセージは適当なファイルへリダイレクトしてから、後で表示すればよい。

$ valgrind -q --leak-check=full ./a.out \
> --log-file=/tmp/log.txt \ # Valgrind の出力をファイルに一時保存
> >/dev/null 2>/dev/nul     # プログラム自体の出力は投げ捨てる
$ cat /tmp/log.txt          # 終了後に、保存しておいた出力を表示
==8057== 42 bytes in 1 blocks are definitely lost in loss record 1 of 1
==8057==    at 0x4C2AB80: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==8057==    by 0x400614: main (sample.c:6)
==8057==

一応できた。

でもこの方法の何がイケてないって、/tmp/log.txtを作ってるのがイケてない。 別にパフォーマンス云々やディスク云々を気にするわけではないが、なんか腑に落ちない。 もうちょっとスマートにできないか。

腑に落ちた方法: --log-fdオプションの使用

ヘルプを眺めてたら、--log-file オプションのすぐ近くに --log-fd オプションがあることに気づく。 以下、抜粋。

$ valgrind --help
  (中略)
    --log-fd=<number>         log messages to file descriptor [2=stderr]

--log-file がファイル名を指定するのに対して、--log-fd はファイルディスクリプタの番号を直接指定する。 最初は「なんだそりゃ」と思ったが、これとリダイレクトを組み合わせれば、中間ファイルを使わずに目的を達成できる。

$ valgrind --log-fd=3 \  # Valgrind の出力をファイルディスクリプタ3番に
> -q --leak-check=full ./a.out 3>&2 \
> 3>&2 \                 # 3番を標準エラー出力に向ける
> >/dev/null 2>/dev/null # プログラム自体の出力は投げ捨てる
==8729== 42 bytes in 1 blocks are definitely lost in loss record 1 of 1
==8729==    at 0x4C2AB80: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==8729==    by 0x400614: main (sample.c:6)
==8729==

今度こそできた!

ここではファイルディスクリプタは3を使っているが、3以上ならなんでもいい(はず)。 1つ落とし穴があり、--log-fdは実行するプログラム(ここでは./a.out)よりも前に置かないと、何故か無視される。解せぬ。

オマケ: --error-exitcodeオプション

デフォルトではValgrindの終了ステータスはエラーの有無に関係なく0となっている。 これではスクリプトで処理するときに何かと不便だが、--error-exitcodeオプションで、メモリリークを1つ以上検出した際の終了ステータスを指定できる。

というわけで、目標だったsimple_leak_checkコマンドは次のようになる。

alias simple_leak_check=
'valgrind --log-fd=3 -q --leak-check=full\
3>&2 >/dev/null 2>/dev/null'

まとめ

Valgrindのエラーメッセージのみを出力する方法を述べた。

正直、--log-fileオプションで中間ファイルを生成する方法でもいい気がするが、一生使う機会が無いと思っていた「3以上のファイルディスクリプタを使ったリダイレクト」が役に立つ形で使えたので、つい記事にまでしてしまった。

というか--log-fdオプションのこれ以外の用途が思いつかないけど、何かあるのかしら。


ちなみに上のsample.cは、コンパイル時に-O2オプションを付けるとmalloc呼出し自体が最適化によって削除される可能性があるので注意(?)*1

*1:手元のClang 3.6では、最適化後のアセンブリを覗いたら無くなってた