世界を動かす技術を、日本語で。

デバッガーでできること、プリントデバッグではできないこと

概要

  • デバッガ の利用率が低い理由と課題
  • debug logging とデバッガの機能比較
  • デバッガの 具体的な利点 の紹介
  • プロジェクトでの debug設定の標準化 の重要性
  • 結論 と追加リソース案内

デバッガが使われない理由と現状

  • デバッガ のセットアップが多くのコードベースで 難解
  • リモート環境 (例:Kubernetes)ではデバッガ利用が困難
  • 多くの開発者が Print/log.Debug を主に利用する傾向
  • debug logging は手軽だが、デバッガ特有の機能は代替不可

デバッガの強力な機能

  • コールスタック全体の可視化
    • 呼び出し元フレームの 変数参照・式評価 が可能
    • どのように現状に至ったかの 追跡
  • 動的な式評価
    • 関数呼び出しやプログラム状態の 変更 が可能
    • REPL として全プログラム状態にアクセス
  • 例外発生箇所での停止
    • 例外が発生した 直後の状態 を確認
    • 未捕捉例外のみ を対象にする設定も可能
  • コード変更なしで挙動変更
    • 変数や設定値を一時的に 書き換え可能
    • 誤ってコードをコミットするリスク の回避
  • デバッグ設定による標準化
    • VSCodeIntelliJ の設定ファイルを共有
    • .envファイル や環境変数、CLI引数の統一管理
    • 各エントリーポイントごとに debug設定 を用意し、新規参加者の学習コスト削減

結論と追加情報

  • 条件付きブレークポイントタイムトラベルデバッガ(TTD) などの高度な機能も存在
    • ただし、TTDは多くの言語で成熟していない現状
  • VSCodeデバッガ でIPython REPLから任意の関数呼び出しをデバッグする方法も推奨リソース

Hackerたちの意見

作者は最高の機能の一つを見逃してるね。それはハードウェアブレークポイントへの簡単なアクセス。メモリの読み書きで、アドレスやシンボルを使ってブレークするのは、時間を節約できるデバッグツールの中で一番だと思う。

このアプローチについてもっと詳しく説明されているところはある?

windbgはスクリプト機能を提供していて、チームが内部データ構造の検証をブレークポイントやウォッチポイントでトリガーできるようにしてた。微妙な状態の破損を検出するのにすごく役立ったし、チーム間でスクリプトを共有することで、複雑なバイナリの知識を共有する手段にもなってた。

まじで同感。これ、手に煙を出した銃を持ってバグを捕まえるみたいな感じで、printfじゃ絶対無理だよね。100回でもアップボートしたいわ。

同じツールボックスから:式のウォッチ。違反している不変条件にウォッチを設定して(例えば「bufpos < buflen」)、それが変わった瞬間にブレークポイントを取得する。

特に、rrやUndoDBの逆実行と組み合わせるとすごいよね!

タイムトラベルと組み合わせると、ほんとに衝撃的だよ。壊れたメモリを見つけた?そしたら、そこにウォッチポイントを置いて、プログラムを逆に実行すればいい。2週間が2分に変わるよ。

ほとんどの言語ではスタックを表示できるから、プリントデバッグで簡単にスタックが見えるよね。私が試したケースでは、動的な式は信じられないくらい遅かった。著者が言っているように、デバッガがうまく動かないケースもいくつかあるし。個人的には、いつも動くツールを選ぶつもりだよ。

でも、プリントデバッグでスタックを遡って、そこでの変数や関連する関数を調べることはできるの?

「いつも機能するツールを選ぶか、時々しか機能しないツールを選ぶか。」これは一つのツールに限られてるなら、論理的だよね。電源が切れることがあるからって、パワーツールを買わないってことはないよね?手動工具しか選べないってわけじゃないし。

デバッガを使っている人には当たり前に聞こえるかもしれないけど、私の経験では、結構な人がデバッガを知らなくて使ってないんだよね。良い情報を広めよう!

そうだね、存在を知らない人がたくさんいる。でも、いろんな理由でそれにアレルギーを持ってる新しい人やベテランも多いよ。新しい言語で作業を始めるとき、最初にデバッガーを設定するのが俺のやり方だし、新しいプロジェクトのコードを探るのにもいつも使ってる。

どこでデバッガーの使い方を学んだか考えてみた。M$が.NETやVB6を簡単にしてくれたのと、プロとして働いて他の人から学んだのが大きかったな。人気がないのは意外だね。テストがあるから必要性が減ったのかもしれないけど、ユニットテストのデバッグはめちゃくちゃ効果的だよ。すぐにブレークポイントに到達できて、シナリオを調整できるからね。

言語や設定によっては、デバッガーが本当に使えないこともある。ここにいる人たちは、きっと逃げ出して自分に合ったスタックを探すだろうけど、もっと実用的な人たちは他のツール(REPL、構造化ログ、APMなど)を使ってデバッグを学ぶんじゃないかな。

これ、テストにも当てはまるよね。未テストのレガシーコードがめっちゃ多い。

銀の弾丸ではないけど、Visual StudioはC/C++コードのデバッグに関してはgdbなどよりも遥かに進んでるよ。「プロセスにアタッチ」して、ウィンドウをクリックするだけでデバッグできるのは、大きなWindowsアプリをデバッグする時にすごく楽だよね。

笑、意見が分かれるね。gdbのインターフェースはイライラするけど、GUIのフロントエンドの選択肢はたくさんあるし。VSは逆に、リリースごとにどんどん悪くなってる。今は遅すぎてバグだらけで耐えられない。昔は素晴らしいソフトウェアだったのに、今はクソみたいなゴミになっちゃった。

X11では、これができるよ:gdb -p xprop _NET_WM_PID | sed 's/.*= //' Waylandでは、全ての良いものを無駄に壊すから、できないかもしれないね。

本当に状況によるね。VSのデバッガーは素晴らしいし、たいていはそっちの方が簡単だと思う。でも、gdbはスクリプトが書きやすくて、特定の状況ではより強力だから、難しいバグのときはgdbを使うこともある。もちろん、VSは主にWindows用で、gdbは主にLinux用だから、あんまり競争してる感じはしないよね。どちらのOSでもどちらのデバッガーも使えるのは知ってるけど、実際にはデバッガーの選択は主にOSに基づいてる。

IMEのコンソールベースのデバッガは、コンソール出力があまりないシングルスレッドコードにはすごく良いけど、それ以外ではあまりうまくいかない。GUIベースのデバッガなら、その問題を解決できるかもしれないけど、私はあまり試してないんだ。pdbはPythonにはすごく良いよ。

よくGoのデバッガーを使って、同時に動いてるGoルーチンをデバッグしてるけど、シングルスレッドのデバッグとあまり変わらないと思う。条件付きブレークポイントを使って、気になる構造体を扱ってるGoルーチンの時だけブレークするようにしてるんだ。他に問題があるのかな?

デバッガーは高価値だけど、REPLにアクセスできることも主要なユースケースをカバーしてるよね。特に、REPLツールはリモートセッションやプレプロダクションサーバーでも使えるし、コードベースがある程度モジュール化されてれば、時にはデバッガーよりも快適だと思う。PHPのデバッグ状況が改善されたのか気になるな。バッチプロセスのデバッグや、サーバーのメモリが無限じゃない時はほとんど使えなかったから、俺たち凡人には大変だったよね。

投稿したフレームベイトの作者です。賛成です。コードが未完成の時に、エントリーポイントなしでランダムな関数を呼び出さなきゃいけない時は、IPythonやJShellのREPLをよく使います。実際、ローカルで実行している時にPythonのREPLからグラフィカルデバッガーにジャンプすることも可能です。PyCharmにはこの機能が標準であります。VSCodeでは、こんな簡単なワークアラウンドが使えますよ: https://mahesh-hegde.github.io/posts/vscode-ipython-debuggin...

どちらか一方じゃないよ。優れたプログラマーは両方の使い方を知っていて、仕事に適したツールを選ぶことができるんだ。

プリントは適切なツールじゃないよ。コードのその行に到達した時にデバッガーが何かをプリントするように設定できるし、自動で続行することもできる。じゃあ、printfの意味は何なの?情報と機能が少ないだけじゃん。

ここであまり話題にされてないけど、非常に役立つ別のデバッグの方法があるんだ。それは歴史的/オフラインデバッグ。ログと標準的なデバッグのハイブリッドみたいなもので、「すべて」がログに記録されていて、探索できるんだ。例えば:https://rr-project.org/

rrでの作業が大好き!残念ながら、最近関わっているプロジェクトがそれを壊しちゃってる(正直、Ubuntuのせいかもしれないけど、Archのインストールでは動くし、テストが必要なところにデプロイすると動かない)。

プリントデバッグは歴史的な/オフラインデバッグで、ただのアドホックな方法って感じ。npmの「debug」パッケージはその中間にあたるもので、デバッグステートメントを挿入する必要があるけど、DEBUG=scope.subscope.*,otherscopeみたいな環境変数を使わない限り出力からは隠れてる。

デバッガーを使うのは全然構わないけど、プリントデバッグの利点はどの言語でも使えることだよね。今、5つの言語でコードを書いてるプロジェクトに関わってるんだけど(ビルドシステムを入れれば6つ、シェルスクリプトを加えれば7つ)、そのうち2つはデバッガーをよく知ってる。1つはなんとかなるかも。もう1つはデバッガーを1回使ったことがあるだけで、最後の1つは触ったこともない。printfデバッグはどれでも使えるし、ビルドシステムやシェルスクリプトでも問題なし。デバッガーは素晴らしいよ、リモートデバッグでもね。私の経験では、知らないシステムを素早く効果的にデバッグできる人は少ない気がする。

私の経験では、条件付きブレークポイントは信頼性が低すぎて、使う気になれない。必要なときは、if (condition) print(""); みたいなコードを追加して、そのprintにブレークポイントを置く。printを呼び出すことで、その行が最適化されることはないからね(普段はデバッグモードでデバッグしてるからあまり関係ないけど、もうこれは反射的にやってる感じ)。何か入れないと気が済まないんだよね。

デバッガーは大好きだけど、条件付きブレークポイントは確かにひどいね。毎回違う入力で同じコードが呼ばれるときに、問題が起きるのはそのうちの1つだけだから、条件付きブレークが必要になることが多い。条件付きブレークポイントは遅すぎて、実行が停止する前に粉々になっちゃいそう。C#で作業してるから、条件を注入してDebugger.Break()を呼び出して、JITが関数をライブで再コンパイルするオプションがない理由が全く分からない。これがあれば、条件付きブレークポイントが使えるようになるのに!

私の経験では、信頼性がないわけじゃないけど、デバッグ中のアプリケーションをかなり遅くするよね。あなたが提案することはよくやるけど、__debugbreak()(int3にコンパイルされる)を使うことが多いかな。