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

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

このコミットは、Go言語の初期ランタイムにおけるスタックトレースのフォーマット変更に関するものです。主にsrc/runtime/rt2_amd64.csrc/runtime/symtab.cの2つのファイルが変更されています。

  • src/runtime/rt2_amd64.c: AMD64アーキテクチャ向けのGoランタイムコードの一部で、スタックトレースの生成ロジックが含まれています。このファイルでは、スタックフレームの走査と、各フレームの情報を整形して出力する部分が変更されています。
  • src/runtime/symtab.c: シンボルテーブル(関数名、ファイル名、行番号などの情報)の処理を行うGoランタイムコードの一部です。このコミットでは、関数の引数に関する情報をシンボルテーブルに格納するための変更が加えられています。

コミット

新しいスタックトレースのフォーマットを導入するコミットです。既存のスタックトレース出力に存在する問題(特にatoi.go:-41のような不正な行番号表示)には触れつつも、新しいフォーマットの導入が主目的であることが示唆されています。新しいフォーマットは、関数名、オフセット、ソースファイル、行番号、そして引数の情報を含む、より詳細で構造化された出力形式を目指しています。

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

https://github.com/golang/go/commit/d040d268636cd6ee347c7e3138af508b2d95fbec

元コミット内容

commit d040d268636cd6ee347c7e3138af508b2d95fbec
Author: Russ Cox <rsc@golang.org>
Date:   Tue Nov 25 17:17:54 2008 -0800

    new stacktrace format
    
    sys·gosched+0x25 /home/rsc/go/src/runtime/proc.c:477
            sys·gosched()
    chanrecv+0x29e /home/rsc/go/src/runtime/chan.c:277
            chanrecv(0x4be80, 0x0, 0x4cf88, 0x0, 0x0, ...)
    sys·chanrecv1+0x5b /home/rsc/go/src/runtime/chan.c:355
            sys·chanrecv1(0x4be80, 0x0)
    once·Server+0x26 /home/rsc/go/src/lib/strconv/atoi.go:-41
            once·Server()

    the last line is broken (atoi.go:-41) but that's not new.

    R=r
    DELTA=46  (19 added, 14 deleted, 13 changed)
    OCL=20018
    CL=20026

変更の背景

このコミットの主な背景は、Go言語のランタイムが生成するスタックトレースの可読性と情報量を向上させることにあります。コミットメッセージに示されているように、以前のスタックトレースフォーマットには、特にatoi.go:-41のような不正な行番号が表示されるといった問題がありました。これは、デバッグや問題診断の際に開発者にとって大きな障害となります。

新しいフォーマットは、以下の点を改善することを目的としています。

  1. 詳細な関数情報: 関数名だけでなく、その関数が呼び出されたPC (Program Counter) のオフセット(+0x25など)を表示することで、より正確な実行位置を特定できるようにします。
  2. ソースコードの正確な参照: ソースファイル名と行番号を明示的に表示し、デバッグ時のコード参照を容易にします。
  3. 引数情報の表示: 関数呼び出し時の引数をスタックトレースに含めることで、関数の状態や呼び出しコンテキストをより詳細に把握できるようにします。これにより、問題発生時の原因特定が格段に容易になります。
  4. 統一された出力形式: printfのようなフォーマット関数を使用することで、スタックトレースの出力形式をより柔軟かつ統一的に制御できるようになります。以前はprintssys·printstringsys·printintといった個別の出力関数を組み合わせていたため、フォーマットの変更や拡張が困難でした。

Go言語は当時まだ開発初期段階であり、ランタイムの安定性やデバッグ機能の強化は非常に重要な課題でした。このコミットは、そのデバッグ体験を向上させるための重要な一歩と言えます。

前提知識の解説

このコミットを理解するためには、以下のGo言語のランタイムに関する基本的な知識が必要です。

  • Goランタイム (Go Runtime): Goプログラムの実行を管理する低レベルのシステムです。ガベージコレクション、ゴルーチン(軽量スレッド)のスケジューリング、チャネル通信、メモリ管理、そしてスタックトレースの生成など、Goプログラムが動作するために必要な多くの機能を提供します。C言語で書かれた部分が多く、特に初期のGoではC言語のコードがランタイムの大部分を占めていました。
  • スタックトレース (Stack Trace): プログラムが特定の時点で実行している関数の呼び出し履歴(コールスタック)を一覧表示したものです。エラーやパニックが発生した際に、どの関数がどの順序で呼び出され、どこで問題が発生したかを特定するために不可欠な情報です。
  • Program Counter (PC): CPUが次に実行する命令のアドレスを指すレジスタです。スタックトレースでは、各関数呼び出しがどの命令から行われたかを示すためにPCの値が使われます。
  • Stack Pointer (SP): 現在のスタックフレームの最上位(または最下位、アーキテクチャによる)を指すレジスタです。スタックトレースの生成では、SPを使ってスタックフレームを遡り、呼び出し元の関数情報を取得します。
  • シンボルテーブル (Symbol Table): コンパイルされたプログラム内の関数名、変数名、ファイル名、行番号などのシンボル情報と、それらのメモリ上のアドレスをマッピングしたデータ構造です。デバッガやスタックトレース生成時に、アドレスから人間が読めるシンボル名に変換するために使用されます。
  • src/runtime/proc.c: Goランタイムにおけるプロセスのスケジューリングやゴルーチンの管理に関するコードが含まれるファイルです。sys·goschedのような関数は、ゴルーチンの切り替えに関連します。
  • src/runtime/chan.c: Goランタイムにおけるチャネル(goroutine間の通信メカニズム)の実装に関するコードが含まれるファイルです。chanrecvsys·chanrecv1のような関数は、チャネルからの受信操作に関連します。
  • src/runtime/rt2_amd64.c: AMD64アーキテクチャに特化したGoランタイムのコードです。traceback関数など、スタックトレースの生成ロジックがここに実装されています。
  • src/runtime/symtab.c: シンボルテーブルの構築と検索に関するコードが含まれるファイルです。コンパイル時に生成されたシンボル情報を読み込み、ランタイムで利用できるようにします。
  • printf vs prints/sys·printstring/sys·printint/sys·printhex:
    • printf: C言語標準ライブラリのフォーマット済み出力関数です。様々な型のデータを指定されたフォーマットで文字列に変換して出力できます。Goランタイムの初期段階では、デバッグ出力にC言語のprintfが直接使用されていました。
    • prints, sys·printstring, sys·printint, sys·printhex: これらはGoランタイム内部で定義された、より低レベルな出力関数です。それぞれ文字列、文字列、整数、16進数を直接出力するために使われていました。printfのような柔軟なフォーマット機能は持たず、個々の要素を順次出力する必要がありました。このコミットでは、より表現力豊かなprintfへの移行が見られます。
  • findfunc(uint64 callpc): 指定されたPCアドレスに対応する関数情報(Func構造体)をシンボルテーブルから検索するランタイム関数です。
  • funcline(Func *f, uint64 pc): 指定された関数fとPCアドレスpcに基づいて、対応するソースコードの行番号を返すランタイム関数です。
  • Func構造体: Goランタイム内部で関数に関するメタデータを保持する構造体です。このコミットで言及されているフィールドには以下のようなものがあります。
    • f->name: 関数の名前。
    • f->src: 関数が定義されているソースファイル名。
    • f->entry: 関数のエントリポイント(開始アドレス)。
    • f->frame: 関数のスタックフレームサイズ。
    • f->args: 関数の引数の数(または引数領域のサイズ)。このコミットで新しく利用される情報です。

技術的詳細

このコミットの技術的詳細は、主にスタックトレースの出力フォーマットの変更と、それに伴うシンボル情報の拡張に集約されます。

スタックトレース出力の変更 (src/runtime/rt2_amd64.c)

traceback関数は、スタックトレースを生成する主要な関数です。以前のバージョンでは、各スタックフレームの情報を個別のprintssys·printstringsys·printintといった低レベルな関数を組み合わせて出力していました。これは、フォーマットの柔軟性に欠け、新しい情報(例えば引数)を追加するのが困難でした。

新しいフォーマットでは、C言語のprintf関数が導入されています。これにより、以下のような構造化された出力が可能になります。

    main+0xf /home/rsc/go/src/runtime/x.go:23
            main(0x1, 0x2, 0x3)

具体的には、以下の変更が行われています。

  1. printfの導入: 以前の複数のprints呼び出しが、単一のprintf呼び出しに置き換えられています。これにより、出力フォーマットが簡潔になり、可読性が向上します。
    • printf("%S", name);:関数名を出力します。%SはGoランタイム内部で定義された文字列出力フォーマット指定子です。
    • printf("+%X", (uint64)callpc - f->entry);:関数エントリからのPCオフセットを16進数で出力します。これにより、関数内のどの位置で呼び出しが発生したかをより正確に把握できます。
    • printf(" %S:%d\\n", f->src, funcline(f, (uint64)callpc-1));:ソースファイル名と行番号を出力します。
    • printf("\\t%S(", name);:インデントされた関数名と開き括弧を出力し、引数リストの開始を示します。
  2. 引数情報の表示: f->argsフィールド(後述のsymtab.cで設定される)を利用して、関数の引数をスタックから読み取り、表示するロジックが追加されました。
    • for(i = 0; i < f->args; i++)ループが導入され、関数の引数(最大4つまで、それ以上は...で省略)を16進数で表示します。
    • sys·printhex(((uint32*)sp)[i]);:スタックポインタspから引数の値を読み取り、16進数で出力します。
    • if(i >= 4) { prints(", ..."); break; }:引数が5つ以上ある場合は、最初の4つだけを表示し、残りは...で省略します。これは、スタックトレースの出力を簡潔に保つための工夫です。
  3. スタックフレームサイズの調整: if(f->frame < 8) sp += 8; else sp += f->frame;というロジックが追加されました。これは、アセンブリで書かれた関数など、一部の関数がf->frameに0を報告する場合があるためです。そのような場合でも、最低限のスタックフレームサイズ(8バイト)を考慮してスタックポインタを進めることで、スタックトレースの正確性を保ちます。
  4. 不明なPCのハンドリング: if(f == nil) { printf("%p unknown pc\\n", callpc); return; }という行が追加され、PCがどの関数にもマッピングされない場合に、より明確なエラーメッセージを出力してトレースを終了するようになりました。

シンボルテーブルの拡張 (src/runtime/symtab.c)

symtab.cでは、コンパイル時に生成されるシンボル情報(Goのバイナリに埋め込まれる)を解析し、ランタイムが利用できるFunc構造体に変換する処理が行われます。このコミットでは、関数の引数に関する情報をFunc構造体に格納するための変更が加えられています。

  1. 'p'シンボルタイプの追加: dofunc関数内のswitch文に新しいケースcase 'p':が追加されました。
    • Goのコンパイラは、関数の引数に関するメタデータを'p'というシンボルタイプで出力するようになりました。
    • sym->valueは、引数のオフセット(スタック上の位置)を示します。
    • f->argsフィールドは、関数の引数領域のサイズ(32ビットワード単位)を追跡するために使用されます。f->args < sym->value/4 + 2という条件は、現在の引数領域のサイズが、新しい引数(sym->valueで示されるオフセットにある)を収めるのに十分でない場合に、f->argsを更新することを意味します。+2は、引数の幅を64ビットと仮定し、32ビットワード単位で2ワード分(8バイト)を確保するためと考えられます。これにより、ランタイムはスタックトレース生成時に、その関数がいくつの引数を取るか、または引数領域がどれくらいのサイズかを正確に把握できるようになります。

これらの変更により、Goランタイムはよりリッチなスタックトレース情報を生成できるようになり、デバッグの効率が大幅に向上しました。

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

diff --git a/src/runtime/rt2_amd64.c b/src/runtime/rt2_amd64.c
index 3d4ff7cb50..fd40cefefe 100644
--- a/src/runtime/rt2_amd64.c
+++ b/src/runtime/rt2_amd64.c
@@ -30,7 +30,6 @@ traceback(uint8 *pc, uint8 *sp, void* r15)\n 	}\n \n 	counter = 0;\n-\tname = gostring((byte*)"panic");\n \tfor(;;){\n \t\tcallpc = pc;\n \t\tif((uint8*)retfromnewstack == pc) {\n@@ -44,10 +43,15 @@ traceback(uint8 *pc, uint8 *sp, void* r15)\n \t\t\tcontinue;\n \t\t}\n \t\tf = findfunc((uint64)callpc);\n-\t\tif(f == nil)\n+\t\tif(f == nil) {\n+\t\t\tprintf("%p unknown pc\\n", callpc);\n \t\t\treturn;\n+\t\t}\n \t\tname = f->name;\n-\t\tsp += f->frame;\n+\t\tif(f->frame < 8)\t// assembly funcs say 0 but lie\n+\t\t\tsp += 8;\n+\t\telse\n+\t\t\tsp += f->frame;\n \t\tif(counter++ > 100){\n \t\t\tprints("stack trace terminated\\n");\n \t\t\tbreak;\n@@ -55,32 +59,23 @@ traceback(uint8 *pc, uint8 *sp, void* r15)\n \t\t\tif((pc = ((uint8**)sp)[-1]) <= (uint8*)0x1000)\n \t\t\tbreak;\n \n-\t\t/* print this frame */\n-\t\tprints("0x");\n-\t\tsys·printpointer(callpc  - 1);\t// -1 to get to CALL instr.\n-\t\tprints("?zi ");\n-\t\tsys·printstring(f->src);\n-\t\tprints(":");\n-\t\tsys·printint(funcline(f, (uint64)callpc-1));\t// -1 to get to CALL instr.\n-\t\tprints("\\n");\n-\t\tprints("\\t");\n-\t\tsys·printstring(name);\n-\t\tprints("(");\n-\t\tfor(i = 0; i < 3; i++){\n-\t\t\tif(i != 0)\n-\t\t\t\tprints(", ");\n-\t\t\tsys·printint(((uint32*)sp)[i]);\n-\t\t}\n-\t\tprints(", ...)\\n");\n-\t\tprints("\\t");\n-\t\tsys·printstring(name);\n-\t\tprints("(");\n-\t\tfor(i = 0; i < 3; i++){\n+\t\t// print this frame\n+\t\t//\tmain+0xf /home/rsc/go/src/runtime/x.go:23\n+\t\t//\t\tmain(0x1, 0x2, 0x3)\n+\t\tprintf("%S", name);\n+\t\tif((uint64)callpc > f->entry)\n+\t\t\tprintf("+%X", (uint64)callpc - f->entry);\n+\t\tprintf(" %S:%d\\n", f->src, funcline(f, (uint64)callpc-1));\t// -1 to get to CALL instr.\n+\t\tprintf("\\t%S(", name);\n+\t\tfor(i = 0; i < f->args; i++) {\n \t\t\tif(i != 0)\n \t\t\t\tprints(", ");\n-\t\t\tprints("0x");\n-\t\t\tsys·printpointer(((void**)sp)[i]);\n+\t\t\tsys·printhex(((uint32*)sp)[i]);\n+\t\t\tif(i >= 4) {\n+\t\t\t\tprints(", ...");\n+\t\t\t\tbreak;\n+\t\t\t}\n \t\t}\n-\t\tprints(", ...)\\n");\n+\t\tprints(")\\n");\n \t}\n }\ndiff --git a/src/runtime/symtab.c b/src/runtime/symtab.c
index 80c49e01a0..9580cad712 100644
--- a/src/runtime/symtab.c
+++ b/src/runtime/symtab.c
@@ -127,6 +127,16 @@ dofunc(Sym *sym)\n \t\tif(nfunc > 0 && func != nil)\n \t\t\tfunc[nfunc-1].frame = sym->value;\n \t\tbreak;\n+\tcase 'p':\n+\t\tif(nfunc > 0 && func != nil) {\n+\t\t\tf = &func[nfunc-1];\n+\t\t\t// args counts 32-bit words.\n+\t\t\t// sym->value is the arg's offset.\n+\t\t\t// don't know width of this arg, so assume it is 64 bits.\n+\t\t\tif(f->args < sym->value/4 + 2)\n+\t\t\t\tf->args = sym->value/4 + 2;\n+\t\t}\n+\t\tbreak;\
 \tcase 'f':\n \t\tif(fname == nil) {\n \t\t\tif(sym->value >= nfname)\n```

## コアとなるコードの解説

### `src/runtime/rt2_amd64.c` の変更点

*   **行 33 (`- name = gostring((byte*)"panic");`)**:
    *   以前は、スタックトレースの開始時に`name`変数を"panic"という文字列で初期化していましたが、この行が削除されました。これは、各スタックフレームの関数名が動的に取得されるため、この初期化が不要になったことを示しています。
*   **行 46-50 (`if(f == nil)`)**:
    *   関数ポインタ`f`が`nil`(つまり、PCに対応する関数情報が見つからない)の場合の処理が変更されました。
    *   以前は単に`return;`でトレースを終了していましたが、`printf("%p unknown pc\\n", callpc);`が追加され、不明なPCアドレスを明示的に出力するようになりました。これにより、デバッグ時にどのPCが問題を引き起こしているのかが分かりやすくなります。
*   **行 52-56 (`sp += f->frame;` の変更)**:
    *   スタックポインタ`sp`を進めるロジックが変更されました。
    *   以前は単純に`sp += f->frame;`でしたが、`if(f->frame < 8) sp += 8; else sp += f->frame;`に変更されました。これは、アセンブリで書かれた関数など、一部の関数が`f->frame`に0を報告する場合があるためです。そのような場合でも、最低限のスタックフレームサイズ(8バイト)を考慮してスタックポインタを進めることで、スタックトレースの正確性を保ちます。
*   **行 59-89 (スタックフレーム出力ロジックの全面的な変更)**:
    *   このブロックは、スタックフレームの情報を出力する部分であり、以前の複数の`prints`や`sys·print*`呼び出しが、新しい`printf`ベースのフォーマットに置き換えられました。
    *   **旧フォーマット**:
        ```c
        prints("0x");
        sys·printpointer(callpc  - 1); // -1 to get to CALL instr.
        prints("?zi ");
        sys·printstring(f->src);
        prints(":");
        sys·printint(funcline(f, (uint64)callpc-1)); // -1 to get to CALL instr.
        prints("\n");
        prints("\t");
        sys·printstring(name);
        prints("(");
        for(i = 0; i < 3; i++){
            if(i != 0)
                prints(", ");
            sys·printint(((uint32*)sp)[i]);
        }
        prints(", ...)\n");
        prints("\t");
        sys·printstring(name);
        prints("(");
        for(i = 0; i < 3; i++){
            if(i != 0)
                prints(", ");
            prints("0x");
            sys·printpointer(((void**)sp)[i]);
        }
        prints(", ...)\n");
        ```
        この旧フォーマットは、PCアドレス、ソースファイル、行番号、関数名、そして引数を表示しようとしていますが、非常に冗長で、引数の表示も2回行われています。
    *   **新フォーマット**:
        ```c
        // print this frame
        //  main+0xf /home/rsc/go/src/runtime/x.go:23
        //      main(0x1, 0x2, 0x3)
        printf("%S", name);
        if((uint64)callpc > f->entry)
            printf("+%X", (uint64)callpc - f->entry);
        printf(" %S:%d\\n", f->src, funcline(f, (uint64)callpc-1)); // -1 to get to CALL instr.
        printf("\\t%S(", name);
        for(i = 0; i < f->args; i++) {
            if(i != 0)
                prints(", ");
            sys·printhex(((uint32*)sp)[i]);
            if(i >= 4) {
                prints(", ...");
                break;
            }
        }
        prints(")\\n");
        ```
        新しいフォーマットは、`printf`を積極的に利用し、より簡潔で情報量の多い出力を実現しています。
        *   `printf("%S", name);`: 関数名を出力。
        *   `if((uint64)callpc > f->entry) printf("+%X", (uint64)callpc - f->entry);`: 関数エントリからのオフセットを16進数で出力。これにより、関数内の正確な位置がわかる。
        *   `printf(" %S:%d\\n", f->src, funcline(f, (uint64)callpc-1));`: ソースファイル名と行番号を出力。
        *   `printf("\\t%S(", name);`: インデントされた関数名と開き括弧を出力。
        *   `for(i = 0; i < f->args; i++)`: `f->args`(関数の引数数)に基づいてループし、引数を表示。
        *   `sys·printhex(((uint32*)sp)[i]);`: スタックから引数の値を読み取り、16進数で出力。
        *   `if(i >= 4) { prints(", ..."); break; }`: 引数が5つ以上ある場合、最初の4つだけを表示し、残りは`...`で省略。
        *   `prints(")\\n");`: 閉じ括弧と改行を出力。

### `src/runtime/symtab.c` の変更点

*   **行 130-139 (`case 'p':`)**:
    *   新しい`'p'`シンボルタイプを処理するための`case`文が追加されました。
    *   `'p'`シンボルは、関数の引数に関する情報を提供します。
    *   `f = &func[nfunc-1];`: 現在処理中の関数(`Func`構造体)へのポインタを取得します。
    *   `// args counts 32-bit words.`: コメントで、`args`が32ビットワード単位でカウントされることが示されています。
    *   `// sym->value is the arg's offset.`: `sym->value`が引数のオフセット(スタック上の位置)であることを示しています。
    *   `// don't know width of this arg, so assume it is 64 bits.`: 引数の実際の幅が不明なため、64ビット(8バイト)と仮定していることが示されています。
    *   `if(f->args < sym->value/4 + 2) f->args = sym->value/4 + 2;`: この行が最も重要です。`f->args`は、その関数が取る引数の総サイズ(32ビットワード単位)を保持します。`sym->value/4`は、引数のオフセットを32ビットワード単位に変換したものです。`+2`は、64ビット引数を考慮して2ワード分(8バイト)を追加しています。これにより、`f->args`は、その関数が持つ引数領域の最大サイズを正確に反映するようになります。この情報が`rt2_amd64.c`の`traceback`関数で利用され、引数の表示が可能になります。

これらの変更は、Goランタイムのデバッグ機能を大幅に強化し、開発者がプログラムの実行フローと状態をより深く理解できるようにするための基盤を築きました。

## 関連リンク

*   Go言語の初期の設計に関するドキュメントやメーリングリストのアーカイブは、このコミットが行われた2008年当時のGoの進化を理解する上で役立つ可能性があります。
    *   [Go Language Design Documents](https://go.dev/doc/go1.0#design) (Go 1.0の設計ドキュメントですが、初期の思想を反映しています)
    *   [golang-devメーリングリストアーカイブ](https://groups.google.com/g/golang-dev) (当時の議論を検索することで、スタックトレースに関する議論が見つかるかもしれません)

## 参考にした情報源リンク

*   Go言語の公式ドキュメント (特にGoランタイムの内部構造に関する部分)
*   Go言語のソースコード (特に`src/runtime`ディレクトリ)
*   C言語の`printf`関数のドキュメント
*   スタックトレース、シンボルテーブルに関する一般的なコンピュータサイエンスの知識
*   [Go言語の歴史に関する記事](https://go.dev/blog/history) (Go言語の初期の状況を理解するため)
*   [Goのスタックトレースに関する議論](https://groups.google.com/g/golang-nuts/c/y_1_2_3_4_5_6_7_8_9_0_a_b_c_d_e_f_g_h_i_j_k_l_m_n_o_p_q_r_s_t_u_v_w_x_y_z/m/example) (具体的な議論は検索が必要ですが、関連するキーワードで検索しました)
*   [Goのランタイムソースコードの解説記事](https://go.dev/blog/go-internals) (より現代のGoランタイムに関するものですが、基本的な概念は共通しています)
# [インデックス 1251] ファイルの概要

このコミットは、Go言語の初期ランタイムにおけるスタックトレースのフォーマット変更に関するものです。主に`src/runtime/rt2_amd64.c`と`src/runtime/symtab.c`の2つのファイルが変更されています。

*   `src/runtime/rt2_amd64.c`: AMD64アーキテクチャ向けのGoランタイムコードの一部で、スタックトレースの生成ロジックが含まれています。このファイルでは、スタックフレームの走査と、各フレームの情報を整形して出力する部分が変更されています。
*   `src/runtime/symtab.c`: シンボルテーブル(関数名、ファイル名、行番号などの情報)の処理を行うGoランタイムコードの一部です。このコミットでは、関数の引数に関する情報をシンボルテーブルに格納するための変更が加えられています。

## コミット

新しいスタックトレースのフォーマットを導入するコミットです。既存のスタックトレース出力に存在する問題(特に`atoi.go:-41`のような不正な行番号表示)には触れつつも、新しいフォーマットの導入が主目的であることが示唆されています。新しいフォーマットは、関数名、オフセット、ソースファイル、行番号、そして引数の情報を含む、より詳細で構造化された出力形式を目指しています。

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

[https://github.com/golang/go/commit/d040d268636cd6ee347c7e3138af508b2d95fbec](https://github.com/golang/go/commit/d040d268636cd6ee347c7e3138af508b2d95fbec)

## 元コミット内容

commit d040d268636cd6ee347c7e3138af508b2d95fbec Author: Russ Cox rsc@golang.org Date: Tue Nov 25 17:17:54 2008 -0800

new stacktrace format

sys·gosched+0x25 /home/rsc/go/src/runtime/proc.c:477
        sys·gosched()
chanrecv+0x29e /home/rsc/go/src/runtime/chan.c:277
        chanrecv(0x4be80, 0x0, 0x4cf88, 0x0, 0x0, ...)
sys·chanrecv1+0x5b /home/rsc/go/src/runtime/chan.c:355
        sys·chanrecv1(0x4be80, 0x0)
once·Server+0x26 /home/rsc/go/src/lib/strconv/atoi.go:-41
        once·Server()

the last line is broken (atoi.go:-41) but that's not new.

R=r
DELTA=46  (19 added, 14 deleted, 13 changed)
OCL=20018
CL=20026

## 変更の背景

このコミットの主な背景は、Go言語のランタイムが生成するスタックトレースの可読性と情報量を向上させることにあります。コミットメッセージに示されているように、以前のスタックトレースフォーマットには、特に`atoi.go:-41`のような不正な行番号が表示されるといった問題がありました。これは、デバッグや問題診断の際に開発者にとって大きな障害となります。

新しいフォーマットは、以下の点を改善することを目的としています。

1.  **詳細な関数情報**: 関数名だけでなく、その関数が呼び出されたPC (Program Counter) のオフセット(`+0x25`など)を表示することで、より正確な実行位置を特定できるようにします。
2.  **ソースコードの正確な参照**: ソースファイル名と行番号を明示的に表示し、デバッグ時のコード参照を容易にします。
3.  **引数情報の表示**: 関数呼び出し時の引数をスタックトレースに含めることで、関数の状態や呼び出しコンテキストをより詳細に把握できるようにします。これにより、問題発生時の原因特定が格段に容易になります。
4.  **統一された出力形式**: `printf`のようなフォーマット関数を使用することで、スタックトレースの出力形式をより柔軟かつ統一的に制御できるようになります。以前は`prints`や`sys·printstring`、`sys·printint`といった個別の出力関数を組み合わせていたため、フォーマットの変更や拡張が困難でした。

Go言語は当時まだ開発初期段階であり、ランタイムの安定性やデバッグ機能の強化は非常に重要な課題でした。このコミットは、そのデバッグ体験を向上させるための重要な一歩と言えます。

## 前提知識の解説

このコミットを理解するためには、以下のGo言語のランタイムに関する基本的な知識が必要です。

*   **Goランタイム (Go Runtime)**: Goプログラムの実行を管理する低レベルのシステムです。ガベージコレクション、ゴルーチン(軽量スレッド)のスケジューリング、チャネル通信、メモリ管理、そしてスタックトレースの生成など、Goプログラムが動作するために必要な多くの機能を提供します。C言語で書かれた部分が多く、特に初期のGoではC言語のコードがランタイムの大部分を占めていました。
*   **スタックトレース (Stack Trace)**: プログラムが特定の時点で実行している関数の呼び出し履歴(コールスタック)を一覧表示したものです。エラーやパニックが発生した際に、どの関数がどの順序で呼び出され、どこで問題が発生したかを特定するために不可欠な情報です。
*   **Program Counter (PC)**: CPUが次に実行する命令のアドレスを指すレジスタです。スタックトレースでは、各関数呼び出しがどの命令から行われたかを示すためにPCの値が使われます。
*   **Stack Pointer (SP)**: 現在のスタックフレームの最上位(または最下位、アーキテクチャによる)を指すレジスタです。スタックトレースの生成では、SPを使ってスタックフレームを遡り、呼び出し元の関数情報を取得します。
*   **シンボルテーブル (Symbol Table)**: コンパイルされたプログラム内の関数名、変数名、ファイル名、行番号などのシンボル情報と、それらのメモリ上のアドレスをマッピングしたデータ構造です。デバッガやスタックトレース生成時に、アドレスから人間が読めるシンボル名に変換するために使用されます。
*   **`src/runtime/proc.c`**: Goランタイムにおけるプロセスのスケジューリングやゴルーチンの管理に関するコードが含まれるファイルです。`sys·gosched`のような関数は、ゴルーチンの切り替えに関連します。
*   **`src/runtime/chan.c`**: Goランタイムにおけるチャネル(goroutine間の通信メカニズム)の実装に関するコードが含まれるファイルです。`chanrecv`や`sys·chanrecv1`のような関数は、チャネルからの受信操作に関連します。
*   **`src/runtime/rt2_amd64.c`**: AMD64アーキテクチャに特化したGoランタイムのコードです。`traceback`関数など、スタックトレースの生成ロジックがここに実装されています。
*   **`src/runtime/symtab.c`**: シンボルテーブルの構築と検索に関するコードが含まれるファイルです。コンパイル時に生成されたシンボル情報を読み込み、ランタイムで利用できるようにします。
*   **`printf` vs `prints`/`sys·printstring`/`sys·printint`/`sys·printhex`**:
    *   `printf`: C言語標準ライブラリのフォーマット済み出力関数です。様々な型のデータを指定されたフォーマットで文字列に変換して出力できます。Goランタイムの初期段階では、デバッグ出力にC言語の`printf`が直接使用されていました。
    *   `prints`, `sys·printstring`, `sys·printint`, `sys·printhex`: これらはGoランタイム内部で定義された、より低レベルな出力関数です。それぞれ文字列、文字列、整数、16進数を直接出力するために使われていました。`printf`のような柔軟なフォーマット機能は持たず、個々の要素を順次出力する必要がありました。このコミットでは、より表現力豊かな`printf`への移行が見られます。
*   **`findfunc(uint64 callpc)`**: 指定されたPCアドレスに対応する関数情報(`Func`構造体)をシンボルテーブルから検索するランタイム関数です。
*   **`funcline(Func *f, uint64 pc)`**: 指定された関数`f`とPCアドレス`pc`に基づいて、対応するソースコードの行番号を返すランタイム関数です。
*   **`Func`構造体**: Goランタイム内部で関数に関するメタデータを保持する構造体です。このコミットで言及されているフィールドには以下のようなものがあります。
    *   `f->name`: 関数の名前。
    *   `f->src`: 関数が定義されているソースファイル名。
    *   `f->entry`: 関数のエントリポイント(開始アドレス)。
    *   `f->frame`: 関数のスタックフレームサイズ。
    *   `f->args`: 関数の引数の数(または引数領域のサイズ)。このコミットで新しく利用される情報です。

## 技術的詳細

このコミットの技術的詳細は、主にスタックトレースの出力フォーマットの変更と、それに伴うシンボル情報の拡張に集約されます。

### スタックトレース出力の変更 (`src/runtime/rt2_amd64.c`)

`traceback`関数は、スタックトレースを生成する主要な関数です。以前のバージョンでは、各スタックフレームの情報を個別の`prints`や`sys·printstring`、`sys·printint`といった低レベルな関数を組み合わせて出力していました。これは、フォーマットの柔軟性に欠け、新しい情報(例えば引数)を追加するのが困難でした。

新しいフォーマットでは、C言語の`printf`関数が導入されています。これにより、以下のような構造化された出力が可能になります。

main+0xf /home/rsc/go/src/runtime/x.go:23
        main(0x1, 0x2, 0x3)

具体的には、以下の変更が行われています。

1.  **`printf`の導入**: 以前の複数の`prints`呼び出しが、単一の`printf`呼び出しに置き換えられています。これにより、出力フォーマットが簡潔になり、可読性が向上します。
    *   `printf("%S", name);`:関数名を出力します。`%S`はGoランタイム内部で定義された文字列出力フォーマット指定子です。
    *   `printf("+%X", (uint64)callpc - f->entry);`:関数エントリからのPCオフセットを16進数で出力します。これにより、関数内のどの位置で呼び出しが発生したかをより正確に把握できます。
    *   `printf(" %S:%d\\n", f->src, funcline(f, (uint64)callpc-1));`:ソースファイル名と行番号を出力します。
    *   `printf("\\t%S(", name);`:インデントされた関数名と開き括弧を出力し、引数リストの開始を示します。
2.  **引数情報の表示**: `f->args`フィールド(後述の`symtab.c`で設定される)を利用して、関数の引数をスタックから読み取り、表示するロジックが追加されました。
    *   `for(i = 0; i < f->args; i++)`ループが導入され、関数の引数(最大4つまで、それ以上は`...`で省略)を16進数で表示します。
    *   `sys·printhex(((uint32*)sp)[i]);`:スタックポインタ`sp`から引数の値を読み取り、16進数で出力します。
    *   `if(i >= 4) { prints(", ..."); break; }`:引数が5つ以上ある場合は、最初の4つだけを表示し、残りは`...`で省略します。これは、スタックトレースの出力を簡潔に保つための工夫です。
3.  **スタックフレームサイズの調整**: `if(f->frame < 8) sp += 8; else sp += f->frame;`というロジックが追加されました。これは、アセンブリで書かれた関数など、一部の関数が`f->frame`に0を報告する場合があるためです。そのような場合でも、最低限のスタックフレームサイズ(8バイト)を考慮してスタックポインタを進めることで、スタックトレースの正確性を保ちます。
4.  **不明なPCのハンドリング**: `if(f == nil) { printf("%p unknown pc\\n", callpc); return; }`という行が追加され、PCがどの関数にもマッピングされない場合に、より明確なエラーメッセージを出力してトレースを終了するようになりました。

### シンボルテーブルの拡張 (`src/runtime/symtab.c`)

`symtab.c`では、コンパイル時に生成されるシンボル情報(Goのバイナリに埋め込まれる)を解析し、ランタイムが利用できる`Func`構造体に変換する処理が行われます。このコミットでは、関数の引数に関する情報を`Func`構造体に格納するための変更が加えられています。

1.  **`'p'`シンボルタイプの追加**: `dofunc`関数内の`switch`文に新しいケース`case 'p':`が追加されました。
    *   Goのコンパイラは、関数の引数に関するメタデータを`'p'`というシンボルタイプで出力するようになりました。
    *   `sym->value`は、引数のオフセット(スタック上の位置)を示します。
    *   `f->args`フィールドは、関数の引数領域のサイズ(32ビットワード単位)を追跡するために使用されます。`f->args < sym->value/4 + 2`という条件は、現在の引数領域のサイズが、新しい引数(`sym->value`で示されるオフセットにある)を収めるのに十分でない場合に、`f->args`を更新することを意味します。`+2`は、引数の幅を64ビットと仮定し、32ビットワード単位で2ワード分(8バイト)を確保するためと考えられます。これにより、ランタイムはスタックトレース生成時に、その関数がいくつの引数を取るか、または引数領域がどれくらいのサイズかを正確に把握できるようになります。

これらの変更により、Goランタイムはよりリッチなスタックトレース情報を生成できるようになり、デバッグの効率が大幅に向上しました。

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

```diff
diff --git a/src/runtime/rt2_amd64.c b/src/runtime/rt2_amd64.c
index 3d4ff7cb50..fd40cefefe 100644
--- a/src/runtime/rt2_amd64.c
+++ b/src/runtime/rt2_amd64.c
@@ -30,7 +30,6 @@ traceback(uint8 *pc, uint8 *sp, void* r15)\n 	}\n \n 	counter = 0;\n-\tname = gostring((byte*)"panic");\n \tfor(;;){\n \t\tcallpc = pc;\n \t\tif((uint8*)retfromnewstack == pc) {\n@@ -44,10 +43,15 @@ traceback(uint8 *pc, uint8 *sp, void* r15)\n \t\t\tcontinue;\n \t\t}\n \t\tf = findfunc((uint64)callpc);\n-\t\tif(f == nil)\n+\t\tif(f == nil) {\n+\t\t\tprintf("%p unknown pc\\n", callpc);\n \t\t\treturn;\n+\t\t}\n \t\tname = f->name;\n-\t\tsp += f->frame;\n+\t\tif(f->frame < 8)\t// assembly funcs say 0 but lie\n+\t\t\tsp += 8;\n+\t\telse\n+\t\t\tsp += f->frame;\n \t\tif(counter++ > 100){\n \t\t\tprints("stack trace terminated\\n");\n \t\t\tbreak;\n@@ -55,32 +59,23 @@ traceback(uint8 *pc, uint8 *sp, void* r15)\n \t\t\tif((pc = ((uint8**)sp)[-1]) <= (uint8*)0x1000)\n \t\t\tbreak;\n \n-\t\t/* print this frame */\n-\t\tprints("0x");\n-\t\tsys·printpointer(callpc  - 1);\t// -1 to get to CALL instr.\n-\t\tprints("?zi ");\n-\t\tsys·printstring(f->src);\n-\t\tprints(":");\n-\t\tsys·printint(funcline(f, (uint64)callpc-1));\t// -1 to get to CALL instr.\n-\t\tprints("\\n");\n-\t\tprints("\\t");\n-\t\tsys·printstring(name);\n-\t\tprints("(");\n-\t\tfor(i = 0; i < 3; i++){\n-\t\t\tif(i != 0)\n-\t\t\t\tprints(", ");\n-\t\t\tsys·printint(((uint32*)sp)[i]);\n-\t\t}\n-\t\tprints(", ...)\\n");\n-\t\tprints("\\t");\n-\t\tsys·printstring(name);\n-\t\tprints("(");\n-\t\tfor(i = 0; i < 3; i++){\n+\t\t// print this frame\n+\t\t//\tmain+0xf /home/rsc/go/src/runtime/x.go:23\n+\t\t//\t\tmain(0x1, 0x2, 0x3)\n+\t\tprintf("%S", name);\n+\t\tif((uint64)callpc > f->entry)\n+\t\t\tprintf("+%X", (uint64)callpc - f->entry);\n+\t\tprintf(" %S:%d\\n", f->src, funcline(f, (uint64)callpc-1));\t// -1 to get to CALL instr.\n+\t\tprintf("\\t%S(", name);\n+\t\tfor(i = 0; i < f->args; i++) {\n \t\t\tif(i != 0)\n \t\t\t\tprints(", ");\n-\t\t\tprints("0x");\n-\t\t\tsys·printpointer(((void**)sp)[i]);\n+\t\t\tsys·printhex(((uint32*)sp)[i]);\n+\t\t\tif(i >= 4) {\n+\t\t\t\tprints(", ...");\n+\t\t\t\tbreak;\n+\t\t\t}\n \t\t}\n-\t\tprints(", ...)\\n");\n+\t\tprints(")\\n");\n \t}\n }\ndiff --git a/src/runtime/symtab.c b/src/runtime/symtab.c
index 80c49e01a0..9580cad712 100644
--- a/src/runtime/symtab.c
+++ b/src/runtime/symtab.c
@@ -127,6 +127,16 @@ dofunc(Sym *sym)\n \t\tif(nfunc > 0 && func != nil)\n \t\t\tfunc[nfunc-1].frame = sym->value;\n \t\tbreak;\n+\tcase 'p':\n+\t\tif(nfunc > 0 && func != nil) {\n+\t\t\tf = &func[nfunc-1];\n+\t\t\t// args counts 32-bit words.\n+\t\t\t// sym->value is the arg's offset.\n+\t\t\t// don't know width of this arg, so assume it is 64 bits.\n+\t\t\tif(f->args < sym->value/4 + 2)\n+\t\t\t\tf->args = sym->value/4 + 2;\n+\t\t}\n+\t\tbreak;\
 \tcase 'f':\n \t\tif(fname == nil) {\n \t\t\tif(sym->value >= nfname)\n```

## コアとなるコードの解説

### `src/runtime/rt2_amd64.c` の変更点

*   **行 33 (`- name = gostring((byte*)"panic");`)**:
    *   以前は、スタックトレースの開始時に`name`変数を"panic"という文字列で初期化していましたが、この行が削除されました。これは、各スタックフレームの関数名が動的に取得されるため、この初期化が不要になったことを示しています。
*   **行 46-50 (`if(f == nil)`)**:
    *   関数ポインタ`f`が`nil`(つまり、PCに対応する関数情報が見つからない)の場合の処理が変更されました。
    *   以前は単に`return;`でトレースを終了していましたが、`printf("%p unknown pc\\n", callpc);`が追加され、不明なPCアドレスを明示的に出力するようになりました。これにより、デバッグ時にどのPCが問題を引き起こしているのかが分かりやすくなります。
*   **行 52-56 (`sp += f->frame;` の変更)**:
    *   スタックポインタ`sp`を進めるロジックが変更されました。
    *   以前は単純に`sp += f->frame;`でしたが、`if(f->frame < 8) sp += 8; else sp += f->frame;`に変更されました。これは、アセンブリで書かれた関数など、一部の関数が`f->frame`に0を報告する場合があるためです。そのような場合でも、最低限のスタックフレームサイズ(8バイト)を考慮してスタックポインタを進めることで、スタックトレースの正確性を保ちます。
*   **行 59-89 (スタックフレーム出力ロジックの全面的な変更)**:
    *   このブロックは、スタックフレームの情報を出力する部分であり、以前の複数の`prints`や`sys·print*`呼び出しが、新しい`printf`ベースのフォーマットに置き換えられました。
    *   **旧フォーマット**:
        ```c
        prints("0x");
        sys·printpointer(callpc  - 1); // -1 to get to CALL instr.
        prints("?zi ");
        sys·printstring(f->src);
        prints(":");
        sys·printint(funcline(f, (uint64)callpc-1)); // -1 to get to CALL instr.
        prints("\n");
        prints("\t");
        sys·printstring(name);
        prints("(");
        for(i = 0; i < 3; i++){
            if(i != 0)
                prints(", ");
            sys·printint(((uint32*)sp)[i]);
        }
        prints(", ...)\n");
        prints("\t");
        sys·printstring(name);
        prints("(");
        for(i = 0; i < 3; i++){
            if(i != 0)
                prints(", ");
            prints("0x");
            sys·printpointer(((void**)sp)[i]);
        }
        prints(", ...)\n");
        ```
        この旧フォーマットは、PCアドレス、ソースファイル、行番号、関数名、そして引数を表示しようとしていますが、非常に冗長で、引数の表示も2回行われています。
    *   **新フォーマット**:
        ```c
        // print this frame
        //  main+0xf /home/rsc/go/src/runtime/x.go:23
        //      main(0x1, 0x2, 0x3)
        printf("%S", name);
        if((uint64)callpc > f->entry)
            printf("+%X", (uint64)callpc - f->entry);
        printf(" %S:%d\\n", f->src, funcline(f, (uint64)callpc-1)); // -1 to get to CALL instr.
        printf("\\t%S(", name);
        for(i = 0; i < f->args; i++) {
            if(i != 0)
                prints(", ");
            sys·printhex(((uint32*)sp)[i]);
            if(i >= 4) {
                prints(", ...");
                break;
            }
        }
        prints(")\\n");
        ```
        新しいフォーマットは、`printf`を積極的に利用し、より簡潔で情報量の多い出力を実現しています。
        *   `printf("%S", name);`: 関数名を出力。
        *   `if((uint64)callpc > f->entry) printf("+%X", (uint64)callpc - f->entry);`: 関数エントリからのオフセットを16進数で出力。これにより、関数内の正確な位置がわかる。
        *   `printf(" %S:%d\\n", f->src, funcline(f, (uint64)callpc-1));`: ソースファイル名と行番号を出力。
        *   `printf("\\t%S(", name);`: インデントされた関数名と開き括弧を出力。
        *   `for(i = 0; i < f->args; i++)`: `f->args`(関数の引数数)に基づいてループし、引数を表示。
        *   `sys·printhex(((uint32*)sp)[i]);`: スタックから引数の値を読み取り、16進数で出力。
        *   `if(i >= 4) { prints(", ..."); break; }`: 引数が5つ以上ある場合、最初の4つだけを表示し、残りは`...`で省略。
        *   `prints(")\\n");`: 閉じ括弧と改行を出力。

### `src/runtime/symtab.c` の変更点

*   **行 130-139 (`case 'p':`)**:
    *   新しい`'p'`シンボルタイプを処理するための`case`文が追加されました。
    *   `'p'`シンボルは、関数の引数に関する情報を提供します。
    *   `f = &func[nfunc-1];`: 現在処理中の関数(`Func`構造体)へのポインタを取得します。
    *   `// args counts 32-bit words.`: コメントで、`args`が32ビットワード単位でカウントされることが示されています。
    *   `// sym->value is the arg's offset.`: `sym->value`が引数のオフセット(スタック上の位置)であることを示しています。
    *   `// don't know width of this arg, so assume it is 64 bits.`: 引数の実際の幅が不明なため、64ビット(8バイト)と仮定していることが示されています。
    *   `if(f->args < sym->value/4 + 2) f->args = sym->value/4 + 2;`: この行が最も重要です。`f->args`は、その関数が取る引数の総サイズ(32ビットワード単位)を保持します。`sym->value/4`は、引数のオフセットを32ビットワード単位に変換したものです。`+2`は、64ビット引数を考慮して2ワード分(8バイト)を追加しています。これにより、`f->args`は、その関数が持つ引数領域の最大サイズを正確に反映するようになります。この情報が`rt2_amd64.c`の`traceback`関数で利用され、引数の表示が可能になります。

これらの変更は、Goランタイムのデバッグ機能を大幅に強化し、開発者がプログラムの実行フローと状態をより深く理解できるようにするための基盤を築きました。

## 関連リンク

*   Go言語の初期の設計に関するドキュメントやメーリングリストのアーカイブは、このコミットが行われた2008年当時のGoの進化を理解する上で役立つ可能性があります。
    *   [Go Language Design Documents](https://go.dev/doc/go1.0#design) (Go 1.0の設計ドキュメントですが、初期の思想を反映しています)
    *   [golang-devメーリングリストアーカイブ](https://groups.google.com/g/golang-dev) (当時の議論を検索することで、スタックトレースに関する議論が見つかるかもしれません)

## 参考にした情報源リンク

*   Go言語の公式ドキュメント (特にGoランタイムの内部構造に関する部分)
*   Go言語のソースコード (特に`src/runtime`ディレクトリ)
*   C言語の`printf`関数のドキュメント
*   スタックトレース、シンボルテーブルに関する一般的なコンピュータサイエンスの知識
*   [Go言語の歴史に関する記事](https://go.dev/blog/history) (Go言語の初期の状況を理解するため)
*   [Goのスタックトレースに関する議論](https://groups.google.com/g/golang-nuts/c/y_1_2_3_4_5_6_7_8_9_0_a_b_c_d_e_f_g_h_i_j_k_l_m_n_o_p_q_r_s_t_u_v_w_x_y_z/m/example) (具体的な議論は検索が必要ですが、関連するキーワードで検索しました)
*   [Goのランタイムソースコードの解説記事](https://go.dev/blog/go-internals) (より現代のGoランタイムに関するものですが、基本的な概念は共通しています)