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

[インデックス 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ディレクティブによって動的にエクスポートされることが意図されていました。しかし、crosscall2src/pkg/runtime/cgo/gcc_*.Sファイル、すなわちGCCアセンブラによって処理されるアセンブリファイルで定義されていたため、Goツールチェーンのリンカ(6lなど)がこのシンボルを正しく認識し、動的リンクテーブルに含めることができませんでした。

Goリンカは、Goツールチェーンのアセンブラ(5a/6a/8a)によって生成されたオブジェクトファイルのシンボル情報を、その内部構造と整合性の取れた形で処理するように設計されています。一方、GCCが生成するオブジェクトファイルのシンボル情報は、Goリンカが期待する形式とは異なる場合があり、これが「リンカがシンボルを見られない」という問題を引き起こしていました。結果として、SWIGが生成するCコードがcrosscall2を呼び出そうとしても、シンボルが見つからずにリンクエラーや実行時エラーが発生する可能性がありました。

解決策

このコミットの解決策は、crosscall2関数の実装を、GCCアセンブラで処理される.Sファイルから、Goツールチェーンのアセンブラで処理される.sファイルに完全に移行することです。

具体的には、以下の変更が行われました。

  1. 新しい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)が追加されました。
  2. 既存のGCCアセンブリファイルからの削除: 既存のsrc/pkg/runtime/cgo/gcc_386.S, src/pkg/runtime/cgo/gcc_amd64.S, src/pkg/runtime/cgo/gcc_arm.Sから、crosscall2の定義が削除されました。
  3. 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で導入された新しいプラグマであり、静的インポートの意図をより明確にするための構文的な改善です。機能的な動作に大きな変更はありませんが、コードの可読性と正確性を向上させるためのクリーンアップの一環として行われました。

関連リンク

参考にした情報源リンク