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

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

このコミットは、Go言語のCgo機能において、Cgoが8KB以上のスタックを消費し、コールバックを行う際に発生していたクラッシュを修正するものです。具体的には、Goランタイムのg0スタックのガード機構に問題があり、スタックが不足した際に適切に検出・処理されなかったことが原因でした。この修正により、CgoとGoランタイム間のスタック管理が改善され、安定性が向上しました。

コミット

commit fbfed49134bca038184dbc1a427e82647fc1f12e
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Wed Nov 9 23:11:48 2011 +0300

    cgo: fix g0 stack guard
    Fixes crash when cgo consumes more than 8K
    of stack and makes a callback.
    
    Fixes #1328.
    
    R=golang-dev, rogpeppe, rsc
    CC=golang-dev, mpimenov
    https://golang.org/cl/5371042

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

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

元コミット内容

このコミットは、Cgoが8KB以上のスタックを消費し、Goへのコールバックを行う際に発生するクラッシュを修正します。これはGoのIssue #1328で報告された問題に対応するものです。

変更の背景

Go言語のCgoは、GoプログラムからC言語のコードを呼び出したり、C言語のコードからGoの関数をコールバックしたりするためのメカニズムを提供します。この相互運用性には、GoランタイムとCランタイム間のスタック管理の複雑さが伴います。

Goランタイムには、Goルーチンが使用するスタックとは別に、ランタイム自身が内部処理(スケジューリング、ガベージコレクションなど)のために使用する特別なスタック「g0スタック」が存在します。Cgoを介してCコードがGoの関数をコールバックする際、GoランタイムはCスタックからg0スタックに切り替えて処理を行います。

報告された問題(Issue #1328)は、Cgoを介したコールバックにおいて、Cコード側で8KBを超えるスタックを消費した場合にクラッシュが発生するというものでした。これは、g0スタックのガードページ(スタックオーバーフローを検出するための保護領域)が適切に設定されていなかったか、またはその検出ロジックに不備があったためと考えられます。結果として、Cgoコールバック中にg0スタックがオーバーフローしても、それが検出されずに不正なメモリ領域へのアクセスが発生し、プログラムがクラッシュしていました。

このコミットは、このスタックガードの問題を修正し、Cgoコールバック時のg0スタックの安全性を確保することを目的としています。

前提知識の解説

Goランタイムとスタック管理

Goランタイムは、Goルーチンごとに可変長のスタックを割り当て、必要に応じてスタックを拡張・縮小します。これは、Goルーチンが非常に軽量であり、数百万ものGoルーチンを同時に実行できるGoの並行処理モデルの基盤となっています。

しかし、Goランタイム自身も内部的な処理のためにスタックを必要とします。これが「g0スタック」と呼ばれるものです。g0スタックは、Goルーチンのスタックとは異なり、固定サイズ(通常は8KBまたは16KB)で、システムコール、Cgoコール、スケジューラ関連の処理など、Goランタイムの低レベルな操作に使用されます。

スタックガード

スタックガードは、スタックオーバーフローを検出するための一般的なメカニズムです。スタックの末尾(通常はスタックが成長する方向の逆側)に「ガードページ」と呼ばれる保護されたメモリページを配置します。プログラムがこのガードページにアクセスしようとすると、ページフォルトが発生し、ランタイムはそのスタックオーバーフローを検出して適切なエラー処理(パニックなど)を行うことができます。これにより、スタックオーバーフローによる不正なメモリアクセスやクラッシュを防ぎます。

Cgoとスタック切り替え

Cgoを介してGoからC関数を呼び出す際、Goランタイムは現在のGoルーチンのスタックからCスタックに切り替えます。同様に、C関数からGo関数をコールバックする際には、CスタックからGoランタイムのg0スタックに切り替える必要があります。このスタック切り替えの過程で、g0スタックのガードが正しく機能しないと、Cコードが大量のスタックを消費した場合に問題が発生する可能性があります。

pthread_attr_getstacksize

pthread_attr_getstacksizeはPOSIXスレッド(pthread)ライブラリの関数で、スレッド属性オブジェクト(pthread_attr_t)からスタックサイズを取得するために使用されます。CgoがCスレッドからGoにコールバックする際、GoランタイムはCスレッドのスタック情報を利用してg0スタックのガードを設定する必要がある場合があります。この関数は、Cスレッドのスタックのベースアドレスとサイズを決定するために利用されます。

技術的詳細

このコミットの主要な変更点は、Goランタイムの初期化プロセスとCgo関連のコードにおけるg0スタックガードの設定方法の修正です。

  1. misc/cgo/test/callback_c.c の変更: テストケースに、意図的に大きなスタック領域(64KB)を使用するコードが追加されました。これは、修正が正しく機能するかどうかを検証するためのものです。volatile char data[64*1024]; のような宣言は、コンパイラによる最適化を防ぎ、実際にスタック領域が確保されることを保証します。

  2. src/pkg/runtime/386/asm.s および src/pkg/runtime/amd64/asm.s の変更: これらのアセンブリファイルは、Goランタイムの初期化ルーチン(_rt0_386, _rt0_amd64)を含んでいます。変更の核心は、initcgo関数が呼び出される際に、g0スタックのガードが適切に設定されるようにすることです。

    • 以前は、initcgoが呼び出される前にg0スタックガードが固定値(例えば -64*1024+104-8192+104)で設定されていました。
    • 修正後は、initcgoが存在し、それがスタックガードを設定する責任がある場合(JNZ stackok)、ランタイムは独自のスタックガード設定をスキップします。
    • initcgoG*(Goルーチン構造体へのポインタ)を引数として受け取るように変更され、initcgo内でg->stackguardを設定できるようになりました。これにより、CgoがOSのスレッドスタック情報を利用して、より正確なg0スタックガードを設定することが可能になります。
  3. src/pkg/runtime/cgo/*.c ファイル群の変更: Darwin, FreeBSD, Linux, Windowsの各アーキテクチャ(386, amd64, arm)に対応するCgoランタイムファイルが変更されました。

    • xinitcgo関数のシグネチャがvoid xinitcgo(void)からvoid xinitcgo(G *g)に変更されました。これにより、xinitcgo関数内で現在のGoルーチン(g0)の情報を直接操作できるようになります。
    • xinitcgo関数内で、pthread_attr_getstacksize(POSIXシステムの場合)またはローカル変数と定義済みスタックサイズ(Windowsの場合)を使用して、現在のCスレッドのスタック情報を取得し、それに基づいてg->stackguardを設定するロジックが追加されました。
      • 具体的には、g->stackguard = (uintptr)&attr - size + 4096; のように、Cスレッドのスタックのベースアドレスからスタックサイズを引いた値にオフセット(4096バイト)を加えることで、スタックガードの境界を設定しています。この4096バイトは、スタックガードページとして確保される領域のサイズを示唆しています。
      • Windowsの場合、g->stackguard = (uintptr)&tmp - STACKSIZE + 4096; のように、ローカル変数のアドレスと定義済みのSTACKSIZE(1MBまたは2MB)を使用して計算しています。
    • これにより、CgoがGoにコールバックする際に使用するg0スタックのガードが、Cスレッドの実際のスタックサイズを考慮して動的に設定されるようになり、スタックオーバーフローの検出精度が向上しました。

これらの変更により、CgoがGoにコールバックする際に、Cコードが大量のスタックを消費しても、g0スタックのガードが適切に機能し、クラッシュが防止されるようになりました。

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

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

  1. src/pkg/runtime/386/asm.s および src/pkg/runtime/amd64/asm.s: Goランタイムの初期化ルーチンにおいて、initcgoの呼び出しとg0スタックガードの設定ロジックが変更されています。

    src/pkg/runtime/386/asm.s の例:

    --- a/src/pkg/runtime/386/asm.s
    +++ b/src/pkg/runtime/386/asm.s
    @@ -26,12 +26,14 @@ TEXT _rt0_386(SB),7,$0
     	// we set up GS ourselves.
     	MOVL	initcgo(SB), AX
     	TESTL	AX, AX
    -	JZ	4(PC)
    +	JZ	needtls
    +	PUSHL	$runtime·g0(SB)
     	CALL	AX
    +	POPL	AX
     	// skip runtime·ldt0setup(SB) and tls test after initcgo for non-windows
     	CMPL runtime·iswindows(SB), $0
     	JEQ ok
    -\n+needtls:
     	// skip runtime·ldt0setup(SB) and tls test on Plan 9 in all cases
     	CMPL	runtime·isplan9(SB), $1
     	JEQ	ok
    @@ -58,9 +60,15 @@ ok:
     	MOVL	CX, m_g0(AX)
     
     	// create istack out of the OS stack
    +\t// if there is an initcgo, it had setup stackguard for us
    +\tMOVL	initcgo(SB), AX
    +\tTESTL	AX, AX
    +\tJNZ	stackok
     	LEAL	(-64*1024+104)(SP), AX	// TODO: 104?
     	MOVL	AX, g_stackguard(CX)
    +stackok:
     	MOVL	SP, g_stackbase(CX)
    +\n
     	CALL	runtime·emptyfunc(SB)	// fault if stack check is wrong
     
     	// convention is D is always cleared
    
  2. src/pkg/runtime/cgo/*.c ファイル群: 各OS/アーキテクチャごとのCgo初期化関数xinitcgoのシグネチャと実装が変更されています。

    src/pkg/runtime/cgo/darwin_386.c の例:

    --- a/src/pkg/runtime/cgo/darwin_386.c
    +++ b/src/pkg/runtime/cgo/darwin_386.c
    @@ -100,12 +100,20 @@ inittls(void)\n }\n \n static void\n-xinitcgo(void)\n+xinitcgo(G *g)\n {\n+\tpthread_attr_t attr;\n+\tsize_t size;\n+\n+\tpthread_attr_init(&attr);\n+\tpthread_attr_getstacksize(&attr, &size);\n+\tg->stackguard = (uintptr)&attr - size + 4096;\n+\tpthread_attr_destroy(&attr);\n+\n \tinittls();\n }\n \n-void (*initcgo)(void) = xinitcgo;\n+void (*initcgo)(G*) = xinitcgo;\n \n void\n libcgo_sys_thread_start(ThreadStart *ts)\n    ```
    
    

コアとなるコードの解説

アセンブリコード (asm.s) の変更

アセンブリコードの変更は、Goランタイムの起動シーケンスにおけるg0スタックガードの設定ロジックを調整しています。

  • initcgoの呼び出しとg0の引き渡し: 以前はinitcgoが引数なしで呼び出されていましたが、修正後はruntime·g0(SB)g0 Goルーチン構造体のアドレス)をスタックにプッシュしてからinitcgoを呼び出し、呼び出し後にポップしています。これは、initcgo関数がG* gという引数を受け取るように変更されたためです。これにより、initcgog0の情報を直接受け取り、そのstackguardフィールドを更新できるようになります。

  • 条件付きスタックガード設定: initcgoが存在する場合(TESTL AX, AXJNZ stackok)、Goランタイムは独自の固定スタックガード設定(LEAL (-64*1024+104)(SP), AX など)をスキップし、initcgoがスタックガードを設定する責任を持つようにします。これは、CgoがOSのスレッドスタック情報を利用してより正確なスタックガードを設定できるためです。

Cgoランタイムコード (.c ファイル) の変更

CgoランタイムのCコードの変更は、各OS/アーキテクチャ固有のxinitcgo関数内で、g0スタックのガードを動的に設定するロジックを導入しています。

  • xinitcgo(G *g) シグネチャの変更: xinitcgo関数がG *gという引数を受け取るようになりました。このgは、Goランタイムの内部でg0として知られる特別なGoルーチン構造体へのポインタです。これにより、xinitcgog0stackguardフィールドに直接アクセスし、その値を設定できるようになります。

  • スタックサイズの取得とstackguardの設定:

    • POSIX系OS (Darwin, FreeBSD, Linux): pthread_attr_t構造体とpthread_attr_init, pthread_attr_getstacksize, pthread_attr_destroy関数を使用して、現在のCスレッドのスタックサイズを取得します。 g->stackguard = (uintptr)&attr - size + 4096; ここで、(uintptr)&attrはスタック上のローカル変数attrのアドレス(スタックの現在のトップに近い位置)を示し、そこからsize(スレッドのスタックサイズ)を引くことでスタックのベースアドレスに近い位置を特定します。さらに+ 4096というオフセットを加えることで、スタックの末尾から4KB(ガードページサイズ)手前の位置をstackguardとして設定しています。これにより、スタックがこの境界を超えて成長しようとすると、ページフォルトが発生し、スタックオーバーフローが検出されます。

    • Windows: Windowsではpthreadは使用できないため、ローカル変数tmpのアドレスと定義済みのSTACKSIZEマクロ(1MBまたは2MB)を使用してスタックガードを設定します。 g->stackguard = (uintptr)&tmp - STACKSIZE + 4096; 基本的な考え方はPOSIX系OSと同じで、スタックの現在の位置から定義済みのスタックサイズを引いてスタックのベースを推定し、そこに4KBのオフセットを加えることでガードページを設定しています。

これらの変更により、CgoがGoにコールバックする際に、Cスレッドの実際のスタックサイズを考慮した上でg0スタックのガードが設定されるようになり、スタックオーバーフローによるクラッシュが効果的に防止されるようになりました。

関連リンク

参考にした情報源リンク

  • Go言語のスタック管理に関するドキュメントや記事 (一般的なGoランタイムのスタック管理、g0スタック、スタックガードの概念について)
  • Cgoの内部動作に関するドキュメントや記事 (Cgoにおけるスタック切り替えのメカニズムについて)
  • POSIXスレッドのpthread_attr_getstacksize関数のドキュメント (Cスレッドのスタックサイズ取得方法について)
  • Windowsにおけるスレッドスタック管理に関するドキュメント (Windowsでのスタックサイズ推定方法について)
  • Go言語のソースコード (特にsrc/pkg/runtimeおよびsrc/pkg/runtime/cgoディレクトリ内のファイル)
  • Go言語のIssueトラッカー (Issue #1328の詳細な議論について)
  • Go言語のコードレビューシステム (CL 5371042のレビューコメントについて)