[インデックス 15779] ファイルの概要
このコミットは、Go言語のランタイムにおける、特定のオペレーティングシステム(Darwin, FreeBSD, NetBSD, OpenBSD)のAMD64アーキテクチャ向けアセンブリコードの修正に関するものです。具体的には、システムコールからの戻り値として負のエラーコードを受け取った際の処理において、32ビットの否定命令(NEGL)を64ビットの否定命令(NEGQ)に置き換えることで、64ビット環境でのエラーコードの解釈が正しく行われるように変更しています。
コミット
commit e3c7a9db83b98a936cd90c46f39d86080c30a0d6
Author: Russ Cox <rsc@golang.org>
Date: Thu Mar 14 23:42:11 2013 -0400
runtime: use 64-bit negative error code on 64-bit machines
NEGL does a negation of the bottom 32 bits and then zero-extends to 64 bits,
resulting in a negative 32-bit number but a positive 64-bit number.
NEGQ does a full 64-bit negation, so that the result is negative both as
a 32-bit and as a 64-bit number.
This doesn't matter for the functions that are declared to return int32.
It only matters for the ones that return int64 or void* [sic].
This will fix the current incorrect error in the OpenBSD/amd64 build.
The build will still be broken, but it won't report a bogus error.
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/7536046
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/e3c7a9db83b98a936cd90c46f39d86080c30a0d6
元コミット内容
このコミットは、Goランタイムが64ビットマシン上でシステムコールからのエラーコードを処理する方法を修正します。以前は、32ビットの否定命令(NEGL)を使用していましたが、これにより32ビットでは負の値となるものの、64ビットにゼロ拡張されると正の値になってしまう問題がありました。この修正では、代わりに64ビットの否定命令(NEGQ)を使用することで、結果が32ビットとしても64ビットとしても負の値となるようにし、特にint64やポインタを返す関数において正しいエラー処理を保証します。これにより、OpenBSD/amd64ビルドで報告されていた誤ったエラーが解消されます。
変更の背景
Go言語のランタイムは、オペレーティングシステムと直接対話するためにアセンブリコードを使用します。システムコールは、ファイル操作、メモリ管理、プロセス制御など、OSのカーネル機能にアクセスするための主要な手段です。Unix系OSでは、システムコールがエラーを返した場合、通常はerrnoと呼ばれるエラー番号の負の値がレジスタ(通常はRAX)に格納されます。Goランタイムは、この負の値を正のerrno値に変換してGoのコードに渡す必要があります。
このコミットが行われた当時、64ビットのAMD64アーキテクチャにおいて、システムコールが返した負のエラーコードを処理する際に、NEGLという32ビットの否定命令が誤って使用されていました。NEGLはレジスタの下位32ビットのみを否定し、その結果を64ビットレジスタの上位32ビットをゼロで埋める(ゼロ拡張する)形で格納します。例えば、32ビットで-1を表す0xFFFFFFFFをNEGLで否定すると0x00000001(32ビットで1)になります。これを64ビットレジスタにゼロ拡張すると0x0000000000000001となり、64ビットとしては正の値になってしまいます。
この挙動は、システムコールがint32を返すように宣言されている場合には問題になりませんでしたが、int64やポインタ(void*)を返すように宣言されている場合には、Goランタイムが期待する負のエラーコード(64ビットとしても負であるべき値)と異なる、正の値として解釈されてしまうという問題を引き起こしました。特にOpenBSD/amd64環境でこの問題が顕在化し、誤ったエラーが報告される原因となっていました。このコミットは、この根本的な問題を解決し、エラーコードの正しい解釈を保証することを目的としています。
前提知識の解説
x86-64アセンブリにおけるNEG命令
NEG命令は、オペランドの2の補数を計算し、その結果をオペランドに格納します。これは実質的に、オペランドの符号を反転させる操作です。x86-64アーキテクチャでは、オペランドのサイズに応じて異なるニーモニックが使用されます。
NEGL(Negate Long): 32ビットオペランドに対して操作を行います。例えば、NEGL AXはRAXレジスタの下位32ビット(EAX)の内容を否定します。NEGQ(Negate Quad): 64ビットオペランドに対して操作を行います。例えば、NEGQ AXはRAXレジスタ全体の64ビットの内容を否定します。
システムコールとエラーハンドリング
Unix系OSでは、システムコールが成功すると通常は非負の値(例えば、読み書きされたバイト数やファイルディスクリプタ)を返します。エラーが発生した場合は、RAXレジスタに負の値が格納され、これは通常、対応するerrno値(例えば、EPERM、ENOENTなど)の負のバージョンです。また、エラーが発生した際には、CPUのフラグレジスタにあるキャリーフラグ (CF) がセットされるのが一般的です。
Goランタイムのような低レベルのコードでは、システムコールが返した値をチェックし、エラーであればその負の値を正のerrno値に変換する必要があります。
符号拡張 (Sign Extension) とゼロ拡張 (Zero Extension)
- ゼロ拡張 (Zero Extension): より小さいビット幅の値をより大きいビット幅のレジスタにコピーする際に、上位ビットをすべて
0で埋めることです。例えば、32ビットの0x00000001を64ビットにゼロ拡張すると0x0000000000000001になります。32ビットの負の値(例:0xFFFFFFFF、これは-1)をゼロ拡張すると、0x00000000FFFFFFFFとなり、64ビットとしては非常に大きな正の値として解釈されます。 - 符号拡張 (Sign Extension): より小さいビット幅の値をより大きいビット幅のレジスタにコピーする際に、元の値の最上位ビット(符号ビット)を上位ビットにコピーして埋めることです。これにより、元の値の符号が保持されます。例えば、32ビットの
0xFFFFFFFF(-1)を64ビットに符号拡張すると0xFFFFFFFFFFFFFFFF(64ビットで-1)になります。
このコミットの文脈では、NEGLが32ビットの否定を行った後、その結果が暗黙的に64ビットレジスタにゼロ拡張されることが問題でした。
JCC命令
JCCは "Jump if Carry Clear" の略で、キャリーフラグ(CF)がクリア(0)の場合にジャンプする条件分岐命令です。システムコールが成功した場合、キャリーフラグはクリアされるため、JCC命令はエラーが発生しなかった場合に後続のエラー処理コードをスキップするために使用されます。
技術的詳細
このコミットの核心は、64ビットシステムにおける負のエラーコードの正確な表現と解釈にあります。
システムコールがエラーを返すと、RAXレジスタには負の値が格納されます。例えば、-EINVAL(無効な引数)は通常-22です。64ビットの2の補数表現では、-22は0xFFFFFFFFFFFFFFEAとなります。
-
NEGL AXの問題点:NEGL AXはRAXレジスタの下位32ビット(EAX)のみを操作します。- もし
RAXが-22(0xFFFFFFFFFFFFFFEA)を含んでいた場合、EAXは0xFFFFFFEA(32ビットで-22)です。 NEGL EAXを実行すると、EAXは0x00000016(32ビットで22)になります。- この
EAXの値がRAXに格納される際、上位32ビットはゼロで埋められます(ゼロ拡張)。 - 結果として
RAXは0x0000000000000016となり、これは64ビットとしては正の22です。 - Goランタイムがこの値を
int64として解釈する場合、エラーを示す負の値ではなく、正の値として扱ってしまい、誤った動作につながります。
-
NEGQ AXによる解決:NEGQ AXはRAXレジスタ全体の64ビットを操作します。- もし
RAXが-22(0xFFFFFFFFFFFFFFEA)を含んでいた場合、NEGQ RAXを実行すると、RAXは0x0000000000000016(64ビットで22)になります。 - この結果は、システムコールがエラーを返した際に、そのエラーコードの絶対値(正の
errno値)を正しく取得するために使用されます。 - Goランタイムがこの値を
int64として受け取る場合、この22という値は、エラーが発生したことを示すために、Goの内部で再度負の値に変換されるか、あるいはエラーを示す特定の構造体の一部として扱われます。重要なのは、アセンブリレベルで負のエラーコードを正しく処理し、その符号を反転させてGoコードに渡すことです。
この修正は、特にint64やポインタを返すシステムコールにおいて重要です。Goの内部では、システムコールからの戻り値がエラーを示す負の値である場合、それをGoのエラー型に変換する処理が行われます。この変換が正しく行われるためには、アセンブリレベルで負のエラーコードが正しく処理され、その符号が反転されてGoに渡される必要があります。NEGLではこの符号反転が64ビットのコンテキストで正しく行われず、結果としてGo側で誤ったエラー処理が行われる可能性がありました。NEGQを使用することで、この問題が解消されます。
コアとなるコードの変更箇所
このコミットでは、以下のファイルでNEGL AXがNEGQ AXに置き換えられています。
src/pkg/runtime/sys_darwin_amd64.ssrc/pkg/runtime/sys_freebsd_amd64.ssrc/pkg/runtime/sys_netbsd_amd64.ssrc/pkg/runtime/sys_openbsd_amd64.s
例として、src/pkg/runtime/sys_darwin_amd64.sの変更箇所を抜粋します。
--- a/src/pkg/runtime/sys_darwin_amd64.s
+++ b/src/pkg/runtime/sys_darwin_amd64.s
@@ -289,7 +289,7 @@ TEXT runtime·bsdthread_create(SB),7,$0
MOVQ $(0x2000000+360), AX // bsdthread_create
SYSCALL
JCC 3(PC)
- NEGL AX
+ NEGQ AX
RET
MOVL $0, AX
RET
@@ -342,7 +342,7 @@ TEXT runtime·bsdthread_register(SB),7,$0
MOVQ $(0x2000000+366), AX // bsdthread_register
SYSCALL
JCC 3(PC)
- NEGL AX
+ NEGQ AX
RET
MOVL $0, AX
RET
@@ -435,7 +435,7 @@ TEXT runtime·sysctl(SB),7,$0
MOVL $(0x2000000+202), AX // syscall entry
SYSCALL
JCC 3(PC)
- NEGL AX
+ NEGQ AX
RET
MOVL $0, AX
RET
@@ -448,7 +448,7 @@ TEXT runtime·kqueue(SB),7,$0
MOVL $(0x2000000+362), AX
SYSCALL
JCC 2(PC)
- NEGL AX
+ NEGQ AX
RET
// int32 runtime·kevent(int kq, Kevent *changelist, int nchanges, Kevent *eventlist, int nevents, Timespec *timeout);\
@@ -462,7 +462,7 @@ TEXT runtime·kevent(SB),7,$0
MOVL $(0x2000000+363), AX
SYSCALL
JCC 2(PC)
- NEGL AX
+ NEGQ AX
RET
// void runtime·closeonexec(int32 fd);
コアとなるコードの解説
上記の変更箇所は、Goランタイムが各OSのシステムコールを呼び出した後のエラー処理部分に存在します。一般的なパターンは以下のようになっています。
SYSCALL // システムコールを実行
JCC 3(PC) // キャリーフラグがクリア(エラーなし)なら3バイト先にジャンプ
NEGL AX // (変更前) AXの下位32ビットを否定
NEGQ AX // (変更後) AXの64ビット全体を否定
RET // 関数からリターン
MOVL $0, AX // エラーがなかった場合の処理(通常は0を返す)
RET // 関数からリターン
SYSCALL: この命令は、AX(またはRAX)レジスタに格納されたシステムコール番号と、他のレジスタに格納された引数を使用して、OSカーネルのシステムコールを呼び出します。システムコールが完了すると、結果は通常RAXレジスタに格納されます。エラーが発生した場合、RAXには負のエラーコードが格納され、キャリーフラグ(CF)がセットされます。JCC 3(PC)/JCC 2(PC):JCCは "Jump if Carry Clear" の略です。システムコールが成功した場合、キャリーフラグはクリアされます。この命令は、キャリーフラグがクリアされていれば、指定されたオフセット(3(PC)または2(PC)は現在のプログラムカウンタからの相対オフセット)だけジャンプします。これにより、エラー処理のコードブロックをスキップし、成功時のリターンパスに進みます。- エラー処理パス:
JCC命令がジャンプしなかった場合(つまり、キャリーフラグがセットされており、エラーが発生した場合)、実行は次の命令に進みます。- 変更前 (
NEGL AX):NEGL AXはRAXレジスタの下位32ビット(EAX)の内容を否定します。システムコールが返した負の32ビットエラーコード(例:0xFFFFFFEAfor -22)は、ここで正の32ビット値(例:0x00000016for 22)に変換されます。しかし、この32ビットの結果が64ビットのRAXレジスタに格納される際、上位32ビットはゼロで埋められます(ゼロ拡張)。結果として、RAXは0x0000000000000016となり、64ビットとしては正の値になってしまいます。 - 変更後 (
NEGQ AX):NEGQ AXはRAXレジスタ全体の64ビットの内容を否定します。これにより、システムコールが返した負の64ビットエラーコード(例:0xFFFFFFFFFFFFFFEAfor -22)は、正の64ビット値(例:0x0000000000000016for 22)に正しく変換されます。この結果は、Goランタイムが期待する形式であり、後続のGoコードでエラーが正しく処理されることを保証します。
- 変更前 (
RET: 関数から呼び出し元にリターンします。
この修正により、Goランタイムは64ビット環境において、システムコールが返す負のエラーコードを常に正しく処理し、Goのコードに適切なerrno値を渡すことができるようになりました。これにより、特にint64やポインタを返すシステムコールにおいて、エラーの誤った解釈が防止されます。
関連リンク
- Go言語のランタイムソースコード: https://github.com/golang/go/tree/master/src/runtime
- Go言語のシステムコールに関するドキュメント(Goのバージョンによって異なる場合がありますが、
syscallパッケージのドキュメントなどが参考になります) - x86-64 Instruction Set Reference (Intel/AMDの公式ドキュメント)
参考にした情報源リンク
- x86 Instruction Set Reference: NEG instruction (例: https://www.felixcloutier.com/x86/neg)
- System call error handling in Unix-like systems (例:
errnoの概念に関する一般的な情報) - Go言語のIssueトラッカーやChange List (CL) (例: https://golang.org/cl/7536046)
- 2の補数表現と符号拡張/ゼロ拡張に関するコンピュータアーキテクチャの基本概念