Go 1.12でデプロイするバイナリをデバッグする

Debugging what you deploy in Go 1.12 by David Chase

はじめに

Go 1.11とGo 1.12では、開発者が本番環境にデプロイするのとまったく同じ最適化済みバイナリをデバッグできるようにするための大きな進展がありました。

Goコンパイラはより高速なバイナリを生成するためにますます積極的な最適化を行うようになってきており、その結果デバッグのしやすさという点では後退してしまっていました。Go 1.10では、Delveのようなインタラクティブなツールでまともなデバッグ体験を得るには、最適化を完全に無効化する必要がありました。しかし、開発者はデバッグのしやすさと引き換えにパフォーマンスを犠牲にするべきではありません。特に本番環境のサービスを実行している場合はなおさらです。問題が本番環境で発生しているのなら、本番環境でデバッグする必要がありますが、そのために最適化されていないバイナリをデプロイし直す必要はないはずです。

Go 1.11と1.12では、(Goコンパイラのデフォルト設定である)最適化済みバイナリでのデバッグ体験の改善に注力しました。改善点は次の通りです。

  • 値の検査、特に関数の入り口における引数の検査の精度が向上したこと
  • 文の境界をより正確に特定できるようになったことで、ステップ実行が飛び飛びになりにくくなり、ブレークポイントもプログラマの想定通りの場所で止まりやすくなったこと
  • Delveから(試験的に)Goの関数を呼び出せるようになったこと(ゴルーチンとガベージコレクションの存在により、これはCやC++の場合よりも難易度が高くなっています)

Delveで最適化済みコードをデバッグする

Delveは、LinuxとmacOSの両方に対応したx86向けのGo用デバッガです。Delveはゴルーチンをはじめとする他のGoの機能を認識しており、Goのデバッグ体験としては最良の部類に入るものを提供しています。DelveはGoLandVS CodeVimのデバッグエンジンとしても使われています。

Delveは通常、デバッグ対象のコードを -gcflags "all=-N -l" を付けて再ビルドします。これによりインライン化とほとんどの最適化が無効化されます。最適化済みのコードをDelveでデバッグするには、まず最適化済みのバイナリをビルドし、そのバイナリに対して dlv exec your_program を使ってデバッグします。あるいは、クラッシュ時のコアファイルがあれば、 dlv core your_program your_core で調べることもできます。1.12と最新版のDelveを組み合わせれば、最適化済みバイナリであっても多くの変数を検査できるようになるはずです。

値の検査の改善

Go 1.10が生成した最適化済みバイナリをデバッグする場合、変数の値はほとんどの場合まったく取得できませんでした。それに対してGo 1.11以降では、完全に最適化によって消え去ってしまった変数を除けば、最適化済みバイナリであっても通常は変数を検査できます。Go 1.11でコンパイラがDWARFのロケーションリストを出力するようになったことで、デバッガは変数がレジスタへ出入りする様子を追跡し、複数のレジスタやスタックスロットにまたがって分割された複雑なオブジェクトを再構築できるようになりました。

ステップ実行の改善

以下は、1.10のデバッガで単純な関数をステップ実行する例です。問題のある箇所(スキップされた行や重複して実行された行)を赤い矢印で強調しています。

Go 1.10でのステップ実行の例

このような不具合があると、プログラムをステップ実行している最中に自分が今どこにいるのかを見失いやすくなり、ブレークポイントで正しく止まることの妨げにもなります。

Go 1.11と1.12では文の境界情報を記録するようになり、最適化やインライン化を経ても元のソースの行番号をより正確に追跡できるようになりました。その結果、Go 1.12では、このコードをステップ実行するとすべての行で確実に止まるようになり、しかも想定通りの順序で止まるようになりました。

関数呼び出し

Delveでの関数呼び出しのサポートはまだ開発途上ですが、単純なケースでは動作します。例えば次のようになります。

(dlv) call fib(6)
> main.main() ./hello.go:15 (PC: 0x49d648)
Values returned:
    ~r1: 8

今後の方向性

Go 1.12は、最適化済みバイナリに対するより良いデバッグ体験に向けた一歩であり、私たちにはさらにそれを改善していく計画があります。

デバッグのしやすさとパフォーマンスの間には根本的なトレードオフがあるため、私たちは優先度の高いデバッグ上の不具合に的を絞って取り組みつつ、進捗を監視し、リグレッションを検知するための自動化されたメトリクスの収集にも力を入れています。

私たちは、変数の位置に関する正しい情報をデバッガに向けて生成すること、つまり変数が出力できる状況であれば必ず正しく出力されるようにすることに注力しています。また、特に関数呼び出し箇所のような重要なポイントで、より多くの場面において変数の値を取得できるようにすることにも取り組んでいますが、多くの場合これを改善するにはプログラムの実行速度を犠牲にする必要が出てきます。最後に、ステップ実行の改善にも取り組んでいます。パニック発生時やループ周りでのステップ実行の順序に注目しており、可能な限りソースコード上の順序に従うようにすることを目指しています。

macOSサポートについての補足

Go 1.11から、バイナリサイズを削減するためにデバッグ情報を圧縮するようになりました。これはDelveではネイティブにサポートされていますが、LLDBもGDBもmacOS上での圧縮済みデバッグ情報には対応していません。LLDBやGDBを使っている場合、次の2つの回避策があります。 -ldflags=-compressdwarf=false を付けてバイナリをビルドするか、あるいはsplitdwarfgo get golang.org/x/tools/cmd/splitdwarf )を使って既存のバイナリ内のデバッグ情報を展開するかです。

By David Chase