セミナー講師:@y_jono
イラスト:@masshirohuyu
Sapporo.cpp
OSC Hokkaido 2015
自己紹介
• Twitter : @y_jono
• Github : y-jono
• Sapporo.cpp (札幌C++勉強会)
登場するキャラクター
知識豊富!
長い経験!
頼れる!
最近ようやく
開発ができる
ようになって
きた。新人。
今日の主役?
虫らしい
レジュメ
•はじめに
• デバッグってなんだろう?
•コウハイ君の事件簿 Part1
•時間がたつとプログラムが落ちる!?
•コウハイ君の事件簿 Part2
•たまにだけどプログラムが落ちることがある!?
•まとめ
センパイ!質問が!
なに?
センパイ!質問が!
え・・・?
この子なにものです
か?
バグとは?
こいつは業界で
なんと呼ばれているか?
知ってる? ばぎー??
バグ関連用語
バグ関連用語
とりあえず・・
今日はバギー ・・・。
バギーについてもっと詳しく
バグ 欠陥 不正なプログラ
ムコード
感染 不正なプログラ
ム状態(メモリ)
障害 不正なプログラ
ムの動作
* 引用:デバッグの理論と実践
難しい
よう・・
つまり?
不正な
(意図しない)
動作
不正な
(意図しない)
コード
不正な
(意図しない)
メモリ
「欠陥」 「感染」 「障害」
センパイ!そろそろ・・・ デバッグ
すりゃいい
じゃん
勉強になります!
バギーのとりかた
おしえてください!
記録
• 障害、仕様誤認などの問題を記録
再現
• 障害を再現
• テストケースの自動化と単純化
特定
• 感染源の疑いがある箇所を特定
• 感染の連鎖を分離
修正
• 欠陥を修正
デバッグの進め方
* 引用:デバッグの理論と実践
こんな
かんじかな
・・・
やれやれだぜ
デバッグ難しそうなので
手伝ってもらえませんか?
ケーススタディ
デバッグ事例を2ケース紹介
• メモリリーク
• データ競合
一緒に考えましょう
• どうしてデバッグが難しいのか
• どう特定するか
• どう予防するか
センパイ!
このプログラム、時間が経つと
クラッシュするんですけど。。。
どんな障害?
メモリ確保で、例外が出てクラッシュ
してしまいます!!!
たぶんメモリが
枯渇したんだな。
どんな障害?
たぶんメモリが
枯渇したんだな。
枯渇??メモリはたくさん
載ってるはずですよね?
メモリ確保で、例外が出てクラッシュ
してしまいます!!!
メモリリークが原因だからさ
記録
• 障害、仕様誤認などの問題を記録
再現
• 障害を再現
• テストケースの自動化と単純化
特定
• 感染源の疑いがある箇所を特定
• 感染の連鎖を分離
修正
• 欠陥を修正
メモリリークのデバッグ
記録
• メモリ確保時に例外でクラッシュ
再現
• 数時間放置で再現
• 数週間、数ヶ月経過で再現
特定
• クラッシュ箇所とリーク箇所が不一致
• ヒントがクラッシュダンプにない
修正
• 解放処理の追加で済むことが多い
メモリリークのデバッグ
主にリーク箇所の特定が問題になる
→難しい
→簡単
→難しい
→難しい
→簡単
メモリリークの特定方法の例
解析ツール (過度に信用しないように注意!)
• リーク特定用のアロケータ(例えばnew/deleteの再定義)
• メモリ確保/解放時にメモリアドレス、
ファイル名、行番号などを記録。
• MFCでは_DEBUGディレクティブで有効になる。
• Valgrind
• Address Sanitizer
• clang static analyzer
 コードレビュー
• new, deleteされた不正ポインタの依存関係の解析
コウハイ君にデバッグさせるには
• プロジェクトで実績のある特定用のツールが必要
• リーク特定用のアロケータ
• 解析ツール
• 用意できなければ、コウハイにデバッグさせるのは止めよう
• センパイだってツールがなければ大変な作業
• それより、メモリー枯渇を予防する方法を考えよう
センパイによるリーク箇所の特定
• デモ
• Address Sanitizer (clang, gcc)
• Valgrind (memcheck)
Address Sanitizer
Valgrind(memcheck)
メモリリークの予防
デバッグはとても大変。
だからメモリリークが
発生しにくいコードを
書くことが大切なんだ
メモリーリークはどうして埋め込まれるの?
オブジェクト共有方法の設計に不備
•オブジェクトの所有者が不明確
•ポインタ、参照の使い方にルールがない
•new, delete を理由なく直接使うべきでない
レガシーなC++の使用
•生ポインタやauto_ptrでメモリを管理している
•Boostを使いたいが使えない環境だ
メモリリークを未然に防ぐには
スマートポインタの利用
• std:: unique_ptr : 所有権を占有する
• std:: shared_ptr : 所有権を共有する
• C++03以前の環境ならBoostのスマートポインタを採用する
スマートポインタって?
• 自動でメモリの解放処理を行うクラス。
• ポインタのように振る舞うように設計されている。
• このクラスをポインタのように使用するとメモリリークを防げる。
メモリリークを未然に防ぐには
コードレビュー
• コウハイ君がnew, deleteを使っている箇所を指摘し、
スマートポインタを使うよう指導しよう。
• どうしてもnew, delete使うときは、対応関係を確認しよう。
• 静的解析ツール(Clang Static Analyzer)を使うと楽
Clang Static Analyzer
センパイ!
このプログラム、たまにクラッ
シュするんですけど。。。
メモリリークを解決したコウハイ君ですが・・・
またバギーくっつい
てしまいました!!
え・・・?
コウハイ君の新たな問題とは?
おさらい
記録
• 障害、仕様誤認などの問題を記録
再現
• 障害を再現
• テストケースの自動化と単純化
特定
• 感染源の疑いがある箇所を特定
• 感染の連鎖を分離
修正
• 欠陥を修正
まずは手順
を確認しよう
障害の確認
それで、
どんな障害なの?
障害の確認
クラッシュするんですけど、
• なかなか再現しなくて・・
• 再現手順がよくわからなく
て・・
• 再現環境は複数あって・・
• プログラム開始後からの
発生時間もさまざま・・
• デバッグビルドでもリリース
ビルドでも発生します
Oh…
それで、
どんな障害なの?
想定される欠陥
たしか並列処理する
プログラムだったよね?
はい。はじめて並列プロ
グラム担当しています。
データ競合による
クラッシュじゃないかな?
データ競合によるクラッシュとは
どんな障害?
• 1つのデータを複数のスレッドが
同時に読み書きすると起こる
• 障害の表れ方はさまざま
• 障害の再現が難しいことが多い
今回は再現も問題だね。
データ競合の例
#include <pthread.h>
int global;
void *thread1(void *x) {
global++;
return NULL;
}
void *thread2(void *x) {
global--;
return NULL;
}
(continued..)
int main() {
pthread_t t[2];
pthread_create(&t[0], NULL,
thread1, NULL);
pthread_create(&t[1], NULL,
thread2, NULL);
pthread_join(t[0], NULL);
pthread_join(t[1], NULL);
}
データ競合の例
#include <pthread.h>
int global;
void *thread1(void *x) {
global++;
return NULL;
}
void *thread2(void *x) {
global--;
return NULL;
}
(continued..)
int main() {
pthread_t t[2];
pthread_create(&t[0], NULL,
thread1, NULL);
pthread_create(&t[1], NULL,
thread2, NULL);
pthread_join(t[0], NULL);
pthread_join(t[1], NULL);
}
データ競合による障害のデバッグ難易度
難しそうなのはどの工程かな?
再現
• スレッド実行は非決定的。
再現性が低いこともある。
特定
• クリティカルセクションで
守られていない変数の特定
修正
• デグレードしない設計を考え、
適用する
どれも難しそう
データ競合による障害のデバッグ難易度
そうだね
再現
• スレッド実行は非決定的。
再現性が低いこともある。
特定
• クリティカルセクションで
守られていない変数の特定
修正
• デグレードしない設計を考え、
適用する
難しい
難しさは
再現性次第
難しい
つまり
全部難しいと
どうやって再現するのか
「障害」を再現するにはプログラムへの様々な
「入力」を再現しなければならないんだ。
例えば…
障害発生時と同じ環境
端末、OS、製作中のソフトウェア
プログラム実行
データ、ユーザ入力、コミュニケーション、時間、乱数、動作環境
プロセスとスレッドのスケジューリング(再現できない)
物理現象、デバッグツール
どうやって特定するのか
•解析ツール
• (デモ:Thread Sanitizer, Helgrind)
•コードレビュー(のちほど紹介)
「特定」する
方法を見せよう!
Thread Sanitizer
Helgrind
Helgrind
データ競合はどうして埋め込まれるの?
なんでデータ競合を埋め込ん
でしまったか、わかる?
意識してなかったから
ですかね。
オイオイ
データ競合はどうして埋め込まれるの?
初めてききました。
じゃあ、データ競合が発生す
る条件は知ってる?
それだ!
1. 対象がatomic変数でないとき、
2. 同一メモリ位置に対するアクセスにおいて、
3. 少なくとも一方が変更(modify)操作であり、
4. 異なるスレッド上から同時に行われるとき。
データ競合が発生する条件
参考:http://yohhoy.hatenablog.jp/entry/2013/12/15/204116#fn-cb31e5fd
同時に「読む」のは問題ないよ
これをしらなかったのが
原因かぁ
未然に防ぐ方法(性能要件確認)
• 時間あたりのデータ処理量について早めに決定する
• データ処理時間の最大時間は?
• データ処理量の最低量は?
• 例えば
• FPS(frame per second)の平常値・最低値は?
• ユーザリクエストからレスポンスまでの間隔は最大何ミリ秒?
性能要件が早めに判明しないと、プロジェクト後期や
リリース前に複雑怪奇なコードが生まれるリスクが高まる
未然に防ぐ方法(コードレビュー)
1. どのデータを並行アクセスから守るの?
2. どんな方法でそのデータを確実に守るの?
3. このソースコードのうち、複数のスレッドが同時にアクセスできる箇所は
どこ?
4. このスレッドが保有するのは、これらのmutexのうちのどれ?
5. これらのmutexのうち、他のスレッドが保有する可能性があるのはどれ?
参考:C++ Concurrency in Action
未然に防ぐ方法(コードレビュー)
6. このスレッドの処理完了と、他のスレッドの処理完了の順番に暗黙の要
求があるか?
7. このスレッドで読み込まれたそのデータはこの時点でまだ有効ですか?
他のスレッドからの修正ができてしまいますか?
8. 他のスレッドがそのデータを変更できると仮定する。その変更はどういっ
た意味を持つのか?その変更が起こらないことを保証するにはどうすれ
ばよいですか?
参考:C++ Concurrency in Action
未然に防ぐ方法(テスト)
テスト
• パフォーマンステスト
• ヒューリスティックテスト(意図的に障害が発生しそうなタイミングを
狙うテスト)
• 耐久・高負荷テスト(高負荷時にのみ起きるバグを狙う)
*参考:C++ Concurrency in Action
おわりに
担当するデバッグ作業の難しさについて考えましょう
• 「バグを埋め込んでしまったのはなぜだろう」
• 「再現も特定もなんでこんなに難しいんだろう」
• 「どうやったらバグを防げただろう」
デバッグが難しい理由を考えることで
• プログラマは成長できる
• プログラマは助けあえる
付録
std:: unique_ptr
オブジェクト1
up1 up2
オブジェクト2
1つのオブジェクトを独占的に所有して管理したいとき用
std::unique_ptr
#include <memory>
int main() {
auto a= new int( 123);
// `new`したら明示的に`delete`
delete a;
}
#include <memory>
int main() {
auto a =
std::make_unique<int>( 123 );
} // `std::unique_ptr`は
// スコープの終わりで自動的に開放される
std:: shared_ptr
• 1つのオブジェクトを複数の所有者で管理したいとき用
• 何箇所から指しているかをカウントしている
sp1 sp2
オブジェクト
std:: shared_ptr
int main() {
std::shared_ptr<Hoge> sp1(nullptr);
{
auto sp2 = make_shared<Hoge>();
sp1 = sp2;
assert(sp1.use_count() == 2);
} // sp2が解放され、参照カウントが1下がる
assert(sp1.use_count() == 1);
} // sp1が解放され、ヒープに確保したオブジェクトが解放される
weak_ptr
sp1 sp2
オブジェク
ト
参照カウンタ
弱い参照カウン
タ
wp
デリータ
• weak_ptrはshared_ptrと
似ているが、weak_ptrか
ら参照した回数はカウン
トされない。
• shared_ptrから参照され
なくなったオブジェクトは
weak_ptrから参照されて
いても破棄される。
* 図引用:プログラミング言語C++第4版
weak_ptr
weak_ptr<Hoge> wp(nullptr);
{ shared_ptr<Hoge> sp1(nullptr);
{ auto sp2 = make_shared<Hoge>();
sp1 = sp2;
wp = sp1;
assert(sp1.use_count() == 2);
}; assert(sp1.use_count() == 1);
}; assert(wp.expired());

センパイ!このプログラムクラッシュするんですけど。。。

Editor's Notes

  • #12 デバッグの理論と実践より。 欠陥(defect): コンポーネント又はシステムに要求された機能が実現できない原因となる、コンポーネント又はシステムに含まれる不備。 たとえば、不正なステートメント又はデータ定義。実行中に欠陥に 遭遇した場合、コンポーネント又はシステムの故障を引き起こす。 バグ 障害に関係する、とりのぞける何か。 定義があいまい 欠陥 不正なプログラムコード コードに含まれるバグ 感染 不正なプログラム状態 状態に含まれるバグ 障害 外部から観察できる、不正なプログラムの動作 動作に含まれるバグ
  • #13 バグが発生する原因は多くの場合、不正なプログラムコードが原因なわけなのですが、 我々が時折出くわすデバッグが困難な状態、それは「不正な挙動から、どこが不正なプログラムなのか割り出しにくい」という 場合になると考えられます。 実際、不正なプログラムは、それが原因で不正なメモリ状態やデータを作り出し、それが不正な動作を生み出すのです。 今回の話では、これらを区別して話を進めていきたいと思います。 ちなみに「デバッグの理論と実践」では、これらをそれぞれ「欠陥」「感染」「障害」と呼んでいます。 ※「欠陥」「感染」「障害」の用語を定義する意義を同時に説明してしまうとよいかと思いました。  (用語の定義だけポンと出されても、聞いている人は忘れてしまうものです。)
  • #15 障害との遭遇 障害の記録(バグチケット作成) 障害の再現(!) 欠陥の特定 再現できたら 再現条件の絞り込み(エンバグリビジョン特定 git bisect, 再現手順の短縮 e.t.c.) 仮説検証しながら感染経路を特定する 再現できなかったら? ダンプファイルを使った事後デバッグ 欠陥箇所の推定(クラッシュログ、コードレビュー、解析ツール e.t.c.) 欠陥の除去
  • #56 センパイとコウハイ コウハイとセンパイはバグを埋め込み、バグを取り除く関係 コウハイは知識・経験・技術を吸収し、早くセンパイになる センパイはコウハイをフォローすることで間接的にバグの総数を減らす デバッグが難しい障害への対応 設計、テスト、コードレビューで予防するのが現実的 解析ツールがあればベター コウハイへのフォローは予防の時点で行い、こういう欠陥を埋め込ませない