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

[インデックス 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_getcallerpcamd64アーキテクチャのDarwinとLinux向けに追加されています。

変更の背景

Go言語の初期段階において、ランタイムエラーやパニックが発生した際に、そのエラーがコードのどの部分で発生したかを特定する情報は限られていました。特に、アセンブリレベルでの低レイヤーなエラーハンドリングにおいては、より詳細なコンテキスト情報がデバッグに不可欠です。

このコミットの背景には、以下の目的があったと考えられます。

  1. デバッグの効率化: パニックや境界チェックエラーが発生した際に、単にエラーの種類と行番号だけでなく、具体的な命令ポインタ(プログラムカウンタ)の値が分かると、デバッガや逆アセンブラを用いて問題発生箇所の正確なコードを特定しやすくなります。これは、特にランタイム内部のバグや、ユーザーコードとランタイムの相互作用によって引き起こされる複雑な問題の解析において非常に重要です。
  2. エラーレポートの改善: ユーザーや開発者がエラー報告を行う際に、PC情報が含まれていることで、より再現性の高い、具体的な情報を提供できるようになります。これにより、Go開発チームがバグを修正する際の負担が軽減されます。
  3. ランタイムの堅牢性向上: エラー発生時の情報が豊富になることで、ランタイム自体の安定性や堅牢性を向上させるためのフィードバックループが強化されます。

Go言語は、その設計思想として「シンプルさ」と「効率性」を重視していますが、同時に「デバッグのしやすさ」も重要な要素です。このコミットは、デバッグ情報の充実という観点から、Goランタイムの品質向上に貢献するものです。

前提知識の解説

このコミットを理解するためには、以下の概念について基本的な知識が必要です。

  1. プログラムカウンタ (Program Counter, PC):

    • CPUのレジスタの一つで、次に実行される命令のアドレス(メモリ上の位置)を指し示します。
    • PCの値は、プログラムの実行フローを追跡する上で最も基本的な情報であり、デバッグ時には特定のコード行や命令に直接対応付けられます。
    • 「フォルト」発生時にPCを出力することは、エラーがどの命令の実行中に発生したかを正確に知ることを意味します。
  2. Goランタイム (Go Runtime):

    • Goプログラムの実行を管理する低レベルなシステムです。
    • ガベージコレクション、ゴルーチンのスケジューリング、チャネル通信、メモリ管理、パニック処理など、Go言語の主要な機能の多くはランタイムによって提供されます。
    • ランタイムは通常、C言語(またはGo言語自体)とアセンブリ言語で記述されており、オペレーティングシステム(OS)と直接対話します。
  3. パニック (Panic):

    • Go言語における回復不可能なエラーメカニズムです。
    • プログラムが予期せぬ状態に陥った際に発生し、通常はプログラムの実行を停止させます。
    • panic関数が明示的に呼び出される場合と、ランタイムが検出するエラー(例: nilポインタ参照、配列の範囲外アクセス)によって暗黙的に発生する場合があります。
    • パニック発生時には、通常、スタックトレースが出力され、問題の診断に役立ちます。このコミットは、その情報にPCを追加するものです。
  4. 境界チェック (Bounds Checking):

    • Go言語では、配列やスライスへのアクセス時に、インデックスが有効な範囲内にあるかを自動的にチェックします。
    • インデックスが範囲外の場合、ランタイムパニック(panic: runtime error: index out of rangeなど)が発生します。
    • このコミットでは、sys_slicestringsys_indexstringといった文字列操作の内部関数で発生する境界チェックエラーに対してもPC情報を出力するように変更されています。
  5. アセンブリ言語 (Assembly Language):

    • CPUが直接理解できる機械語に非常に近い低レベルなプログラミング言語です。
    • 特定のCPUアーキテクチャ(例: amd64)に特化しており、レジスタ操作やメモリへの直接アクセスなど、ハードウェアに近い制御が可能です。
    • Goランタイムでは、OSとのインターフェースや、パフォーマンスが重要な低レベル処理(例: コンテキストスイッチ、システムコール、PCの取得)にアセンブリ言語が使用されます。
    • MOVQはx86-64アセンブリにおけるデータ転送命令で、64ビットの値を移動させます。FPはフレームポインタ、SPはスタックポインタに関連するレジスタです。
  6. フレームポインタ (Frame Pointer, FP):

    • 関数呼び出しの際に、現在のスタックフレームの基点を示すレジスタです。
    • スタックフレームには、関数のローカル変数、引数、呼び出し元のリターンアドレスなどが格納されます。
    • sys_getcallerpc関数では、FPレジスタを基準にスタックを遡り、呼び出し元のリターンアドレス(PC)を取得しています。

技術的詳細

このコミットの技術的な核心は、Goランタイムがエラー発生時に呼び出し元のプログラムカウンタ(PC)を正確に取得し、出力するメカニズムを導入した点にあります。

  1. sys_getcallerpc アセンブリ関数の追加:

    • src/runtime/rt0_amd64_darwin.ssrc/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関数に渡された引数pvoid*型)の値をAXレジスタにロードします。x+0(FP)は、フレームポインタFPからのオフセットで引数x(この場合はp)にアクセスしていることを示唆しています。
      • MOVQ -8(AX),AX: ここが重要な部分です。AXレジスタには現在、pのアドレスが格納されています。psys_paniclなどの関数内でローカル変数として宣言された引数(例: &lno&si)のアドレスです。Goの関数呼び出し規約(当時のもの)では、呼び出し元のリターンアドレス(PC)が、呼び出された関数のスタックフレームの特定のオフセット(通常はフレームポインタのすぐ上、または引数の直後)に格納されます。-8(AX)は、AXが指すアドレスから8バイト手前(通常はリターンアドレスが格納されている場所)の値をAXレジスタにロードし直しています。これにより、AXには呼び出し元のPCが格納されます。
      • RET: AXレジスタに格納されたPCの値を関数の戻り値として返します。
    • このアセンブリ関数は、C言語の関数sys_getcallerpc(void* p)としてGoランタイムのCコードから呼び出せるようにエクスポートされています。
  2. sys_printpc C関数の追加と利用:

    • src/runtime/runtime.csys_printpc という新しいC関数が追加されました。
    • この関数は、sys_getcallerpcを呼び出してPCを取得し、その値を16進数形式で標準出力にPC=0x...という形式で出力します。
    • sys_panicl(パニック処理)、sys_slicestring(文字列スライス)、sys_indexstring(文字列インデックスアクセス)の各関数内で、エラーが発生する条件分岐の直前にsys_printpcが呼び出されるようになりました。
    • これにより、パニックや境界チェックエラーが発生した際に、エラーメッセージの一部としてPC情報が自動的に含まれるようになります。
  3. sys_panicl の変更:

    • 以前は行番号のみを出力していましたが、sys_printpcの呼び出しが追加され、PC情報も出力されるようになりました。
    • sys_paniclは、Goプログラム内でpanicが起こった際にランタイムが最終的に呼び出す関数の一つです。
  4. 境界チェック関数の変更:

    • sys_slicestringsys_indexstringは、それぞれ文字列のスライス操作とインデックスアクセスにおける境界チェックを行うランタイム内部関数です。
    • これらの関数内で境界外アクセスが検出された場合、以前はprbounds関数を呼び出してエラーメッセージを出力していましたが、このコミットにより、prboundsの呼び出しの前にsys_printpcが呼び出されるようになりました。これにより、文字列操作における境界エラーの発生箇所もPCで特定できるようになります。

この変更は、Goランタイムのデバッグ能力を大幅に向上させるものであり、特に低レベルなランタイムの挙動を解析する際に非常に有用です。アセンブリレベルでのPC取得は、OSやアーキテクチャに依存する部分であり、Goランタイムが移植性を保ちつつ、このような詳細なデバッグ情報を提供するための基盤を築いています。

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

このコミットにおけるコアとなるコードの変更箇所は以下のファイルに集中しています。

  1. 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
    
  2. src/runtime/runtime.h:

    • sys_getcallerpc 関数のプロトタイプ宣言の追加。これにより、Cコードからこのアセンブリ関数を呼び出せるようになります。
    // src/runtime/runtime.h
    void* sys_getcallerpc(void*);
    
  3. 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関数、そしてこれらを既存のランタイムエラー処理に組み込む部分です。

  1. sys_getcallerpc (アセンブリ):

    • この関数は、GoランタイムがC言語で書かれた部分から呼び出し元のPCを取得するために特別に設計されています。
    • MOVQ x+0(FP),AX: ここでxsys_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)に特化した実装となっています。
  2. sys_printpc (C言語):

    • この関数は、sys_getcallerpcアセンブリ関数をC言語から呼び出すためのラッパーです。
    • sys_getcallerpc(p)を呼び出すことで、呼び出し元のPCを取得します。ここでpは、sys_paniclなどの関数内でPCを取得したい箇所のローカル変数のアドレス(例: &lno&si)を渡しています。これは、sys_getcallerpcがスタックフレームを遡るための基準点として利用されます。
    • 取得したPCの値は、sys_printpointer(ポインタ値を16進数で出力するランタイム関数)を使ってPC=0x...という形式で標準エラー出力(または標準出力)に書き出されます。
  3. エラー処理への組み込み (sys_panicl, sys_slicestring, sys_indexstring):

    • sys_panicl: Goプログラムでパニックが発生した際に最終的に呼び出されるランタイム関数です。以前は行番号のみを出力していましたが、sys_printpc(&lno);が追加されたことで、パニック発生時のPCも出力されるようになりました。&lnoは、sys_paniclの引数である行番号のローカル変数のアドレスをsys_getcallerpcに渡すことで、sys_paniclを呼び出した関数のPCを取得するための基準点としています。
    • sys_slicestringsys_indexstring: これらはGoの文字列操作における境界チェックを行うランタイム内部関数です。文字列のスライスやインデックスアクセスが範囲外であった場合、これらの関数内でエラーが検出されます。このコミットにより、エラー検出時にsys_printpc(&si);sys_printpc(&s);が呼び出され、それぞれスライス対象の文字列やインデックス対象の文字列のローカル変数のアドレスを基準に、エラーを発生させた呼び出し元のPCが出力されるようになりました。

これらの変更により、Goプログラムがランタイムエラーやパニックで異常終了した際に、より詳細なデバッグ情報(特にPC)が提供されるようになり、開発者が問題の原因を特定する作業が大幅に効率化されました。これは、Go言語のデバッグ体験を向上させる上で重要な一歩と言えます。

関連リンク

参考にした情報源リンク