[インデックス 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)向けのシステムコールをアセンブリ言語で実装している部分です。
変更の要点は以下の通りです。
seek
関数の戻り値(newoffset
)とエラー文字列(err
)をスタックに配置する際のオフセット計算が修正されました。- 特に、
newoffset
の扱いにおいて、以前は32-bitの「low」と「high」に分割して扱っていた箇所が、64-bitの単一の値として扱うように変更されました。これは、64-bitアーキテクチャにおけるデータ表現の整合性を保つための修正です。 - エラーハンドリングのロジックにおいて、32-bitコードからの不適切な変換があった部分が修正されました。
変更の背景
この変更の背景には、Go言語が様々なオペレーティングシステムやアーキテクチャをサポートする上で、それぞれの環境に合わせた低レベルなシステムコール実装が必要となるという事情があります。特に、Plan 9のようなUNIX系とは異なる設計思想を持つOSでは、システムコールの呼び出し規約やデータ構造が異なるため、専用のアセンブリコードが必要になります。
コミットメッセージによると、この修正は以下の2つの主要な問題に対処しています。
- 戻り値のオフセット計算ミス:
seek
システムコールが返す新しいファイルオフセット(newoffset
)をスタックに格納する際のアドレス計算に誤りがありました。これにより、正しいメモリ位置に値が書き込まれず、後続の処理で誤った値が参照される可能性がありました。 - 32-bitコードからのエラーハンドリング変換ミス: 以前のコードが32-bitアーキテクチャ向けの実装を64-bitアーキテクチャに移植する際に、エラーハンドリングのロジックが正しく変換されていませんでした。特に、エラーを示す戻り値の処理や、エラー文字列の格納方法に問題があったと考えられます。
これらの問題は、seek
システムコールの正確な動作を妨げ、ファイル操作における予期せぬ挙動やエラーを引き起こす可能性がありました。
前提知識の解説
このコミットを理解するためには、以下の前提知識が必要です。
- Go言語の
syscall
パッケージ: Go言語の標準ライブラリの一部で、オペレーティングシステムの低レベルな機能(システムコール)にアクセスするためのインターフェースを提供します。ファイル操作、プロセス管理、ネットワーク通信など、OSカーネルが提供する機能を利用する際に使用されます。 - アセンブリ言語 (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"。op1
とop2
を比較し、フラグレジスタを設定します。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値をコピーし、SI
とDI
を更新します。
- レジスタ: CPU内部にある高速な記憶領域。
- Plan 9オペレーティングシステム: ベル研究所で開発された分散型オペレーティングシステム。UNIXとは異なる設計思想を持ち、すべてをファイルとして扱うという原則が徹底されています。Go言語はPlan 9の設計思想に影響を受けており、Goの初期開発者にはPlan 9の開発者が含まれています。
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
関数がplaceholder
、fd
、offset
、whence
という引数を取り、newoffset
(新しいオフセット)とerr
(エラー文字列)という2つの戻り値を返すことを示しています。
アセンブリコードでは、これらの引数や戻り値はスタック上に配置されるか、レジスタを介して渡されます。このコミットの修正は、スタック上のこれらの変数のオフセットが正しく計算されていなかったことに起因します。
具体的には、以下の点が修正されています。
newoffset
のスタックオフセット修正:- 変更前:
LEAQ newoffset+48(SP), AX
- 変更後:
LEAQ newoffset+40(SP), AX
これは、newoffset
変数がスタック上のSP+48
ではなく、SP+40
の位置にあるべきだったことを示しています。スタックフレームのレイアウトや、他の変数、戻りアドレス、保存されたレジスタなどの配置が変更されたか、初期の計算が誤っていた可能性があります。
- 変更前:
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)
に統一されています。- 変更前:
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
コアとなるコードの解説
変更された各行について解説します。
-
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バイトオフセット先に格納しています。
- この行は、
-
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 low
とnewoffset 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値の正しい扱いが実現されました。
- このブロックは、
-
LEAQ err+64(SP), DI
からLEAQ err+48(SP), DI
:- この行は、
seek
関数のもう一つの戻り値であるerr
(エラー文字列)がスタック上のどこに配置されるべきかを計算し、そのアドレスをDI
レジスタにロードしています。 - 変更前は
SP
から64
バイトオフセットした位置を指していましたが、変更後は48
バイトオフセットした位置を指すようになりました。これもerr
文字列がスタック上の正しい位置に配置されるように、オフセットが調整されたことを意味します。 CLD
とMOVSQ
は、runtime·emptystring(SB)
(Goランタイムの空文字列)をerr
のスタック位置にコピーする処理の一部です。CLD
は方向フラグをクリアし、MOVSQ
はSI
からDI
へ64-bit単位でデータをコピーします。これは、エラーがない場合に空文字列を返すための処理です。
- この行は、
これらの修正は、Goのランタイムが期待するスタックフレームのレイアウトと、64-bit Plan 9システムコールが期待するデータ表現にアセンブリコードが完全に準拠するようにするために行われました。
関連リンク
- Go言語の公式リポジトリ: https://github.com/golang/go
- このコミットのChange List (CL): https://golang.org/cl/7181045 (GoのコードレビューシステムGerritへのリンク)
- Plan 9 from Bell Labs: https://9p.io/plan9/
参考にした情報源リンク
- 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オペレーティングシステムに関する一般的な情報源)