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

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

このコミットは、Go言語のランタイムにおけるPlan 9オペレーティングシステム向けのerrstr関数の修正に関するものです。具体的には、C言語で実装されたruntime.findnull()関数を呼び出す際の引数渡し規約の不一致を解消し、スタックポインタ(SP)のオフセット0(SP)に引数が正しく配置されるように修正しています。

コミット

commit ef7705f6dd1dacdc3d3cf97893dd942b37b61744
Author: Akshat Kumar <seed@mail.nanosouffle.net>
Date:   Sat Mar 9 05:39:15 2013 +0100

    runtime: Plan 9: fix errstr
    
    The call to the C function runtime.findnull() requires
    that we provide the argument at 0(SP).
    
    R=rsc, rminnich, ality
    CC=golang-dev
    https://golang.org/cl/7559047

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

https://github.com/golang/go/commit/ef7705f6dd1dacdc3d3cf97893dd942b37b61744

元コミット内容

Goランタイムのerrstr関数において、C言語で実装されたruntime.findnull()関数を呼び出す際に、引数が0(SP)(スタックポインタの最上位)に配置される必要があるという要件を満たしていなかった問題を修正します。

変更の背景

Go言語は、様々なオペレーティングシステムやアーキテクチャをサポートするように設計されています。その中には、ベル研究所で開発された分散オペレーティングシステムであるPlan 9も含まれます。Goランタイムは、OSの機能を利用するために、C言語で書かれた低レベルの関数(システムコールラッパーやユーティリティ関数など)を呼び出すことがあります。

このコミットの背景には、GoランタイムがPlan 9上でruntime.findnull()というC関数を呼び出す際の、**呼び出し規約(Calling Convention)**の不一致がありました。呼び出し規約とは、関数が呼び出される際に、引数をどのように渡し、戻り値をどのように受け取るか、レジスタをどのように保存・復元するかといった、関数呼び出しに関する取り決めです。

特に、アセンブリ言語で書かれたコードがC言語の関数を呼び出す場合、両者の呼び出し規約が厳密に一致している必要があります。Goのランタイムコードの一部はパフォーマンスや低レベルな操作のためにアセンブリ言語で書かれており、これがC関数とのインターフェースで問題を引き起こすことがありました。

runtime.findnull()は、おそらく文字列の終端(ヌル文字)を見つけるためのユーティリティ関数であり、errstr(エラー文字列を取得する関数)の処理の一部として使用されていたと考えられます。この関数が期待する引数の位置(0(SP))と、Goランタイムのアセンブリコードが実際に引数を配置していた位置が異なっていたため、正しく動作しない、あるいは未定義の動作を引き起こす可能性がありました。

また、コメントにある// syscall requires caller-saveは、システムコールを行う際に特定のレジスタ(この場合はCX)が呼び出し元によって保存される必要があることを示唆しています。これは、システムコールがこれらのレジスタを破壊する可能性があるため、呼び出し元がその値を保持したい場合に、事前にスタックに退避させる必要があるという一般的なプログラミングの慣習です。

前提知識の解説

  1. Goランタイム (Go Runtime): Goプログラムの実行を管理する低レベルな部分です。ガベージコレクション、スケジューラ、システムコールインターフェースなどが含まれます。Goプログラムは、OSと直接やり取りするのではなく、ランタイムを介してOSサービスを利用します。
  2. Plan 9 from Bell Labs: ベル研究所で開発された分散オペレーティングシステムです。Go言語の開発者の一部はPlan 9の開発にも携わっており、Go言語の設計思想にはPlan 9の影響が見られます。Goは当初からPlan 9を含む複数のOSをサポートしていました。
  3. アセンブリ言語 (Assembly Language): コンピュータのプロセッサが直接実行できる機械語命令を、人間が読みやすいニーモニックで記述した低レベル言語です。Goランタイムの一部は、パフォーマンスの最適化やOSとの直接的なやり取りのためにアセンブリ言語で書かれています。
  4. 呼び出し規約 (Calling Convention): 関数呼び出しにおいて、引数の渡し方(レジスタ、スタック)、戻り値の受け取り方、スタックの管理、レジスタの保存・復元などのルールを定めたものです。異なる言語やコンパイラ、OS、アーキテクチャ間で呼び出し規約が異なると、関数呼び出しが正しく行われません。
    • スタック (Stack): プログラム実行中に一時的なデータを格納するためのメモリ領域です。関数呼び出しの際には、引数、ローカル変数、戻りアドレスなどがスタックに積まれます(プッシュ)。関数から戻る際には、これらのデータがスタックから取り除かれます(ポップ)。
    • スタックポインタ (SP): スタックの現在の最上位(または最下位、アーキテクチャによる)を指すレジスタです。
    • 0(SP): スタックポインタが指すアドレス(スタックの最上位)から0バイトオフセットした位置を意味します。これは、関数に渡される最初の引数や、関数が使用する一時的な領域の開始点として使われることがあります。
    • 4(SP) / 8(SP): スタックポインタから4バイトまたは8バイトオフセットした位置を意味します。これは、スタック上の他の引数やローカル変数にアクセスするために使用されます。32ビットシステムでは4バイト、64ビットシステムでは8バイトが一般的なワードサイズです。
  5. レジスタ (Registers): CPU内部にある高速な記憶領域です。計算やデータ転送に頻繁に使用されます。
    • AX / EAX / RAX: 汎用レジスタ。通常、関数の戻り値を格納するために使用されます。
    • CX / ECX / RCX: 汎用レジスタ。特定の操作や、呼び出し規約によっては引数として使用されます。
    • BP / EBP / RBP: ベースポインタレジスタ。スタックフレームの基準点として使用されることがあります。
  6. PUSHL / PUSHQ: スタックに値をプッシュするアセンブリ命令です。LはLong (32ビット)、QはQuad (64ビット) を意味します。
  7. POPL / POPQ: スタックから値をポップするアセンブリ命令です。
  8. MOVL / MOVQ: データを移動するアセンブリ命令です。
  9. CALL: 関数を呼び出すアセンブリ命令です。
  10. RET: 関数から戻るアセンブリ命令です。
  11. INT $64 (386) / SYSCALL (amd64): システムコールを実行するための命令です。Plan 9では、INT $64が386アーキテクチャで、SYSCALLがAMD64アーキテクチャでシステムコールをトリガーします。
  12. TEXT runtime·errstr(SB),7,$0: Goのアセンブリ言語における関数定義の構文です。
    • TEXT: 関数定義の開始。
    • runtime·errstr: 関数名。Goのアセンブリでは、パッケージ名と関数名が·で区切られます。SBはStatic Baseで、グローバルシンボルを参照するための基準点です。
    • 7: Goのアセンブリにおけるフラグ。この場合は、NOSPLITフラグ(スタックの拡張を許可しない)とRODATAフラグ(読み取り専用データ)の組み合わせかもしれません。
    • $0: 関数のスタックフレームサイズ(ローカル変数などに使用するスタック領域のサイズ)。この場合は0バイトです。

技術的詳細

このコミットは、GoランタイムがPlan 9上でC関数runtime.findnull()を呼び出す際の引数渡しに関する問題を解決しています。

Goのランタイムは、OSの機能を利用するために、C言語で書かれた低レベルの関数を呼び出すことがあります。このようなクロス言語(GoアセンブリからC)の呼び出しでは、両者の呼び出し規約が一致していることが極めて重要です。

問題は、runtime.findnull()が、その引数をスタックの最上位、すなわち0(SP)に期待していたのに対し、Goのアセンブリコードがそのように引数を配置していなかった点にありました。

修正前は、runtime.errstr関数内でシステムコール(INT $64またはSYSCALL)が実行された後、runtime.findnull()が呼び出されていました。システムコールは、特定のレジスタ(特にCX)の値を変更する可能性があります。コミットメッセージのコメント// syscall requires caller-saveが示すように、CXレジスタは呼び出し元が保存すべきレジスタ(caller-saved register)であるため、システムコールによってその値が破壊される可能性があります。

修正では、以下の手順が追加されました。

  1. CXレジスタの退避:

    • 386アーキテクチャ (sys_plan9_386.s): MOVL 4(SP), CX
    • AMD64アーキテクチャ (sys_plan9_amd64.s): MOVQ 8(SP), CX これは、システムコールによって破壊される可能性のあるCXレジスタの値を、スタック上の適切なオフセット(386では4(SP)、AMD64では8(SP))から読み出してCXにロードしています。この値がruntime.findnull()に渡すべき引数であると考えられます。
  2. 引数のスタックへのプッシュ:

    • 386アーキテクチャ: PUSHL CX
    • AMD64アーキテクチャ: PUSHQ CX これにより、CXレジスタに格納された引数がスタックの最上位(0(SP))にプッシュされます。これは、runtime.findnull()が期待する引数の位置です。
  3. runtime.findnull()の呼び出し:

    • CALL runtime·findnull(SB) 引数が正しくスタックに配置された状態で、runtime.findnull()が呼び出されます。
  4. CXレジスタの復元:

    • 386アーキテクチャ: POPL CX
    • AMD64アーキテクチャ: POPQ CX runtime.findnull()の呼び出し後、スタックにプッシュした引数をポップしてCXレジスタを元の状態に戻します。これは、スタックのバランスを保ち、後続のコードが正しいスタックポインタを参照できるようにするために重要です。

この修正により、runtime.findnull()が期待する呼び出し規約(引数が0(SP)にあること)が満たされ、errstr関数がPlan 9上で正しく動作するようになりました。これは、低レベルなアセンブリコードとC言語のインターフェースにおける厳密な呼び出し規約の遵守がいかに重要であるかを示す典型的な例です。

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

src/pkg/runtime/sys_plan9_386.s

--- a/src/pkg/runtime/sys_plan9_386.s
+++ b/src/pkg/runtime/sys_plan9_386.s
@@ -187,6 +187,13 @@ TEXT runtime·errstr(SB),7,$0
 	MOVL	$ERRMAX, 8(SP)
 	MOVL	$41, AX
 	INT	$64
+
+	// syscall requires caller-save
+	MOVL	4(SP), CX
+
+	// push the argument
+	PUSHL	CX
 	CALL	runtime·findnull(SB)
+	POPL	CX
 	MOVL	AX, 8(SP)
 	RET

src/pkg/runtime/sys_plan9_amd64.s

--- a/src/pkg/runtime/sys_plan9_amd64.s
+++ b/src/pkg/runtime/sys_plan9_amd64.s
@@ -224,6 +224,13 @@ TEXT runtime·errstr(SB),7,$0
 	MOVQ	$0x8000, AX
 	MOVQ	$41, BP
 	SYSCALL
+
+	// syscall requires caller-save
+	MOVQ	8(SP), CX
+
+	// push the argument
+	PUSHQ	CX
 	CALL	runtime·findnull(SB)
+	POPQ	CX
 	MOVQ	AX, 16(SP)
 	RET

コアとなるコードの解説

両方のファイル(386とAMD64アーキテクチャ向け)で同様の変更が行われています。

  1. // syscall requires caller-save: このコメントは、直前のシステムコール(INT $64またはSYSCALL)が、CXレジスタのような「呼び出し元が保存すべき」レジスタの値を破壊する可能性があることを示しています。そのため、CXレジスタの値をruntime.findnullに渡す引数として使用する前に、その値をスタックから読み出す必要があります。

  2. MOVL 4(SP), CX (386) / MOVQ 8(SP), CX (AMD64): これは、スタック上の特定のオフセット(386では4(SP)、AMD64では8(SP))に格納されている値をCXレジスタに移動しています。この値が、runtime.findnull()に渡すべき引数(おそらくエラー文字列へのポインタ)です。システムコールによってCXが破壊された場合でも、スタックに保存されている元の値を取得できます。

  3. // push the argument: このコメントは、引数をスタックにプッシュする意図を示しています。

  4. PUSHL CX (386) / PUSHQ CX (AMD64): CXレジスタに格納されている引数の値をスタックにプッシュします。これにより、引数はスタックの最上位、すなわち0(SP)に配置されます。これは、C関数runtime.findnull()が引数を期待する位置です。

  5. CALL runtime·findnull(SB): 引数が正しくスタックに配置された状態で、runtime.findnull()関数が呼び出されます。

  6. POPL CX (386) / POPQ CX (AMD64): runtime.findnull()の呼び出し後、スタックにプッシュした引数をポップしてCXレジスタを元の状態に戻します。これは、スタックのバランスを保ち、スタックポインタを元の位置に戻すために必要です。これにより、runtime.errstr関数の残りの部分が正しく実行され、最終的にRETで呼び出し元に戻ることができます。

この一連の変更により、GoランタイムのアセンブリコードとC言語のruntime.findnull()関数との間の呼び出し規約の不一致が解消され、Plan 9上でのエラー文字列処理が安定しました。

関連リンク

  • Go言語の公式ドキュメント: https://go.dev/
  • Plan 9 from Bell Labs: https://9p.io/plan9/
  • Goのアセンブリ言語に関するドキュメント (Go 1.2以降の形式): https://go.dev/doc/asm (このコミットは2013年のものであり、当時のアセンブリ構文は現在のものと若干異なる可能性がありますが、基本的な概念は共通です。)

参考にした情報源リンク

  • Go言語のソースコード (GitHub): https://github.com/golang/go
  • Go CL (Change List) 7559047: https://golang.org/cl/7559047 (コミットメッセージに記載されているリンク)
  • Goの呼び出し規約に関する一般的な情報 (Goのバージョンやアーキテクチャによって異なるため、一般的な概念の理解に役立つ情報源):
    • "Go's execution model": https://go.dev/doc/articles/go_mem.html (直接呼び出し規約を説明するものではないが、ランタイムの動作を理解する上で役立つ)
    • Goのコンパイラやランタイムの内部に関するブログ記事やプレゼンテーション (例: "Go's runtime and calling conventions"などで検索)
  • x86/x64アセンブリ言語と呼び出し規約に関する一般的な情報 (例: System V AMD64 ABI, Microsoft x64 calling conventionなど)
  • Plan 9のシステムコールに関するドキュメント (例: Plan 9 manual pages for system calls)
  • C言語の呼び出し規約に関する一般的な情報 (例: cdecl, stdcall, fastcallなど)
  • Goのruntimeパッケージのソースコード (特にsys_plan9_386.s, sys_plan9_amd64.s, findnull.goなど)
    • src/runtime/sys_plan9_386.s (現在のGoリポジトリでのパス)
    • src/runtime/sys_plan9_amd64.s (現在のGoリポジトリでのパス)
    • src/runtime/string.go (現在のGoリポジトリでfindnullが関連する可能性のあるファイル)
    • src/runtime/error.go (現在のGoリポジトリでerrstrが関連する可能性のあるファイル)
    • src/runtime/sys_plan9.go (現在のGoリポジトリでPlan 9関連のシステムコールやユーティリティが定義されている可能性のあるファイル)