[インデックス 13783] ファイルの概要
このコミットは、Go言語のx86アーキテクチャ向けリンカである6l (32-bit) および 8l (64-bit) における浮動小数点演算命令(FSUBD, FSUBRD, FDIVD, FDIVRD)のオペコード生成に関するバグ修正です。具体的には、これらの命令の演算結果がFPUスタックのトップ(F0)以外に格納される場合に、正しいオペコードが生成されず、通常の減算/除算と逆順の減算/除算のオペコードが誤って入れ替わってしまう問題が修正されました。
コミット
commit 72fa142fc54366f3009d47afd51bd031cc946454
Author: Adam Langley <agl@golang.org>
Date: Mon Sep 10 15:35:39 2012 -0400
6l/8l: emit correct opcodes to F(SUB|DIV)R?D.
When the destination was not F0, 6l and 8l swapped FSUBD/FSUBRD and
FDIVD/FDIVRD.
R=golang-dev, dave, rsc
CC=golang-dev
https://golang.org/cl/6498092
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/72fa142fc54366f3009d47afd51bd031cc946454
元コミット内容
Go言語のx86リンカである6lおよび8lにおいて、浮動小数点減算(FSUBD, FSUBRD)および浮動小数点除算(FDIVD, FDIVRD)命令のオペコード生成に関するバグを修正します。具体的には、これらの命令の演算結果がFPUスタックのトップ(F0)以外に格納される場合に、通常の演算と逆順の演算のオペコードが誤って入れ替わって出力されていました。
変更の背景
Go言語のコンパイラおよびリンカは、Goのソースコードを最終的な実行可能バイナリに変換する役割を担っています。このプロセスにおいて、アセンブリコードを生成し、それを機械語のオペコードに変換する段階があります。x86アーキテクチャの浮動小数点演算は、FPU(Floating-Point Unit)スタックレジスタを使用して行われます。
このコミット以前の6lおよび8lリンカには、特定の浮動小数点命令(FSUBD, FSUBRD, FDIVD, FDIVRD)において、オペコードの誤った生成が行われるバグが存在していました。このバグは、命令の演算結果がFPUスタックのトップ(F0)以外のレジスタに格納される場合に顕在化しました。具体的には、通常の減算/除算命令(FSUBD, FDIVD)と、オペランドが逆順になった減算/除算命令(FSUBRD, FDIVRD)のオペコードが入れ替わって出力されていました。
これにより、Goプログラム内でこれらの浮動小数点演算が使用された際に、意図しない計算結果が生じる可能性がありました。このバグは、Go言語の数値計算の正確性に直接影響を与えるため、修正が不可欠でした。
前提知識の解説
Go言語のリンカ (6l, 8l)
Go言語のツールチェインにおいて、6lと8lはそれぞれ32-bit (x86) と64-bit (x86-64) アーキテクチャ向けのリンカです。リンカは、コンパイラによって生成されたオブジェクトファイル(機械語コード)を結合し、実行可能なバイナリファイルを生成する役割を担います。この過程で、アセンブリ命令を対応する機械語オペコードに変換する処理も行われます。
x86 FPU (Floating-Point Unit) スタックレジスタ
x86アーキテクチャのFPUは、浮動小数点演算のために専用のレジスタセットを使用します。これらのレジスタはスタック構造(ST(0)からST(7)まで)として機能します。
- ST(0) または F0: FPUスタックのトップにあるレジスタを指します。ほとんどのFPU命令は、暗黙的にST(0)をオペランドとして使用したり、結果をST(0)に格納したりします。
- ST(i): スタックのi番目の要素を指します。
FPU命令は、スタックに値をプッシュ(FLD)したり、スタックから値をポップ(FSTP)したり、スタック内のレジスタ間で演算を行ったりします。
浮動小数点演算命令 (FSUBD, FSUBRD, FDIVD, FDIVRD)
これらの命令は、x86 FPUにおける倍精度浮動小数点数(Double-precision floating-point, 64-bit)の減算および除算を行います。
-
FSUBD(Floating-point Subtract Double):- 通常形式:
FSUBD ST(i), ST(0)またはFSUBD ST(0), ST(i) - 意味:
ST(0) = ST(0) - ST(i)またはST(i) = ST(i) - ST(0) - このコミットで問題となったのは、メモリオペランドやレジスタオペランドを持つ形式で、特に
FSUBD src, destのような場合に、dest = dest - srcの演算を行います。
- 通常形式:
-
FSUBRD(Floating-point Subtract Reverse Double):- 通常形式:
FSUBRD ST(i), ST(0)またはFSUBRD ST(0), ST(i) - 意味:
ST(0) = ST(i) - ST(0)またはST(i) = ST(0) - ST(i) FSUBDとは異なり、オペランドの順序が逆転した減算を行います。つまり、dest = src - destの演算を行います。
- 通常形式:
-
FDIVD(Floating-point Divide Double):- 通常形式:
FDIVD ST(i), ST(0)またはFDIVD ST(0), ST(i) - 意味:
ST(0) = ST(0) / ST(i)またはST(i) = ST(i) / ST(0) dest = dest / srcの演算を行います。
- 通常形式:
-
FDIVRD(Floating-point Divide Reverse Double):- 通常形式:
FDIVRD ST(i), ST(0)またはFDIVRD ST(0), ST(i) - 意味:
ST(0) = ST(i) / ST(0)またはST(i) = ST(0) / ST(i) FDIVDとは異なり、オペランドの順序が逆転した除算を行います。つまり、dest = src / destの演算を行います。
- 通常形式:
optab.c
src/cmd/6l/optab.c および src/cmd/8l/optab.c は、Go言語のリンカがアセンブリ命令を機械語オペコードに変換するための「オペレーションテーブル」を定義しているファイルです。このテーブルには、各アセンブリ命令に対応するオペコード、オペランドの型、およびその他のエンコーディング情報が記述されています。
Optab構造体は、アセンブリ命令のシンボル(例: AFSUBD)、オペランドの型(例: yfadd)、プレフィックス(例: Px)、そして実際のオペコードバイト列とそれに続くオペランドエンコーディング情報(例: 0xdc,(04))を含んでいます。
0xdc,0xd8,0xdeなどは、x86命令のオペコードの最初のバイトです。(04),(05),(06),(07)などは、ModR/MバイトやSIBバイト、または命令の拡張オペコードの一部としてエンコードされるレジスタ情報やオペランドの種類を示していると考えられます。FPU命令の場合、これらはFPUスタックレジスタのインデックス(ST(0)からST(7))や、命令のバリアント(例: ST(0)をポップするかどうか)をエンコードするために使用されます。
技術的詳細
このバグは、FSUBD, FSUBRD, FDIVD, FDIVRD 命令が、FPUスタックのトップ(F0またはST(0))以外のレジスタを宛先として使用する場合に発生しました。リンカは、これらの命令に対して、通常の演算と逆順の演算のオペコードを誤って入れ替えて生成していました。
具体的には、optab.c内のOptab構造体の定義において、これらの命令のオペコードとオペランドエンコーディングの組み合わせが誤っていました。
例えば、FSUBD命令は dest = dest - src を計算するのに対し、FSUBRD命令は dest = src - dest を計算します。リンカがこれらの命令を機械語に変換する際、optab.cに定義された情報に基づいてオペコードを決定します。バグのある状態では、FSUBDが期待される場所でFSUBRDのオペコードが、FSUBRDが期待される場所でFSUBDのオペコードが生成されていました。これはFDIVDとFDIVRDについても同様でした。
修正は、src/cmd/6l/optab.cとsrc/cmd/8l/optab.c内の該当するOptabエントリのオペコード定義を修正することで行われました。特に、yfaddオペランドタイプを持つ命令のオペコードシーケンスが調整されました。
元の定義では、例えばAFSUBDの行で0xdc,(05)が使われていた箇所が0xdc,(04)に、AFSUBRDの行で0xdc,(04)が使われていた箇所が0xdc,(05)に修正されています。これは、FPU命令のオペコードエンコーディングにおけるレジスタ指定や命令バリアントのビットパターンが誤っていたことを示唆しています。04と05は、FPU命令のModR/Mバイトのregフィールドまたはopcode extensionフィールドの一部として、特定のFPUレジスタ(ST(0)など)や命令の動作(例: ポップするかしないか、オペランドの順序)をエンコードするために使用される値です。この修正により、リンカは正しいオペコードを生成し、Goプログラムが期待通りの浮動小数点演算を実行できるようになりました。
この修正には、test/fixedbugs/bug453.dir/bug453.goとtest/fixedbugs/bug453.dir/bug453.sという新しいテストケースが追加されました。bug453.sは、問題のあったFSUBRDとFSUBD命令を直接アセンブリで記述し、その結果が期待通りになるかを確認するものです。これにより、修正が正しく適用されたことを検証しています。
コアとなるコードの変更箇所
src/cmd/6l/optab.c
--- a/src/cmd/6l/optab.c
+++ b/src/cmd/6l/optab.c
@@ -1199,25 +1199,25 @@ Optab optab[] =
{ AFSUBW, yfmvx, Px, 0xde,(04) },
{ AFSUBL, yfmvx, Px, 0xda,(04) },
{ AFSUBF, yfmvx, Px, 0xd8,(04) },
- { AFSUBD, yfadd, Px, 0xdc,(04),0xd8,(04),0xdc,(05) },
+ { AFSUBD, yfadd, Px, 0xdc,(04),0xd8,(04),0xdc,(04) },
{ AFSUBRDP, yfaddp, Px, 0xde,(04) },
{ AFSUBRW, yfmvx, Px, 0xde,(05) },
{ AFSUBRL, yfmvx, Px, 0xda,(05) },
{ AFSUBRF, yfmvx, Px, 0xd8,(05) },
- { AFSUBRD, yfadd, Px, 0xdc,(05),0xd8,(05),0xdc,(04) },
+ { AFSUBRD, yfadd, Px, 0xdc,(05),0xd8,(05),0xdc,(05) },
{ AFDIVDP, yfaddp, Px, 0xde,(07) },
{ AFDIVW, yfmvx, Px, 0xde,(06) },
{ AFDIVL, yfmvx, Px, 0xda,(06) },
{ AFDIVF, yfmvx, Px, 0xd8,(06) },
- { AFDIVD, yfadd, Px, 0xdc,(06),0xd8,(06),0xdc,(07) },
+ { AFDIVD, yfadd, Px, 0xdc,(06),0xd8,(06),0xdc,(06) },
{ AFDIVRDP, yfaddp, Px, 0xde,(06) },
{ AFDIVRW, yfmvx, Px, 0xde,(07) },
{ AFDIVRL, yfmvx, Px, 0xda,(07) },
{ AFDIVRF, yfmvx, Px, 0xd8,(07) },
- { AFDIVRD, yfadd, Px, 0xdc,(07),0xd8,(07),0xdc,(06) },
+ { AFDIVRD, yfadd, Px, 0xdc,(07),0xd8,(07),0xdc,(07) },
{ AFXCHD, tyfxch, Px, 0xd9,(01),0xd9,(01) },
{ AFFREE },
src/cmd/8l/optab.c
--- a/src/cmd/8l/optab.c
+++ b/src/cmd/8l/optab.c
@@ -651,25 +651,25 @@ Optab optab[] =
{ AFSUBW, yfmvx, Px, 0xde,(04) },
{ AFSUBL, yfmvx, Px, 0xda,(04) },
{ AFSUBF, yfmvx, Px, 0xd8,(04) },
- { AFSUBD, yfadd, Px, 0xdc,(04),0xd8,(04),0xdc,(05) },
+ { AFSUBD, yfadd, Px, 0xdc,(04),0xd8,(04),0xdc,(04) },
{ AFSUBRDP, yfaddp, Px, 0xde,(04) },
{ AFSUBRW, yfmvx, Px, 0xde,(05) },
{ AFSUBRL, yfmvx, Px, 0xda,(05) },
{ AFSUBRF, yfmvx, Px, 0xd8,(05) },
- { AFSUBRD, yfadd, Px, 0xdc,(05),0xd8,(05),0xdc,(04) },
+ { AFSUBRD, yfadd, Px, 0xdc,(05),0xd8,(05),0xdc,(05) },
{ AFDIVDP, yfaddp, Px, 0xde,(07) },
{ AFDIVW, yfmvx, Px, 0xde,(06) },
{ AFDIVL, yfmvx, Px, 0xda,(06) },
{ AFDIVF, yfmvx, Px, 0xd8,(06) },
- { AFDIVD, yfadd, Px, 0xdc,(06),0xd8,(06),0xdc,(07) },
+ { AFDIVD, yfadd, Px, 0xdc,(06),0xd8,(06),0xdc,(06) },
{ AFDIVRDP, yfaddp, Px, 0xde,(06) },
{ AFDIVRW, yfmvx, Px, 0xde,(07) },
{ AFDIVRL, yfmvx, Px, 0xda,(07) },
{ AFDIVRF, yfmvx, Px, 0xd8,(07) },
- { AFDIVRD, yfadd, Px, 0xdc,(07),0xd8,(07),0xdc,(06) },
+ { AFDIVRD, yfadd, Px, 0xdc,(07),0xd8,(07),0xdc,(07) },
{ AFXCHD, tyfxch, Px, 0xd9,(01),0xd9,(01) },
{ AFFREE },
コアとなるコードの解説
上記の差分は、optab.cファイル内のOptab配列の定義を変更しています。この配列は、Goリンカがアセンブリ命令を機械語オペコードに変換する際に参照するテーブルです。
各行は一つのアセンブリ命令に対応し、以下の情報を含んでいます(簡略化):
{ <アセンブリ命令シンボル>, <オペランドタイプ>, <プレフィックス>, <オペコードバイト1>, <オペランドエンコーディング1>, <オペコードバイト2>, <オペランドエンコーディング2>, ... }
このコミットで修正されたのは、AFSUBD, AFSUBRD, AFDIVD, AFDIVRD の各エントリです。これらの命令は、yfaddというオペランドタイプを持ち、複数のオペコードバイトとオペランドエンコーディングの組み合わせで定義されています。
例えば、AFSUBDの行を見てみましょう。
- 修正前:
{ AFSUBD, yfadd, Px, 0xdc,(04),0xd8,(04),0xdc,(05) } - 修正後:
{ AFSUBD, yfadd, Px, 0xdc,(04),0xd8,(04),0xdc,(04) }
この変更は、最後のオペランドエンコーディングが(05)から(04)に変わっています。同様に、AFSUBRDでは(04)から(05)に、AFDIVDでは(07)から(06)に、AFDIVRDでは(06)から(07)にそれぞれ変更されています。
これらの(04), (05), (06), (07)といった数値は、x86 FPU命令のオペコードエンコーディングにおける、ModR/Mバイトのregフィールドやopcode extensionフィールド、あるいは命令の特定のバリアントを示すビットパターンに対応しています。FPU命令は、同じオペコードプレフィックス(例: 0xdc)を持つ場合でも、ModR/Mバイトの特定のビット(通常はregフィールド)によって、異なるFPUレジスタを対象としたり、異なる演算バリアント(例: 通常の減算 vs. 逆順減算)をエンコードしたりします。
この修正は、リンカがFSUBDとFSUBRD(およびFDIVDとFDIVRD)の命令を機械語に変換する際に、正しいFPU命令のバリアントに対応するオペコードを生成するように、これらのエンコーディング値を調整したものです。これにより、FPUスタックのトップ以外のレジスタを宛先とする浮動小数点演算が、意図した通りに実行されるようになりました。
関連リンク
- Go Code Review: https://golang.org/cl/6498092
参考にした情報源リンク
- x86 Instruction Set Reference (Intel/AMD manuals): FPU命令の詳細なオペコードエンコーディングについて
- Go言語のリンカのソースコード(
src/cmd/6l/optab.c,src/cmd/8l/optab.c) - Go言語のテストフレームワークとアセンブリテストの慣習について