競プロ勢のためのデバッグテクニック
Facebook Hacker Cup Round1の問題をオーバーフローと配列外参照で2問落としてしまい、反省を込めてデバッグテクニックを調べました。
競技勢がやらかすケアレスミスなバグの多くはこの2つだと思うので、この2つのみについて述べていきます。また、僕は普段の実装にMacを使っているので、gccのフリをしたMac仕様Clang(クソ)についても言及します。
オーバーフロー
これは-ftrapvオプションを付けてコンパイルしたファイルを実行すればオーバーフローした瞬間に検知(abort)する事が出来ます。
しかし、どこで死ぬのかがわからずこのままでは使い勝手が悪いので、-g(デバッグ)オプションも付けてコンパイルした後に、gdbで実行すると良いです。
#include <iostream> using namespace std; int main(){ int x = 10000000; int y = 10000000; cout << x * y << endl; }
$ clang++ -ftrapv -g overflow.cpp $ gdb -q ./a.out Reading symbols from ./a.out...done. (gdb) r Starting program: /tmp/a.out Program received signal SIGILL, Illegal instruction. 0x00000000004008cf in main () at overflow.cpp:8 8 cout << x * y << endl;
これはUbuntuでビルドした最新のClang++、Mac版Clang++でも動きます。 Ubuntuにもとから入っていたg++では__GI_raiseでSIGQUITを送出して終了した点で終了しました。(これはよく考えたらg++ではなくてgdbがデフォルトでユーザーのプログラムの中まで戻ってくるかどうかというだけかもしれない?あと、No such file or directory.と出ているのがややこしいですが、これは多分gdbのエラーメッセージ。)
$ g++ -ftrapv -g overflow.cpp $ gdb -q ./a.out Reading symbols from ./a.out...done. (gdb) r Starting program: /tmp/a.out Program received signal SIGABRT, Aborted. 0x00007ffff74ab418 in __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:54 54 ../sysdeps/unix/sysv/linux/raise.c: No such file or directory.
一応、backtraceをすればどこで死んだかは分かります。
(gdb) bt #0 0x00007ffff74ab418 in __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:54 #1 0x00007ffff74ad01a in __GI_abort () at abort.c:89 #2 0x00007ffff7842522 in __mulvsi3 () from /lib/x86_64-linux-gnu/libgcc_s.so.1 #3 0x00000000004008b7 in main () at overflow.cpp:8 (gdb) frame 3 #3 0x00000000004008b7 in main () at memory.cpp:8 8 cout << a * b << endl;
Ubuntuにはgdbがデフォルトで入っているので、ICPCでも使えますね。
配列外参照
これはg++とMac版Clang++で対応が若干変わります。
g++
g++の場合、最初に#define _GLIBCXX_DEBUG
を書くことで検出出来ます。
最初に書かないと動かないので注意してください。
これは、vectorファイルの中に
#ifdef _GLIBCXX_DEBUG # include <debug/vector> #endif
というコードがあるためです。つまり、include前であれば良いですが、誤りを避けるために最初に書いておくことをおすすめします。
#define _GLIBCXX_DEBUG #include <vector> using namespace std; int main(){ vector<int> a(10); for(int i=0; i<100; i++) a[i] = 1; return 0; }
例によってgdbを使います。
$ g++ -g memory.cpp $ gdb -q ./a.out Reading symbols from ./a.out...done. (gdb) r Starting program: /tmp/a.out /usr/include/c++/5/debug/vector:409:error: attempt to subscript container with out-of-bounds index 10, but container only holds 10 elements. Objects involved in the operation: sequence "this" @ 0x0x7fffffffdd00 { type = NSt7__debug6vectorIiSaIiEEE; } Program received signal SIGABRT, Aborted. 0x00007ffff74ab428 in __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:54 54 ../sysdeps/unix/sysv/linux/raise.c: No such file or directory. (gdb) bt #0 0x00007ffff74ab428 in __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:54 #1 0x00007ffff74ad02a in __GI_abort () at abort.c:89 #2 0x00007ffff7b0af95 in __gnu_debug::_Error_formatter::_M_error() const () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6 #3 0x000000000040110c in std::__debug::vector<int, std::allocator<int> >::operator[] (this=0x7fffffffdd00, __n=10) at /usr/include/c++/5/debug/vector:409 #4 0x0000000000400ba8 in main () at memory.cpp:8 (gdb)
Mac版Clang++
Mac版Clangの場合、_GLIBCXX_DEBUG
の代わりに_LIBCPP_DEBUG
を用います。
_LIBCPP_DEBUG
の設定した値によってデバッグレベルが変わるらしく、0か1をdefineすれば良いのですが、何故か1を指定するとバグりました。
#define _LIBCPP_DEBUG 0 #include <vector> using namespace std; int main(){ vector<int> a(10); for(int i=0; i<100; i++) a[i] = 1; return 0; }
$ g++ memory.cpp $ ./a.out vector[] index out of bouns Abort trap: 6
ここまではICPCで使うことも想定して電子的な準備に該当しない領域でどこまで出来るのか考えました
valgrindを使う方法
Facebook Hacker CupやGoogle Code Jam、または一般のプログラミングに関してはこれが一番やりやすいかもしれません。 -gオプションを付けてコンパイルした後
valgrind ./a.out
とするだけで詳細な結果が出てきます。
$ g++ -g -O0 test.cpp $ valgrind ./a.out ==1120== Memcheck, a memory error detector ==1120== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al. ==1120== Using Valgrind-3.12.0 and LibVEX; rerun with -h for copyright info ==1120== Command: ./a.out ==1120== ==1120== Invalid write of size 4 ==1120== at 0x10000156D: main (test.cpp:7) ==1120== Address 0x100a8e068 is 0 bytes after a block of size 40 alloc'd ==1120== at 0x10000A681: malloc (in /usr/local/Cellar/valgrind/3.12.0/lib/valgrind/vgpreload_memcheck-amd64-darwin.so) ==1120== by 0x10004F7DD: operator new(unsigned long) (in /usr/lib/libc++.1.dylib) ==1120== by 0x10000179E: std::__1::vector<int, std::__1::allocator<int> >::allocate(unsigned long) (new:156) ==1120== by 0x1000016B1: std::__1::vector<int, std::__1::allocator<int> >::vector(unsigned long) (vector:1070) ==1120== by 0x1000015CC: std::__1::vector<int, std::__1::allocator<int> >::vector(unsigned long) (vector:1073) ==1120== by 0x10000152E: main (test.cpp:5) ==1120== ==1120== ==1120== HEAP SUMMARY: ==1120== in use at exit: 22,153 bytes in 191 blocks ==1120== total heap usage: 258 allocs, 67 frees, 27,993 bytes allocated ==1120== ==1120== LEAK SUMMARY: ==1120== definitely lost: 0 bytes in 0 blocks ==1120== indirectly lost: 0 bytes in 0 blocks ==1120== possibly lost: 0 bytes in 0 blocks ==1120== still reachable: 0 bytes in 0 blocks ==1120== suppressed: 22,153 bytes in 191 blocks ==1120== ==1120== For counts of detected and suppressed errors, rerun with: -v ==1120== ERROR SUMMARY: 90 errors from 1 contexts (suppressed: 0 from 0)
追記
オーバーフロー時の挙動がMacとUbuntuで異なっていたのはどうやらftrapvの実装の違いのせいのようです
@aki33524 clangはftrapvの実装にundefinedなオペコードぶち込んで対応してるらしい https://t.co/3Yq4hde8JU
— るくす (@RKX1209) 2017年1月16日
" -ftrapv flag will cause clang to intenionally insert ud2"