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

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

このコミットは、Goコンパイラのcmd/5gcmd/6gcmd/8g(それぞれARM、x86-64、x86アーキテクチャ向けのコンパイラ)において、組み込みトランポリン(embedded trampolines)に引数のサイズ情報を提供する変更を加えています。これにより、特殊な方法で生成されるこれらの関数が、より正確なスタックおよび引数処理を行えるようになります。

コミット

commit 31be5deae44efce520817b88cc3fd73f1bbf5788
Author: Carl Shapiro <cshapiro@google.com>
Date:   Fri May 31 13:34:57 2013 -0700

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

https://github.com/golang/go/commit/31be5deae44efce520817b88cc3fd73f1bbf5788

元コミット内容

    cmd/5g, cmd/6g, cmd/8g: provide embedded trampolines with argument size information
    
    An embedded trampoline is a function that exists to marshal
    a receiver of type *S to a receiver of type *T when T is an
    embedded field in S.
    
    Embedded trampolines are generated by a special path through
    the compiler and are not subject to the general analysis and
    annotation done to functions.  Their effects must be provided
    explicitly.
    
    R=golang-dev, r, daniel.morsing, minux.ma
    CC=golang-dev
    https://golang.org/cl/9874043

変更の背景

Go言語では、構造体へのフィールドの埋め込み(embedding)という機能があります。これにより、ある構造体が別の構造体のフィールドを「継承」したかのように振る舞うことができます。例えば、type S struct { T } のようにTSに埋め込まれている場合、SのインスタンスからTのメソッドを直接呼び出すことができます。

このとき、コンパイラは内部的に「組み込みトランポリン(embedded trampoline)」と呼ばれる特殊な関数を生成することがあります。これは、*S型のレシーバを*T型のレシーバに変換(マーシャリング)して、埋め込まれた型Tのメソッドを呼び出すための橋渡しをする役割を担います。

問題は、これらの組み込みトランポリンが、通常の関数とは異なる「特殊なパス」でコンパイラによって生成される点にありました。そのため、通常の関数に対して行われるような一般的な解析やアノテーション(メタデータの付与)が適用されず、特に引数のサイズ情報が明示的に提供されていませんでした。引数のサイズ情報が欠落していると、関数呼び出し時のスタックフレームの管理や引数の受け渡しが正しく行われず、ランタイムエラーや予期せぬ動作を引き起こす可能性がありました。

このコミットは、この問題を解決し、組み込みトランポリンが正しく機能するために必要な引数サイズ情報を明示的に付与することを目的としています。

前提知識の解説

  • 組み込みトランポリン (Embedded Trampolines): Go言語の構造体埋め込み機能において、埋め込まれた型のメソッドを呼び出す際に、レシーバの型を変換するためにコンパイラが内部的に生成する小さなコード片(関数)です。例えば、struct S { T } の場合、SのインスタンスからTのメソッドを呼び出す際に、SのレシーバをTのレシーバに変換する役割を担います。これらはGoランタイムの低レベルなメカニズムであり、通常のGoプログラマが直接意識することはほとんどありません。

  • cmd/5g, cmd/6g, cmd/8g: これらはGoコンパイラのバックエンドの一部です。

    • cmd/5g: ARMアーキテクチャ向けのGoコンパイラ(歴史的に5はARMを指す)
    • cmd/6g: x86-64(AMD64)アーキテクチャ向けのGoコンパイラ(歴史的に6はx86-64を指す)
    • cmd/8g: x86(32ビット)アーキテクチャ向けのGoコンパイラ(歴史的に8はx86を指す) これらのコンパイラは、Goのソースコードを各アーキテクチャの機械語に変換する役割を担っており、特にアセンブリコードの生成や低レベルな最適化を行います。
  • Prog構造体とフィールド: Goコンパイラの内部では、アセンブリ命令を表現するためにProgという構造体が使われます。この構造体には、命令の種類、オペランド(引数)、レジスタなどの情報が含まれます。

    • p->from.name = D_EXTERN;: fromオペランドの名前が外部シンボルであることを示します。
    • p->from.sym = newnam;: fromオペランドのシンボル(名前)を設定します。
    • p->to.type = D_CONST2; / p->to.type = D_CONST;: toオペランドの型が定数であることを示します。D_CONST2は2つの定数値を保持できることを示唆します。
    • p->to.offset: toオペランドのオフセット値。通常、スタック上の位置やメモリのアドレスオフセットを示します。
    • p->to.offset2: toオペランドの2番目のオフセット値。特定の命令で追加のオフセット情報が必要な場合に使用されます。
    • p->reg: 命令で使用されるレジスタ。
    • p->from.scale: fromオペランドのスケールファクタ。アセンブリ命令によっては、メモリのアドレス計算にスケールファクタが使われます。この文脈ではtextflag(後述)を格納するために使われています。
    • textflag: Goランタイムにおいて、関数の特性を示すフラグ。例えば、スタックフレームを持たない関数や、特別なリンケージを持つ関数などに設定されます。7という値は、特定の内部的な意味を持つフラグの組み合わせを示している可能性があります。
  • rnd(value, align): valuealignの倍数に切り上げる関数。メモリのアライメント(整列)を保証するために使用されます。例えば、rnd(10, 8)16を返します。

  • method->type->argwid: メソッドの型情報から、そのメソッドが受け取る引数の合計幅(バイト単位のサイズ)を取得します。

  • widthptr: ポインタのサイズ(通常は32ビットシステムで4バイト、64ビットシステムで8バイト)を示す定数。

技術的詳細

このコミットの核心は、Goコンパイラが組み込みトランポリンを生成する際に、そのトランポリンが呼び出すメソッドの引数サイズ情報を正確に記録することです。これは、コンパイラが生成するアセンブリ命令のオペランドに、このサイズ情報を埋め込むことで実現されます。

具体的には、src/cmd/5g/gobj.csrc/cmd/6g/gobj.csrc/cmd/8g/gobj.cの各ファイルで、Prog構造体のフィールドが変更されています。

  • cmd/5g (ARM): p->to.offset2に引数サイズが格納されます。 p->to.offset = 0; // stack size p->to.offset2 = rnd(method->type->argwid, widthptr); // argument size p->reg = 7; // textflag ARMアーキテクチャでは、to.offset2が引数サイズを保持するために利用されます。to.offsetはスタックサイズ(この場合は0)を示し、regtextflag(関数の特性フラグ)を保持します。

  • cmd/6g (x86-64): p->to.offsetの上位32ビットに引数サイズが格納されます。 p->to.offset = 0; // stack size p->to.offset |= rnd(method->type->argwid, widthptr) << 32; // argument size p->from.scale = 7; // textflag x86-64アーキテクチャでは、to.offsetフィールドが64ビット幅であるため、下位32ビットをスタックサイズ(0)に、上位32ビットを引数サイズに利用しています。これはビットシフト演算子<< 32によって実現されます。from.scaletextflagを保持します。

  • cmd/8g (x86): p->to.offset2に引数サイズが格納されます。 p->to.offset = 0; // stack skize (原文ママ、skizeはsizeの誤植) p->to.offset2 = rnd(method->type->argwid, widthptr); // argument size p->from.scale = 7; // textflag x86アーキテクチャでは、cmd/5gと同様にto.offset2が引数サイズを保持するために利用されます。to.offsetはスタックサイズ(0)を示し、from.scaletextflagを保持します。

これらの変更により、組み込みトランポリンが生成される際に、そのトランポリンが呼び出すメソッドの引数サイズが正確に計算され、コンパイラ内部のデータ構造に埋め込まれるようになります。この情報は、ランタイムがトランポリンを介してメソッドを呼び出す際に、スタックポインタの調整や引数のコピーを適切に行うために不可欠です。

特に、rnd(method->type->argwid, widthptr)は、メソッドの引数の合計サイズをポインタのサイズ(通常はワードサイズ)にアライメントすることで、メモリ効率と正しいスタックフレームの構築を保証しています。

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

diff --git a/src/cmd/5g/gobj.c b/src/cmd/5g/gobj.c
index 9c5fb2a962..3bdb3268a4 100644
--- a/src/cmd/5g/gobj.c
+++ b/src/cmd/5g/gobj.c
@@ -548,8 +548,9 @@ out:
  	p->from.name = D_EXTERN;
  	p->from.sym = newnam;
  	p->to.type = D_CONST2;
- 	p->reg = 7;
- 	p->to.offset2 = 0;
+ 	p->to.offset = 0;  // stack size
+ 	p->to.offset2 = rnd(method->type->argwid, widthptr);  // argument size
+ 	p->reg = 7;  // textflag
  	p->to.reg = NREG;
  //print("1. %P\n", p);
  
diff --git a/src/cmd/6g/gobj.c b/src/cmd/6g/gobj.c
index cdbbd5d9db..c7e87f1c81 100644
--- a/src/cmd/6g/gobj.c
+++ b/src/cmd/6g/gobj.c
@@ -528,8 +528,9 @@ out:
  	p->from.type = D_EXTERN;
  	p->from.sym = newnam;
  	p->to.type = D_CONST;
- 	p->to.offset = 0;
- 	p->from.scale = 7;
+ 	p->to.offset = 0; // stack size
+ 	p->to.offset |= rnd(method->type->argwid, widthptr) << 32;  // argument size
+ 	p->from.scale = 7;  // textflag
  //print("1. %P\n", p);
  
  	mov = AMOVQ;
diff --git a/src/cmd/8g/gobj.c b/src/cmd/8g/gobj.c
index 39717d5b1a..f695468cdf 100644
--- a/src/cmd/8g/gobj.c
+++ b/src/cmd/8g/gobj.c
@@ -534,8 +534,9 @@ out:
  	p->from.type = D_EXTERN;
  	p->from.sym = newnam;
  	p->to.type = D_CONST;
- 	p->to.offset = 0;
- 	p->from.scale = 7;
+ 	p->to.offset = 0;  // stack skize
+ 	p->to.offset2 = rnd(method->type->argwid, widthptr);  // argument size
+ 	p->from.scale = 7;  // textflag
  //print("1. %P\n", p);
  
  	mov = AMOVL;

コアとなるコードの解説

各ファイルの変更は、Prog構造体のフィールドに引数サイズ情報を設定する部分に集中しています。

  • src/cmd/5g/gobj.c (ARM):

    • - p->reg = 7;
    • - p->to.offset2 = 0;
      • 変更前はp->reg7(textflag)を設定し、p->to.offset20に初期化していました。
    • + p->to.offset = 0; // stack size
      • p->to.offsetをスタックサイズとして0に設定します。
    • + p->to.offset2 = rnd(method->type->argwid, widthptr); // argument size
      • p->to.offset2に、method->type->argwid(メソッドの引数の合計サイズ)をwidthptr(ポインタサイズ)でアライメントした値を設定します。これが引数サイズ情報です。
    • + p->reg = 7; // textflag
      • p->regtextflagである7を再設定します。
  • src/cmd/6g/gobj.c (x86-64):

    • - p->to.offset = 0;
    • - p->from.scale = 7;
      • 変更前はp->to.offset0に初期化し、p->from.scale7(textflag)を設定していました。
    • + p->to.offset = 0; // stack size
      • p->to.offsetをスタックサイズとして0に設定します。
    • + p->to.offset |= rnd(method->type->argwid, widthptr) << 32; // argument size
      • p->to.offsetの上位32ビットに引数サイズ情報を設定します。rnd(method->type->argwid, widthptr)で計算された引数サイズを32ビット左にシフトし、既存のp->to.offset(この場合は0)にビットORで結合します。これにより、p->to.offsetがスタックサイズと引数サイズの両方を保持するようになります。
    • + p->from.scale = 7; // textflag
      • p->from.scaletextflagである7を再設定します。
  • src/cmd/8g/gobj.c (x86):

    • - p->to.offset = 0;
    • - p->from.scale = 7;
      • 変更前はp->to.offset0に初期化し、p->from.scale7(textflag)を設定していました。
    • + p->to.offset = 0; // stack skize
      • p->to.offsetをスタックサイズとして0に設定します。(コメントのskizesizeの誤植)
    • + p->to.offset2 = rnd(method->type->argwid, widthptr); // argument size
      • p->to.offset2に、method->type->argwid(メソッドの引数の合計サイズ)をwidthptr(ポインタサイズ)でアライメントした値を設定します。これが引数サイズ情報です。
    • + p->from.scale = 7; // textflag
      • p->from.scaletextflagである7を再設定します。

これらの変更により、各アーキテクチャのコンパイラが組み込みトランポリンを生成する際に、そのトランポリンが呼び出すメソッドの引数サイズが正確に計算され、Prog構造体の適切なフィールドに格納されるようになります。これにより、Goランタイムがトランポリンを介してメソッドを呼び出す際のスタック管理が正確に行われ、潜在的なバグが修正されます。

関連リンク

参考にした情報源リンク