[インデックス 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.s
src/pkg/runtime/sys_freebsd_amd64.s
src/pkg/runtime/sys_netbsd_amd64.s
src/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ビットエラーコード(例:0xFFFFFFEA
for -22)は、ここで正の32ビット値(例:0x00000016
for 22)に変換されます。しかし、この32ビットの結果が64ビットのRAX
レジスタに格納される際、上位32ビットはゼロで埋められます(ゼロ拡張)。結果として、RAX
は0x0000000000000016
となり、64ビットとしては正の値になってしまいます。 - 変更後 (
NEGQ AX
):NEGQ AX
はRAX
レジスタ全体の64ビットの内容を否定します。これにより、システムコールが返した負の64ビットエラーコード(例:0xFFFFFFFFFFFFFFEA
for -22)は、正の64ビット値(例:0x0000000000000016
for 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の補数表現と符号拡張/ゼロ拡張に関するコンピュータアーキテクチャの基本概念