「この位置にｐｒｉｎｔｆが無いとなぜか動かないんだ。」の原因を調査する

# はじめに

[「この位置にｐｒｉｎｔｆが無いとなぜか動かないんだ。」 - Qiita](https://qiita.com/fujitanozomu/items/8b7d9e4e51baf0764dfc)

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

# 検証環境

- 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でコンパイルしてみます。

```c: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);
}
```

```Terminal
$ gcc -O2 -Wall example1.c
```

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

調べてみると、どうもGCCのバージョンによっても動作が変わるとかなんとか？
[c - gcc failing to warn of uninitialized variable - Stack Overflow](https://stackoverflow.com/questions/17705880/gcc-failing-to-warn-of-uninitialized-variable)

>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 を使えばいいらしい。良いことを聞きました。検証してみましょう。

```Terminal
$ 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でメモリに関するデバッグ](http://ecei-tohoku.github.io/ppa/tips/valgrind.html)

```Terminal
$ 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の検証

```c: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行追加して検証。

```c: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);
}
```

```Terminal
$ 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でもいけます。

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

なお、このパターンはスタック変数の問題なので、例1で使った ``valgrind`` では検出できないようです。
[IPA ISEC　セキュア・プログラミング講座：C/C++言語編　第10章 著名な脆弱性対策：バッファオーバーフロー: #4 あふれを検出するデバッグ](https://www.ipa.go.jp/security/awareness/vendor/programmingv2/contents/c904.html)

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

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

# まとめ

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