[インデックス 14488] ファイルの概要
このコミットは、Go言語のリンカである8l
(x86アーキテクチャ向け)と6l
(amd64アーキテクチャ向け)におけるデータ破損のバグを修正するものです。具体的には、MULB SI
(バイト乗算命令)のような特定の命令において、リンカが自動的に挿入するXCHG
(レジスタ内容交換)命令が、AX
レジスタの内容を意図せず上書きしてしまうことで発生する問題を解決します。
コミット
commit 54023a94a6d7a282571ab18da7862a909d36d894
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date: Mon Nov 26 21:31:42 2012 +0100
cmd/8l: fix data corruption for MULB SI,
The 8l linker automatically inserts XCHG instructions
to support otherwise impossible byte registers
(only available on AX, BX, CX, DX).
Sometimes AX or DX is needed (for MUL and DIV) so
we need to avoid clobbering them.
R=golang-dev, dave, iant, iant, rsc
CC=golang-dev
https://golang.org/cl/6846057
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/54023a94a6d7a282571ab18da7862a909d36d894
元コミット内容
Go言語のリンカ(8l
および6l
)は、特定のバイトレジスタ(AX
, BX
, CX
, DX
のみが利用可能)をサポートするために、自動的にXCHG
命令を挿入していました。しかし、MUL
(乗算)やDIV
(除算)命令のように、AX
やDX
レジスタを暗黙的に使用する命令の場合、この自動挿入されたXCHG
命令がAX
やDX
の内容を破壊し、結果としてデータ破損を引き起こす可能性がありました。このコミットは、この問題に対処し、MUL
やDIV
命令がAX
レジスタを必要とする場合にXCHG
命令の挿入を回避するようにリンカの挙動を修正します。
変更の背景
x86アーキテクチャのプロセッサでは、MUL
やDIV
といった一部の算術命令は、特定のレジスタ(特にAX
とDX
)を暗黙的に使用します。例えば、8ビットの乗算命令であるMULB
は、AL
レジスタ(AX
の下位8ビット)とオペランドを乗算し、結果をAX
レジスタに格納します。同様に、除算命令もAX
やDX:AX
レジスタペアを被除数として使用し、結果をAX
とDX
に格納します。
Go言語のリンカは、アセンブリコードを機械語に変換する際に、特定の最適化やレジスタ割り当ての調整を行います。この文脈で、リンカは「不可能」なバイトレジスタ(例えば、SI
レジスタのバイト部分を直接操作しようとする場合など)を扱うために、XCHG
命令を挿入して、利用可能なバイトレジスタ(AX
, BX
, CX
, DX
)との間で値を交換していました。
しかし、MULB SI
のような命令の場合、SI
レジスタのバイト部分をAL
レジスタに移動させるためにXCHG
が挿入されると、その直後に実行されるMULB
命令がAL
(ひいてはAX
)を暗黙的に使用するため、リンカが挿入したXCHG
がAX
の本来の値を上書きしてしまい、計算結果が不正になるというデータ破損が発生していました。このコミットは、このような特定のシナリオでのAX
レジスタの意図しない上書きを防ぐことを目的としています。
前提知識の解説
- リンカ (Linker): コンパイラによって生成されたオブジェクトファイル(機械語コード)を結合し、実行可能なプログラムを生成するソフトウェアツールです。この過程で、ライブラリのリンク、アドレスの解決、レジスタの最適化など、様々な処理を行います。Go言語のリンカは、Goプログラムのビルドプロセスにおいて重要な役割を担っています。
- x86アーキテクチャ: Intelが開発したマイクロプロセッサの命令セットアーキテクチャです。PCのCPUなどで広く使われています。
- レジスタ (Register): CPU内部にある高速な記憶領域です。プログラムの実行中にデータやアドレスを一時的に保持するために使用されます。x86アーキテクチャには、汎用レジスタ(
AX
,BX
,CX
,DX
,SI
,DI
,BP
,SP
など)や特殊レジスタがあります。AX
(Accumulator Register): 算術演算の主要なレジスタとしてよく使われます。乗算や除算命令では暗黙的に使用されます。DX
(Data Register):AX
と組み合わせて、乗算や除算の拡張レジスタとして使われます。- バイトレジスタ:
AX
レジスタはAH
(上位8ビット)とAL
(下位8ビット)に分割できます。同様にBX
,CX
,DX
もバイト単位でアクセス可能です。
XCHG
命令 (Exchange): 2つのオペランドの内容を交換する命令です。例えば、XCHG AX, BX
はAX
とBX
の内容を入れ替えます。MUL
命令 (Multiply): 乗算命令です。オペランドのサイズによって、AL
、AX
、EAX
、RAX
レジスタを暗黙的に使用します。例えば、MULB src
はAL
とsrc
を乗算し、結果をAX
に格納します。DIV
命令 (Divide): 除算命令です。オペランドのサイズによって、AX
、DX:AX
、EAX
、EDX:EAX
、RAX
、RDX:RAX
レジスタを暗黙的に使用します。
技術的詳細
このコミットの技術的詳細を理解するためには、Goリンカの内部動作、特にレジスタ割り当てと命令変換のメカニズムを深く掘り下げる必要があります。
Goリンカ(6l
および8l
)は、Goコンパイラが生成した中間表現(Plan 9アセンブリ形式)を最終的な機械語に変換する際に、様々な最適化とコード生成を行います。その一つに、特定の命令が直接サポートしないレジスタを使用している場合に、それをサポートするレジスタに値を移動させるための補助命令を挿入する処理があります。
問題となっていたのは、x86アーキテクチャにおいて、SI
やDI
などのインデックスレジスタのバイト部分(例: SIL
, DIL
)を直接操作する命令が、古いx86プロセッサや特定の命令セットでは存在しない、あるいは効率的ではないという点です。Goリンカは、このような状況で、XCHG
命令を挿入して、SI
レジスタのバイト値をAX
、BX
、CX
、DX
といったバイトアクセスが可能な汎用レジスタに一時的に移動させていました。
しかし、MUL
やDIV
命令は、その設計上、AX
レジスタ(およびDX
レジスタ)を暗黙的な入力および出力として使用します。例えば、MULB
命令はAL
レジスタの内容とオペランドを乗算します。もし、リンカがMULB SI
のような命令を処理する際に、SI
のバイト値をAL
に移動させるためにXCHG SI, AX
のような命令を挿入してしまうと、そのXCHG
命令がAX
レジスタの既存の値を破壊してしまいます。その直後に実行されるMULB
命令は、破壊されたAX
レジスタの値を基に乗算を行うため、結果として誤った計算結果(データ破損)が生じることになります。
このコミットは、この問題を解決するために、span.c
ファイル内のレジスタ交換ロジックを変更しています。具体的には、XCHG
命令を挿入する条件に、p->to.type == D_NONE
という条件を追加しました。D_NONE
は、命令の宛先オペランドが明示的に指定されていない、つまり暗黙的なレジスタ(この文脈ではMUL
やDIV
命令におけるAX
レジスタ)が使用されることを示唆しています。
これにより、リンカは、命令がAX
レジスタを宛先として明示的に指定している場合(isax(&p->to)
)だけでなく、MUL
やDIV
のようにAX
レジスタを暗黙的に使用する場合(p->to.type == D_NONE
)にも、XCHG
命令の挿入を回避するようになります。これにより、AX
レジスタがMUL
やDIV
命令によって正しく使用されることが保証され、データ破損が防止されます。
コアとなるコードの変更箇所
変更はsrc/cmd/6l/span.c
とsrc/cmd/8l/span.c
の2つのファイルにわたって行われています。両ファイルで同じ修正が適用されています。
--- a/src/cmd/6l/span.c
+++ b/src/cmd/6l/span.c
@@ -1618,7 +1618,9 @@ bad:
pp = *p;
z = p->from.type;
if(z >= D_BP && z <= D_DI) {
- if(isax(&p->to)) {
+ if(isax(&p->to) || p->to.type == D_NONE) {
+ // We certainly don't want to exchange
+ // with AX if the op is MUL or DIV.
*andptr++ = 0x87; /* xchg lhs,bx */
asmando(&p->from, reg[D_BX]);
subreg(&pp, z, D_BX);
src/cmd/8l/span.c
も同様の変更です。
コアとなるコードの解説
変更の中心は、if
文の条件式の追加です。
元のコード:
if(isax(&p->to)) {
変更後のコード:
if(isax(&p->to) || p->to.type == D_NONE) {
// We certainly don't want to exchange
// with AX if the op is MUL or DIV.
isax(&p->to)
: これは、命令の宛先オペランド(p->to
)がAX
レジスタであるかどうかをチェックする関数です。もし宛先がAX
であれば、リンカはXCHG
命令の挿入を回避していました。p->to.type == D_NONE
: この新しい条件が追加されました。D_NONE
は、オペランドが明示的に指定されていないことを示す型です。MUL
やDIV
のような命令は、その操作のためにAX
やDX
レジスタを暗黙的に使用するため、これらの命令の宛先オペランドはD_NONE
として扱われることがあります。||
: 論理OR演算子です。これにより、宛先がAX
レジスタである場合、または宛先がD_NONE
である場合(つまり、暗黙的にAX
が使用される場合)に、続くXCHG
命令の挿入ロジックが実行されないようになります。- コメント:
// We certainly don't want to exchange // with AX if the op is MUL or DIV.
このコメントは、この変更の意図を明確に示しています。MUL
やDIV
命令がAX
レジスタを必要とする場合に、AX
とのXCHG
を避けるべきであるという理由を説明しています。
この修正により、リンカはMUL
やDIV
命令がAX
レジスタを暗黙的に使用するシナリオを正しく認識し、AX
レジスタの内容を保護するためのXCHG
命令の不適切な挿入を回避できるようになりました。これにより、これらの命令が期待通りに動作し、データ破損が防止されます。
関連リンク
- Go言語の公式ドキュメント: https://golang.org/
- Go言語のリンカに関する情報(古いバージョンを含む)は、Goのソースコードリポジトリや関連する設計ドキュメントで確認できます。
参考にした情報源リンク
- コミットメッセージ内のGo CLリンク: https://golang.org/cl/6846057
- x86アセンブリ言語の
MUL
およびDIV
命令に関する一般的な情報源(例: Intel Software Developer's Manuals, x86 Assembly Language Programming guides)。 XCHG
命令に関する情報源。- Go言語のリンカの内部構造に関する情報源(Goのソースコード自体や、Goのツールチェインに関する技術記事など)。
- Web検索結果:
Go linker 8l 6l XCHG instruction byte registers MUL DIV AX register data corruption