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

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

このコミットは、GoランタイムにおけるARMアーキテクチャ特有のスタックポインタ破損バグを修正するものです。具体的には、runtime.cgocallback_gofunc()内で発生する問題で、runtime.setmg()関数が別の関数(cgo_save_gm)を呼び出す際に、リンクレジスタ(LR)をスタックに保存していなかったことが原因です。この修正により、Cgoコールバック時の安定性が向上し、関連するテストが再有効化されました。

コミット

commit 5b78cee3764ea71722a56dc2e1b33ae7e90e5427
Author: Shenghou Ma <minux.ma@gmail.com>
Date:   Tue Apr 30 04:13:32 2013 +0800

    runtime: fix stack pointer corruption in runtime.cgocallback_gofunc()
    runtime.setmg() calls another function (cgo_save_gm), so it must save
    LR onto stack.
    Re-enabled TestCthread test in misc/cgo/test.
    
    Fixes #4863.
    
    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/9019043

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

https://github.com/golang/go/commit/5b78cee3764ea71722a56dc2e1b33ae7e90e5427

元コミット内容

このコミットは、Goランタイムのruntime.cgocallback_gofunc()関数におけるスタックポインタの破損を修正します。runtime.setmg()関数がcgo_save_gmという別の関数を呼び出す際に、ARMアーキテクチャのリンクレジスタ(LR)をスタックに保存していなかったため、関数呼び出しからの復帰時にスタックポインタが不正な状態になる問題がありました。この修正により、runtime.setmg()がLRを適切に保存するようになり、問題が解決されました。また、このバグのために無効化されていたmisc/cgo/testディレクトリ内のTestCthreadテストが再有効化されています。この修正は、Issue #4863を解決します。

変更の背景

Go言語は、C言語で書かれたライブラリを呼び出すためのCgoというメカニズムを提供しています。Cgoは、GoとCの間の相互運用性を可能にしますが、異なるランタイム環境間でのスタック管理やレジスタの保存・復元は非常に複雑です。

このコミットの背景には、ARMアーキテクチャにおけるCgoコールバックの特定のシナリオで発生するスタックポインタの破損バグがありました。具体的には、CコードからGoコードへのコールバック(runtime.cgocallback_gofuncが関与)の際に、Goランタイムが現在のM(OSスレッド)とG(Goroutine)を設定するためにruntime.setmg()を呼び出します。このsetmg()関数が内部でcgo_save_gmという別の関数を呼び出す際、ARMの関数呼び出し規約に従って、呼び出し元のアドレスを保持するリンクレジスタ(LR)をスタックに保存する必要がありました。しかし、これが適切に行われていなかったため、setmg()から戻る際にLRが不正な値を指し、結果としてスタックポインタが破損し、プログラムがクラッシュする可能性がありました。

この問題は、GoのIssue #4863として報告されており、特にARM環境でのCgoを利用するアプリケーションの安定性に影響を与えていました。このバグのため、misc/cgo/test/cthread.go内のTestCthreadテストがARMアーキテクチャでスキップされるように一時的に無効化されていました。このコミットは、根本原因であるLRの保存漏れを修正し、テストを再有効化することで、Cgoの堅牢性を向上させることを目的としています。

前提知識の解説

このコミットを理解するためには、以下の概念についての知識が必要です。

  1. Goランタイム (Go Runtime):

    • Goプログラムの実行を管理する低レベルのシステムです。スケジューラ、メモリ管理(ガベージコレクション)、Goroutineの管理、Cgoとの連携などを担当します。
    • M (Machine): OSスレッドを表します。Goランタイムは、OSスレッド上でGoroutineを実行します。
    • G (Goroutine): Go言語の軽量な並行処理単位です。OSスレッドとは異なり、Goランタイムによってスケジューリングされます。
    • P (Processor): 論理プロセッサを表し、MとGの間の仲介役となります。Pは実行可能なGoroutineのキューを持ち、MにGoroutineを供給します。
    • スタック (Stack): 関数呼び出しの際に、ローカル変数、引数、戻りアドレスなどを一時的に保存するメモリ領域です。GoのGoroutineはそれぞれ独自のスタックを持ちます。
  2. Cgo:

    • Go言語からC言語のコードを呼び出したり、C言語のコードからGo言語のコードを呼び出したりするためのGoの機能です。
    • 異なる言語間の呼び出しでは、それぞれの言語の関数呼び出し規約(Calling Convention)やメモリ管理の違いを吸収するための特別な処理が必要です。
    • Cgoコールバック: CコードからGoコードの関数を呼び出すことを指します。このプロセスは特に複雑で、GoランタイムがCスタックからGoスタックへの切り替えや、レジスタの状態保存・復元を適切に行う必要があります。
  3. ARMアーキテクチャとアセンブリ (ARM Architecture and Assembly):

    • ARMは、モバイルデバイスなどで広く使われているCPUアーキテクチャです。
    • レジスタ (Registers): CPU内部にある高速な記憶領域です。関数呼び出しの際に引数を渡したり、計算結果を一時的に保持したりします。
    • スタックポインタ (SP: Stack Pointer): 現在のスタックの最上位(または最下位)のアドレスを指すレジスタです。関数呼び出しやローカル変数の確保によって値が変化します。
    • リンクレジスタ (LR: Link Register): 関数呼び出しの際に、呼び出し元に戻るためのアドレス(戻りアドレス)を保存するレジスタです。関数が別の関数を呼び出す場合、LRの値をスタックに保存しないと、元の呼び出し元に戻れなくなり、スタックポインタが破損する原因となります。
    • TEXTディレクティブ: Goのアセンブリ言語で関数を定義する際に使用されます。TEXT runtime·setmg(SB), 7, $-4のような形式で、関数の名前、フラグ、スタックフレームサイズなどを指定します。
      • $-4$0は、関数のスタックフレームサイズを示します。これは、関数がローカル変数やレジスタの保存のために確保するスタック領域のサイズです。負の値は、スタックフレームが動的に決定されることを示唆することがあります。

これらの概念が、Cgoコールバック時のスタック管理の複雑さと、なぜLRの保存が重要であるかを理解する上で不可欠です。

技術的詳細

このバグは、GoランタイムがCgoコールバックを処理する際の、ARMアーキテクチャ特有のスタック管理の不備に起因していました。

  1. Cgoコールバックのフロー:

    • CコードがGo関数を呼び出す際、Goランタイムは特別なエントリポイント(runtime.cgocallback_gofuncなど)を通じて制御を受け取ります。
    • このエントリポイントでは、CスタックからGoスタックへの切り替え、Goroutineのコンテキストの確立、そして現在のOSスレッド(M)とGoroutine(G)の関連付けが行われます。
    • この関連付けを行うのがruntime.setmg(m, g)関数です。
  2. runtime.setmg()の役割と問題点:

    • runtime.setmg(m, g)は、現在のMとGレジスタ(Goランタイムが内部的に使用するレジスタ)を更新し、現在のOSスレッドがどのGoroutineを実行しているかをランタイムに知らせます。
    • 問題は、runtime.setmg()が内部でcgo_save_gmという別の関数を呼び出していた点にありました。cgo_save_gmは、CgoのコンテキストでMとGの情報を保存するための関数です。
    • ARMアーキテクチャの関数呼び出し規約では、関数が別の関数を呼び出す場合、呼び出し元のアドレスを保持するリンクレジスタ(LR)の値をスタックに保存する必要があります。これにより、呼び出された関数から戻った際に、元の実行フローに戻ることができます。
    • しかし、runtime.setmg()のアセンブリコード(src/pkg/runtime/asm_arm.s)では、cgo_save_gmを呼び出す前にLRをスタックに保存していませんでした。
  3. スタックポインタの破損:

    • LRが保存されないままcgo_save_gmが呼び出され、その後にruntime.setmg()が終了しようとすると、LRにはcgo_save_gmからの戻りアドレスが残ったままになります。
    • runtime.setmg()が本来戻るべきアドレス(つまり、runtime.cgocallback_gofunc内のsetmg呼び出しの次の命令のアドレス)が失われてしまいます。
    • これにより、runtime.setmg()からの復帰時に不正なアドレスにジャンプしたり、スタックポインタ(SP)が期待される位置に戻らなかったりして、スタックが破損し、最終的にプログラムのクラッシュや予期せぬ動作を引き起こしていました。
  4. 修正内容:

    • 修正は、src/pkg/runtime/asm_arm.s内のruntime·setmg関数の定義を変更することです。
    • 元の定義はTEXT runtime·setmg(SB), 7, $-4でした。ここで$-4は、スタックフレームサイズが動的に決定されることを示唆していました。
    • 修正後はTEXT runtime·setmg(SB), 7, $0となります。$0は、この関数自体がスタックフレームを確保しないことを意味します。これは、setmgcgo_save_gmを呼び出す際に、cgo_save_gmがLRを保存する責任を持つか、あるいはsetmgの呼び出し元がLRを保存する責任を持つように、呼び出し規約が変更されたことを示唆しています。
    • この変更により、setmgcgo_save_gmを呼び出す際のLRの保存が適切に行われるようになり、スタックポインタの破損が解消されました。
  5. テストの再有効化:

    • このバグが修正されたことで、misc/cgo/test/cthread.go内のTestCthreadテストがARMアーキテクチャでスキップされる必要がなくなりました。
    • runtime.GOARCH == "arm"のチェックとt.Skip()の行が削除され、テストが再び実行されるようになりました。これにより、将来的に同様の回帰バグが発生しないことを保証できます。

この修正は、Goランタイムの低レベルな部分、特にアセンブリ言語とアーキテクチャ固有の挙動を深く理解している必要がある、非常に技術的なものです。

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

このコミットによるコードの変更は、主に以下の2つのファイルにあります。

  1. misc/cgo/test/cthread.go
  2. src/pkg/runtime/asm_arm.s

misc/cgo/test/cthread.go の変更

--- a/misc/cgo/test/cthread.go
+++ b/misc/cgo/test/cthread.go
@@ -8,7 +8,6 @@ package cgotest
 import "C"
 
 import (
-	"runtime"
 	"sync"
 	"testing"
 )
@@ -31,10 +30,6 @@ func Add(x int) {
 }
 
 func testCthread(t *testing.T) {
-	if runtime.GOARCH == "arm" {
-		t.Skip("testCthread disabled on arm")
-	}
-
 	sum.i = 0
 	C.doAdd(10, 6)
 
  • runtimeパッケージのインポートが削除されました。
  • testCthread関数内の、ARMアーキテクチャでテストをスキップする条件分岐(if runtime.GOARCH == "arm" { t.Skip("testCthread disabled on arm") })が削除されました。

src/pkg/runtime/asm_arm.s の変更

--- a/src/pkg/runtime/asm_arm.s
+++ b/src/pkg/runtime/asm_arm.s
@@ -417,7 +417,7 @@ havem:
 	RET
 
 // void setmg(M*, G*); set m and g. for use by needm.
-TEXT runtime·setmg(SB), 7, $-4
+TEXT runtime·setmg(SB), 7, $0
 	MOVW	mm+0(FP), m
 	MOVW	gg+4(FP), g
 
  • runtime·setmg関数のTEXTディレクティブのスタックフレームサイズ指定が変更されました。
    • 変更前: TEXT runtime·setmg(SB), 7, $-4
    • 変更後: TEXT runtime·setmg(SB), 7, $0

コアとなるコードの解説

misc/cgo/test/cthread.go の変更の解説

このファイルは、Cgoのcthreadテストケースです。このテストは、CスレッドからGo関数を呼び出すシナリオを検証します。

  • import "runtime" の削除:

    • 以前はruntime.GOARCHを使用して現在のアーキテクチャをチェックしていたため、runtimeパッケージをインポートする必要がありました。
    • テストスキップのロジックが削除されたため、runtimeパッケージは不要になり、インポートリストから削除されました。これはクリーンアップの一環です。
  • ARMアーキテクチャでのテストスキップロジックの削除:

    • if runtime.GOARCH == "arm" { t.Skip("testCthread disabled on arm") } の行が削除されました。
    • この行は、以前のGoランタイムのバグ(本コミットで修正されるスタックポインタ破損)のために、ARM環境でこのテストが失敗するのを避けるために一時的に追加されていました。
    • バグが修正されたため、このテストはARM環境でも正しく動作するようになり、スキップする必要がなくなりました。これにより、Cgoの重要なテストがすべてのサポート対象アーキテクチャで実行されるようになり、回帰テストの網羅性が向上します。

src/pkg/runtime/asm_arm.s の変更の解説

このファイルは、ARMアーキテクチャ用のGoランタイムのアセンブリコードを含んでいます。

  • TEXT runtime·setmg(SB), 7, $-4 から TEXT runtime·setmg(SB), 7, $0 への変更:
    • これは、runtime·setmg関数の定義におけるスタックフレームサイズの指定を変更するものです。
    • TEXTディレクティブの3番目の引数は、関数のスタックフレームサイズ(関数がローカル変数やレジスタの保存のために確保するスタック領域のサイズ)を示します。
    • $-4: 負の値は、Goのアセンブリでは特別な意味を持つことがあります。この場合、関数がスタックフレームを動的に決定するか、あるいは特定のレジスタ(この場合はLR)の保存を呼び出し元に依存していることを示唆している可能性があります。以前のコードでは、setmgcgo_save_gmを呼び出す際に、LRをスタックに保存する責任が明確でなかったか、あるいは誤って省略されていました。
    • $0: これは、runtime·setmg関数自体が、その関数内でローカル変数やレジスタを保存するためのスタック領域を明示的に確保しないことを意味します。この変更は、setmgcgo_save_gmを呼び出す際のLRの保存方法に関するGoランタイムの内部的な規約変更を反映しています。具体的には、setmgcgo_save_gmを呼び出す前にLRをスタックに保存する責任を負うか、あるいはcgo_save_gmがLRを保存する責任を負うように、呼び出し規約が調整されたことを示唆しています。この修正により、cgo_save_gmへの呼び出し時にLRが適切に保存されるようになり、関数からの復帰時にスタックポインタが破損する問題が解決されました。

このアセンブリレベルの変更は、GoランタイムのCgoコールバックメカニズムの堅牢性を確保するために不可欠であり、ARMアーキテクチャにおける低レベルなスタック管理の正確性を保証します。

関連リンク

参考にした情報源リンク