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

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

このコミットは、Goランタイムにおけるamd64p32アーキテクチャ上でのcgocallback関数のスタックサイズに関するバグ修正です。具体的には、cgocallback関数のスタックフレームサイズが誤って設定されていた問題を修正し、正しい値に調整することで、Cgoコールバックが正しく機能するようにしています。

コミット

commit 4bc632cead0cd2a3c92a22e47c587ae8b76c400b
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date:   Mon Mar 10 07:57:58 2014 +0100

    runtime: fix cgocallback stack size on amd64p32.
    
    LGTM=dave
    R=rsc, dave, iant
    CC=golang-codereviews
    https://golang.org/cl/73160043

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

https://github.com/golang/go/commit/4bc632cead0cd2a3c92a22e47c587ae8b76c400b

元コミット内容

runtime: fix cgocallback stack size on amd64p32.

LGTM=dave
R=rsc, dave, iant
CC=golang-codereviews
https://golang.org/cl/73160043

変更の背景

このコミットは、amd64p32という特定のアーキテクチャにおいて、Cgo(GoとC言語の相互運用機能)のコールバック処理におけるスタックサイズの問題を修正するために行われました。

GoプログラムがC言語のコードを呼び出す際(C.func()のような形式)、またはC言語のコードからGoの関数が呼び出される際(Cgoコールバック)、Goランタイムはこれらの呼び出しを適切に処理するためのメカニズムを提供します。特にCgoコールバックの場合、CコードからGoコードへの遷移が発生するため、スタックの切り替えや引数の受け渡しなど、低レベルな処理が必要となります。

amd64p32は、64ビットのレジスタと命令セットを使用しながらも、ポインタサイズが32ビットであるという特殊な環境です。このような環境では、通常の64ビットシステムとは異なるメモリレイアウトやアライメントの考慮が必要になることがあります。

元のコードでは、runtime·cgocallback関数のスタックフレームサイズが誤って$12-12と設定されていました。これは、関数が引数を受け取らない場合や、スタック上にローカル変数を確保しない場合に用いられる表記ですが、実際にはCgoコールバックの処理に必要なスタック領域が確保されていなかった可能性があります。この不正確なスタックサイズの設定は、Cgoコールバックが実行される際にスタックオーバーフローやメモリ破壊を引き起こす可能性があり、プログラムのクラッシュや予期せぬ動作につながる重大なバグでした。

この修正は、amd64p32環境でCgoコールバックが安定して動作するために不可欠なものでした。

前提知識の解説

このコミットを理解するためには、以下の概念について理解しておく必要があります。

  1. Goランタイム (Go Runtime): Goランタイムは、Goプログラムの実行を管理するシステムです。これには、ガベージコレクション、スケジューラ(ゴルーチンの管理)、メモリ管理、スタック管理、Cgoインターフェースなどが含まれます。Goプログラムは、オペレーティングシステム上で直接実行されるのではなく、Goランタイムの制御下で実行されます。

  2. Cgo: Cgoは、GoプログラムからC言語のコードを呼び出したり、C言語のコードからGoの関数を呼び出したりするためのGoの機能です。これにより、既存のCライブラリをGoから利用したり、パフォーマンスが重要な部分をCで記述したりすることが可能になります。Cgoは、GoとCの異なる呼び出し規約、メモリモデル、スタック管理などを橋渡しする役割を担います。

  3. スタック (Stack): スタックは、プログラムの実行中に一時的なデータを格納するために使用されるメモリ領域です。関数が呼び出されるたびに、その関数のローカル変数、引数、戻りアドレスなどがスタックフレームとしてスタックにプッシュされます。関数が終了すると、そのスタックフレームはポップされ、スタックポインタが元の位置に戻ります。スタックはLIFO(Last-In, First-Out)の原則で動作します。

  4. スタックサイズ (Stack Size): 関数が実行される際に必要となるスタック領域の量です。これは、関数の引数の数、ローカル変数のサイズ、およびその関数が呼び出す他の関数のスタック使用量によって決まります。スタックサイズが不足すると、スタックオーバーフローが発生し、プログラムがクラッシュする原因となります。

  5. amd64p32アーキテクチャ: amd64p32は、64ビットのAMD64命令セットを使用しながらも、ポインタのサイズが32ビットに制限されている特殊な実行環境です。これは、主に一部の組み込みシステムや、メモリ使用量を抑えつつ64ビットの計算能力を利用したい場合に用いられることがあります。通常のamd64(またはx86-64)アーキテクチャではポインタは64ビットですが、amd64p32では32ビットであるため、メモリのアドレス指定やデータ構造のレイアウトにおいて特別な考慮が必要になります。

  6. アセンブリ言語 (Assembly Language): このコミットで変更されているファイルはアセンブリ言語で書かれています。アセンブリ言語は、CPUが直接理解できる機械語に非常に近い低レベルのプログラミング言語です。Goランタイムのコア部分は、パフォーマンスや特定のハードウェア操作のためにアセンブリ言語で記述されることがあります。Goのアセンブリは、Plan 9アセンブラの構文に似ています。

  7. TEXTディレクティブ: Goのアセンブリ言語におけるTEXTディレクティブは、関数の定義を開始するために使用されます。その構文は通常 TEXT symbol(SB), flags, args_size-frame_size のようになります。

    • symbol(SB): 関数のシンボル名。SBは静的ベースポインタ(Static Base pointer)を表し、グローバルシンボルであることを示します。
    • flags: 関数の特性を示すフラグ(例: NOSPLITはスタックの分割を許可しないことを示します)。
    • args_size-frame_size: これは非常に重要で、args_sizeは関数が受け取る引数の合計サイズ(バイト単位)、frame_sizeは関数自身のスタックフレーム(ローカル変数やレジスタ退避領域など)のサイズ(バイト単位)を示します。このコミットでは、このframe_sizeが問題となっていました。

技術的詳細

このコミットの技術的な核心は、amd64p32アーキテクチャにおけるcgocallback関数のスタックフレームサイズの正確な設定にあります。

Goランタイムでは、Cgoコールバック(CコードからGo関数を呼び出す)を処理するために、runtime·cgocallbackというアセンブリ関数が使用されます。この関数は、Cの呼び出し規約からGoの呼び出し規約への変換、スタックの切り替え、そしてGoのスケジューラへの制御の引き渡しなど、複雑な低レベル処理を行います。

Goのアセンブリ言語では、TEXTディレクティブのargs_size-frame_sizeという部分で、関数の引数とローカルスタックフレームのサイズを明示的に指定します。

  • args_size: 関数が呼び出し元から受け取る引数の合計サイズ。
  • frame_size: 関数自身のローカル変数や、レジスタを退避するために使用するスタック領域のサイズ。

元のコードでは、runtime·cgocallbackの定義が以下のようになっていました。 TEXT runtime·cgocallback(SB),NOSPLIT,$12-12

ここで、$12-12は、引数のサイズが12バイト、スタックフレームのサイズも12バイトであることを示しています。しかし、cgocallback関数は実際には引数を受け取らず、また、その内部で特別なスタック領域を必要としない場合、スタックフレームサイズは0であるべきです。

$12-12という表記は、Goのアセンブリにおいて、引数サイズとフレームサイズが同じ値である場合に、フレームサイズが0であることを意味する慣例的な表現として使われることがあります。しかし、これは誤解を招きやすく、特にamd64p32のような特殊な環境では、コンパイラやアセンブラがこれを正しく解釈しない可能性がありました。

このコミットでは、この部分を以下のように修正しています。 TEXT runtime·cgocallback(SB),NOSPLIT,$0-12

この変更により、引数のサイズが$0(0バイト)と明示され、スタックフレームのサイズが$12(12バイト)と設定されました。 なぜ$0-12なのかというと、cgocallbackは実際には引数を受け取らないにもかかわらず、GoのABI(Application Binary Interface)や特定のレジスタ退避の必要性から、内部的に12バイトのスタック領域を必要とするためと考えられます。この12バイトは、おそらく戻りアドレスや特定のレジスタの退避、あるいはCgoコールバックの内部処理に必要な最小限のスタックアライメントを確保するためのものです。

この修正により、amd64p32環境でcgocallbackが呼び出された際に、必要なスタック領域が正しく確保されるようになり、スタックオーバーフローや関連するメモリの問題が解消されました。これは、Goランタイムの安定性とCgoの信頼性を向上させる上で重要な修正です。

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

変更はsrc/pkg/runtime/asm_amd64p32.sファイルの一箇所のみです。

--- a/src/pkg/runtime/asm_amd64p32.s
+++ b/src/pkg/runtime/asm_amd64p32.s
@@ -617,7 +617,7 @@ TEXT runtime·asmcgocall(SB),NOSPLIT,$0-8
 
 // cgocallback(void (*fn)(void*), void *frame, uintptr framesize)
 // Not implemented.
-TEXT runtime·cgocallback(SB),NOSPLIT,$12-12
+TEXT runtime·cgocallback(SB),NOSPLIT,$0-12
 	MOVL	0, AX
 	RET
 

コアとなるコードの解説

変更された行は、runtime·cgocallback関数の定義部分です。

元のコード: TEXT runtime·cgocallback(SB),NOSPLIT,$12-12

修正後のコード: TEXT runtime·cgocallback(SB),NOSPLIT,$0-12

この変更のポイントは、TEXTディレクティブの最後の部分であるargs_size-frame_sizeの値を$12-12から$0-12に変更したことです。

  • $12-12 (変更前): これは、関数が12バイトの引数を受け取り、かつ12バイトのスタックフレームを持つことを示唆していました。しかし、cgocallbackはGoの内部関数であり、Cgoのメカニズムを通じて呼び出されるため、Goの観点からは直接的な引数を受け取らない場合があります。また、$X-Xという形式は、フレームサイズが0であることを示す慣例として使われることもありますが、amd64p32のような特殊な環境では、これが正しく解釈されず、スタックフレームが適切に確保されない問題を引き起こしていた可能性があります。

  • $0-12 (変更後):

    • $0: これは、関数がGoの観点から0バイトの引数を受け取ることを明確に示しています。
    • $12: これは、関数が自身の実行のために12バイトのスタックフレームを必要とすることを明示しています。この12バイトは、おそらくCgoコールバックの処理に必要な最小限のスタックアライメントや、内部的なレジスタ退避などのために確保される領域です。

この修正により、amd64p32環境においてruntime·cgocallback関数が呼び出された際に、必要なスタック領域が正確に確保されるようになり、スタック関連のバグが解消されました。これにより、Cgoコールバックの安定性と信頼性が向上しました。

関連リンク

  • Go言語のCgoに関する公式ドキュメント: https://go.dev/blog/c-go-is-not-c (Cgoの概念的な理解に役立ちます)
  • Goのアセンブリ言語に関するドキュメント: https://go.dev/doc/asm (Goのアセンブリ構文とTEXTディレクティブについて詳しく解説されています)
  • GoのIssueトラッカー (Gerrit CLへのリンク): https://golang.org/cl/73160043 (このコミットの元のレビュープロセスを確認できます)

参考にした情報源リンク