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

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

このコミットは、Go言語のsyscallパッケージにおける、64-bit Plan 9アーキテクチャ向けのseekシステムコールのアセンブリ実装に関する修正です。具体的には、スタック上のオフセット計算の誤りと、32-bitコードからのエラーハンドリングの変換ミスを修正しています。

コミット

commit 85f86399f417b0bf494a62bcbb90b91928a067e4
Author: Akshat Kumar <seed@mail.nanosouffle.net>
Date:   Tue Jan 22 14:03:30 2013 -0500

    syscall: fix arithmetic errors in assembly for seek function for 64-bit Plan 9
    
    Offsets for return values from seek were miscalculated
    and a translation from 32-bit code for error handling
    was incorrect.
    
    R=rsc, rminnich, npe
    CC=golang-dev
    https://golang.org/cl/7181045

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

https://github.com/golang/go/commit/85f86399f417b0bf494a62bcbb90b91928a067e4

元コミット内容

このコミットは、src/pkg/syscall/asm_plan9_amd64.sファイルに対する変更です。このファイルは、Go言語のsyscallパッケージがPlan 9オペレーティングシステム上で動作する際に、AMD64アーキテクチャ(64-bit)向けのシステムコールをアセンブリ言語で実装している部分です。

変更の要点は以下の通りです。

  1. seek関数の戻り値(newoffset)とエラー文字列(err)をスタックに配置する際のオフセット計算が修正されました。
  2. 特に、newoffsetの扱いにおいて、以前は32-bitの「low」と「high」に分割して扱っていた箇所が、64-bitの単一の値として扱うように変更されました。これは、64-bitアーキテクチャにおけるデータ表現の整合性を保つための修正です。
  3. エラーハンドリングのロジックにおいて、32-bitコードからの不適切な変換があった部分が修正されました。

変更の背景

この変更の背景には、Go言語が様々なオペレーティングシステムやアーキテクチャをサポートする上で、それぞれの環境に合わせた低レベルなシステムコール実装が必要となるという事情があります。特に、Plan 9のようなUNIX系とは異なる設計思想を持つOSでは、システムコールの呼び出し規約やデータ構造が異なるため、専用のアセンブリコードが必要になります。

コミットメッセージによると、この修正は以下の2つの主要な問題に対処しています。

  1. 戻り値のオフセット計算ミス: seekシステムコールが返す新しいファイルオフセット(newoffset)をスタックに格納する際のアドレス計算に誤りがありました。これにより、正しいメモリ位置に値が書き込まれず、後続の処理で誤った値が参照される可能性がありました。
  2. 32-bitコードからのエラーハンドリング変換ミス: 以前のコードが32-bitアーキテクチャ向けの実装を64-bitアーキテクチャに移植する際に、エラーハンドリングのロジックが正しく変換されていませんでした。特に、エラーを示す戻り値の処理や、エラー文字列の格納方法に問題があったと考えられます。

これらの問題は、seekシステムコールの正確な動作を妨げ、ファイル操作における予期せぬ挙動やエラーを引き起こす可能性がありました。

前提知識の解説

このコミットを理解するためには、以下の前提知識が必要です。

  1. Go言語のsyscallパッケージ: Go言語の標準ライブラリの一部で、オペレーティングシステムの低レベルな機能(システムコール)にアクセスするためのインターフェースを提供します。ファイル操作、プロセス管理、ネットワーク通信など、OSカーネルが提供する機能を利用する際に使用されます。
  2. アセンブリ言語 (x86-64): コンピュータのCPUが直接実行できる機械語を人間が読める形式で記述した低レベルプログラミング言語です。x86-64は、IntelおよびAMDの64-bitプロセッサ向けの命令セットアーキテクチャです。
    • レジスタ: CPU内部にある高速な記憶領域。AX, SP, DI, SIなどが登場します。
      • AX (Accumulator Register): 汎用レジスタ。演算結果や関数の戻り値などに使われます。
      • SP (Stack Pointer): スタックの現在のトップ(最上位アドレス)を指すレジスタ。
      • DI (Destination Index): 汎用レジスタ。文字列操作命令などで目的地のメモリアドレスを指すのに使われます。
      • SI (Source Index): 汎用レジスタ。文字列操作命令などでソースのメモリアドレスを指すのに使われます。
    • スタック: プログラムの実行中に一時的なデータを格納するためのメモリ領域。関数呼び出し時の引数、ローカル変数、戻りアドレスなどがスタックに積まれます。スタックは通常、アドレスの大きい方から小さい方へ向かって成長します。
    • 命令:
      • TEXT symbol(SB), flags, $framesize: Goアセンブリにおける関数の宣言。symbolは関数名、SBは静的ベースポインタ(グローバルシンボルを参照するための擬似レジスタ)、flagsは関数属性、$framesizeはスタックフレームサイズです。
      • LEAQ dest, src: "Load Effective Address"。srcで指定されたアドレスを計算し、その結果をdestレジスタに格納します。メモリの内容を読み込むのではなく、アドレス自体を計算してレジスタに入れる点が重要です。例えば、LEAQ newoffset+48(SP), AXは、SPレジスタの値に48を加えたアドレスを計算し、そのアドレスをAXレジスタに格納します。
      • MOVQ dest, src: "Move Quadword"。srcの64-bit値(クアッドワード)をdestに移動します。
      • CMPQ op1, op2: "Compare Quadword"。op1op2を比較し、フラグレジスタを設定します。
      • JNE label: "Jump if Not Equal"。直前の比較結果が等しくない場合にlabelにジャンプします。
      • SUBQ val, reg: "Subtract Quadword"。regからvalを減算します。SUBQ $16, SPはスタックポインタを16バイト減らし、スタックフレームを拡張します。
      • CALL func: funcを呼び出します。
      • CLD: "Clear Direction Flag"。方向フラグをクリアします。文字列操作命令(MOVSQなど)がメモリを順方向(アドレス増加方向)に処理するように設定します。
      • MOVSQ: "Move String Quadword"。SIが指すアドレスからDIが指すアドレスへ64-bit値をコピーし、SIDIを更新します。
  3. Plan 9オペレーティングシステム: ベル研究所で開発された分散型オペレーティングシステム。UNIXとは異なる設計思想を持ち、すべてをファイルとして扱うという原則が徹底されています。Go言語はPlan 9の設計思想に影響を受けており、Goの初期開発者にはPlan 9の開発者が含まれています。
  4. seekシステムコール: ファイルポインタの位置を変更するためのシステムコール。ファイル内の特定の位置から読み書きを開始するために使用されます。通常、ファイルディスクリプタ、オフセット、およびwhence(オフセットの基準位置:ファイルの先頭、現在位置、ファイルの末尾)を引数に取ります。

技術的詳細

このコミットの技術的詳細は、主に64-bit Plan 9環境におけるGo言語のシステムコール呼び出し規約とスタックフレーム管理の正確性に関するものです。

Go言語の関数は、コンパイラによって生成されたアセンブリコード、または手書きのアセンブリコード(このケースのようにasm_plan9_amd64.sファイル)によって実装されます。システムコールを呼び出すアセンブリコードは、OSのカーネルが期待する形式で引数を渡し、戻り値を受け取る必要があります。

seek関数のGo言語のシグネチャはコメントに示されています: //func seek(placeholder uintptr, fd int, offset int64, whence int) (newoffset int64, err string)

これは、seek関数がplaceholderfdoffsetwhenceという引数を取り、newoffset(新しいオフセット)とerr(エラー文字列)という2つの戻り値を返すことを示しています。

アセンブリコードでは、これらの引数や戻り値はスタック上に配置されるか、レジスタを介して渡されます。このコミットの修正は、スタック上のこれらの変数のオフセットが正しく計算されていなかったことに起因します。

具体的には、以下の点が修正されています。

  1. newoffsetのスタックオフセット修正:
    • 変更前: LEAQ newoffset+48(SP), AX
    • 変更後: LEAQ newoffset+40(SP), AX これは、newoffset変数がスタック上のSP+48ではなく、SP+40の位置にあるべきだったことを示しています。スタックフレームのレイアウトや、他の変数、戻りアドレス、保存されたレジスタなどの配置が変更されたか、初期の計算が誤っていた可能性があります。
  2. newoffsetの64-bit値としての扱い:
    • 変更前:
      MOVQ	AX, 48(SP)	// newoffset low
      MOVQ	AX, 56(SP)	// newoffset high
      
    • 変更後:
      MOVQ	AX, 40(SP)	// newoffset
      
    これは非常に重要な変更です。以前のコードでは、newoffsetという64-bitの値を、スタック上の2つの32-bitワード(48(SP)56(SP))に分割して格納しようとしていました。これは、32-bitアーキテクチャのコードを64-bitに移植する際に、データ幅の変更を考慮しきれていなかった典型的なミスです。64-bitアーキテクチャでは、64-bitの値を単一のMOVQ命令で直接扱うことができます。この修正により、newoffsetが正しく64-bit値としてスタックに格納されるようになりました。オフセットも40(SP)に統一されています。
  3. errのスタックオフセット修正:
    • 変更前: LEAQ err+64(SP), DI
    • 変更後: LEAQ err+48(SP), DI これもerr文字列のスタック上のオフセットがSP+64ではなく、SP+48であるべきだったことを示しています。errはGoの文字列型であり、通常はポインタと長さのペアとして表現されます。このペアがスタック上のどこに配置されるべきかという計算が誤っていたと考えられます。

これらの修正は、64-bit Plan 9環境におけるGoのランタイムとシステムコール間のインターフェースの整合性を確保するために不可欠です。誤ったオフセットは、データの破損、クラッシュ、または不正な動作につながる可能性があります。

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

src/pkg/syscall/asm_plan9_amd64.sファイルにおける変更箇所は以下の通りです。

--- a/src/pkg/syscall/asm_plan9_amd64.s
+++ b/src/pkg/syscall/asm_plan9_amd64.s
@@ -128,7 +128,7 @@ TEXT	·RawSyscall6(SB),7,$0

 //func seek(placeholder uintptr, fd int, offset int64, whence int) (newoffset int64, err string)
 TEXT ·seek(SB),7,$0
-	LEAQ	newoffset+48(SP), AX
+	LEAQ	newoffset+40(SP), AX
 	MOVQ	AX, placeholder+8(SP)

 	MOVQ	$0x8000, AX	// for NxM
@@ -137,8 +137,7 @@ TEXT ·seek(SB),7,$0

 	CMPQ	AX, $-1
 	JNE	ok6
-	MOVQ	AX, 48(SP)	// newoffset low
-	MOVQ	AX, 56(SP)	// newoffset high
+	MOVQ	AX, 40(SP)	// newoffset

 	SUBQ	$16, SP
 	CALL	syscall·errstr(SB)
@@ -150,7 +149,7 @@ ok6:
 	LEAQ	runtime·emptystring(SB), SI

 copyresult6:
-	LEAQ	err+64(SP), DI
+	LEAQ	err+48(SP), DI

 	CLD
 	MOVSQ

コアとなるコードの解説

変更された各行について解説します。

  1. LEAQ newoffset+48(SP), AX から LEAQ newoffset+40(SP), AX:

    • この行は、seek関数の戻り値であるnewoffset(新しいファイルオフセット)がスタック上のどこに配置されるべきかを計算し、そのアドレスをAXレジスタにロードしています。
    • 変更前はSP(スタックポインタ)から48バイトオフセットした位置を指していましたが、変更後は40バイトオフセットした位置を指すようになりました。これは、newoffsetがスタック上の正しい位置に配置されるように、スタックフレームのレイアウトに合わせてオフセットが調整されたことを意味します。
    • MOVQ AX, placeholder+8(SP)は、計算されたnewoffsetのアドレスを、placeholder引数(おそらくGoのランタイムが内部的に使用するポインタ)の8バイトオフセット先に格納しています。
  2. MOVQ AX, 48(SP)MOVQ AX, 56(SP) が削除され、MOVQ AX, 40(SP) が追加:

    • このブロックは、seekシステムコールがエラーを返した場合(CMPQ AX, $-1でチェックされ、JNE ok6でスキップされない場合)に、newoffsetにエラー値(通常は-1)を格納する部分です。
    • 変更前は、AXレジスタの値を48(SP)56(SP)という2つの異なるスタックオフセットにMOVQ(64-bit移動)しようとしていました。コメントにはそれぞれnewoffset lownewoffset highと書かれており、これは64-bit値を2つの32-bitワードとして扱おうとしていたことを示唆しています。しかし、MOVQは64-bit値を移動する命令であり、この記述は矛盾しています。おそらく、32-bit環境からの移植ミスで、64-bit値を2つの32-bitレジスタに分割して扱うようなロジックが誤って残っていたか、あるいは単にコメントが誤解を招くものであった可能性があります。
    • 変更後は、MOVQ AX, 40(SP)という単一の命令で、AXレジスタの64-bit値を40(SP)という正しいnewoffsetのスタック位置に格納するようになりました。これにより、64-bitアーキテクチャにおける64-bit値の正しい扱いが実現されました。
  3. LEAQ err+64(SP), DI から LEAQ err+48(SP), DI:

    • この行は、seek関数のもう一つの戻り値であるerr(エラー文字列)がスタック上のどこに配置されるべきかを計算し、そのアドレスをDIレジスタにロードしています。
    • 変更前はSPから64バイトオフセットした位置を指していましたが、変更後は48バイトオフセットした位置を指すようになりました。これもerr文字列がスタック上の正しい位置に配置されるように、オフセットが調整されたことを意味します。
    • CLDMOVSQは、runtime·emptystring(SB)(Goランタイムの空文字列)をerrのスタック位置にコピーする処理の一部です。CLDは方向フラグをクリアし、MOVSQSIからDIへ64-bit単位でデータをコピーします。これは、エラーがない場合に空文字列を返すための処理です。

これらの修正は、Goのランタイムが期待するスタックフレームのレイアウトと、64-bit Plan 9システムコールが期待するデータ表現にアセンブリコードが完全に準拠するようにするために行われました。

関連リンク

参考にした情報源リンク

  • Go Assembly Language (Goのアセンブリ言語に関する公式ドキュメント): https://go.dev/doc/asm
  • x86-64 Assembly Language Programming (x86-64アセンブリ言語の一般的な情報源)
  • System Calls in Go (Goにおけるシステムコールに関する一般的な情報源)
  • Plan 9 Operating System (Plan 9オペレーティングシステムに関する一般的な情報源)