6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

「この位置にprintfが無いとなぜか動かないんだ。」の原因を調査する

Posted at

はじめに

「この位置にprintfが無いとなぜか動かないんだ。」 - Qiita

上のコードはわかりやすい例ですが、「なぜか動かない」コードがこんなに単純なことは珍しいです。
よって、デバッグを始めるとたいてい沼にハマります(個人の感想)。
というわけで沼にハマったときに試したい方法を調べました。

検証環境

  • Linux
    • Ubuntu 18.04.5 LTS
    • GCC 7.5.0
    • clang 6.0.0
  • Windows
    • Windows 10 Home (1909)
    • Visual Studio Community 2019 (Version 16.7.3)

例1の検証

-Wall オプションで拾えるか

元記事のコードをGCCでコンパイルしてみます。

example1.c
# include <stdio.h>

void fizzbuzz(int n)
{
    int next;
    int i = 1;
    do {
        printf(i % 15 ? i % 5 ? i % 3 ? "%d\n" : "Fizz\n" : "Buzz\n" : "FizzBuzz\n", i);
        if (i++ >= n) next = 0;
    } while (next); // 未初期化のまま参照されていることを検知したい
}

int main(void)
{
    //printf((char[]){""});
    fizzbuzz(100);
}
$ gcc -O2 -Wall example1.c

-Wall (あるいは -Wuninitialized) オプションを付けたからには拾ってほしいところですが、残念ながら何も警告が出ずにコンパイルが通ってしまいます。

調べてみると、どうもGCCのバージョンによっても動作が変わるとかなんとか?
c - gcc failing to warn of uninitialized variable - Stack Overflow

Since it looks like the root-cause bug is almost 10 years old, it would seem it's not an easy problem to solve. In fact, the second bug I linked to has the phrase "Never going to be fixed" in the discussion, so that doesn't look good.

「修正されることはない」とは不吉な言葉ですね…。

If it's really important to you, clang does catch this one with -Wsometimes-uninitialized, which is included with -Wall:

clang を使えばいいらしい。良いことを聞きました。検証してみましょう。

$ clang -O2 -Wall example1.c

ダメだ、普通に通ってしまう…。
clangでしか拾えないパターンもあるものの、全部拾えるわけではないようです。

なお、gccもclangも if 文をコメントアウトをした状態なら拾ってくれます。以下はclangの警告の例です。

example1_1.c
# include <stdio.h>

void fizzbuzz(int n)
{
    int next;
    int i = 1;
    do {
        printf(i % 15 ? i % 5 ? i % 3 ? "%d\n" : "Fizz\n" : "Buzz\n" : "FizzBuzz\n", i);
        // if (i++ >= n) next = 0;
    } while (next);
}

int main(void)
{
    fizzbuzz(100);
}
example1.c:10:14: warning: variable 'next' is uninitialized when used here [-Wuninitialized]
    } while (next);
             ^~~~
example1.c:5:13: note: initialize the variable 'next' to silence this warning
    int next;
            ^
             = 0
1 warning generated.

実行時に検出できるか

コンパイラが賢くなっているとはいえ、実行する前に検出させるのは難しいのかもしれません。
というわけで、せめて実行時には検出したいものです。

Linux環境でメモリ関係のデバッグに使う定番のツールに valgrind があります。
Valgrindでメモリに関するデバッグ

$ gcc -O0 -g -Wall example1.c
$ valgrind ./a.out
==3704== Memcheck, a memory error detector
==3704== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==3704== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==3704== Command: ./a.out
==3704== 
1
==3704== Conditional jump or move depends on uninitialised value(s)
==3704==    at 0x10871E: fizzbuzz (example1.c:10)
==3704==    by 0x108734: main (example1.c:16)
==3704== 
:
:

おお、Conditional jump or move depends on uninitialised value(s) の文字が…!

Visual Studio だとどうなる

Visual Studio 2019のIDEに貼り付けてみると警告が出ました。

重大度レベル	コード	説明	プロジェクト	ファイル	行	抑制状態
警告	C6001	初期化されていないメモリ 'next' を使用しています。	ConsoleApplication1	C:\...\ConsoleApplication1\ConsoleApplication1.cpp	13	

警告に関するオプションが /W4 以上のときは、コンパイル時にも警告が出ます。

重大度レベル	コード	説明	プロジェクト	ファイル	行	抑制状態
警告	C4701	初期化されていない可能性のあるローカル変数 'next' が使用されます	ConsoleApplication1	C:\...\ConsoleApplication1\ConsoleApplication1.cpp	13	

デバッグ実行すると、実行時にも以下のように未初期化変数の参照が検知されました。いい感じですね。

Run-Time Check Failure #3 - The variable 'next' is being used without being initialized.

例2の検証

example2.c
# include <stdio.h>

int main(void)
{
    char buf[/*十分な大きさ*/256] = "", *p = buf;
    char spec[] = "1から100までの数をプリントするプログラムを書け。\nただし3の倍数のときは数の代わりに「Fizz」と、\n5の倍数のときは「Buzz」とプリントし、\n3と5両方の倍数の場合には「FizzBuzz」とプリントすること。\n";

    //printf(spec); // この位置にprintfが無いとなぜか動かない

    for (int i = 1; i <= 100; i++) {
        p += sprintf(p, (const char*[2][2]){{"%d\n", "Fizz\n"}, {"Buzz\n", "FizzBuzz\n"}}[i % 5 == 0][i % 3 == 0], i);
    }
    printf("%s", buf);
}

確かに printf をコメントアウトして以下を実行すると落ちてしまいました。
spec が使われてないぞ!という警告は出ましたが、配列をはみ出した(バッファオーバーラン)ことについては教えてくれません。
また、100までループが回ってしまっているので、どのタイミングで配列をはみ出したのかがわかりません。

$ gcc -O2 -Wall example2.c
$ ./a.out
1
2
:
:
Fizz
Buzz
*** stack smashing detected ***: <unknown> terminated
中止 (コアダンプ)

このケースでは、-O0 で最適化を無効にしたうえで -fsanitize=address オプションを使用することにより、はみ出したタイミングでプログラムを終了してくれるようになります。

わかりやすいように1行追加して検証。

example2_1.c
# include <stdio.h>

int main(void)
{
    char buf[/*十分な大きさ*/256] = "", *p = buf;
    char spec[] = "1から100までの数をプリントするプログラムを書け。\nただし3の倍数のときは数の代わりに「Fizz」と、\n5の倍数のときは「Buzz」とプリントし、\n3と5両方の倍数の場合には「FizzBuzz」とプリントすること。\n";

    //printf(spec); // この位置にprintfが無いとなぜか動かない

    for (int i = 1; i <= 100; i++) {
        p += sprintf(p, (const char*[2][2]){{"%d\n", "Fizz\n"}, {"Buzz\n", "FizzBuzz\n"}}[i % 5 == 0][i % 3 == 0], i);
        printf("%ld\n", p-buf); // この行を追加
    }
    printf("%s", buf);
}
$ gcc -O0 -Wall -fsanitize=address example2_1.c
$ ./a.out
2
4
9
:
:
253
=================================================================
==29158==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffde3ed0d00 at pc 0x7f3d0c9a78f9 bp 0x7ffde3ed0a90 sp 0x7ffde3ed0220

これはわかりやすい。clangでもいけます。

$ clang -O0 -Wall -fsanitize=address example2_1.c
$ ./a.out

なお、このパターンはスタック変数の問題なので、例1で使った valgrind では検出できないようです。
IPA ISEC セキュア・プログラミング講座:C/C++言語編 第10章 著名な脆弱性対策:バッファオーバーフロー: #4 あふれを検出するデバッグ

また、mudflap は最近のバージョンのGCCでは使えなくなってしまっています。

$ gcc -fmudflap example2.c
gcc: warning: switch ‘-fmudflap’ is no longer supported

まとめ

  • いろんなOSやコンパイラが使えるなら試してみる
  • ツールやコンパイルオプションを活用して検出できるケースもある
  • 人間が書いたコードを信用してはいけない(普遍の真理)
  • 他にも色々パターンありそうですが(malloc で確保したメモリの場合は?など)とりあえずこれだけ。
6
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?