[インデックス 180] ファイルの概要
このコミットは、Goランタイムにおいて、パニックや境界チェックエラーなどの「フォルト」(障害)発生時に、その発生箇所のプログラムカウンタ(PC)を出力するように変更を加えるものです。これにより、デバッグ時の情報が強化され、問題の特定が容易になります。
コミット
commit 88a3371a91ac01fb8bcc8083c0f32300514846c3
Author: Rob Pike <r@golang.org>
Date: Mon Jun 16 17:04:30 2008 -0700
print pc on faults
SVN=123030
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/88a3371a91ac01fb8bcc8083c0f32300514846c3
元コミット内容
このコミットの元の内容は、Goランタイムが障害(faults)発生時にプログラムカウンタ(PC)を出力するように変更することです。具体的には、sys_panicl
(パニック発生時)、sys_slicestring
(文字列スライス時の境界チェック)、sys_indexstring
(文字列インデックスアクセス時の境界チェック)の各関数において、エラーメッセージにPC情報を含めるように修正されています。これに伴い、呼び出し元のPCを取得するためのアセンブリ関数sys_getcallerpc
がamd64
アーキテクチャのDarwinとLinux向けに追加されています。
変更の背景
Go言語の初期段階において、ランタイムエラーやパニックが発生した際に、そのエラーがコードのどの部分で発生したかを特定する情報は限られていました。特に、アセンブリレベルでの低レイヤーなエラーハンドリングにおいては、より詳細なコンテキスト情報がデバッグに不可欠です。
このコミットの背景には、以下の目的があったと考えられます。
- デバッグの効率化: パニックや境界チェックエラーが発生した際に、単にエラーの種類と行番号だけでなく、具体的な命令ポインタ(プログラムカウンタ)の値が分かると、デバッガや逆アセンブラを用いて問題発生箇所の正確なコードを特定しやすくなります。これは、特にランタイム内部のバグや、ユーザーコードとランタイムの相互作用によって引き起こされる複雑な問題の解析において非常に重要です。
- エラーレポートの改善: ユーザーや開発者がエラー報告を行う際に、PC情報が含まれていることで、より再現性の高い、具体的な情報を提供できるようになります。これにより、Go開発チームがバグを修正する際の負担が軽減されます。
- ランタイムの堅牢性向上: エラー発生時の情報が豊富になることで、ランタイム自体の安定性や堅牢性を向上させるためのフィードバックループが強化されます。
Go言語は、その設計思想として「シンプルさ」と「効率性」を重視していますが、同時に「デバッグのしやすさ」も重要な要素です。このコミットは、デバッグ情報の充実という観点から、Goランタイムの品質向上に貢献するものです。
前提知識の解説
このコミットを理解するためには、以下の概念について基本的な知識が必要です。
-
プログラムカウンタ (Program Counter, PC):
- CPUのレジスタの一つで、次に実行される命令のアドレス(メモリ上の位置)を指し示します。
- PCの値は、プログラムの実行フローを追跡する上で最も基本的な情報であり、デバッグ時には特定のコード行や命令に直接対応付けられます。
- 「フォルト」発生時にPCを出力することは、エラーがどの命令の実行中に発生したかを正確に知ることを意味します。
-
Goランタイム (Go Runtime):
- Goプログラムの実行を管理する低レベルなシステムです。
- ガベージコレクション、ゴルーチンのスケジューリング、チャネル通信、メモリ管理、パニック処理など、Go言語の主要な機能の多くはランタイムによって提供されます。
- ランタイムは通常、C言語(またはGo言語自体)とアセンブリ言語で記述されており、オペレーティングシステム(OS)と直接対話します。
-
パニック (Panic):
- Go言語における回復不可能なエラーメカニズムです。
- プログラムが予期せぬ状態に陥った際に発生し、通常はプログラムの実行を停止させます。
panic
関数が明示的に呼び出される場合と、ランタイムが検出するエラー(例: nilポインタ参照、配列の範囲外アクセス)によって暗黙的に発生する場合があります。- パニック発生時には、通常、スタックトレースが出力され、問題の診断に役立ちます。このコミットは、その情報にPCを追加するものです。
-
境界チェック (Bounds Checking):
- Go言語では、配列やスライスへのアクセス時に、インデックスが有効な範囲内にあるかを自動的にチェックします。
- インデックスが範囲外の場合、ランタイムパニック(
panic: runtime error: index out of range
など)が発生します。 - このコミットでは、
sys_slicestring
やsys_indexstring
といった文字列操作の内部関数で発生する境界チェックエラーに対してもPC情報を出力するように変更されています。
-
アセンブリ言語 (Assembly Language):
- CPUが直接理解できる機械語に非常に近い低レベルなプログラミング言語です。
- 特定のCPUアーキテクチャ(例:
amd64
)に特化しており、レジスタ操作やメモリへの直接アクセスなど、ハードウェアに近い制御が可能です。 - Goランタイムでは、OSとのインターフェースや、パフォーマンスが重要な低レベル処理(例: コンテキストスイッチ、システムコール、PCの取得)にアセンブリ言語が使用されます。
MOVQ
はx86-64アセンブリにおけるデータ転送命令で、64ビットの値を移動させます。FP
はフレームポインタ、SP
はスタックポインタに関連するレジスタです。
-
フレームポインタ (Frame Pointer, FP):
- 関数呼び出しの際に、現在のスタックフレームの基点を示すレジスタです。
- スタックフレームには、関数のローカル変数、引数、呼び出し元のリターンアドレスなどが格納されます。
sys_getcallerpc
関数では、FP
レジスタを基準にスタックを遡り、呼び出し元のリターンアドレス(PC)を取得しています。
技術的詳細
このコミットの技術的な核心は、Goランタイムがエラー発生時に呼び出し元のプログラムカウンタ(PC)を正確に取得し、出力するメカニズムを導入した点にあります。
-
sys_getcallerpc
アセンブリ関数の追加:src/runtime/rt0_amd64_darwin.s
とsrc/runtime/rt0_amd64_linux.s
に、TEXT sys_getcallerpc+0(SB),0,$0
という新しいアセンブリ関数が追加されました。- この関数は、
amd64
アーキテクチャ(64ビットIntel/AMDプロセッサ)に特化しており、Darwin(macOS)とLinuxの両OSで動作します。 - アセンブリコードは以下のようになっています。
MOVQ x+0(FP),AX MOVQ -8(AX),AX RET
MOVQ x+0(FP),AX
: これは、sys_getcallerpc
関数に渡された引数p
(void*
型)の値をAX
レジスタにロードします。x+0(FP)
は、フレームポインタFP
からのオフセットで引数x
(この場合はp
)にアクセスしていることを示唆しています。MOVQ -8(AX),AX
: ここが重要な部分です。AX
レジスタには現在、p
のアドレスが格納されています。p
はsys_panicl
などの関数内でローカル変数として宣言された引数(例:&lno
や&si
)のアドレスです。Goの関数呼び出し規約(当時のもの)では、呼び出し元のリターンアドレス(PC)が、呼び出された関数のスタックフレームの特定のオフセット(通常はフレームポインタのすぐ上、または引数の直後)に格納されます。-8(AX)
は、AX
が指すアドレスから8バイト手前(通常はリターンアドレスが格納されている場所)の値をAX
レジスタにロードし直しています。これにより、AX
には呼び出し元のPCが格納されます。RET
:AX
レジスタに格納されたPCの値を関数の戻り値として返します。
- このアセンブリ関数は、C言語の関数
sys_getcallerpc(void* p)
としてGoランタイムのCコードから呼び出せるようにエクスポートされています。
-
sys_printpc
C関数の追加と利用:src/runtime/runtime.c
にsys_printpc
という新しいC関数が追加されました。- この関数は、
sys_getcallerpc
を呼び出してPCを取得し、その値を16進数形式で標準出力にPC=0x...
という形式で出力します。 sys_panicl
(パニック処理)、sys_slicestring
(文字列スライス)、sys_indexstring
(文字列インデックスアクセス)の各関数内で、エラーが発生する条件分岐の直前にsys_printpc
が呼び出されるようになりました。- これにより、パニックや境界チェックエラーが発生した際に、エラーメッセージの一部としてPC情報が自動的に含まれるようになります。
-
sys_panicl
の変更:- 以前は行番号のみを出力していましたが、
sys_printpc
の呼び出しが追加され、PC情報も出力されるようになりました。 sys_panicl
は、Goプログラム内でpanic
が起こった際にランタイムが最終的に呼び出す関数の一つです。
- 以前は行番号のみを出力していましたが、
-
境界チェック関数の変更:
sys_slicestring
とsys_indexstring
は、それぞれ文字列のスライス操作とインデックスアクセスにおける境界チェックを行うランタイム内部関数です。- これらの関数内で境界外アクセスが検出された場合、以前は
prbounds
関数を呼び出してエラーメッセージを出力していましたが、このコミットにより、prbounds
の呼び出しの前にsys_printpc
が呼び出されるようになりました。これにより、文字列操作における境界エラーの発生箇所もPCで特定できるようになります。
この変更は、Goランタイムのデバッグ能力を大幅に向上させるものであり、特に低レベルなランタイムの挙動を解析する際に非常に有用です。アセンブリレベルでのPC取得は、OSやアーキテクチャに依存する部分であり、Goランタイムが移植性を保ちつつ、このような詳細なデバッグ情報を提供するための基盤を築いています。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は以下のファイルに集中しています。
-
src/runtime/rt0_amd64_darwin.s
およびsrc/runtime/rt0_amd64_linux.s
:sys_getcallerpc
アセンブリ関数の追加。この関数は、呼び出し元のプログラムカウンタ(PC)をスタックフレームから取得します。
// src/runtime/rt0_amd64_darwin.s (同様の変更が linux.s にも適用) TEXT sys_getcallerpc+0(SB),0,$0 MOVQ x+0(FP),AX MOVQ -8(AX),AX RET
-
src/runtime/runtime.h
:sys_getcallerpc
関数のプロトタイプ宣言の追加。これにより、Cコードからこのアセンブリ関数を呼び出せるようになります。
// src/runtime/runtime.h void* sys_getcallerpc(void*);
-
src/runtime/runtime.c
:sys_printpc
C関数の追加。この関数はsys_getcallerpc
を呼び出し、取得したPCを整形して出力します。sys_panicl
関数の変更。パニック発生時に行番号に加えてPCを出力するように修正。sys_slicestring
関数の変更。文字列スライス時の境界チェックエラーでPCを出力するように修正。sys_indexstring
関数の変更。文字列インデックスアクセス時の境界チェックエラーでPCを出力するように修正。
// src/runtime/runtime.c // 新規追加 void sys_printpc(void *p) { prints("PC=0x"); sys_printpointer(sys_getcallerpc(p)); } // sys_panicl の変更 void sys_panicl(int32 lno) { prints("\npanic on line "); sys_printint(lno); prints(" "); // スペースを追加 sys_printpc(&lno); // PCを出力 prints("\n"); *(int32*)0 = 0; } // sys_slicestring の変更 if(lindex < 0 || lindex > si->len || hindex < lindex || hindex > si->len) { sys_printpc(&si); // PCを出力 prints(" "); prbounds("slice", lindex, si->len, hindex); } // sys_indexstring の変更 if(i < 0 || i >= s->len) { sys_printpc(&s); // PCを出力 prints(" "); prbounds("index", 0, i, s->len); }
コアとなるコードの解説
このコミットの核となるのは、アセンブリ言語で実装されたsys_getcallerpc
関数と、それを利用してPCを出力するC言語のsys_printpc
関数、そしてこれらを既存のランタイムエラー処理に組み込む部分です。
-
sys_getcallerpc
(アセンブリ):- この関数は、GoランタイムがC言語で書かれた部分から呼び出し元のPCを取得するために特別に設計されています。
MOVQ x+0(FP),AX
: ここでx
はsys_getcallerpc
に渡される引数p
を指します。FP
はフレームポインタで、現在のスタックフレームの基点を示します。x+0(FP)
は、FP
を基準としたx
のアドレスをAX
レジスタにロードします。MOVQ -8(AX),AX
: この命令がPC取得の鍵です。Goの関数呼び出し規約(当時のもの)では、関数が呼び出される際に、呼び出し元のリターンアドレス(つまり、呼び出し元のPC)がスタックにプッシュされます。このリターンアドレスは、呼び出された関数のスタックフレームの特定のオフセットに位置します。-8(AX)
は、AX
が現在指しているアドレス(引数p
のアドレス)から8バイト手前(amd64
では通常64ビット=8バイトのアドレス)に、呼び出し元のリターンアドレスが格納されていることを利用して、その値をAX
レジスタにロードし直しています。RET
:AX
レジスタに格納された値(呼び出し元のPC)を関数の戻り値として返します。- このアセンブリコードは、スタックフレームの構造と関数呼び出し規約に深く依存しており、アーキテクチャ(
amd64
)とOS(Darwin/Linux)に特化した実装となっています。
-
sys_printpc
(C言語):- この関数は、
sys_getcallerpc
アセンブリ関数をC言語から呼び出すためのラッパーです。 sys_getcallerpc(p)
を呼び出すことで、呼び出し元のPCを取得します。ここでp
は、sys_panicl
などの関数内でPCを取得したい箇所のローカル変数のアドレス(例:&lno
や&si
)を渡しています。これは、sys_getcallerpc
がスタックフレームを遡るための基準点として利用されます。- 取得したPCの値は、
sys_printpointer
(ポインタ値を16進数で出力するランタイム関数)を使ってPC=0x...
という形式で標準エラー出力(または標準出力)に書き出されます。
- この関数は、
-
エラー処理への組み込み (
sys_panicl
,sys_slicestring
,sys_indexstring
):sys_panicl
: Goプログラムでパニックが発生した際に最終的に呼び出されるランタイム関数です。以前は行番号のみを出力していましたが、sys_printpc(&lno);
が追加されたことで、パニック発生時のPCも出力されるようになりました。&lno
は、sys_panicl
の引数である行番号のローカル変数のアドレスをsys_getcallerpc
に渡すことで、sys_panicl
を呼び出した関数のPCを取得するための基準点としています。sys_slicestring
とsys_indexstring
: これらはGoの文字列操作における境界チェックを行うランタイム内部関数です。文字列のスライスやインデックスアクセスが範囲外であった場合、これらの関数内でエラーが検出されます。このコミットにより、エラー検出時にsys_printpc(&si);
やsys_printpc(&s);
が呼び出され、それぞれスライス対象の文字列やインデックス対象の文字列のローカル変数のアドレスを基準に、エラーを発生させた呼び出し元のPCが出力されるようになりました。
これらの変更により、Goプログラムがランタイムエラーやパニックで異常終了した際に、より詳細なデバッグ情報(特にPC)が提供されるようになり、開発者が問題の原因を特定する作業が大幅に効率化されました。これは、Go言語のデバッグ体験を向上させる上で重要な一歩と言えます。
関連リンク
- Go言語の公式ドキュメント: https://go.dev/doc/
- Goランタイムのソースコード (現在のバージョン): https://github.com/golang/go/tree/master/src/runtime
- x86-64 アセンブリ言語の基本 (例): https://en.wikipedia.org/wiki/X86_assembly_language
参考にした情報源リンク
- Go言語のコミット履歴 (GitHub): https://github.com/golang/go/commits/master
- Go言語の初期の設計に関する議論 (Go Mailing Listなど、当時の情報源を特定するのは困難な場合がありますが、一般的なGoの歴史的背景として): https://groups.google.com/g/golang-nuts
- プログラムカウンタに関する一般的な情報: https://ja.wikipedia.org/wiki/%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%A0%E3%82%AB%E3%82%A6%E3%83%B3%E3%82%BF
- スタックフレームと関数呼び出し規約に関する情報 (CPUアーキテクチャ依存): https://en.wikipedia.org/wiki/Call_stack
- Go言語のパニックと回復: https://go.dev/blog/defer-panic-and-recover
- Go言語の境界チェック: https://go.dev/ref/spec#Index_expressions (Go言語仕様)
- Go言語のランタイムに関する書籍や記事 (例: "Go言語による並行処理" by Alan A. A. Donovan and Brian W. Kernighan など)
- Go言語のソースコード内のコメントや関連するコミットメッセージ。
- Go言語のIssue Tracker (当時の関連Issueがあれば): https://github.com/golang/go/issues