printfデバッグTips

作成:

今回は、システムコールや一部のライブラリ関数の実行に失敗した際、 どのような原因で失敗したのかを教えてくれるerrnoについて解説する。 errnoは、システムコールやライブラリ関数を利用し始めると大変お世話になる。

errnoの表示

errnoは、例えばfopenについてmanで調べると

...
返り値
       fopen(), fdopen(), freopen() は成功すると FILE 型のポインタを返す。
       失敗すると NULL が返され、 errno がエラーを示す値にセットされる。
...

英語の環境だと

...
RETURN VALUE
      Upon successful completion fopen(), fdopen() and freopen() return a FILE pointer.
      Otherwise, NULL is returned and errno is set to indicate the error.
...

と言った形で記述されていて、ライブラリ関数やシステムコールの実行に失敗した場合、 その原因を格納する先として利用される、errno自体はint型の整数としてアクセスできる。

#include <errno.h>

と、errno.hをインクルードすると参照できるようになる。

また、このerrnoは、ただのグローバル変数ではなく、 スレッドローカルストレージとして定義されており、スレッドセーフにアクセス可能だ。 簡単に言うと、各スレッドから同様にerrnoというシンボルでアクセスできるのだが、 その実体はスレッドごとに異なっているのだ。 そのため、あるスレッド上である関数コールによってerrnoにある値が設定された場合、 その値を参照するまでに、他のスレッドで発生したエラーによって上書きされたりはしない。 スレッド関係のエラーについてもerrnoが使われているし、 そのような仕組みがなければ、複数のスレッドが動作し始めた時点で使いものにならないだろう。

この辺の話がちんぷんかんぷんという場合も安心して欲しい。 要するに、余計なことは考えずに使って問題ない。ということだ。

errnoの参照

次に、実際errnoを参照するサンプルプログラムを動かしてみよう。

#include <stdio.h>
#include <errno.h>

int main(int argc, char**argv) {
  FILE *file;
  errno = 0;
  file = fopen("hoge.txt", "r");
  if (file == NULL) {
    fprintf(stderr, "%d\n", errno);
    return 1;
  }
  fclose(file);
  return 0;
}

単に、hoge.txtというファイルをオープンし、 失敗したらerrnoを表示、成功したら何もせず終了というシンプルな内容だ。

errnoで検出しようとしている処理の直前で、errnoを0に初期化している。 なぜなら、errnoは各処理共通で使用される変数で、 エラーが起こらなければerrnoの値は更新されない。 そのため初期化しないと、それ以前に発生したエラーを拾ってしまう可能性があるからだ。 ただ、この例の場合、fopenがNULLを返した時はエラーが発生していて、 その時はerrnoに何らかの値がセットされるためなくても問題はない。 しかし、世の中には、エラー発生時も戻り値は有効な値で、 errnoの値を見ないと失敗したかどうかわからない、という関数もある。 その場合は、errnoの初期化は必須となる。

ファイルがない状態で実行すると

$ gcc -o errno errno.c
$ ./errno
2

ファイルはあるがリード不可なパーミッションを設定すると

$ touch hoge.txt && chmod -r hoge.txt
$ ./errno
13

と、一応エラーが区別されているようだが、数字だけではちんぷんかんぷん。 manには

...
       E2BIG           Argument list too long (POSIX.1)

       EACCES          Permission denied (POSIX.1)

       EADDRINUSE      Address already in use (POSIX.1)

       EADDRNOTAVAIL   Address not available (POSIX.1)

       EAFNOSUPPORT    Address family not supported (POSIX.1)
...

と、エラーの定義リストがある。 もちろん、コード上でどのようなエラーが発生したかを判断して対処する場合は、 errnoの値をこれらと比較して判定するようなコードを書くのが妥当だが、 表示するためだけに、毎回これを調べたりというのはやってられない。

strerrorの利用

デバッグのたびに毎回調べたり、 表示のためにコード上で毎回比較したりというのをやるのは非効率すぎる。 ということで、当然というかその辺をフォローしてくれる関数も用意されている。 まずは、strerrorを紹介する。

早速使ってみる。

#include <stdio.h>
#include <string.h>
#include <errno.h>

int main(int argc, char**argv) {
  FILE *file;
  file = fopen("hoge.txt", "r");
  if (file == NULL) {
    fprintf(stderr, "%d %s\n", errno, strerror(errno));
    return 1;
  }
  fclose(file);
  return 0;
}

そして実行

$ ./errno
13 Permission denied

$ rm hoge.txt
$ ./errno
2 No such file or directory

と、番号で表示されるよりも意味のわかるメッセージが得られた。

manでフォーマットを調べると

#include <string.h>
char *strerror(int errnum);
int strerror_r(int errnum, char *buf, size_t buflen);
char *strerror_r(int errnum, char *buf, size_t buflen);

今回使った、strerrorの他にバリエーションがあるが、 実はstrerrorは、必ずしもスレッドセーフではない。 内部で用意された不変文字列を返している場合は問題はないが、 内部で持っている文字列バッファへ書き出し、そのポインタを返す場合がある、 その場合、strerrorの呼び出しのたびに同じ領域が上書きされてしまうからだ。 マルチスレッドな環境で安全に利用したい場合はstrerror_rの方を利用する必要がある。 これは、文字列バッファに書き出す先を呼び出し側で指定するもので、ちょいと面倒ではあるが、 複数のスレッドから同時にコールされても問題は起こらない。 strerror_rに戻り値のバリエーションがあるが、 XSI準拠版と、GNU版で戻り値が違っているらしい。 移植性を考えるなら、戻り値を無視して利用したほうが良さそうだ。

strerrorが内部文字列バッファを返している可能性があるという点について、 マルチスレッド環境以外にも問題が発生する使い方がある。 例えば、

if (hoge() != SUCCESS) {
  errno1 = errno;
  if (fuga() != SUCCESS) {
    fprintf(stderr, "%s %s\n", strerror(errno1), strerror(errno));
  }
}

といった具合に2つのエラーをまとめて表示しようとした場合、 意図通りに動作せず、2つ同じ文字列が表示されてしまう場合があるので注意しよう。

perrorの利用

次は、perrorを紹介する。 サンプルプログラムを以下のように書き換える。

#include <stdio.h>

int main(int argc, char**argv) {
  FILE *file;
  file = fopen("hoge.txt", "r");
  if (file == NULL) {
    perror("error");
    return 1;
  }
  fclose(file);
  return 0;
}

そして実行結果

$ ./errno
error: No such file or directory

$ touch hoge.txt && chmod -r hoge.txt
$ ./errno
error: Permission denied

引数に指定した文字列に追加して、エラー文字列が表示されるという関数である。 一番シンプルな記述で、いい感じのメッセージを表示してくれる。 manによる定義は以下のようになっている。

#include <stdio.h>
void perror(const char *s);

必要なヘッダはstdio.hだけで、 errnoperror関数内で参照されているので、 自前でコードを書く必要はない。 内部ではstrerror_rが使われているので、スレッドセーフでもある。 当然、出力先は標準エラー出力となっている。 単純にエラー出力をしたいというだけなら、この関数だけで事足りるだろう。

ただし、printf系の出力と異なり、 付与できる文字列は固定で、この関数自体では書式指定などはできない。

また、コード上でエラーが発生した後の処理を分岐させたいなら、 直接errnoを参照する必要があるだろうし、 環境によっては、perrorの利用を禁止している場合もあるだろう。 リリース時には余計なメッセージを出さないようにしたい、とか エラー出力のフォーマットや出力先が決められているなどの場合、 特定の出力関数の利用しかできない場合がある、 そのような環境下で同様の出力を行いたい場合は、errnoを参照して、 strerrorでエラーメッセージを表示する必要があるだろう。

おまけ:printfのglibc拡張書式の利用

最後に、おまけとしてもう一つ、printfのglibc拡張書式を紹介する。 %d とか %f とか書くアレだ。 C言語の環境ならどこでも使えるという方法ではないので、 必ずしもおすすめできない方法であるが利用のしかたによっては便利だ。 それ故、「おまけ」として紹介する。

サンプルプログラムを以下のように書き換える。

#include <stdio.h>

int main(int argc, char**argv) {
  FILE *file;
  file = fopen("hoge.txt", "r");
  if (file == NULL) {
    printf("error: %m\n");
    return 1;
  }
  fclose(file);
  return 0;
}

そして実行結果

$ ./errno
error: No such file or directory

$ touch hoge.txt && chmod -r hoge.txt
$ ./errno
error: Permission denied

完全に、perrorの実行例と同じになる。 いわゆる%○という書式は一般に引数を取るのだが、 %mは引数を取らず、 単体でstrerror(errno)の結果を表示する。という意味になる。 実装的にはちゃんとstrerror_rが使われているので、スレッドセーフに利用できる。 その辺の心配も無用だ。

perrorに対するメリットとしては、 通常のprintf系の書式変換と合わせて利用できるため、 出力フォーマットの自由度が高いという点があげられる。

最初に書いたようにこの方法はglibcの拡張なので、 C言語が使えるところならどこででも使える、というものではなく、 使ってしまうと移植性に問題が出てしまう。 しかし、一時的なデバッグコードとしてや、 glibcが使用されることが確定できる状況下であれば、 有益に利用できるだろう。

というわけで、エラーが発生した時のその原因を表示させる方法について解説した。 開発中はこのような記述を面倒臭がらずにきちんと行うことで、デバッグがしやすくなるだろう。