Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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(除算)命令のように、AXDXレジスタを暗黙的に使用する命令の場合、この自動挿入されたXCHG命令がAXDXの内容を破壊し、結果としてデータ破損を引き起こす可能性がありました。このコミットは、この問題に対処し、MULDIV命令がAXレジスタを必要とする場合にXCHG命令の挿入を回避するようにリンカの挙動を修正します。

変更の背景

x86アーキテクチャのプロセッサでは、MULDIVといった一部の算術命令は、特定のレジスタ(特にAXDX)を暗黙的に使用します。例えば、8ビットの乗算命令であるMULBは、ALレジスタ(AXの下位8ビット)とオペランドを乗算し、結果をAXレジスタに格納します。同様に、除算命令もAXDX:AXレジスタペアを被除数として使用し、結果をAXDXに格納します。

Go言語のリンカは、アセンブリコードを機械語に変換する際に、特定の最適化やレジスタ割り当ての調整を行います。この文脈で、リンカは「不可能」なバイトレジスタ(例えば、SIレジスタのバイト部分を直接操作しようとする場合など)を扱うために、XCHG命令を挿入して、利用可能なバイトレジスタ(AX, BX, CX, DX)との間で値を交換していました。

しかし、MULB SIのような命令の場合、SIレジスタのバイト部分をALレジスタに移動させるためにXCHGが挿入されると、その直後に実行されるMULB命令がAL(ひいてはAX)を暗黙的に使用するため、リンカが挿入したXCHGAXの本来の値を上書きしてしまい、計算結果が不正になるというデータ破損が発生していました。このコミットは、このような特定のシナリオでの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, BXAXBXの内容を入れ替えます。
  • MUL 命令 (Multiply): 乗算命令です。オペランドのサイズによって、ALAXEAXRAXレジスタを暗黙的に使用します。例えば、MULB srcALsrcを乗算し、結果をAXに格納します。
  • DIV 命令 (Divide): 除算命令です。オペランドのサイズによって、AXDX:AXEAXEDX:EAXRAXRDX:RAXレジスタを暗黙的に使用します。

技術的詳細

このコミットの技術的詳細を理解するためには、Goリンカの内部動作、特にレジスタ割り当てと命令変換のメカニズムを深く掘り下げる必要があります。

Goリンカ(6lおよび8l)は、Goコンパイラが生成した中間表現(Plan 9アセンブリ形式)を最終的な機械語に変換する際に、様々な最適化とコード生成を行います。その一つに、特定の命令が直接サポートしないレジスタを使用している場合に、それをサポートするレジスタに値を移動させるための補助命令を挿入する処理があります。

問題となっていたのは、x86アーキテクチャにおいて、SIDIなどのインデックスレジスタのバイト部分(例: SIL, DIL)を直接操作する命令が、古いx86プロセッサや特定の命令セットでは存在しない、あるいは効率的ではないという点です。Goリンカは、このような状況で、XCHG命令を挿入して、SIレジスタのバイト値をAXBXCXDXといったバイトアクセスが可能な汎用レジスタに一時的に移動させていました。

しかし、MULDIV命令は、その設計上、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は、命令の宛先オペランドが明示的に指定されていない、つまり暗黙的なレジスタ(この文脈ではMULDIV命令におけるAXレジスタ)が使用されることを示唆しています。

これにより、リンカは、命令がAXレジスタを宛先として明示的に指定している場合(isax(&p->to))だけでなく、MULDIVのようにAXレジスタを暗黙的に使用する場合(p->to.type == D_NONE)にも、XCHG命令の挿入を回避するようになります。これにより、AXレジスタがMULDIV命令によって正しく使用されることが保証され、データ破損が防止されます。

コアとなるコードの変更箇所

変更はsrc/cmd/6l/span.csrc/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は、オペランドが明示的に指定されていないことを示す型です。MULDIVのような命令は、その操作のためにAXDXレジスタを暗黙的に使用するため、これらの命令の宛先オペランドはD_NONEとして扱われることがあります。
  • ||: 論理OR演算子です。これにより、宛先がAXレジスタである場合、または宛先がD_NONEである場合(つまり、暗黙的にAXが使用される場合)に、続くXCHG命令の挿入ロジックが実行されないようになります。
  • コメント: // We certainly don't want to exchange // with AX if the op is MUL or DIV. このコメントは、この変更の意図を明確に示しています。MULDIV命令がAXレジスタを必要とする場合に、AXとのXCHGを避けるべきであるという理由を説明しています。

この修正により、リンカはMULDIV命令が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