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

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

このコミットは、GoランタイムにおけるWindows/amd64環境でのCgo呼び出し時のスタックフレームの増加に関する修正です。具体的には、Cgoを介してC言語の関数を呼び出す際に、Windows x64の呼び出し規約に準拠するために必要なスタック領域を適切に確保することで、スタックオーバーフローによるクラッシュを防ぐことを目的としています。

コミット

commit 7f075ece42fbbfcd7d0ae64807651618333bd2eb
Author: Alex Brainman <alex.brainman@gmail.com>
Date:   Mon Sep 3 12:12:51 2012 +1000

    runtime: increase stack frame during cgo call on windows/amd64
    
    Fixes #3945.
    
    R=golang-dev, minux.ma
    CC=golang-dev, vcc.163
    https://golang.org/cl/6490056

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

https://github.com/golang/go/commit/7f075ece42fbbfcd7d0ae64807651618333bd2eb

元コミット内容

runtime: increase stack frame during cgo call on windows/amd64

Fixes #3945.

R=golang-dev, minux.ma
CC=golang-dev, vcc.163
https://golang.org/cl/6490056

変更の背景

このコミットは、Go言語のIssue #3945「Using CString on Windows 7 (amd64) causes crash」を修正するために行われました。この問題は、Windows 7 (amd64) 環境でGoのCgo機能を使用してC言語の関数を呼び出す際に、プログラムがクラッシュするというものでした。具体的には、printfのようなC関数を呼び出す際に、GoランタイムがC関数が必要とするスタック領域を十分に確保していなかったことが原因と考えられます。

Windows x64の呼び出し規約では、関数呼び出し時に「シャドウスペース(またはホームスペース、スピルスペース)」と呼ばれる32バイトの領域をスタック上に確保することが義務付けられています。この領域は、呼び出される関数が最初の4つの引数(レジスタで渡される)の値を保存するために使用されます。GoランタイムがCgoを介してC関数を呼び出す際、このシャドウスペースを適切に確保しないと、C関数がスタックにアクセスしようとしたときに不正なメモリ領域にアクセスし、クラッシュを引き起こす可能性がありました。

このコミットは、特にprintfのような可変引数関数や、プロトタイプ宣言されていないC/C++関数を扱う際に、すべての引数に対して連続したメモリ位置を提供するためにシャドウスペースが重要であることを考慮し、GoランタイムがCgo呼び出し時に必要なスタック領域を確保するように修正することで、このクラッシュ問題を解決しています。

前提知識の解説

Cgo

Cgoは、GoプログラムからC言語のコードを呼び出したり、C言語のコードからGoのコードを呼び出したりするためのGoの機能です。これにより、既存のCライブラリをGoプロジェクトで利用したり、パフォーマンスが重要な部分をCで記述したりすることが可能になります。Cgoを使用する際には、GoとCの間でデータ型や呼び出し規約の変換が必要となり、これが複雑さの原因となることがあります。

呼び出し規約 (Calling Convention)

呼び出し規約とは、関数が呼び出される際に、引数をどのように渡し、戻り値をどのように返し、スタックをどのように管理するかといった、関数呼び出しに関する取り決めです。CPUアーキテクチャやオペレーティングシステムによって異なる呼び出し規約が存在します。

Windows x64 呼び出し規約

Windows x64(64ビット版Windows)における標準の呼び出し規約は、Microsoft x64 calling conventionとして知られています。この規約にはいくつかの重要な特徴があります。

  1. レジスタ渡し: 最初の4つの整数引数(RCX, RDX, R8, R9)と浮動小数点引数(XMM0, XMM1, XMM2, XMM3)はレジスタで渡されます。
  2. シャドウスペース (Shadow Space): 呼び出し元関数は、CALL命令の前に、スタック上に32バイトの「シャドウスペース」を確保する義務があります。このスペースは、呼び出される関数がレジスタで渡された引数をスタックに保存するために使用できます。これはデバッグの容易性や、可変引数関数の実装を簡素化するために重要です。
  3. スタックアライメント: スタックポインタ(RSP)は、CALL命令の直前で16バイト境界にアライメントされている必要があります。これにより、SIMD命令などが正しく動作することが保証されます。
  4. スタッククリーンアップ: 呼び出し元関数がスタックをクリーンアップします。

スタックフレーム

スタックフレームは、関数が呼び出されるたびにスタック上に割り当てられるメモリ領域です。これには、関数のローカル変数、引数、戻りアドレス、およびその他のコンテキスト情報が含まれます。関数が実行を終了すると、そのスタックフレームは解放されます。

技術的詳細

このコミットの核心は、GoランタイムがCgo呼び出しを行う際に、Windows x64の呼び出し規約で要求されるシャドウスペースを適切に確保することです。

Goのruntime·asmcgocall関数は、GoルーチンからC関数を呼び出す際のエントリポイントとなるアセンブリコードです。この関数は、GoのスタックからCのスタック(通常はOSのスレッドスタック)に切り替え、C関数を呼び出し、その後Goのスタックに戻るという一連の処理を行います。

元のコードでは、Cgo呼び出し時にスタックポインタ(SP)を48バイト減らしていました(SUBQ $48, SP)。これは、Goの内部的なスタック管理や、一部のレジスタの保存に必要なスペースを確保するためのものでした。しかし、Windows x64の呼び出し規約では、レジスタで渡される最初の4つの引数に対応する32バイトのシャドウスペースが必須です。48バイトの確保では、この32バイトのシャドウスペースと、さらに追加で必要なスタック領域(例えば、戻りアドレスやその他のレジスタ保存用)を十分にカバーできていなかった可能性があります。

この修正では、スタックポインタを64バイト減らすように変更されました(SUBQ $64, SP)。これにより、32バイトのシャドウスペースに加えて、さらに32バイトの追加領域が確保されます。この追加領域は、Windows x64の高速呼び出しレジスタ(fast-call registers)がスタックにバックアップされる可能性を考慮したものです。

また、スタックに保存されるレジスタ(gSP)のオフセットも、スタックフレームのサイズ変更に合わせて調整されています。具体的には、32(SP)から48(SP)へ、24(SP)から40(SP)へと変更されています。これは、スタックポインタがより大きく減らされたため、相対的なオフセットも調整する必要があるためです。

この変更により、GoランタイムはCgoを介してC関数を呼び出す際に、Windows x64の呼び出し規約に完全に準拠したスタックフレームを構築できるようになり、スタック関連のクラッシュが解消されました。

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

変更は主にsrc/pkg/runtime/asm_amd64.sファイル内のTEXT runtime·asmcgocall(SB),7,$0セクションに集中しています。

--- a/src/pkg/runtime/asm_amd64.s
+++ b/src/pkg/runtime/asm_amd64.s
@@ -489,19 +489,21 @@ TEXT runtime·asmcgocall(SB),7,$0
  	MOVQ	(g_sched+gobuf_sp)(SI), SP
 
  	// Now on a scheduling stack (a pthread-created stack).
- 	SUBQ	$48, SP
+ 	// Make sure we have enough room for 4 stack-backed fast-call
+ 	// registers as per windows amd64 calling convention.
+ 	SUBQ	$64, SP
  	ANDQ	$~15, SP	// alignment for gcc ABI
- 	MOVQ	DI, 32(SP)	// save g
- 	MOVQ	DX, 24(SP)	// save SP
+ 	MOVQ	DI, 48(SP)	// save g
+ 	MOVQ	DX, 40(SP)	// save SP
  	MOVQ	BX, DI		// DI = first argument in AMD64 ABI
  	MOVQ	BX, CX		// CX = first argument in Win64
  	CALL	AX
 
  	// Restore registers, g, stack pointer.
  	get_tls(CX)
- 	MOVQ	32(SP), DI
+ 	MOVQ	48(SP), DI
  	MOVQ	DI, g(CX)
- 	MOVQ	24(SP), SP
+ 	MOVQ	40(SP), SP
  	RET
 
  // cgocallback(void (*fn)(void*), void *frame, uintptr framesize)

また、この修正を検証するためのテストケースが追加されています。

  • misc/cgo/test/cgo_test.go: TestPrintf関数が追加されました。
  • misc/cgo/test/issue3945.go: 新しいテストファイルとして追加され、Cgoを介してCのprintf関数を呼び出す簡単なテストが含まれています。

コアとなるコードの解説

src/pkg/runtime/asm_amd64.sの変更点に焦点を当てて解説します。

  1. スタックフレームサイズの増加:

    - 	SUBQ	$48, SP
    + 	// Make sure we have enough room for 4 stack-backed fast-call
    + 	// registers as per windows amd64 calling convention.
    + 	SUBQ	$64, SP
    

    SUBQ $48, SPからSUBQ $64, SPへの変更は、スタックポインタを48バイトではなく64バイト減らすことを意味します。これにより、GoランタイムがC関数を呼び出す前に、スタック上に合計64バイトの領域を確保します。この64バイトは、Windows x64呼び出し規約で必須の32バイトのシャドウスペースと、さらに追加のスタック領域(例えば、レジスタの退避など)をカバーするために十分なサイズです。コメントにもあるように、これはWindows amd64の呼び出し規約に従って、4つのスタックバックアップされた高速呼び出しレジスタのための十分なスペースを確保するためです。

  2. レジスタ保存オフセットの調整:

    - 	MOVQ	DI, 32(SP)	// save g
    - 	MOVQ	DX, 24(SP)	// save SP
    + 	MOVQ	DI, 48(SP)	// save g
    + 	MOVQ	DX, 40(SP)	// save SP
    

    スタックポインタが64バイト減らされたため、以前の48バイトのスタックフレームを基準にしていたレジスタの保存オフセットも調整する必要があります。

    • gレジスタ(Goルーチン構造体へのポインタ)の保存先が32(SP)から48(SP)に変更されました。
    • SPレジスタ(Goのスタックポインタ)の保存先が24(SP)から40(SP)に変更されました。 これらの変更は、スタックフレームの基点(SP)がより低いアドレスに移動したため、相対的なオフセットを増やすことで、レジスタが正しいメモリ位置に保存されるようにするためです。
  3. レジスタ復元オフセットの調整:

    - 	MOVQ	32(SP), DI
    + 	MOVQ	48(SP), DI
     	MOVQ	DI, g(CX)
    
  • MOVQ 24(SP), SP
  • MOVQ 40(SP), SP
    レジスタの保存と同様に、復元時も新しいオフセットを使用するように変更されています。これにより、保存された`g`と`SP`の値が正しく復元され、GoランタイムがCgo呼び出しから正常に復帰できるようになります。
    
    

これらの変更により、GoランタイムはWindows/amd64環境でCgoを介してC関数を呼び出す際に、OSの呼び出し規約に厳密に準拠し、スタック関連の不安定性を解消しました。

関連リンク

参考にした情報源リンク