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

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

このコミットは、GoランタイムにおけるWindows環境での例外処理メカニズムの改善に焦点を当てています。特に、Cgo(C言語との相互運用)を使用するプログラムにおいて、Windowsの構造化例外処理(SEH: Structured Exception Handling)が適切に機能するように修正が加えられています。これにより、GoプログラムがWindows上で予期せぬ例外(例えば、ヌルポインタ参照など)に遭遇した場合でも、ランタイムがそれを捕捉し、panic/recoverメカニズムを通じて安全に処理できるようになります。

コミット

commit afe0e97aa65be9dd0c147c4c824c12b1442ef2df
Author: Alex Brainman <alex.brainman@gmail.com>
Date:   Wed May 30 15:10:54 2012 +1000

    runtime: handle windows exceptions, even in cgo programs
    
    Fixes #3543.
    
    R=golang-dev, kardianos, rsc
    CC=golang-dev, hectorchu, vcc.163
    https://golang.org/cl/6245063

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

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

元コミット内容

このコミットの元のメッセージは以下の通りです。

runtime: handle windows exceptions, even in cgo programs

Fixes #3543.

R=golang-dev, kardianos, rsc
CC=golang-dev, hectorchu, vcc.163
https://golang.org/cl/6245063

これは、GoランタイムがWindows例外を、Cgoプログラム内であっても適切に処理できるようにするという目的を明確に示しています。Fixes #3543 は、このコミットが特定のバグまたは問題(Go issue 3543)を解決することを示唆しています。

変更の背景

Goプログラムは、通常、Goランタイムが提供する独自の例外処理メカニズム(panicrecover)を使用します。しかし、WindowsのようなOSでは、OSレベルで構造化例外処理(SEH)という独自の例外処理メカニズムが存在します。GoプログラムがCgoを介してC/C++コードと連携する場合、C/C++コード内で発生したOSレベルの例外がGoランタイムに適切に伝播されず、Goのpanic/recoverメカニズムで捕捉できない問題が発生することがありました。

特に、Go issue 3543では、Windows上でCgoを使用するGoプログラムが、Cコード内で発生したアクセス違反などのOS例外によってクラッシュする問題が報告されていました。Goランタイムは、これらのOS例外を捕捉し、Goのpanicに変換してGoらしい方法で処理できるようにする必要があります。このコミットは、この問題を解決し、GoプログラムがWindows環境でより堅牢に動作するようにするために導入されました。具体的には、SEHフレームのセットアップと管理を改善し、GoランタイムがOS例外を捕捉できるようにすることが目的です。

前提知識の解説

1. Goランタイム

Goランタイムは、Goプログラムの実行を管理するシステムです。これには、ガベージコレクション、スケジューラ、メモリ管理、そしてpanic/recoverのような例外処理メカニズムが含まれます。Goプログラムは、OS上で直接実行されるのではなく、このランタイム上で動作します。

2. Cgo

Cgoは、GoプログラムからC言語のコードを呼び出すためのGoの機能です。これにより、既存のCライブラリを利用したり、パフォーマンスが重要な部分をCで記述したりすることが可能になります。Cgoを使用すると、GoとCのコードが同じプロセス空間で実行されるため、両者の間でデータや制御フローがやり取りされます。

3. Windows Structured Exception Handling (SEH)

Windowsの構造化例外処理(SEH)は、ハードウェア例外(例: ゼロ除算、アクセス違反)やソフトウェア例外(例: RaiseException関数によって意図的に発生させられる例外)を処理するためのOSレベルのメカニズムです。SEHは、例外が発生した際に、登録された例外ハンドラを呼び出すことで、プログラムがクラッシュするのを防ぎ、回復処理を行うことを可能にします。

SEHは、スレッドのスタック上に「例外フレーム」を連鎖させることで機能します。各例外フレームには、そのフレームに対応する例外ハンドラへのポインタが含まれています。例外が発生すると、OSはスタックを巻き戻しながら例外フレームを順にたどり、適切なハンドラを見つけて実行します。

4. panicrecover

Go言語におけるエラー処理のメカニズムの一つです。

  • panic: プログラムの異常終了を示すために使用されます。panicが発生すると、現在の関数の実行が中断され、遅延関数(defer)が実行されながら、呼び出しスタックを遡ります。
  • recover: panicが発生した際に、そのpanicを捕捉し、プログラムの異常終了を防ぐために使用されます。recoverdefer関数内でしか効果がありません。

このコミットの目的は、WindowsのSEH例外をGoのpanicに変換し、Goプログラムがrecoverでそれを捕捉できるようにすることです。

技術的詳細

このコミットの主要な技術的変更点は、WindowsのSEHフレームの管理方法を改善し、GoランタイムがOSレベルの例外を捕捉できるようにすることです。

  1. SEHフレームの動的な登録: 以前は、Goプログラムの起動時や新しいスレッドの開始時に、アセンブリコード内で静的にSEHフレームが設定されていました。このコミットでは、runtime·install_exception_handlerという新しい関数が導入され、SEHフレームのセットアップがより動的に、かつGoランタイムの初期化プロセスの一部として行われるようになりました。
  2. M構造体へのsehフィールドの追加: GoランタイムのM構造体(マシン、つまりOSスレッドを表す)にSEH* seh;フィールドが追加されました。これにより、各OSスレッドが自身のSEHフレームへのポインタを保持できるようになり、ランタイムがスレッドごとの例外ハンドラを管理しやすくなります。
  3. runtime·mstartでのSEHフレームの初期化: runtime·mstart関数は、GoのスケジューラがOSスレッド上でGoルーチンを実行する際に呼び出される重要な関数です。この関数内でSEH seh;が宣言され、m->seh = &seh;によって現在のM構造体にSEHフレームが関連付けられます。これにより、Goルーチンが実行されるOSスレッド上でSEHが有効になります。
  4. アセンブリコードの変更:
    • src/pkg/runtime/rt0_windows_386.s および src/pkg/runtime/sys_windows_386.s から、起動時およびスレッド開始時のSEHフレームの静的な設定ロジックが削除されました。
    • src/pkg/runtime/sys_windows_386.sruntime·install_exception_handler の実装が追加されました。この関数は、M構造体に格納されたsehフィールドを利用して、現在のスレッドのSEHフレームをセットアップします。具体的には、sigtramp(シグナル/例外トラップハンドラ)を例外ハンドラとして登録し、既存の例外フレームチェーンの先頭に自身のフレームを挿入します。
    • src/pkg/runtime/sys_windows_amd64.s にも runtime·install_exception_handler が追加されていますが、amd64ではまだ完全な実装ではなく、将来的な拡張のプレースホルダーとして機能しています。
  5. テストの追加: crash_cgo_test.gocrash_test.go という新しいテストファイルが追加されました。これらのテストは、Goプログラムが意図的にヌルポインタ参照などのクラッシュを引き起こし、Goランタイムがそれを捕捉してrecoverできることを検証します。特にcrash_cgo_test.goは、Cgoプログラムにおける例外処理のテストに特化しています。

これらの変更により、GoランタイムはWindowsのOSレベル例外をより確実に捕捉し、Goのpanicメカニズムに変換できるようになります。これにより、Cgoプログラムを含むGoアプリケーションの堅牢性が向上します。

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

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

  1. src/pkg/runtime/runtime.h:

    • SEH構造体の定義が追加されました。この構造体は、WindowsのSEHフレームの基本的な要素(前の例外フレームへのポインタとハンドラへのポインタ)を模倣しています。
      typedef struct SEH SEH;
      struct SEH
      {
          void*   prev;
          void*   handler;
      };
      
    • M構造体(GoランタイムのOSスレッドを表す構造体)にSEH* seh;フィールドが追加されました。これにより、各OSスレッドが自身のSEHフレームへのポインタを保持できるようになります。
      struct  M
      {
          // ...
          #ifdef GOOS_windows
          void*   thread;         // thread handle
          #endif
          SEH*    seh;
          uintptr end[];
      };
      
  2. src/pkg/runtime/os_windows.h:

    • runtime·install_exception_handler関数のプロトタイプ宣言が追加されました。
      void runtime·install_exception_handler(void);
      
  3. src/pkg/runtime/proc.c:

    • runtime·mstart関数内で、SEH seh;が宣言され、m->seh = &seh;によって現在のM構造体にSEHフレームが関連付けられるようになりました。これにより、Goルーチンが実行されるOSスレッド上でSEHが有効になります。
      void
      runtime·mstart(void)
      {
          // It is used by windows-386 only. Unfortunately, seh needs
          // to be located on os stack, and mstart runs on os stack
          // for both m0 and m.
          SEH seh;
      
          if(g != m->g0)
              runtime·throw("bad runtime·mstart");
      
          // ...
          m->g0->sched.pc = (void*)-1;  // make sure it is never used
          m->seh = &seh; // ここでSEHフレームをMに紐付け
          runtime·asminit();
          runtime·minit();
      
          // ...
      }
      
  4. src/pkg/runtime/thread_windows.c:

    • runtime·minit関数(Goランタイムの初期化の一部として各OSスレッドで実行される)内で、runtime·install_exception_handler()が呼び出されるようになりました。これにより、スレッドがGoルーチンを実行する準備ができた時点でSEHハンドラがインストールされます。
      void
      runtime·minit(void)
      {
          runtime·install_exception_handler();
      }
      
  5. src/pkg/runtime/sys_windows_386.s:

    • _rt0_386_windowsおよびruntime·tstartから、SEHフレームをスタックに直接プッシュする古いアセンブリコードが削除されました。
    • runtime·install_exception_handlerのアセンブリ実装が追加されました。この関数は、M構造体のsehフィールドを利用して、sigtrampを例外ハンドラとして登録し、現在のスレッドのSEHフレームチェーンに自身を挿入します。
      // void install_exception_handler()
      TEXT runtime·install_exception_handler(SB),7,$0
          get_tls(CX)
          MOVL    m(CX), CX       // m
      
          // Set up SEH frame
          MOVL    m_seh(CX), DX
          MOVL    $runtime·sigtramp(SB), AX
          MOVL    AX, seh_handler(DX)
          MOVL    0(FS), AX
          MOVL    AX, seh_prev(DX)
      
          // Install it
          MOVL    DX, 0(FS)
      
          RET
      
  6. src/pkg/runtime/crash_cgo_test.go および src/pkg/runtime/crash_test.go:

    • Windows例外処理の動作を検証するための新しいテストが追加されました。これらのテストは、意図的にヌルポインタ参照などの例外を発生させ、Goのrecoverメカニズムがそれを捕捉できることを確認します。特にcrash_cgo_test.goはCgo環境でのテストを含みます。

コアとなるコードの解説

このコミットの核心は、WindowsのSEHをGoランタイムのpanic/recoverメカニズムと統合することです。

  1. SEH構造体とM.seh: SEH構造体は、WindowsのSEHフレームの構造をGoランタイム内で表現するためのものです。prevは前の例外フレームへのポインタ、handlerはこのフレームの例外ハンドラ関数へのポインタです。 M構造体にsehフィールドが追加されたことで、各OSスレッド(M)が自身のSEHフレームの情報を保持できるようになりました。これにより、GoランタイムはスレッドごとにSEHフレームを管理し、必要に応じてアクセスできるようになります。

  2. runtime·mstartでのSEH初期化: runtime·mstartは、Goルーチンが実行されるOSスレッドの初期化を行う関数です。ここでSEH seh;をスタック上に確保し、そのアドレスをm->sehに設定することで、現在のOSスレッドのスタック上にSEHフレームの領域を確保し、Goランタイムがそのフレームを操作できるようにします。これは、SEHフレームがスレッドのスタック上に存在する必要があるというWindowsの要件を満たすための重要なステップです。

  3. runtime·install_exception_handler: この関数は、実際にSEHハンドラをインストールする役割を担います。

    • get_tls(CX): スレッドローカルストレージ(TLS)から現在のM構造体へのポインタを取得します。
    • MOVL m_seh(CX), DX: M構造体から、先ほどruntime·mstartで設定されたSEH構造体のアドレスをDXレジスタにロードします。
    • MOVL $runtime·sigtramp(SB), AX / MOVL AX, seh_handler(DX): runtime·sigtrampのアドレスをAXレジスタにロードし、それをSEH構造体のhandlerフィールドに設定します。runtime·sigtrampは、GoランタイムがOSからのシグナルや例外を捕捉した際に呼び出される共通のトラップハンドラです。
    • MOVL 0(FS), AX / MOVL AX, seh_prev(DX): 0(FS)は、WindowsのFSセグメントレジスタが指すスレッド情報ブロック(TEB: Thread Environment Block)の先頭に格納されている、現在のSEHフレームチェーンの先頭へのポインタです。これをAXにロードし、SEH構造体のprevフィールドに設定します。これにより、新しいSEHフレームが既存のチェーンの先頭にリンクされます。
    • MOVL DX, 0(FS): 新しく設定したSEH構造体のアドレス(DX)を0(FS)に書き込みます。これにより、この新しいSEHフレームが現在のスレッドのSEHチェーンの先頭として登録され、以降に発生する例外はこのハンドラによって最初に処理されるようになります。

この一連の処理により、GoランタイムはOSレベルで発生した例外をruntime·sigtrampで捕捉し、それをGoのpanicに変換して、Goプログラムがrecoverで処理できるようにします。これにより、Cgoコード内で発生した例外であっても、Goの例外処理メカニズムを通じて安全に扱うことが可能になります。

関連リンク

参考にした情報源リンク