コンパイラと共に戦う【コンパイルオプション編】
はじめに
バグとの戦いは辛く苦しいもの。
コンパイラと共にバグへ立ち向かっているプログラマの記録です。
今回はコンパイルオプション編。
バグとの遭遇確率を減らし、生まれてしまったバグは早期に潰す戦い方を目指します。
相棒コンパイラはClang
LLVM/Clangは多くの機能を提供している。
Clangはコンパイルエラーがこちらの意図を汲もうとしてくれていたり、相棒としても心強い。
というわけで、この記事に関しては Clang 9.0.0 にて動作確認。
Warningは全て有効にしエラーに
コンパイルオプションには -Weverything -Werror
を指定する。
そこから自分に必要ないwarningを抜いていくのが一番良い。
私の場合、次のコンパイルオプションを指定している。
-Weverything -Werror -Wno-c++98-compat -Wno-c++98-compat-pedantic -Wno-padded
口うるさく感じる場合もあるがコードがクリーンに保てるのでバグと出会いにくくなる。
もちろん、Warningを回避するためにトリッキーなコードを書いては元も子もない。
どうしても局所的に回避したい場合は以下のように記述する。多用厳禁。
// -Wgnu-zero-variadic-macro-argumentsを回避したい例
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wgnu-zero-variadic-macro-arguments"
#define MY_PRINTF(format, ...) printf(format, ##__VA_ARGS__)
#pragma GCC diagnostic pop
各オプションについて
-
-Weverything
すべての警告オプションを有効にする -
-Werror
すべての警告をエラーとする -
-Wno-c++98-compat
C++98互換とはしない -
-Wno-c++98-compat-pedantic
C++98互換とはしない -
-Wno-padded
パディングの発生を許容する
Sanitizerの力を借りる
gccやclangには、Address SanitizerやUndefined Behavior Sanitizerという、ランタイムでのエラー検出ツールが付属している。
これらは非常に強力で、何気なく記述したコードに存在するメモリに関する問題や未定義動作とされるコードを実行中に検出してくれる。
Address Sanitizer
メモリに関する問題を検出してくれる。
たとえば、以下のコード。
#include <functional>
void func()
{
std::function<void(int)> f;
// 関数作成
{
int value = 0;
// ...
f = [&](int v)
{
// ...
value = v; // !!!! テンポラリの変数を使用してしまっている !!!!
// ...
};
// ...
}
f(10);
}
int main(int, char **)
{
func();
return 0;
}
関数作成のタイミングで、テンポラリ変数のvalueをキャプチャしてしまっている。
この関数を使用しようとすると値valueのメモリは死んでいるので、未定義動作となる。
これを-fsanitize=address
オプション付きでビルド・実行すると問題箇所でcrashして以下のようなエラーが得られる。
=================================================================
==98792==ERROR: AddressSanitizer: stack-use-after-scope on address 0x7ffee3639290 at pc 0x00010c5d3802 bp 0x7ffee3638f80 sp 0x7ffee3638f78
WRITE of size 4 at 0x7ffee3639290 thread T0
#0 0x10c5d3801 in func()::$_0::operator()(int) const AddressSanitizer.cpp:14
#1 0x10c5d36c9 in decltype(std::__1::forward<func()::$_0&>(fp)(std::__1::forward<int>(fp0))) std::__1::__invoke<func()::$_0&, int>(func()::$_0&, int&&) type_traits:3545
#2 0x10c5d342e in void std::__1::__invoke_void_return_wrapper<void>::__call<func()::$_0&, int>(func()::$_0&, int&&) __functional_base:348
#3 0x10c5d328c in std::__1::__function::__alloc_func<func()::$_0, std::__1::allocator<func()::$_0>, void (int)>::operator()(int&&) functional:1546
#4 0x10c5cc657 in std::__1::__function::__func<func()::$_0, std::__1::allocator<func()::$_0>, void (int)>::operator()(int&&) functional:1720
#5 0x10c5d545b in std::__1::__function::__value_func<void (int)>::operator()(int&&) const functional:1873
#6 0x10c5ca205 in std::__1::function<void (int)>::operator()(int) const functional:2548
#7 0x10c5c9b55 in func() AddressSanitizer.cpp:20
#8 0x10c5ca2eb in main AddressSanitizer.cpp:25
#9 0x7fff6a218cc8 in start+0x0 (libdyld.dylib:x86_64+0x1acc8)
Address 0x7ffee3639290 is located in stack of thread T0 at offset 112 in frame
#0 0x10c5c997f in func() AddressSanitizer.cpp:4
This frame has 3 object(s):
[32, 80) 'f' (line 5)
[112, 116) 'value' (line 9) <== Memory access at offset 112 is inside this variable
[128, 136) 'ref.tmp' (line 11)
HINT: this may be a false positive if your program uses some custom stack unwind mechanism, swapcontext or vfork
(longjmp and C++ exceptions *are* supported)
SUMMARY: AddressSanitizer: stack-use-after-scope AddressSanitizer.cpp:14 in func()::$_0::operator()(int) const
Shadow bytes around the buggy address:
0x1fffdc6c7200: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x1fffdc6c7210: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x1fffdc6c7220: 00 00 00 00 00 00 00 00 f1 f1 f1 f1 04 f3 f3 f3
0x1fffdc6c7230: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x1fffdc6c7240: 00 00 00 00 f1 f1 f1 f1 00 00 00 00 00 00 f2 f2
=>0x1fffdc6c7250: f2 f2[f8]f2 f8 f3 f3 f3 00 00 00 00 00 00 00 00
0x1fffdc6c7260: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x1fffdc6c7270: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x1fffdc6c7280: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x1fffdc6c7290: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x1fffdc6c72a0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
Shadow gap: cc
==98792==ABORTING
この例の場合ならば、ERROR: AddressSanitizer: stack-use-after-scope
のメッセージと共に
...
#0 0x10c5d3801 in func()::$_0::operator()(int) const AddressSanitizer.cpp:14
...
という記述があるため、すぐに原因に気づくことができる。
Undefined Behavior Sanitizer
未定義動作とされる挙動があった場合に検出してくれる。
例えば、以下のコード。
#include <limits>
int main(int, char **)
{
int value = std::numeric_limits<int>::max();
value += 1; // !!!!! 最大値に+1している !!!!!
return 0;
}
このコードではint最大値に対して+1してしまっているため、未定義動作となる。
これを-fsanitize=undefined
でビルド・実行するとcrashせずに以下のエラーメッセージが出力される。
UndefinedBehaviorSanitizer.cpp:6:11: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior UndefinedBehaviorSanitizer.cpp:6:11 in
こちらも検出理由と行数が出力されるため、すぐに問題箇所を特定できるだろう。
開発ステージ別のビルド構成
ステージによってコンパイルオプションを切り替えて使う。
ステージ | 構成 |
---|---|
コーディング&テスト | Develop |
仕上げ(Runtime Errorチェック) | Debug |
仕上げのステージを用意しているのはC++未定義動作(Undefined Behavior)なコードを書いても正しく動いてしまう場合があるからである。
未定義動作なコードは最適化時に予想外なコードを生成されてしまったり、「数千回に1回ぐらい落ちる」のような退治しにくいバグを生成しかねないので1つ実装するたびにチェックしておく。
Develop構成
-Weverything -Werror -O0 -fno-rtti
ビルド速度重視。
Debug構成
-Weverything -Werror -g3 -O0 -fno-rtti -fsanitize=undefined,address -fno-omit-frame-pointer
実装エラーを検出するための構成。通常よりもビルドに時間がかかる。
実装時「なんかよくわからない動作やcrashが発生する」場合にとりあえずこの構成を使うこともできる。
おわりに
とりあえずコンパイルオプションから記載させていただきました。
次は コンパイラと共に戦う【プログラミング編】 です。
Discussion