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

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

このコミットは、Goランタイムの初期化プロセスにおける重要な変更を導入しています。具体的には、シンボルテーブルの初期化関数である runtime·symtabinit の呼び出しタイミングを早めることで、ランタイムの初期段階で発生するパニック(クラッシュ)において、より有用なスタックトレースが得られるように改善しています。

コミット

runtime: call symtabinit earlier

Otherwise, we won't get a stack trace in some of the early init.

Here's one example:

    http://build.golang.org/log/a96d10f6aee1fa3e3ae51f41da46d414a7ab02de

After walking the stack by hand in acid, I was able to determine that the stackalloc inside mpreinit was causing the throw.

LGTM=rsc R=rsc, dvyukov CC=golang-codereviews https://golang.org/cl/72450044

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

https://github.com/golang/go/commit/64e041652aa2c431ff2aec6745dfecf32abac66a

元コミット内容

commit 64e041652aa2c431ff2aec6745dfecf32abac66a
Author: Anthony Martin <ality@pbrane.org>
Date:   Wed Mar 12 19:42:58 2014 -0700

    runtime: call symtabinit earlier
    
    Otherwise, we won't get a stack trace in some of the early init.
    
    Here's one example:
    
            http://build.golang.org/log/a96d10f6aee1fa3e3ae51f41da46d414a7ab02de
    
    After walking the stack by hand in acid, I was able to determine
    that the stackalloc inside mpreinit was causing the throw.
    
    LGTM=rsc
    R=rsc, dvyukov
    CC=golang-codereviews
    https://golang.org/cl/72450044
---
 src/pkg/runtime/proc.c | 4 +---\n 1 file changed, 1 insertion(+), 3 deletions(-)\n

変更の背景

このコミットの背景には、Goランタイムの非常に初期の段階で発生するクラッシュ(パニック)において、デバッグ情報が不足しているという問題がありました。コミットメッセージに記載されているビルドログの例(http://build.golang.org/log/a96d10f6aee1fa3e3ae51f41da46d414a7ab02de)では、runtime パッケージ内で「fatal error: stack size not a power of 2」という致命的なエラーが発生し、ビルドが失敗しています。

この種のエラーは、通常、スタックオーバーフローやスタックアロケーションの問題を示唆しています。しかし、問題が発生した時点ではシンボルテーブルがまだ初期化されていなかったため、スタックトレースが適切に生成されず、デバッグが困難でした。コミットの作者は、手動でスタックを解析(acid というツールを使用)することで、mpreinit 内の stackalloc が原因でパニックが発生していることを特定しました。

この問題に対処するため、シンボルテーブルの初期化を早めることで、ランタイムの初期段階で発生するパニックでも、意味のあるスタックトレースが出力されるようにし、デバッグの効率を向上させることを目的としています。

前提知識の解説

Goランタイムの初期化プロセス

Goプログラムが実行される際、ユーザーが記述した main() 関数が呼び出される前に、Goランタイムは複雑な初期化プロセスを実行します。このプロセスは、Goプログラムが動作するために必要な基盤(スケジューラ、メモリ管理、ガベージコレクタなど)をセットアップします。

  1. 低レベルのランタイムブートストラップ: Go実行ファイルのエントリポイントは、ユーザーの main() 関数ではなく、アーキテクチャとOSに特化した低レベルのアセンブリコードです。このコードがGoランタイム内の main 関数に制御を渡します。
  2. runtime.schedinit(): この関数は、スケジューラ、メモリ管理、ガベージコレクタなど、Goランタイムの主要なコンポーネントの初期化を開始します。
  3. runtime.mallocinit(): ヒープメモリのアロケータを初期化します。
  4. mcommoninit(): 新しいOSスレッド(Goランタイムでは m (machine) と呼ばれる)の共通初期化を行います。これには mpreinit の呼び出しも含まれます。
  5. パッケージ初期化: すべてのパッケージレベル変数が初期化され、各パッケージの init() 関数が実行されます。
  6. main() Goroutineの起動: ランタイムとすべてのインポートされたパッケージが初期化された後、ユーザーの main() 関数を実行するためのゴルーチンが作成されます。

シンボルテーブル (symtabinit)

Goランタイムにおけるシンボルテーブルは、実行中のプログラムに関するメタデータを提供する重要なデータ構造です。これには、関数名、ファイル名、行番号、変数情報などが含まれます。

  • pclntab (Program Counter Line Table): これはシンボルテーブルの中核をなす部分で、プログラムカウンタ(PC、つまりメモリアドレス)を関数名、ファイル名、行番号にマッピングするために使用されます。このマッピングは、スタックトレースを生成したり、デバッガがコードの実行位置を特定したりするために不可欠です。
  • runtime.symtabinit(): この関数は、これらのシンボルテーブルを初期化し、プログラムの実行に必要なデバッグ情報やリフレクション関連のデータ構造をセットアップします。シンボルテーブルが適切に初期化されていないと、パニック発生時に意味のあるスタックトレースが得られず、デバッグが極めて困難になります。

スタックアロケーション (stackalloc)

Goのゴルーチンは、OSスレッドとは異なり、動的にサイズが変更されるスタックを持っています。

  • 動的なスタック成長: ゴルーチンのスタックは通常、小さな初期サイズ(Go 1.20では2KB)で開始されます。関数呼び出しのたびに、Goランタイムはスタックに十分なスペースがあるかを確認します。スペースが不足している場合、ランタイムは新しい、より大きなスタックを割り当て、古いスタックの内容を新しいスタックにコピーし、ポインタを調整します。
  • stackalloc: これは、スタックメモリを割り当てるための内部的なランタイム関数です。スタックの成長が必要な場合や、特定のデータ構造をスタック上に配置する必要がある場合に呼び出されます。
  • スタックオーバーフロー: スタックが利用可能なメモリを超えて成長しようとすると、スタックオーバーフローが発生し、プログラムがクラッシュする可能性があります。コミットメッセージの「stackalloc inside mpreinit was causing the throw」という記述は、mpreinit の実行中にスタックアロケーションが原因でパニックが発生したことを示しています。

mpreinit

mpreinit は、Goランタイムの内部関数で、新しいOSスレッド(m)の事前初期化に関与します。これは、mcommoninit 関数の一部として呼び出され、Goスケジューラがゴルーチンを実行するために使用できる新しいOSスレッドをセットアップする広範なプロセスの一部です。mpreinit の目的は、スレッドのライフサイクルの非常に早い段階で、メモリ管理やシグナルハンドリングなど、スレッド固有の必要なセットアップ手順を実行することです。

技術的詳細

このコミットが解決しようとしている技術的な問題は、Goランタイムの初期化シーケンスにおける runtime·symtabinit の呼び出しタイミングです。以前のバージョンでは、runtime·symtabinitruntime·mallocinit()mcommoninit() の後に呼び出されていました。

コミットメッセージの例で示されているように、mpreinit 内の stackalloc が原因でパニックが発生した場合、これは runtime·symtabinit が実行される前に発生します。その結果、パニックが発生した時点ではシンボルテーブルがまだ構築されていないため、Goランタイムは関数名、ファイル名、行番号などのデバッグ情報をスタックトレースに含めることができませんでした。これにより、開発者は「どこで何が起こったのか」を特定するのが非常に困難になり、デバッグ作業が著しく妨げられていました。

このコミットは、runtime·symtabinit の呼び出しを runtime·schedinit の中で、runtime·mallocinit() よりも前に移動させることで、この問題を解決します。これにより、メモリ割り当てやOSスレッドの初期化など、ランタイムの他の重要な部分が開始される前にシンボルテーブルが利用可能になります。その結果、ランタイムの初期段階で発生するパニックであっても、シンボルテーブルが既に初期化されているため、完全で有用なスタックトレースが生成されるようになり、デバッグが大幅に容易になります。

この変更は、Goランタイムの堅牢性とデバッグ可能性を向上させる上で重要です。特に、低レベルのランタイムコードで発生する稀な、しかし深刻な問題の診断に役立ちます。

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

--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -146,6 +146,7 @@ runtime·schedinit(void)
 	runtime·sched.maxmcount = 10000;
 	runtime·precisestack = true; // haveexperiment("precisestack");
 
+	runtime·symtabinit();
 	runtime·mallocinit();
 	mcommoninit(m);
 	
@@ -159,9 +160,6 @@ runtime·schedinit(void)
 	runtime·goenvs();
 	runtime·parsedebugvars();
 
-	// Allocate internal symbol table representation now, we need it for GC anyway.
-	runtime·symtabinit();
-
 	runtime·sched.lastpoll = runtime·nanotime();
 	procs = 1;
 	p = runtime·getenv("GOMAXPROCS");

コアとなるコードの解説

このコミットのコアとなる変更は、src/pkg/runtime/proc.c ファイル内の runtime·schedinit 関数における runtime·symtabinit() の呼び出し位置の移動です。

元のコードでは、runtime·symtabinit()runtime·mallocinit()mcommoninit(m) の後に呼び出されていました。これは、コメント「// Allocate internal symbol table representation now, we need it for GC anyway.」からもわかるように、ガベージコレクションのためにシンボルテーブルが必要になるため、そのタイミングで初期化するという意図があったと考えられます。

変更後のコードでは、runtime·symtabinit() の呼び出しが runtime·schedinit 関数の冒頭近く、具体的には runtime·mallocinit() の直前に移動されています。

この変更の意義は以下の通りです。

  • 早期のデバッグ情報利用可能化: runtime·symtabinit()runtime·mallocinit()mcommoninit() よりも前に呼び出すことで、メモリ割り当てやOSスレッドの初期化といった、ランタイムの非常に初期段階で発生しうる問題(例: スタックアロケーションの失敗)によってパニックが発生した場合でも、シンボルテーブルが既に初期化されている状態になります。
  • 有用なスタックトレースの生成: シンボルテーブルが利用可能であれば、パニック発生時に出力されるスタックトレースには、関数名、ファイル名、行番号といった詳細なデバッグ情報が含まれるようになります。これにより、開発者は問題の根本原因を特定しやすくなります。
  • デバッグ効率の向上: ランタイムの初期段階でのクラッシュは、再現が難しく、デバッグ情報が少ないと解決に時間がかかります。この変更は、そのような困難なケースのデバッグ効率を大幅に向上させます。

要するに、この変更は「シンボルテーブルはガベージコレクションのためだけでなく、ランタイムの初期段階でのデバッグのためにも必要である」という認識に基づいています。

関連リンク

参考にした情報源リンク