[インデックス 15526] ファイルの概要
このコミットは、Go言語のruntime/cgo
パッケージにおけるcrosscall2
関数の実装を、GCCアセンブラ(.S
ファイル)からGoツールチェーンのアセンブラ(5a
/6a
/8a
、.s
ファイル)でアセンブルされる形式に移行するものです。この変更の主な目的は、crosscall2
シンボルがGoリンカによって正しく動的にエクスポートされるようにし、特にSWIGのようなツールがGoランタイムと連携する際に発生していたシンボル解決の問題を解決することにあります。
コミット
commit d17506e52d1c6625a204727f4e1fc79ce918a54a
Author: Russ Cox <rsc@golang.org>
Date: Thu Feb 28 22:14:55 2013 -0800
runtime/cgo: make crosscall2 5a/6a/8a-assembled
There is a #pragma dynexport crosscall2, to help SWIG,
and 6l cannot export the symbol if it doesn't get to see it.
R=golang-dev, iant
CC=golang-dev
https://golang.org/cl/7448044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/d17506e52d1c6625a204727f4e1fc79ce918a54a
元コミット内容
runtime/cgo: make crosscall2 5a/6a/8a-assembled
There is a #pragma dynexport crosscall2, to help SWIG,
and 6l cannot export the symbol if it doesn't get to see it.
変更の背景
Go言語のruntime/cgo
パッケージに存在するcrosscall2
関数は、GoとC言語の間の相互運用において非常に重要な役割を担っています。特に、SWIG (Simplified Wrapper and Interface Generator) のようなツールがC/C++ライブラリのGoバインディングを生成する際や、CコードからGoコードへのコールバックを処理する際に利用されます。
crosscall2
のような内部関数が、SWIGによって生成されるような動的リンクライブラリ(DLLや共有オブジェクト)からアクセスされるためには、そのシンボルが動的にエクスポートされている必要があります。このエクスポートは、通常#pragma dynexport crosscall2
のようなディレクティブを通じて指示されます。
しかし、Goのリンカ(6l
はamd64用、5l
は386用、8l
はARM用)は、crosscall2
関数がGCCアセンブラ(.S
拡張子を持つファイル)によってコンパイルされたオブジェクトファイル内に定義されている場合、そのシンボルを適切に認識し、動的にエクスポートすることができませんでした。これは、GoリンカがGCCが生成するオブジェクトファイルのシンボル情報と、Goツールチェーンが期待するシンボル情報の間に不整合があったためと考えられます。結果として、SWIGが生成するコードがcrosscall2
シンボルを見つけられず、GoとCの間の連携が正常に機能しないという問題が発生していました。
このコミットは、このシンボル可視性の問題を解決することを目的としています。crosscall2
の実装を、Goツールチェーンのアセンブラ(5a
, 6a
, 8a
)によって直接処理される.s
拡張子を持つアセンブリファイルに移行することで、リンカがシンボルを正しく認識し、動的にエクスポートできるようになります。
前提知識の解説
cgo
cgo
は、Go言語とC言語の間の相互運用を可能にするGoの組み込み機能です。GoプログラムからC関数を呼び出したり、CプログラムからGo関数を呼び出したりするために使用されます。これにより、既存のC/C++ライブラリをGoプロジェクトで再利用したり、パフォーマンスが重要な部分をCで記述したりすることが可能になります。cgo
は、GoとCの間の呼び出し規約の変換、スタックの管理、メモリの受け渡しなどを自動的に処理するための低レベルなメカニズムを提供します。
crosscall2
crosscall2
は、Goのruntime/cgo
パッケージ内に存在する内部関数です。その主な役割は、CコードからGoコードへのコールバックを安全かつ効率的に処理することです。Cの世界からGoの世界へ制御が移る際、Goランタイムのコンテキスト(例えば、現在のゴルーチンやM(OSスレッド)の状態)を適切に設定し、レジスタの状態を保存・復元する必要があります。crosscall2
は、これらの低レベルなアセンブリ操作を実行し、CからGo関数を呼び出すための橋渡しをします。
SWIG (Simplified Wrapper and Interface Generator)
SWIGは、C/C++で書かれたライブラリを、Go、Python、Java、Rubyなどを含む様々なスクリプト言語や高水準言語から利用できるようにするためのオープンソースツールです。SWIGは、C/C++のヘッダファイル(.h
ファイル)を解析し、ターゲット言語のバインディングコードを自動生成します。この生成されたバインディングコードは、C/C++関数をターゲット言語の関数として呼び出せるようにするためのラッパーを提供します。Goの場合、SWIGが生成するコード内で、GoとCの間のコールバックやデータ変換にcgo
の機能、特にcrosscall2
のような内部関数が利用されることがあります。
Goリンカ (6l, 5l, 8l)
Goツールチェーンは、Goのソースコードをコンパイルし、実行可能ファイルを生成するための一連のツールを含んでいます。その中には、アセンブラ、コンパイラ、リンカが含まれます。
6l
: amd64 (64-bit x86) アーキテクチャ用のGoリンカ。5l
: 386 (32-bit x86) アーキテクチャ用のGoリンカ。8l
: ARM アーキテクチャ用のGoリンカ。 これらのリンカは、Goのコンパイラやアセンブラによって生成されたオブジェクトファイル(.o
ファイル)を結合し、最終的な実行可能ファイルや共有ライブラリを作成します。シンボル解決、外部ライブラリのリンク、アドレスの再配置などが主な役割です。
#pragma dynexport
#pragma
は、C/C++コンパイラに対する特別な指示(プラグマディレクティブ)です。#pragma dynexport
は、特定の関数や変数を、コンパイルされるモジュール(実行可能ファイルや共有ライブラリ)の外部に公開(エクスポート)するようにコンパイラに指示するために使用されます。これにより、他のプログラムやライブラリがそのシンボルを動的にロードし、利用できるようになります。これは、Windowsの__declspec(dllexport)
やLinuxの__attribute__((visibility("default")))
に似た機能を提供します。
Goアセンブラ (.s
ファイル) とGCCアセンブラ (.S
ファイル)
Goプロジェクトでは、アセンブリ言語のコードを記述する際に、主に2種類のファイル拡張子が使用されます。
.s
ファイル: Goツールチェーンのアセンブラ(5a
,6a
,8a
など)によってアセンブルされるアセンブリ言語のソースファイルです。これらのファイルはGo独自のPlan 9アセンブリ構文を使用します。Goのランタイムや標準ライブラリの低レベルな部分でよく使われます。Goリンカは、Goアセンブラによって生成されたオブジェクトファイルのシンボル情報を、Goツールチェーンの内部構造と整合性の取れた形で処理できます。.S
ファイル: GCCによってアセンブルされるアセンブリ言語のソースファイルです。この拡張子は大文字のS
であり、Cプリプロセッサによって前処理されることを意味します。そのため、Cマクロや#include
ディレクティブを使用できます。通常、GNUアセンブラ(GAS)の構文を使用します。Goプロジェクトでは、Cコードと連携するcgo
の文脈で、GCCでコンパイルされるアセンブリコードが必要な場合に使用されることがあります。
このコミットの背景にある問題は、Goリンカが、GCCアセンブラによって生成された.S
ファイル内のシンボルを、動的エクスポートのために適切に「見ることができなかった」という点にあります。これは、GoツールチェーンとGCCツールチェーンの間でのシンボル情報の表現方法や処理方法の違いに起因していました。
技術的詳細
問題の特定
crosscall2
関数は、GoランタイムとCコードの間の重要な接点であり、特にSWIGのようなツールがGoの関数をC側から呼び出す際に必要とされます。この関数は、#pragma dynexport
ディレクティブによって動的にエクスポートされることが意図されていました。しかし、crosscall2
がsrc/pkg/runtime/cgo/gcc_*.S
ファイル、すなわちGCCアセンブラによって処理されるアセンブリファイルで定義されていたため、Goツールチェーンのリンカ(6l
など)がこのシンボルを正しく認識し、動的リンクテーブルに含めることができませんでした。
Goリンカは、Goツールチェーンのアセンブラ(5a
/6a
/8a
)によって生成されたオブジェクトファイルのシンボル情報を、その内部構造と整合性の取れた形で処理するように設計されています。一方、GCCが生成するオブジェクトファイルのシンボル情報は、Goリンカが期待する形式とは異なる場合があり、これが「リンカがシンボルを見られない」という問題を引き起こしていました。結果として、SWIGが生成するCコードがcrosscall2
を呼び出そうとしても、シンボルが見つからずにリンクエラーや実行時エラーが発生する可能性がありました。
解決策
このコミットの解決策は、crosscall2
関数の実装を、GCCアセンブラで処理される.S
ファイルから、Goツールチェーンのアセンブラで処理される.s
ファイルに完全に移行することです。
具体的には、以下の変更が行われました。
- 新しいGoアセンブリファイルの追加: 各アーキテクチャ(386, amd64, ARM)向けに、
crosscall2
関数をGoアセンブリ構文で記述した新しいファイル(src/pkg/runtime/cgo/asm_386.s
,src/pkg/runtime/cgo/asm_amd64.s
,src/pkg/runtime/cgo/asm_arm.s
)が追加されました。 - 既存のGCCアセンブリファイルからの削除: 既存の
src/pkg/runtime/cgo/gcc_386.S
,src/pkg/runtime/cgo/gcc_amd64.S
,src/pkg/runtime/cgo/gcc_arm.S
から、crosscall2
の定義が削除されました。 callbacks.c
のプラグマ変更:src/pkg/runtime/cgo/callbacks.c
では、#pragma cgo_static_import
が#pragma cgo_import_static
に変更されました。これはGo 1.1で導入された新しいプラグマであり、静的インポートの意図をより明確にするための構文的な改善です。この変更はcrosscall2
の動的エクスポート問題とは直接関係ありませんが、関連するコードのクリーンアップの一環として行われました。
この移行により、crosscall2
のシンボル情報はGoリンカが期待する形式で生成されるようになり、リンカがこのシンボルを正しく認識し、動的にエクスポートできるようになりました。これにより、SWIGなどのツールがcrosscall2
を問題なく利用できるようになり、GoとCの間の相互運用性が向上しました。
コアとなるコードの変更箇所
このコミットでは、以下のファイルが変更されています。
src/pkg/runtime/cgo/asm_386.s
: 新規追加。32-bit x86 (386) アーキテクチャ用のcrosscall2
アセンブリコードが記述されています。src/pkg/runtime/cgo/asm_amd64.s
: 新規追加。64-bit x86 (amd64) アーキテクチャ用のcrosscall2
アセンブリコードが記述されています。src/pkg/runtime/cgo/asm_arm.s
: 新規追加。ARMアーキテクチャ用のcrosscall2
アセンブリコードが記述されています。src/pkg/runtime/cgo/callbacks.c
:#pragma cgo_static_import
ディレクティブが#pragma cgo_import_static
に変更されています。src/pkg/runtime/cgo/gcc_386.S
: 既存のcrosscall2
関数の定義が削除されています。src/pkg/runtime/cgo/gcc_amd64.S
: 既存のcrosscall2
関数の定義が削除されています。src/pkg/runtime/cgo/gcc_arm.S
: 既存のcrosscall2
関数の定義が削除されています。
合計で7つのファイルが変更され、100行が追加され、98行が削除されています。これは、主にアセンブリコードの移動と、それに伴う既存コードの削除によるものです。
コアとなるコードの解説
src/pkg/runtime/cgo/asm_386.s
(32-bit x86)
このファイルには、32-bit x86アーキテクチャ向けのcrosscall2
関数のGoアセンブリ実装が含まれています。
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
/*
* void crosscall2(void (*fn)(void*, int32), void*, int32)
* Save registers and call fn with two arguments.
*/
TEXT crosscall2(SB),7,$0
PUSHL BP // ベースポインタをスタックに保存
MOVL SP, BP // スタックポインタをベースポインタに設定
PUSHL BX // 呼び出し元が保存すべきレジスタを保存
PUSHL SI
PUSHL DI
SUBL $8, SP // 2つの引数(void*とint32)のためにスタックに8バイト確保
MOVL 16(BP), AX // 第2引数 (int32) をAXレジスタにロード
MOVL AX, 4(SP) // AXの内容をスタックにプッシュ (第2引数)
MOVL 12(BP), AX // 第1引数 (void*) をAXレジスタにロード
MOVL AX, 0(SP) // AXの内容をスタックにプッシュ (第1引数)
MOVL 8(BP), AX // 関数ポインタfnをAXレジスタにロード
CALL AX // AXに格納されたアドレス(fn)を呼び出す
ADDL $8, SP // スタックから引数をポップ (スタックポインタを元に戻す)
POPL DI // 保存したレジスタを復元
POPL SI
POPL BX
POPL BP // ベースポインタを復元
RET // 呼び出し元に戻る
このコードは、標準的な関数呼び出し規約に従い、呼び出し元が保存すべきレジスタ(BX
, SI
, DI
)を保存し、引数をスタックにプッシュしてから、指定された関数ポインタfn
を呼び出します。関数呼び出し後、スタックをクリーンアップし、保存したレジスタを復元して呼び出し元に戻ります。
src/pkg/runtime/cgo/asm_amd64.s
(64-bit x86)
このファイルには、64-bit x86 (amd64) アーキテクチャ向けのcrosscall2
関数のGoアセンブリ実装が含まれています。
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
/*
* void crosscall2(void (*fn)(void*, int32), void*, int32)
* Save registers and call fn with two arguments.
*/
TEXT crosscall2(SB),7,$0
SUBQ $0x58, SP /* スタックポインタを調整し、レジスタ保存領域と32バイトアライメントを確保 */
MOVQ BX, 0x10(SP) // 呼び出し元が保存すべきレジスタをスタックに保存
MOVQ BP, 0x18(SP)
MOVQ R12, 0x20(SP)
MOVQ R13, 0x28(SP)
MOVQ R14, 0x30(SP)
MOVQ R15, 0x38(SP)
#ifdef GOOS_windows
// Win64 save RBX, RBP, RDI, RSI, RSP, R12, R13, R14, and R15
MOVQ DI, 0x40(SP) // Windows固有の呼び出し規約で保存すべきレジスタ
MOVQ SI, 0x48(SP)
MOVQ DX, 0(SP) /* arg */ // Windowsでは引数がレジスタで渡される
MOVQ R8, 8(SP) /* argsize (includes padding) */
CALL CX /* fn */ // Windowsでは関数ポインタがCXレジスタで渡される
MOVQ 0x40(SP), DI // Windows固有のレジスタを復元
MOVQ 0x48(SP), SI
#else
MOVQ SI, 0(SP) /* arg */ // Unix-likeシステムでは引数がレジスタで渡される
MOVQ DX, 8(SP) /* argsize (includes padding) */
CALL DI /* fn */ // Unix-likeシステムでは関数ポインタがDIレジスタで渡される
#endif
MOVQ 0x10(SP), BX // 保存したレジスタを復元
MOVQ 0x18(SP), BP
MOVQ 0x20(SP), R12
MOVQ 0x28(SP), R13
MOVQ 0x30(SP), R14
MOVQ 0x38(SP), R15
ADDQ $0x58, SP // スタックポインタを元に戻す
RET // 呼び出し元に戻る
amd64アーキテクチャでは、WindowsとUnix-likeシステムで異なる呼び出し規約(Calling Convention)が使用されるため、条件付きコンパイル(#ifdef GOOS_windows
)によって分岐しています。どちらのパスでも、呼び出し元が保存すべきレジスタを保存し、引数を適切に配置してから関数を呼び出し、その後レジスタを復元して戻ります。
src/pkg/runtime/cgo/asm_arm.s
(ARM)
このファイルには、ARMアーキテクチャ向けのcrosscall2
関数のGoアセンブリ実装が含まれています。
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
/*
* void crosscall2(void (*fn)(void*, int32), void*, int32)
* Save registers and call fn with two arguments.
*/
TEXT crosscall2(SB),7,$-4
/*
* We still need to save all callee save register as before, and then
* push 2 args for fn (R1 and R2).
* Also note that at procedure entry in 5c/5g world, 4(R13) will be the
* first arg, so we must push another dummy reg (R0) for 0(R13).
* Additionally, cgo_tls_set_gm will clobber R0, so we need to save R0
* nevertheless.
*/
MOVM.WP [R0, R1, R2, R4, R5, R6, R7, R8, R9, R10, R11, R12, R14], (R13) // 複数のレジスタをスタックに保存
BL x_cgo_load_gm(SB) // gとm(GoのゴルーチンとM構造体)をTLSからロード
MOVW PC, R14 // 現在のプログラムカウンタをリンクレジスタR14に保存
MOVW -4(R13), PC // スタックから関数ポインタをロードし、それにジャンプ
MOVM.IAW (R13), [R0, R1, R2, R4, R5, R6, R7, R8, R9, R10, R11, R12, PC] // 保存したレジスタを復元し、PCを復元して呼び出し元に戻る
ARMアーキテクチャのコードは、Goの5c/5gツールチェーンの呼び出し規約と、cgo_tls_set_gm
関数がR0
レジスタを破壊する可能性を考慮しています。複数のレジスタをスタックに保存し、x_cgo_load_gm
を呼び出してGoランタイムのコンテキストを設定した後、関数ポインタを介して目的のGo関数を呼び出します。最後に、保存したレジスタを復元して呼び出し元に戻ります。
src/pkg/runtime/cgo/callbacks.c
の変更
-#pragma cgo_static_import x_cgo_init
+#pragma cgo_import_static x_cgo_init
extern void x_cgo_init(G*);
void (*_cgo_init)(G*) = x_cgo_init;
-#pragma cgo_static_import x_cgo_malloc
+#pragma cgo_import_static x_cgo_malloc
extern void x_cgo_malloc(void*);
void (*_cgo_malloc)(void*) = x_cgo_malloc;
-#pragma cgo_static_import x_cgo_free
+#pragma cgo_import_static x_cgo_free
extern void x_cgo_free(void*);
void (*_cgo_free)(void*) = x_cgo_free;
-#pragma cgo_static_import x_cgo_thread_start
+#pragma cgo_import_static x_cgo_thread_start
extern void x_cgo_thread_start(void*);
void (*_cgo_thread_start)(void*) = x_cgo_thread_start;
この変更は、#pragma cgo_static_import
を#pragma cgo_import_static
に置き換えるものです。これはGo 1.1で導入された新しいプラグマであり、静的インポートの意図をより明確にするための構文的な改善です。機能的な動作に大きな変更はありませんが、コードの可読性と正確性を向上させるためのクリーンアップの一環として行われました。
関連リンク
- Go CL (Change List): https://golang.org/cl/7448044
参考にした情報源リンク
- Go言語の公式ドキュメント (cgo): https://go.dev/blog/c-go-is-not-c
- SWIG公式ウェブサイト: http://www.swig.org/
- Goアセンブリのドキュメント (Go 1.2以降の形式): https://go.dev/doc/asm (このコミットはGo 1.1以前の時期のものですが、基本的な概念は共通しています)
- Goのリンカに関する情報 (非公式): https://www.ardanlabs.com/blog/2013/11/go-linker-part-1-object-files.html