Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

ゆるバグ

771 views

Published on

バグについてゆるく語り合う会

Published in: Technology
  • Be the first to comment

ゆるバグ

  1. 1. ゆるバグ バグについてゆるく語り合う会 #yurubug 2020/4/29, 2020/5/06 光成滋生
  2. 2. • Windows Xpのバグ • gdbのバグ • objdumpのバグ • Windowsのstackの仕様 • Linux on Travis-CIでだけエラー • inline化される? • Visual Studioのバグ • PHPのバグ 遭遇したもの 2 / 36
  3. 3. • 発端 • 当時(2001) Windows 2000でとあるコーデックを開発中 • ソースコードのインデントがおかしかったのでツールで整形 • 一部手動でスペースをタブに変換するなど • コーデック自体の挙動が変わるはずはない • しかし • 実行するとWindows 2000が突如再起動 • ??? • Windows 98みたいにすぐ落ちることはあまりないんだけど • もう一度試してもやっぱり再起動 • 整形までだとちゃんと動く • ??? 整形したらえらいことになった 3 / 36
  4. 4. • リブートを繰り返しつつコードを小さくする • 再起動待ち辛い • ディスクが壊れないかも心配 • 原因判明 • コーデックは無関係 • コーデックの進捗状況を示すメッセージで スペースだったところをタブにしたら発生 原因追求 4 / 36
  5. 5. • 文字を出力するだけ • 後にループしなくてもOKと判明 • 発売直前の(開発用)Windows Xpでも発生 • こんなのでOSが落ちるの? コード最小化 #include <stdio.h> int main(void) { for (;;) { printf("hung up¥t¥t¥b¥b¥b¥b¥b¥b"); } return 0; } 5 / 36
  6. 6. • ML(メーリングリスト) ; 今のTwitterみたいなもの • fj.os.ms-windows.programming • https://groups.google.com/forum/#!msg/fj.os.ms- windows.programming/0c2WdfjwK4Q/fC7sHDh2jkgJ • omp.os.ms-windows.programmer.win32 • https://groups.google.com/forum/#!msg/comp.os.ms- windows.programmer.win32/uZd_19YEdRM/JXBR0FTsV2sJ • 反応 • this is not just a joke • NT4でも落ちた • printf("¥t¥b¥b");だけでも落ちた • Perlでも落ちた / ○○でも落ちた / 言語に依らない • 実は結構なセキュリティホールだったかも fjなどのニュースグループに投稿 6 / 36
  7. 7. • よくある日常 • あるプログラムが落ちたのでデバッグしようとgdb上で起動 • まずはrで実行して落ちたところでバックトレース(bt) • あれ、Command not foundって何? • よくみるとコマンドプロンプトに戻ってる いつもと違うgdb % gdb ./a.out GNU gdb (GDB) 7.7 Copyright (C) 2014 Free Software Foundation, Inc. ... Type "apropos word" to search for commands related to "word"... Reading symbols from ./a.out...Segmentation fault (core dumped) % r r: Command not found. % bt bt: Command not found. % 7 / 36
  8. 8. • 再掲 • dmesgを見てみる よく見ると落ちているのはgdb Reading symbols from ./a.out...Segmentation fault (core dumped) % dmesg | tail [8895844.655909] gdb[18402]: segfault at 7ffc284a1ff8 ip 0000000000741fc7 sp 00007ffc284a1fc0 error 6 in gdb[400000+5a0000] 8 / 36
  9. 9. • 何が原因だろう • 10万行以上あるコードを減らす • 少し削る & コンパイル & gdbで実行 • 落ちる → もっと削る • 落ちない → 少し戻す • なかなか辛い 原因調査 9 / 36
  10. 10. • 中身に意味はないがgdb 7.7が落ちるコード 削られたコード #include <utility> #include <stdio.h> struct BaseHolder { }; template<class Func> struct Holder : public BaseHolder { Func func; explicit Holder(Func&& func):func(std::forward<Func>(func)) {} }; struct Runner { BaseHolder *holder_; template<class Func> explicit Runner(Func && func):holder_(new Holder<Func>(func)) {} ~Runner() { delete holder_; } }; template<class T>void f(T &) { auto g =[&](){}; Runner{g}; } int main() { int a = 0; f(a); } 10 / 36
  11. 11. • C++11のtemplateとラムダ関数の組み合わせでできる シンボルが複雑でgdbのパーサがおかしくなった • それっぽいキーワードで探すと同時期に似たバグ報告 • https://sourceware.org/bugzilla/show_bug.cgi?id=16845 • バージョンを上げて解決 原因の推測 11 / 36
  12. 12. • AVX-512の命令エンコーディングはとても複雑 • Xbyakのブロードキャスト(レジスタ全体に伝搬させる) 指定のテストでobjdumpで逆アセンブルしながら確認中 • なんかテストがたまに失敗する • しかし落ちたところを取り出してテストしてもpass • うーん • 調査中(略) 時々失敗するテスト 12 / 36
  13. 13. • 原因判明 • 同じバイト列なのに逆アセンブル結果が違う • vcvtpd2dqxとvcvtpd2dq / vcvtpd2dqyとvcvtpd2dq • 途中に(Z)のバイト列が入るとそのあと間違えるバグ • 逆アセンブラが状態を持つとは思わなかった 時々間違えるobjdump (2.3.0) % objdump -M x86-64 -D -b binary -m i386 vcvtpd2dq.bin vcvtpd2dq.bin: file format binary Disassembly of section .data: 00000000 <.data>: 0: 67 c5 fb e6 40 20 vcvtpd2dqx 0x20(%eax),%xmm0 ; (X) 6: 67 c5 ff e6 40 20 vcvtpd2dqy 0x20(%eax),%xmm0 ; (Y) c: 67 62 f1 ff 18 e6 40 vcvtpd2dq 0x20(%eax){1to2},%xmm0 ; (Z) 13: 04 14: 67 c5 fb e6 40 20 vcvtpd2dq 0x20(%eax),%xmm0 ; (X') 1a: 67 c5 ff e6 40 20 vcvtpd2dq 0x20(%eax),%xmm0 ; (Y') 13 / 36
  14. 14. • inconsistent disassemble of vcvtpd2dq • https://sourceware.org/bugzilla/show_bug.cgi?id=23025 報告したら速攻で修正された 14 / 36
  15. 15. • 普段はちゃんと動いているのにたまに落ちる • それ自体はpureな関数(状態を持たない) • アライメントなどの問題ではない • Windowsでだけ発生 • バッファオーバーフローでもない • スタックオーバーフローでもない • 静的解析ツールの警告で原因判明 • 問題が発生する最小コード たまに例外で落ちる自分のコード foo: sub rsp, 1024 * 6 mov rax, [rsp] add rsp, 1024 * 6 ret 15 / 36
  16. 16. • スタックレイアウト • スタックの割り当ては4KiBずつ • 新しいスタックは4KiBずつ伸ばさなければならない Windowsのstackは自動的に伸びる ←現在のスタック ←過去に使われたところのあるスタックの先端 小さいアドレス 大きいアドレス ここから↓はまだ割り当てられていないページ 16 / 36
  17. 17. • スタックレイアウト Windowsで16KiBのスタックが欲しいとき ←現在のスタック ←過去に使われたところのあるスタックの先端 ここから↓はまだ割り当てられていないページ -4096 ; まずここまで -4096*2 ; 次にここ -4096*3 ; そしてここ こんなふうにして伸ばす foo: sub rsp, 1024 * 16 mov rax, [rsp + 1024 * 15] mov rax, [rsp + 1024 * 14] ... 17 / 36
  18. 18. • 関数のプロローグ • 元のコードが時々落ちていたのは • 普段は他のコードがスタックを利用してスタック領域が伸び ていた大丈夫 • 通常と異なるパスでスタックがあまり伸びてないときに突入 • 落ちる 専用の関数__chkstkがある foo: mov [rsp+8], ecx mov eax, STACK_SIZE call __chkstk sub rsp, rax ... 18 / 36
  19. 19. • https://github.com/herumi/test-travis-release • GoからCの関数を呼ぶサンプル • 手元のUbuntu(x64, arm64), macOS, Windows(mingw) で動作 Travis-CIで悩み中の問題 /* void blsFunc(const char buf[][8]); */ import "C" import ( "unsafe" ) func BlsFunc(buf []byte) { C.blsFunc((*[8]C.char)(unsafe.Pointer(&buf[0]))) } 19 / 36
  20. 20. • Travis-CIでの結果 • Goやコンパイラのバージョンを合わせても駄目 • Goの中間生成物を見る • _obj/にいろいろファイルができる • 関係ある部分を眺める なぜかTravis-CI/Linux上でのみエラー cd bls go tool cgo bls 20 / 36
  21. 21. • 何かのツールがchar [][8]のパースに失敗してる? • でもなんで??? 詳しい人プリーズ 違い // bls.cgo1.goのOKなとき v := (_Cfunc_blsFunc)((*[8] _Ctype_char)(unsafe.Pointer(&buf[0]))) // ERRなとき v := func() _Ctype_int{ _cgoIndex0 := &buf; _cgo0 := (*[8]_Ctype_char)(unsafe.Pointer(&(*_cgoIndex0)[0])); _cgoCheckPointer(_cgo0, *_cgoIndex0); ... }() // bls.cgo2.cのOKなとき _cgo_..._Cfunc_blsFunc(void *v) { struct { __typeof__(char const[8])* p0; ... // ERRなとき _cgo_..._Cfunc_blsFunc(void *v) { struct { void* p0; ... 21 / 36
  22. 22. • gccのバージョンを上げたらある処理が4倍遅くなった • gccが生成する関数のそれぞれのasm出力は問題なさそう • バージョンが変わっても大差ない • 謎のベンチマーク挙動 • ベンチマークの後ろに exit()を挿入すると速度が変わる • 挿入箇所と効果の関係は? • (B)でexitすると遅いまま • (C)でexitすると速い • (D)でexitすると(C)より少し遅い • (E)でexitすると(C)よりもう少し遅い おかしな因果関係? void bench() { (A)ベンチマークコード // (B) } int main() { bench(); // (C) unitTest1(); // (D) unitTest2(); // (E) unitTest3(); } 22 / 36
  23. 23. • gccはinline対象関数の総量を管理している • --param max-inline-insns-single=N オプション • この範囲内でinline化する関数を選んでいる • 原因判明 • gccのバージョンが上がってinline対象となる関数が増えた • bench内の関数がinline対象外となり遅くなった • 単体でみるとそれは分からない • exitすると速くなったり遅くなったりした理由 • mainの途中でexitした後のコードは生成されない • inline対象が減るのでbenchがinline化されて速くなる • bench内でexitしてもmain内でのinline対象は減らなかった • benchは速くならない • -Winlineで該当関数がinlineされているか確認 inlineされるかされないか 23 / 36
  24. 24. • Visual Studioでsinやexpが遅くなる現象 • 理由がさっぱり分からない • とても苦労して見つけた再現コード • struct A notUsedを作るとmainの中のsin等が遅くなる • ループ回数の8を7にすると遅くならない 何故か遅くなる数学関数 const struct A { float a[8]; A() { const float x = log(2.0); for (int i = 0; i < 8; i++) a[i] = x; } } notUsed; int main() { ... } 24 / 36
  25. 25. • ループ回数8と7で違いがでる要因といえば • AVX2を適用可能かどうか • 原因判明 • VCは8回ループをAVX2を使って最適化した • しかしSSEに切り戻すコード(vzeroupper)を入れ忘れ VCの最適化バグ 25 / 36
  26. 26. • レジスタの形 • SSEは128bitレジスタxmm • AVXは256bitレジスタymm • ymmの下位128bitがxmmレジスタ • SSEの命令padddはxmmレジスタ同士の足し算 • AVXの命令vpadddはymmレジスタ同士の足し算 • SSE命令はymmの上位128bitの存在を知らない • その部分(d7:d6:d5:d4)は変更されない SSEとAVX xmm0 [d3:d2:d1:d0] ; diは32bit ymm0 [d7:d6:d5:d4:d3:d2:d1:d0] 26 / 36
  27. 27. • x86はレジスタの一部しか書き換えない操作は苦手 • 32bitレジスタの下位16bitのみを操作する • フラグの一部しか更新しないinc/decなど • ちょっと遅くなる(だけだった) パーシャルレジスタストール 27 / 36
  28. 28. • 上位128bitがclean(zero)な状態と そうじゃないdirtyな状態 • SSE/AVX両方動くけれども dirtyなときにSSEを使うと大きなペナルティ • AVXを利用後SSEに戻るときにvzeroupperを呼ぶ必要 • VCのバグ • notUsed内でAVXを利用したがvzeroupperせずにmainに突入 CPUは状態を持っている clean upper dirty upper AVX使用 vzeroupper SSE AVX SSE 速度低下 28 / 36
  29. 29. • Broadwellまで Skylake(2015~)以降 • Intel 64 and IA-32最適化マニュアル • MIXING AVX CODE WITH SSE CODE (Skylake/Ice Lake)以降は改善されてる 29 / 36
  30. 30. • Intelシステムプログラミングガイド • Performance Monitoring Events • C1H:08H ; OTHER_ASSISTS.AVX_TO_SSE • CPU内でAVX→SSEでペナルティを受けた回数を記録してる • perf stat -e r08c1 -e r10c1 ./実行ファイル • perfが動かない場合(VM上など)はsdeを使う • https://software.intel.com/en-us/articles/intel-software-development-emulator • sde -oast out.txt -- ./実行ファイル • # AVX_to_SSE_transition_instances: 10000005 perfやsdeによる検出方法 Ice Lakeでは無くなった 30 / 36
  31. 31. • PHPで実行時にコードをコンパイルして共有メモリに 保存して再利用する機能 • PHP5.5(当時)でOPcacheを利用しているとsegvする現象 • どうにかしたい • 調査開始 • segvからbtしたぐらいでは分からない(PHPは巨大) • ASan(address sanitizer –faddress=sanitize)で実行 • <?php echo “abc”;だけで落ちる • ASanを封じられた • LD_PRELOADでtcmallocやjemallocなどのデバッグ支援機構 • エラーで落ちた • 仕方がないので自前malloc/freeを作成 PHPのOPcacheにまつわる問題 31 / 36
  32. 32. • malloc/freeだけではない • memalign, aligned_alloc, posix_memalignなど • 偽陽性が高い? • mallocしてないのにfreeに渡される知らないポインタ • strdupもラップが必要だった • -Dmalloc=my_malloc –Dfree=my_free ... • 偽陽性消える • 作業中にPHPのバグをいくつか見つける • いろいろなmalloc/free • Apache APR, MySQL由来のpstrdupなどの外部ライブラリ • PHP内部のfree, interned_free, ... • 整合性を保つよう調整 ラップが難しい(1/2) 32 / 36
  33. 33. • dlopen • dlopenの中でもmalloc • デバッグ用にfprintfするとfprintfで先にmallocされて順序が変 わる • 戦略 • (コードレベルで)全てのmalloc, freeを置き換える • LD_PRELOADで自前のmalloc/freeに置き換え • これで普通のプログラムは落ちなくなった • ASanで落ちていた(ASanの誤動作)の場所も判明 ラップが難しい(2/2) 33 / 36
  34. 34. • 泥臭い方法 • ASLRを無効化しておく • sudo sh -c "echo 0 > /proc/sys/kernel/randomize_va_space" • 実行 • おかしなfreeを受けたところでそのポインタを記録 • そのポインタをmallocした箇所でbreak & btで該当ソース • 主な結論 • PHPのオプションのfast_shutdownが有効なときに malloc/freeの不一致コードに突入 • コードがカオスなので修正時間は無い • いくつかバグ報告したしまあいいか • fast_shutdown=0で回避可能 当時の解決 34 / 36
  35. 35. • clang • dlopenにRTLD_DEEPBINDをつけるとエラー • 昔はこれで誤検知? • typo発見 incompatibe → incompatible • macOSのdlopenは最初からRTLD_DEEPBIND相当の挙動 • gccのdlopenにはこの制約はなさそう 最近のclang/gcc shared library with RTLD_DEEPBIND flag which is incompatibe with sanitizer runtime (see https://github.com/google/sanitizers/issues/611 for details). 35 / 36
  36. 36. • RTLD_DEEPBIND • dlopenするライブラリのシンボルの参照領域を グローバル領域よりも前に配置する • 例 • RTLD_DEEPBINDなし ; clock()は123を返す • RTLD_DEEPBINDあり ; clock()はオリジナルの値を返す 補足 main.c h = dlopen("sub.so"); f = dlsym(h, "sub"); f(); sub.c // sub.so void sub() { int t = (int)clock(); printf("clock()=%d¥n", t); } pred.c // pred.so clock_t clock() { return 123; } LD_PRELOAD=./pred.so ./main 36 / 36

×