[インデックス 13025] ファイルの概要
このコミットは、Go言語のランタイムにおいて、Linux/ARMアーキテクチャでのCgoサポートを強化するためのものです。特に、GoのランタイムとC言語のコード間の相互運用性、スタック管理、そしてGoroutine (g) とMachine (m) の状態管理に焦点を当てています。
コミット
commit 5cffce611a9fb698cbe8f3ab7b24429f269b5d6d
Author: Shenghou Ma <minux.ma@gmail.com>
Date: Fri May 4 18:20:09 2012 +0800
runtime: cgo support for Linux/ARM
Part 2 of CL 5601044 (cgo: Linux/ARM support)
R=dave, rsc
CC=golang-dev
https://golang.org/cl/5989057
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/5cffce611a9fb698cbe8f3ab7b24429f269b5d6d
元コミット内容
このコミットは、GoランタイムにおけるLinux/ARM環境でのCgoサポートの第2弾です。具体的には、GoとCのコード間でスタックを切り替えたり、Goroutine (g) とMachine (m) の状態を適切に保存・復元したりするための低レベルなアセンブリコードとCコードの変更が含まれています。これにより、GoプログラムがCライブラリを呼び出したり、CライブラリからGoのコールバックを受け取ったりする際の安定性と正確性が向上します。
変更の背景
Go言語は、その効率的な並行処理モデル(Goroutine)と独自のランタイムによって、高いパフォーマンスと生産性を提供します。しかし、既存のC言語で書かれたライブラリやシステムコールを利用するためには、GoとCのコード間で安全かつ効率的に連携するメカニズムが必要です。これがCgoの役割です。
ARMアーキテクチャは、モバイルデバイスや組み込みシステムで広く利用されており、Goプログラムをこれらの環境で実行する際には、Cgoによる既存のCライブラリとの連携が不可欠となります。特に、Linux/ARM環境では、Goのランタイムが管理するスタックと、C言語のコードが使用するスタック(通常はOSが管理するスレッドスタック)の間で、スタックポインタやレジスタの状態を正確に切り替える必要があります。
このコミットは、以前のCgoサポートの取り組み(CL 5601044)に続くもので、特に以下の課題に対処しています。
- スタックの切り替え: GoのGoroutineは小さなスタックを持ち、必要に応じて拡張されます。一方、Cコードは通常、より大きな固定サイズのスタックを使用します。Cgo呼び出しの際には、GoのスタックからCのスタックへ、またはその逆へと安全に切り替える必要があります。
- Goroutine (g) と Machine (m) の状態管理: Goランタイムは、現在のGoroutine (g) とそれを実行しているOSスレッド(Machine, m)の情報を内部的に管理しています。Cgo呼び出し中にこれらの情報が失われたり、不正な状態になったりすると、ランタイムの動作が不安定になったり、デバッグが困難になったりします。特に、シグナルハンドラのような非同期イベントが発生した場合に、
g
とm
の情報を正確に保持することが重要です。 - ARM特有のレジスタとABI: ARMアーキテクチャには独自のレジスタセットとアプリケーションバイナリインターフェース (ABI) があります。GoのランタイムとCコンパイラ(GCC)が生成するコードの間で、レジスタの使用方法や関数呼び出し規約が異なるため、これらを適切に橋渡しする必要があります。
- スレッドローカルストレージ (TLS) の利用:
g
とm
の情報をCコードからアクセスできるように、スレッドローカルストレージ (TLS) を利用するメカニズムが必要でした。これにより、Cgo呼び出し中にg
とm
のポインタを安全に保存・復元できます。
これらの課題を解決することで、Linux/ARM環境でのGoとCgoの安定性と信頼性を向上させ、より複雑なGoアプリケーションがCライブラリと連携できるようになります。
前提知識の解説
このコミットを理解するためには、以下の概念について基本的な知識が必要です。
-
Goランタイム:
- Goroutine (g): Goの軽量な実行単位。OSスレッドよりもはるかに軽量で、数百万個作成することも可能です。それぞれが独自のスタックを持ちます。
- Machine (m): OSスレッドを表します。Goランタイムは、複数のGoroutineを少数のOSスレッド上で多重化して実行します。
- Scheduler: Goroutineを
m
に割り当て、実行を管理するGoランタイムのコンポーネント。 - Stack: Goroutineは動的に伸縮するスタックを使用します。関数呼び出しやローカル変数がここに格納されます。
gobuf
: Goroutineの実行コンテキスト(スタックポインタ、プログラムカウンタ、現在のGoroutineなど)を保存するための構造体。gosave
やgogo
などの関数で利用されます。
-
Cgo:
- GoプログラムからC言語の関数を呼び出したり、C言語の関数からGoの関数を呼び出したりするためのGoの機能。
- GoとCの間でデータを受け渡すための規約や、スタックの切り替えメカニズムを提供します。
-
ARMアーキテクチャ:
- レジスタ: ARMプロセッサには、汎用レジスタ (R0-R12)、スタックポインタ (SP/R13)、リンクレジスタ (LR/R14)、プログラムカウンタ (PC/R15) などがあります。
- ABI (Application Binary Interface): 関数呼び出し規約、レジスタの使用規約、スタックフレームのレイアウトなど、バイナリレベルでのインターフェースを定義します。GoのランタイムとGCCが生成するCコードは、それぞれ異なるABIを使用する場合があります。
- スレッドローカルストレージ (TLS): 各スレッドが独自のデータを持つことができるメモリ領域。
g
とm
のポインタをスレッドごとに保存するために利用されます。
-
アセンブリ言語:
- CPUが直接実行できる機械語に対応する低レベル言語。Goランタイムのコア部分は、パフォーマンスとOSとの直接的な連携のためにアセンブリ言語で書かれています。
- ARMアセンブリでは、
MOVW
(Move Word),BL
(Branch with Link),CMP
(Compare),BEQ
(Branch if Equal),RET
(Return) などの命令が使われます。
-
pthread
:- POSIXスレッドのAPI。C言語でマルチスレッドプログラミングを行う際に使用されます。
pthread_create
で新しいスレッドを作成したり、pthread_attr_getstacksize
でスレッドのスタックサイズを取得したりします。
- POSIXスレッドのAPI。C言語でマルチスレッドプログラミングを行う際に使用されます。
技術的詳細
このコミットの技術的な核心は、GoのランタイムがCgo呼び出しを処理する際に、GoのGoroutineコンテキストとCの実行コンテキストの間でシームレスに切り替える方法にあります。特にARMアーキテクチャの特性を考慮し、以下のメカニズムが導入または強化されています。
-
g
とm
のTLS管理:src/pkg/runtime/cgo/gcc_linux_arm.c
に、__aeabi_read_tp
,cgo_tls_set_gm
,cgo_tls_get_gm
というアセンブリインライン関数が追加されています。これらは、ARMのTLSポインタ(通常はTPレジスタまたは特定のシステムコールで取得)を利用して、現在のGoroutine (g
) とMachine (m
) のポインタをスレッドローカルストレージに保存・復元します。cgo_load_gm
とcgo_save_gm
という関数ポインタが導入され、Cgoが利用可能な場合に動的リンカによってこれらのTLS関数が設定されます。これにより、Goランタイムの様々な場所(_rt0_arm
,gosave
,gogo
,gogocall
,sigtramp
など)で、Cgo呼び出しの前後やシグナルハンドリング時にg
とm
の情報を安全に保存・復元できるようになります。
-
スタック切り替えとABI適合:
runtime·asmcgocall
(Go側からCを呼び出す) とruntime·cgocallback
(C側からGoを呼び出す) の実装がsrc/pkg/runtime/asm_arm.s
に追加されました。runtime·asmcgocall
: Goのスケジューラースタック(m->g0
スタック)に切り替えてからC関数を呼び出します。CのABI(Application Binary Interface)に適合するようにスタックアライメントを調整し、Goのg
とm
の情報を保存します。C関数からの戻り時には、元のGoroutineのスタックとコンテキストに復元します。runtime·cgocallback
: CコードからGoのコールバック関数が呼び出された際に使用されます。m->g0
スタックからm->curg
スタック(現在のGoroutineのスタック)に切り替え、Goのコールバック関数を実行します。この際、トレースバックが正しく機能するように、gobuf.pc
(プログラムカウンタ)をスタックにプッシュするなどの工夫がされています。src/pkg/runtime/cgo/gcc_arm.S
には、GoのランタイムとCコンパイラ(GCC)が生成するコードの間でレジスタを保存・復元し、スタックを切り替えるためのcrosscall_arm2
とcrosscall2
というアセンブリ関数が実装されています。これらは、ARM EABI(Embedded Application Binary Interface)とGoの5c/5gツールチェーンのABIの違いを吸収します。
-
スレッド作成とスタックガード:
libcgo_sys_thread_start
関数がsrc/pkg/runtime/cgo/gcc_linux_arm.c
に実装され、pthread_create
を使用して新しいOSスレッドを作成します。この際、GoのGoroutine (g
) のスタックガード(スタックオーバーフロー検出のための境界)を適切に設定します。xinitcgo
関数は、初期スレッドのg
とm
をTLSに保存し、スタックガードを設定します。
-
シグナルハンドリングの強化:
src/pkg/runtime/signal_linux_arm.c
のruntime·sighandler
では、シグナル発生時に現在のGoroutine (gp
) とMachine (m
) のポインタをレジスタ (r->arm_r10
,r->arm_r9
) に保存するようになりました。これにより、Cコード実行中にシグナルが発生した場合でも、Goランタイムがg
とm
の情報を正確に把握し、適切なパニック処理などを行えるようになります。src/pkg/runtime/sys_linux_arm.s
のruntime·sigtramp
(シグナルハンドラの入り口)では、cgo_load_gm
を呼び出してg
とm
の情報をTLSからロードする処理が追加されました。これは、シグナルが外部のCコードコンテキストで発生した場合に、Goランタイムがg
とm
の情報を利用できるようにするためです。
これらの変更により、GoのランタイムはLinux/ARM環境でCgoをより堅牢にサポートし、GoとCのコード間の複雑な相互作用を安全に処理できるようになります。
コアとなるコードの変更箇所
このコミットで変更された主要なファイルと、その中でのコアとなる変更箇所は以下の通りです。
-
src/pkg/runtime/asm_arm.s
:_rt0_arm
: Goランタイムの初期化ルーチン。initcgo
が設定されている場合に呼び出す処理が追加されました。runtime·gosave
,runtime·gogo
,runtime·gogocall
: Goroutineのコンテキスト保存・復元を行う関数。Cgo呼び出しの前後でcgo_save_gm
を呼び出し、g
とm
の情報を保存するロジックが追加されました。runtime·asmcgocall
: GoからCを呼び出すためのアセンブリ関数。スタック切り替え、レジスタ保存、ABI適合のためのアライメント調整、g
とm
の保存・復元ロジックが実装されました。runtime·cgocallback
: CからGoを呼び出すためのアセンブリ関数。スタック切り替え、gobuf.pc
の保存、runtime·cgocallbackg
の呼び出し、g
とm
の復元ロジックが実装されました。
-
src/pkg/runtime/cgo/gcc_arm.S
:crosscall_arm2
: GoのランタイムからCの関数を呼び出す際に使用されるアセンブリ関数。ARM EABIとGoのABIの違いを吸収し、レジスタ(特にcallee-saveレジスタ)の保存・復元とcgo_tls_set_gm
によるg
とm
のTLS保存を行います。crosscall2
: 汎用的なクロス呼び出し関数。同様にレジスタの保存・復元とcgo_tls_get_gm
によるg
とm
のTLS復元を行います。
-
src/pkg/runtime/cgo/gcc_linux_arm.c
:__aeabi_read_tp
,cgo_tls_set_gm
,cgo_tls_get_gm
: ARMのTLSポインタを利用してg
とm
のポインタをTLSに保存・復元するためのアセンブリインライン関数。xinitcgo
: Cgoの初期化関数。初期スレッドのg
とm
をTLSに保存し、スタックガードを設定します。libcgo_sys_thread_start
: 新しいOSスレッドをpthread_create
で作成し、GoのGoroutineのスタックガードを適切に設定します。threadentry
:pthread_create
で作成されたスレッドのエントリポイント。crosscall_arm2
を呼び出してGoのランタイムに制御を渡します。
-
src/pkg/runtime/cgocall.c
:cgo_load_gm
,cgo_save_gm
:g
とm
のTLS操作を行う関数ポインタの宣言。ARMアーキテクチャでのみ使用されます。unwindm
: スタックアンワインド処理にARM ('5'
) のケースが追加されました。
-
src/pkg/runtime/signal_linux_arm.c
:runtime·sighandler
: シグナルハンドラ。シグナル発生時に現在のg
とm
のポインタをレジスタに保存するロジックが追加されました。
-
src/pkg/runtime/sys_linux_arm.s
:runtime·sigtramp
: シグナルハンドラの入り口。cgo_load_gm
を呼び出してg
とm
の情報をTLSからロードする処理が追加されました。これは、外部Cコードコンテキストでシグナルが発生した場合にGoランタイムがg
とm
の情報を利用できるようにするためです。
コアとなるコードの解説
このコミットの主要な目的は、GoのランタイムがCgo呼び出しを処理する際に、GoのGoroutineコンテキストとCの実行コンテキストの間でシームレスに切り替えることを可能にすることです。特にARMアーキテクチャの特性を考慮し、以下の点が重要です。
-
g
とm
のTLSによる管理:- Goランタイムは、現在のGoroutine (
g
) とそれを実行しているOSスレッド (m
) のポインタを、特定のレジスタ(ARMではR10とR9)に保持しています。しかし、Cコードが実行されている間は、これらのレジスタがCコードによって上書きされる可能性があります。 - この問題を解決するため、
cgo_tls_set_gm
とcgo_tls_get_gm
という関数が導入されました。これらは、ARMのTLS(スレッドローカルストレージ)を利用して、Cgo呼び出しの前にg
とm
のポインタをTLSに保存し、Cgo呼び出しから戻った後にTLSから復元します。これにより、Cコードの実行中にg
とm
のレジスタが変更されても、Goランタイムは常に正しいg
とm
の情報を参照できるようになります。 - 特に、シグナルハンドラ (
runtime·sigtramp
) のような非同期に割り込む可能性のあるコードパスでは、g
とm
の情報を確実にロードすることが重要です。sigtramp
にcgo_load_gm
の呼び出しが追加されたのはこのためです。
- Goランタイムは、現在のGoroutine (
-
asmcgocall
とcgocallback
によるスタックとコンテキストの切り替え:- GoからCを呼び出す (
runtime·asmcgocall
) 際、GoのGoroutineスタックからOSスレッドのスタック(m->g0
スタック)に切り替えます。これは、C関数がGoのスタック規約ではなく、OSのスタック規約に従って動作するためです。asmcgocall
は、CのABIに適合するようにスタックアライメントを調整し、C関数を呼び出します。C関数が完了すると、元のGoroutineのスタックとコンテキストに安全に戻ります。 - CからGoのコールバックを呼び出す (
runtime·cgocallback
) 際も同様に、OSスレッドのスタックからGoのGoroutineスタック(m->curg
スタック)に切り替えます。この関数は、GoのランタイムがCgoコールバックを処理するためのエントリポイントとして機能し、トレースバックが正しく機能するようにgobuf.pc
をスタックに保存するなどの工夫がされています。
- GoからCを呼び出す (
-
crosscall_arm2
とcrosscall2
によるABIブリッジング:gcc_arm.S
に実装されたこれらのアセンブリ関数は、Goのツールチェーン(5c/5g)とGCCのABIの違いを吸収する役割を担います。特に、ARM EABIでは特定のレジスタ(R4-R11)がcallee-save(呼び出された関数が保存・復元する責任がある)ですが、Goのツールチェーンでは異なる場合があります。これらの関数は、GoとCの間で関数を呼び出す際に、必要なレジスタを適切に保存・復元することで、ABIの不整合による問題を回避します。
これらの変更は、GoのランタイムがARMアーキテクチャ上でCgoを介してCコードと安全かつ効率的に相互作用するための基盤を確立します。これにより、Goプログラムは既存のCライブラリをより広範に利用できるようになり、ARMベースのシステムでのGoアプリケーションの開発が促進されます。
関連リンク
- Go CL 5989057 (このコミットのGo Code Reviewサイトのリンク)
- Go言語のCgoドキュメント (一般的なCgoの概念について)
- Go言語のランタイムに関するドキュメント (Goのメモリ管理とランタイムの概要)
参考にした情報源リンク
- Go言語のソースコード (特に
src/pkg/runtime
およびsrc/pkg/runtime/cgo
ディレクトリ) - ARMアーキテクチャのリファレンスマニュアル (ARM ABIなど)
- GCCのドキュメント (ARMターゲットのABIに関する情報)
- POSIXスレッド (
pthread
) のドキュメント - スレッドローカルストレージ (TLS) に関する一般的な情報
- Go言語のIssueトラッカーやメーリングリストでの議論 (CgoやARMサポートに関するもの)