Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 19271] ファイルの概要

このコミットは、Goランタイムの src/pkg/runtime/runtime.c ファイルに対する変更です。runtime.c はGoプログラムの実行時環境、特にメモリ管理、ゴルーチン管理、スケジューリング、シグナルハンドリング、そしてクラッシュ時のトレースバック生成など、低レベルな機能の中核を担うC言語で書かれた部分です。このファイルは、GoプログラムがどのようにOSと対話し、どのように内部的に動作するかを定義する上で非常に重要です。

コミット

このコミットは、GOTRACEBACK=crash 設定がコアダンプの生成を抑制してしまうバグを修正することを目的としています。具体的には、gotraceback 関数が環境変数 GOTRACEBACK の値をキャッシュする際に、getenv 関数がまだ完全に準備できていない段階でキャッシュが行われるため、誤った値が設定され、結果としてパニック発生時にコアダンプが生成されない問題に対処しています。修正は、getenv が準備できた後にキャッシュをリセットすることで、この問題を解決しています。

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/28c515f40faba1cd1589dcc3f6f0fe3e2f5f6325

元コミット内容

runtime: fix bug in GOTRACEBACK=crash causing suppression of core dumps.

Because gotraceback is called early and often, its cache commits to the value of getenv("GOTRACEBACK") before getenv is even ready. So now we reset its cache once getenv becomes ready. Panicking programs now dump core again.

LGTM=rsc
R=rsc
CC=golang-codereviews
https://golang.org/cl/97800045

変更の背景

Goプログラムがクラッシュした際、デバッグのためにコアダンプを生成することは非常に重要です。コアダンプには、プログラムがクラッシュした時点のメモリ状態やレジスタ情報などが含まれており、これらを解析することで問題の原因を特定できます。

Goランタイムには、GOTRACEBACK という環境変数があり、これはプログラムがパニックを起こした際のトレースバックの挙動を制御します。この環境変数にはいくつかの設定値がありますが、特に GOTRACEBACK=crash は、パニック発生時にコアダンプを生成し、トレースバックを出力するよう指示するものです。

しかし、このコミットが修正しようとしているバグは、GOTRACEBACK=crash が設定されているにもかかわらず、コアダンプが生成されないというものでした。この問題の根本原因は、gotraceback 関数が getenv("GOTRACEBACK") の値をキャッシュするタイミングにありました。gotraceback はランタイムの非常に早い段階で頻繁に呼び出されるため、環境変数を読み取るための getenv 関数がまだ完全に初期化されていない状態で、GOTRACEBACK の値がキャッシュされてしまうことがありました。この「早すぎるキャッシュ」によって、GOTRACEBACK の値が正しく取得されず、結果としてコアダンプの生成が抑制されてしまっていたのです。

このバグは、Goプログラムのデバッグプロセスを著しく妨げるものであり、特に本番環境でのクラッシュ解析において深刻な問題を引き起こす可能性がありました。そのため、この問題の修正は、Goランタイムの安定性とデバッグ能力を向上させる上で不可欠でした。

前提知識の解説

Goランタイム (Go Runtime)

Goランタイムは、Go言語で書かれたプログラムを実行するために必要な低レベルな機能を提供するシステムです。これには、ガベージコレクション、ゴルーチン(軽量スレッド)のスケジューリング、メモリ割り当て、チャネル通信、システムコールインターフェースなどが含まれます。Goプログラムは、コンパイル時にこのランタイムとリンクされ、独立したバイナリとして実行されます。src/pkg/runtime/ ディレクトリには、このランタイムのC言語およびGo言語で書かれたソースコードが含まれています。

GOTRACEBACK 環境変数

GOTRACEBACK は、Goプログラムがパニック(実行時エラー)を起こした際のトレースバック(スタックトレース)の出力挙動を制御する環境変数です。主な設定値は以下の通りです。

  • GOTRACEBACK=0: トレースバックを完全に抑制します。
  • GOTRACEBACK=1 (デフォルト): 簡潔なトレースバックを出力します。現在のゴルーチンのスタックトレースのみが表示されます。
  • GOTRACEBACK=2: すべてのゴルーチンのスタックトレースを出力します。デバッグ時に非常に有用です。
  • GOTRACEBACK=crash: トレースバックを出力し、さらにコアダンプを生成します。コアダンプは、プログラムがクラッシュした時点のメモリイメージであり、デバッガで詳細な解析を行うために使用されます。

コアダンプ (Core Dump)

コアダンプは、プログラムが異常終了(クラッシュ)した際に、その時点のプロセスのメモリ内容、レジスタの状態、スタック情報などをファイルに書き出したものです。これは、クラッシュの原因を特定するための重要なデバッグ情報となります。LinuxなどのUnix系OSでは、通常 core という名前のファイルとして生成されます。デバッガ(例: GDB)を使用してこのコアダンプファイルを読み込むことで、クラッシュ時のプログラムの状態を再現し、詳細な解析を行うことができます。

getenv 関数

getenv は、C標準ライブラリ(stdlib.h)に含まれる関数で、指定された環境変数の値を取得するために使用されます。Goランタイムの文脈では、OSから環境変数の値を取得するために内部的に利用されます。

キャッシュ (Caching)

キャッシュとは、頻繁にアクセスされるデータを一時的に高速な記憶領域に保存しておくことで、その後のアクセスを高速化する技術です。gotraceback 関数が GOTRACEBACK 環境変数の値をキャッシュしていたのは、この関数が頻繁に呼び出されるため、毎回 getenv を呼び出して環境変数を読み取るオーバーヘッドを避けるためです。しかし、キャッシュは、元のデータが変更されたり、キャッシュが生成された時点のデータが不完全であったりする場合に、古いまたは誤った情報を保持してしまうという問題(キャッシュの不整合)を引き起こす可能性があります。

アトミック操作 (runtime·atomicload, runtime·atomicstore)

アトミック操作とは、複数のスレッドやゴルーチンから同時にアクセスされた場合でも、その操作全体が不可分(atomic)に行われることを保証する操作です。つまり、操作の途中で他のスレッドからの割り込みや競合が発生せず、常に一貫性のある結果が得られます。runtime·atomicload はアトミックに値を読み込み、runtime·atomicstore はアトミックに値を書き込みます。これらは、並行処理環境でのデータ競合を防ぐために使用されます。

技術的詳細

このバグの核心は、Goランタイムの初期化プロセスと gotraceback 関数のキャッシュ戦略の間のタイミングの問題にありました。

  1. gotraceback の早期呼び出しとキャッシュ: gotraceback 関数は、Goプログラムの実行開始直後から、パニック発生の可能性のある様々な状況で頻繁に呼び出されます。パフォーマンスを最適化するため、この関数は GOTRACEBACK 環境変数の値を一度読み込んで内部のキャッシュ(以前は cache という名前の静的変数)に保存し、その後の呼び出しではキャッシュされた値を使用するようになっていました。

  2. getenv の初期化遅延: しかし、getenv 関数(環境変数を読み取るためのOSレベルの関数)自体が、Goランタイムの初期化プロセスの比較的遅い段階で完全に準備されることがあります。これは、OSの環境変数をGoランタイムが内部的に処理し、利用可能にするための時間が必要なためです。

  3. キャッシュの不整合: 問題は、gotracebackgetenv がまだ完全に準備できていない段階で呼び出され、その時点で getenv("GOTRACEBACK") が誤った(例えば nil)値を返してしまう場合に発生しました。この誤った値がキャッシュに保存されてしまうと、その後 getenv が完全に準備されたとしても、gotraceback は常にキャッシュされた誤った値を使用し続けてしまいます。

  4. コアダンプの抑制: GOTRACEBACK=crash が設定されている場合でも、キャッシュされた値が正しくないために、ランタイムはコアダンプを生成すべきではないと判断してしまい、結果としてコアダンプが抑制されていました。これは、デバッグ時に非常に困る状況です。

このコミットの修正は、このキャッシュの不整合問題を解決するために、getenv が完全に準備された後に gotraceback のキャッシュを明示的にリセットするというアプローチを取っています。これにより、gotracebackgetenv が利用可能になった後、正しい GOTRACEBACK の値を再読み込みし、キャッシュに保存するようになります。

コアとなるコードの変更箇所

変更は src/pkg/runtime/runtime.c ファイルで行われています。

--- a/src/pkg/runtime/runtime.c
+++ b/src/pkg/runtime/runtime.c
@@ -11,6 +11,13 @@ enum {
  	maxround = sizeof(uintptr),
 };
 
+// Keep a cached value to make gotraceback fast,
+// since we call it on every call to gentraceback.
+// The cached value is a uint32 in which the low bit
+// is the "crash" setting and the top 31 bits are the
+// gotraceback value.
+static uint32 traceback_cache = ~(uint32)0;
+
 // The GOTRACEBACK environment variable controls the
 // behavior of a Go program that is crashing and exiting.
 //	GOTRACEBACK=0   suppress all tracebacks
@@ -20,12 +27,6 @@ enum {
 int32
 runtime·gotraceback(bool *crash)
 {
--	// Keep a cached value to make gotraceback fast,
--	// since we call it on every call to gentraceback.
--	// The cached value is a uint32 in which the low bit
--	// is the "crash" setting and the top 31 bits are the
--	// gotraceback value.
--	static uint32 cache = ~(uint32)0;
  	byte *p;\n \tuint32 x;\n \n@@ -33,7 +34,7 @@ runtime·gotraceback(bool *crash)\n  \t\t*crash = false;\n  \tif(m->traceback != 0)\n  \t\treturn m->traceback;\n--\tx = runtime·atomicload(&cache);\n++\tx = runtime·atomicload(&traceback_cache);\n  \tif(x == ~(uint32)0) {\n  \t\tp = runtime·getenv("GOTRACEBACK");\n  \t\tif(p == nil)\n@@ -44,7 +45,7 @@ runtime·gotraceback(bool *crash)\n  \t\tx = (2<<1) | 1;\n  \t\telse\n  \t\t\tx = runtime·atoi(p)<<1;\t\n--\truntime·atomicstore(&cache, x);\n++\truntime·atomicstore(&traceback_cache, x);\n  \t}\n  \tif(crash != nil)\n  \t\t*crash = x&1;\n@@ -137,6 +138,8 @@ runtime·goenvs_unix(void)\n  \tsyscall·envs.array = (byte*)s;\n  \tsyscall·envs.len = n;\n  \tsyscall·envs.cap = n;\n+\n+\ttraceback_cache = ~(uint32)0;\n }\n \n int32

具体的な変更点は以下の通りです。

  1. キャッシュ変数の移動とリネーム:

    • runtime·gotraceback 関数内にローカル静的変数として定義されていた static uint32 cache = ~(uint32)0; が削除されました。
    • 代わりに、ファイルのグローバルスコープに static uint32 traceback_cache = ~(uint32)0; が追加されました。これにより、キャッシュ変数がグローバルになり、他の関数からもアクセスできるようになります。
  2. キャッシュ変数の参照変更:

    • runtime·gotraceback 関数内で cache を参照していた箇所 (runtime·atomicload(&cache);runtime·atomicstore(&cache, x);) が、新しく定義されたグローバル変数 traceback_cache を参照するように変更されました (runtime·atomicload(&traceback_cache);runtime·atomicstore(&traceback_cache, x);)。
  3. キャッシュのリセット処理の追加:

    • runtime·goenvs_unix 関数(Unix系システムで環境変数を初期化する役割を持つ関数)の最後に、traceback_cache = ~(uint32)0; という行が追加されました。

コアとなるコードの解説

このコミットの核となる修正は、runtime·goenvs_unix 関数に traceback_cache = ~(uint32)0; という行を追加した点です。

  • traceback_cache の初期値 ~(uint32)0: この値は、すべてのビットが1である uint32 の最大値です。gotraceback 関数内では、x == ~(uint32)0 という条件でキャッシュが初期化されていないかどうかをチェックしています。この値が設定されている場合、キャッシュは無効であり、getenv("GOTRACEBACK") を呼び出して環境変数の値を読み込む必要があることを示します。

  • runtime·goenvs_unix の役割: runtime·goenvs_unix 関数は、GoランタイムがOSの環境変数を読み込み、内部的に利用可能な状態にするための重要な初期化ステップを実行します。この関数が完了した時点では、getenv 関数は確実に正しく動作する状態になっています。

  • キャッシュのリセットの重要性: runtime·goenvs_unix の最後に traceback_cache = ~(uint32)0; を追加することで、getenv が完全に準備された後に、gotraceback のキャッシュが強制的に無効化されます。これにより、gotraceback が次に呼び出された際には、キャッシュされた古い(そして誤っている可能性のある)値を使用するのではなく、getenv を通じて GOTRACEBACK 環境変数の正しい値を再読み込みするようになります。

この修正により、GOTRACEBACK=crash が設定されている場合でも、gotraceback 関数が正しい環境変数の値を認識し、パニック発生時に期待通りにコアダンプが生成されるようになります。これは、Goプログラムのデバッグ可能性と信頼性を大幅に向上させる重要なバグ修正です。

関連リンク

参考にした情報源リンク