🦋

C/C++のためのデバッグツール

2020/12/25に公開

この記事はISer Advent Calendar 2020の一環です。
https://adventar.org/calendars/4946

C/C++のデバッグに有用なソフトや機能を紹介したいと思います。

MacにおけるGCCとClang

MacでXcodeを入れるとClangがインストールされ、しかもなぜかgccコマンドでClangが起動されるようになります。これは

gcc --version

とすると確認できます。この状態でGCCを入れるには

brew install gcc

とすればよいのですが、これでもなおgccコマンドではClangが起動されます。この状態でGCCを使うにはgcc-10コマンドを使います。GCCが起動されることを確認するには

gcc-10 --version

とすればよいです。ただし、GCCのバージョンが上がった場合には、gcc-11, gcc-12, ...とする必要があります。

静的解析

ソースコードの解析には動的解析と静的解析があります。動的解析は実際にプログラムを実行して行う解析で、静的解析はプログラムを実行することなく行う解析です。動的解析は普段printfデバッグやデバッガで行っていると思うので、ここでは静的解析を紹介します。新たにソフトを入れなくても、GCCとClangには静的解析機能が付いています。GCCでは-fanalyzer、Clangでは--analyzeを付けてコンパイルすると、静的解析が行われます。また、いくつかのプログラムで試したのですが、どうやらGCCとClangの静的解析機能は異なるもののようです。
https://gcc.gnu.org/onlinedocs/gcc-10.2.0/gcc/Static-Analyzer-Options.html
https://clang-analyzer.llvm.org/

静的解析の例

以下のコードを静的解析してみます。

sa.c
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main(void)
{
    srand((unsigned)time(NULL));
    int array_size;
    scanf("%d", &array_size);
    int *array = malloc(sizeof(int) * array_size);
    for (int i = 0; i < array_size; i++) {
        array[i] = rand();
    }
    for (int i = 0; i < array_size; i++) {
        printf("%d\n", array[i]);
    }
    return 0;
}

GCCでは

gcc-10 sa.c -fanalyzer

としてコンパイルします。
Clangでは

gcc sa.c --analyze

としてコンパイルします。
GCCの場合は警告を吐いてくれないのですが、Clangの場合は以下のような警告を吐いてくれます。

sa.c:17:12: warning: Potential leak of memory pointed to by 'array'
    return 0;
           ^
1 warning generated.

Xcode

Xcodeでも静的解析機能が使えて、おそらくClangと同一のものと思われます。使い方は簡単で、Product->Analyzeを押すだけです。GUIで操作できてとても便利です。
https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/debugging_with_xcode/chapters/static_analyzer.html

Sanitizers

Sanitizersという便利機能の集まりみたいなものがあって、それがGCCおよびClangから利用できます。
https://github.com/google/sanitizers

AddressSanitizer

AddressSanitizerはSanitizersの1つで、C/C++のメモリに関係するバグを検出してくれます。GCCまたはClangで-fsanitize=addressを付けてコンパイルすると、AddressSanitizerが有効になった実行ファイルが生成されます。また、-fno-omit-frame-pointerを付けるとスタックトレースが改善されるそうです。
https://github.com/google/sanitizers/wiki/AddressSanitizer
https://gcc.gnu.org/onlinedocs/gcc-10.2.0/gcc/Instrumentation-Options.html
https://clang.llvm.org/docs/AddressSanitizer.html

AddressSanitizerの例

as.c
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main(void)
{
    srand((unsigned)time(NULL));
    size_t array_size;
    scanf("%zu", &array_size);
    int *array = malloc(sizeof(int) * array_size);
    for (size_t i = 0; i < array_size; i++) {
        array[i] = rand();
    }
    for (size_t i = array_size - 1; i >= 0; i--) {
        printf("%d\n", array[i]);
    }
    free(array);
    return 0;
}

GCCでは

gcc-10 as.c -fsanitize=address -fno-omit-frame-pointer

としてコンパイルします。
Clangでは

gcc as.c -fsanitize=address -fno-omit-frame-pointer

としてコンパイルします。
そして、生成された実行ファイルを(いつもの方法で)実行すると、heap-buffer-overflowというエラーが出ます。

UndefinedBehaviorSanitizer

UndefinedBehaviorSanitizerはSanitizersの1つで、C/C++のいろいろな未定義動作を検出してくれます。基本的にはGCCまたはClangで-fsanitize=undefinedとすると有効になるのですが、一部機能は個別に指定しないと有効になりません。
https://gcc.gnu.org/onlinedocs/gcc-10.2.0/gcc/Instrumentation-Options.html
https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html

UndefinedBehaviorSantizerの例

us.c
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define ARRAY_SIZE 10

int main(void)
{
    int array[ARRAY_SIZE];
    srand((unsigned)time(NULL));
    for (size_t i = 0; i < ARRAY_SIZE; i++) {
        array[i] = rand();
    }
    for (size_t i = ARRAY_SIZE - 1; i >= 0; i--) {
        printf("%d\n", array[i]);
    }
    return 0;
}

GCCではこの場合エラーを検出してくれません。
Clangでは

gcc us.c -fsanitize=undefined -fsanitize=bounds

としてコンパイルし、生成された実行ファイルを(いつもの方法で)実行すると

us.c:15:24: runtime error: index 18446744073709551615 out of bounds for type 'int [10]'

というエラーが出ます。ここで、-fsanitize=boundsは「配列の範囲が静的に決まっている場合の範囲外アクセスを検知する」というオプションです。

Discussion