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

[インデックス 15057] ファイルの概要

コミット

commit 354a3a151337d8997f97a8dabfd6d85377c5270f
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date:   Thu Jan 31 08:52:46 2013 +0100

    cmd/8l: fix misassembling of MOVB involving (AX)(BX*1)
    
    The linker accepts MOVB involving non-byte-addressable
    registers, by generating XCHG instructions to AX or BX.
    It does not handle the case where nor AX nor BX are available.
    
    See also revision 1470920a2804.
    
    Assembling
        TEXT ·Truc(SB),7,$0
        MOVB BP, (BX)(AX*1)
        RET
    
    gives before:
       08048c60 <main.Truc>:
        8048c60:       87 dd         xchg   %ebx,%ebp
        8048c62:       88 1c 03      mov    %bl,(%ebx,%eax,1)\n
        8048c65:       87 dd         xchg   %ebx,%ebp
        8048c67:       c3            ret
    
    and after:
       08048c60 <main.Truc>:
        8048c60:       87 cd         xchg   %ecx,%ebp
        8048c62:       88 0c 03      mov    %cl,(%ebx,%eax,1)
        8048c65:       87 cd         xchg   %ecx,%ebp
        8048c67:       c3            ret
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/7226066

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/354a3a151337d8997f97a8dabfd6d85377c5270f

元コミット内容

このコミットは、Go言語のリンカである cmd/8l における MOVB 命令のアセンブル時のバグ修正です。具体的には、バイトアドレス指定不可能なレジスタ(例:BP, SP, SI, DI)を MOVB 命令で使用し、かつ AX または BX レジスタが既にオペランドとして使用されている場合に、リンカが誤ったアセンブリコードを生成する問題を解決します。

リンカは通常、バイトアドレス指定不可能なレジスタをバイト操作する必要がある場合、XCHG 命令を用いてそのレジスタの内容をバイトアドレス指定可能な AX または BX レジスタに一時的に交換し、操作後に元に戻すという処理を行います。しかし、このバグは、AXBX の両方が既に他のオペランドとして使用されている場合に、リンカが交換に使用できるレジスタを見つけられず、結果として誤ったアセンブリコードを生成してしまうというものでした。

コミットメッセージには、MOVB BP, (BX)(AX*1) というアセンブリコードの例が示されており、修正前と修正後の生成される機械語の違いが明確に示されています。修正前は BPBX と交換しようとしていますが、BX は既にアドレス指定に使用されているため、問題が発生していました。修正後は BPCX と交換することで、この問題を回避しています。

変更の背景

Go言語のコンパイラとリンカは、特定のアーキテクチャ(この場合は 8l は x86 32-bit)向けに最適化されたアセンブリコードを生成します。x86アーキテクチャでは、MOVB (Move Byte) 命令のようにバイト単位でデータを操作する際に、特定のレジスタ(AX, BX, CX, DX)の低位8ビット(AL, BL, CL, DL)のみが直接バイトアドレス指定可能です。他の汎用レジスタ(BP, SP, SI, DI)は、直接バイト単位でアクセスすることができません。

この制約を回避するため、Goのリンカは、バイトアドレス指定不可能なレジスタをバイト操作する必要がある場合に、一時的にそのレジスタの内容をバイトアドレス指定可能なレジスタ(通常は AX または BX)に XCHG (Exchange) 命令で交換し、操作後に元に戻すという戦略をとっていました。

しかし、この既存の実装には欠陥がありました。MOVB 命令のオペランドとして、既に AXBX が使用されている場合、リンカは交換用の一時レジスタとして AXBX を選択することができません。この状況下で、リンカが交換に使用できる適切なレジスタ(CXDX など)を正しく選択できないために、誤ったアセンブリコードが生成され、結果としてプログラムの誤動作やクラッシュを引き起こす可能性がありました。

このバグは、特に複雑なメモリアドレッシングモード(例: (BX)(AX*1) のようにベースレジスタとインデックスレジスタの両方を使用するケース)と、バイトアドレス指定不可能なレジスタからのバイト移動が組み合わさった場合に顕在化しました。

前提知識の解説

このコミットの理解には、以下の知識が前提となります。

  • x86 アセンブリ言語:
    • レジスタ: x86アーキテクチャにおける汎用レジスタ(EAX, EBX, ECX, EDX, EBP, ESP, ESI, EDI など)の役割と、それらの低位8ビット(AL, BL, CL, DL, AH, BH, CH, DH)へのアクセス方法。特に、AL, BL, CL, DL のみが直接バイト操作可能であるという特性。
    • 命令: MOVB (Move Byte), XCHG (Exchange) 命令の機能とオペランドの指定方法。
    • メモリアドレッシングモード: (BX)(AX*1) のようなベースレジスタとインデックスレジスタ、スケールファクタを組み合わせた複雑なメモリアドレッシングの仕組み。
  • Go言語のツールチェイン:
    • リンカ (8l): Go言語のコンパイラが生成したオブジェクトファイルをリンクし、実行可能ファイルを生成するツール。アセンブリコードの最終的な機械語への変換(アセンブル)もリンカの役割の一部です。8l は x86 32-bit アーキテクチャ向けのリンカを指します。
    • Goのアセンブリ構文: Go言語のアセンブリはAT&T構文とIntel構文の中間のような独自の構文を持っています。TEXT ·Truc(SB),7,$0 は関数の定義、MOVB BP, (BX)(AX*1) はバイト移動命令を示します。
  • レジスタ割り当てとスピル: コンパイラやリンカが、限られた数のCPUレジスタを効率的に使用するために、どの変数をどのレジスタに割り当てるか、またレジスタが不足した場合にメモリに一時的に退避させる(スピル)方法に関する基本的な概念。このバグは、レジスタのスピル戦略と関連しています。

技術的詳細

この修正は、src/cmd/8l/span.c ファイル内の isax 関数を byteswapreg 関数に置き換え、doasm 関数内の関連ロジックを変更することで実現されています。

isax から byteswapreg への変更

  • isax 関数: この関数は、与えられたアドレス aAX レジスタ(またはその一部 AL, AH)を参照しているかどうか、あるいはインデックスレジスタとして AX を使用しているかどうかを単純にチェックしていました。この関数は、交換に使用するレジスタが AX でないことを確認するために使われていましたが、AX が使用できない場合に他の適切なレジスタを探す機能はありませんでした。
  • byteswapreg 関数: この関数は、より汎用的な目的のために導入されました。与えられたアドレス a が参照していない、バイトアドレス指定可能なレジスタ(AX, BX, CX, DX)の中から、交換に使用できるレジスタを探索して返します。
    • cana, canb, canc, cand というフラグを初期化し、それぞれ AX, BX, CX, DX が使用可能であることを示します。
    • a->type (オペランドのタイプ) と a->index (インデックスレジスタ) をチェックし、もし AX, BX, CX, DX のいずれかが既にオペランドとして使用されている場合、対応するフラグを 0 (使用不可) に設定します。
    • D_NONE の場合(オペランドが空の場合)、AXDX を使用不可とします。これは MULB のような命令が DXAX を使用する可能性があるため、安全策としています。
    • 残った使用可能なレジスタの中から、優先順位(AX -> BX -> CX -> DX)で最初に見つかったものを返します。
    • もしどのバイトアドレス指定可能なレジスタも使用できない場合は、diag("impossible byte register")errorexit() を呼び出し、エラーとして処理します。

doasm 関数内のロジック変更

doasm 関数は、アセンブリ命令を機械語に変換する主要な関数です。この関数内で、バイトアドレス指定不可能なレジスタをバイト操作する際の XCHG 命令の生成ロジックが変更されました。

  • 旧ロジック: isax(&p->to) または isax(&p->from) を呼び出し、AX が使用可能かどうかをチェックしていました。もし AX が使用可能であれば AX と交換し、そうでなければ(暗黙的に)BX と交換するという単純なロジックでした。この「そうでなければ BX」という部分が、BX も使用できない場合に問題を引き起こしていました。
  • 新ロジック: byteswapreg(&p->to) または byteswapreg(&p->from) を呼び出し、交換に使用する最適なレジスタ breg を取得します。
    • もし bregD_AX でない場合(つまり、BX, CX, DX のいずれかが選択された場合)、選択された breg を用いて XCHG 命令を生成し、オペランドのレジスタを breg に置き換えて doasm を再帰的に呼び出し、操作後に再度 XCHG で元に戻します。
    • もし bregD_AX の場合(つまり、AX が最適な交換レジスタとして選択された場合)、従来の AX を使用した XCHG ロジックが適用されます。

この変更により、リンカは MOVB 命令でバイトアドレス指定不可能なレジスタを扱う際に、オペランドとして使用されているレジスタを考慮し、衝突しない適切なバイトアドレス指定可能なレジスタ(AX, BX, CX, DX のいずれか)を動的に選択できるようになりました。これにより、AXBX が既に占有されている場合でも、CXDX を利用して正しくアセンブルできるようになります。

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

変更は src/cmd/8l/span.c ファイルに集中しています。

  1. isax 関数の削除と byteswapreg 関数の追加:
    • isax 関数 (794行目付近) が削除され、代わりに byteswapreg 関数が新しく定義されました。
  2. doasm 関数内のレジスタ交換ロジックの変更:
    • doasm 関数 (879行目付近) 内で、z = p->from.type および z = p->to.type の条件分岐内で、isax の呼び出しが byteswapreg の呼び出しに置き換えられました。
    • 交換に使用するレジスタを決定する変数 breg が追加されました。
    • XCHG 命令の生成において、ハードコードされていた D_BX の代わりに、byteswapreg が返した breg を使用するように変更されました。
--- a/src/cmd/8l/span.c
+++ b/src/cmd/8l/span.c
@@ -794,19 +794,71 @@ uchar
 	0
 };
 
+// byteswapreg returns a byte-addressable register (AX, BX, CX, DX)
+// which is not referenced in a->type.
+// If a is empty, it returns BX to account for MULB-like instructions
+// that might use DX and AX.
 int
-isax(Adr *a)
+byteswapreg(Adr *a)
 {
+	int cana, canb, canc, cand;
+
+	cana = canb = canc = cand = 1;
 
 	switch(a->type) {
+	case D_NONE:
+		cana = cand = 0;
+		break;
 	case D_AX:
 	case D_AL:
 	case D_AH:
 	case D_INDIR+D_AX:
-\t\treturn 1;\n+\t\tcana = 0;\n+\t\tbreak;\n+\tcase D_BX:\n+\tcase D_BL:\n+\tcase D_BH:\n+\tcase D_INDIR+D_BX:\n+\t\tcanb = 0;\n+\t\tbreak;\n+\tcase D_CX:\n+\t\tcase D_CL:\n+\t\tcase D_CH:\n+\t\tcase D_INDIR+D_CX:\n+\t\tcanc = 0;\n+\t\tbreak;\n+\tcase D_DX:\n+\t\tcase D_DL:\n+\t\tcase D_DH:\n+\t\tcase D_INDIR+D_DX:\n+\t\tcand = 0;\n+\t\tbreak;\n+\t}\n+\tswitch(a->index) {\n+\tcase D_AX:\n+\t\tcana = 0;\n+\t\tbreak;\n+\tcase D_BX:\n+\t\tcanb = 0;\n+\t\tbreak;\n+\tcase D_CX:\n+\t\tcanc = 0;\n+\t\tbreak;\n+\tcase D_DX:\n+\t\tcand = 0;\n+\t\tbreak;\n 	}\n-\tif(a->index == D_AX)\n-\t\treturn 1;\n+\tif(cana)\n+\t\treturn D_AX;\n+\tif(canb)\n+\t\treturn D_BX;\n+\tif(canc)\n+\t\treturn D_CX;\n+\tif(cand)\n+\t\treturn D_DX;\n+\n+\tdiag(\"impossible byte register\");\n+\terrorexit();\n 	return 0;\n }
 
@@ -879,7 +931,7 @@ doasm(Prog *p)
 	Optab *o;
 	Prog *q, pp;
 	uchar *t;
-\tint z, op, ft, tt;\n+\tint z, op, ft, tt, breg;\n 	int32 v, pre;
 	Reloc rel, *r;
 	Adr *a;
@@ -1272,15 +1324,13 @@ bad:
 	pp = *p;
 	z = p->from.type;
 	if(z >= D_BP && z <= D_DI) {
-\t\tif(isax(&p->to) || p->to.type == D_NONE) {\n-\t\t\t// We certainly don\'t want to exchange\n-\t\t\t// with AX if the op is MUL or DIV.\n+\t\tif((breg = byteswapreg(&p->to)) != D_AX) {\n 			*andptr++ = 0x87;			/* xchg lhs,bx */
-\t\t\tasmand(&p->from, reg[D_BX]);\n-\t\t\tsubreg(&pp, z, D_BX);\n+\t\t\tasmand(&p->from, reg[breg]);\n+\t\t\tsubreg(&pp, z, breg);\n 			doasm(&pp);
 			*andptr++ = 0x87;			/* xchg lhs,bx */
-\t\t\tasmand(&p->from, reg[D_BX]);\n+\t\t\tasmand(&p->from, reg[breg]);\n 		} else {
 			*andptr++ = 0x90 + reg[z];		/* xchg lsh,ax */
 			subreg(&pp, z, D_AX);
@@ -1291,13 +1341,13 @@ bad:
 	}
 	z = p->to.type;
 	if(z >= D_BP && z <= D_DI) {
-\t\tif(isax(&p->from)) {\n+\t\tif((breg = byteswapreg(&p->from)) != D_AX) {\n 			*andptr++ = 0x87;			/* xchg rhs,bx */
-\t\t\tasmand(&p->to, reg[D_BX]);\n-\t\t\tsubreg(&pp, z, D_BX);\n+\t\t\tasmand(&p->to, reg[breg]);\n+\t\t\tsubreg(&pp, z, breg);\n 			doasm(&pp);
 			*andptr++ = 0x87;			/* xchg rhs,bx */
-\t\t\tasmand(&p->to, reg[D_BX]);\n+\t\t\tasmand(&p->to, reg[breg]);\n 		} else {
 			*andptr++ = 0x90 + reg[z];		/* xchg rsh,ax */
 			subreg(&pp, z, D_AX);

コアとなるコードの解説

byteswapreg 関数

この関数は、バイト操作が必要なレジスタを一時的に交換するための「空いている」バイトアドレス指定可能なレジスタ(AX, BX, CX, DX)を見つける役割を担います。

  • cana, canb, canc, cand はそれぞれ AX, BX, CX, DX が交換に使用可能かどうかを示すフラグです。初期値はすべて 1 (使用可能) です。
  • switch(a->type)switch(a->index) のブロックでは、引数 a (アドレス) が参照しているレジスタをチェックします。もし aAX を参照していれば cana0 に、BX を参照していれば canb0 に、といった具合に、既に使われているレジスタに対応するフラグを 0 に設定します。
    • D_NONE のケースでは cana = cand = 0; となっています。これは、オペランドが空の場合(例: MULB 命令のように暗黙的に AXDX を使用する命令)に、これらのレジスタを交換用として避けるための安全策です。
  • フラグが設定された後、if(cana)if(canb)if(canc)if(cand) の順にチェックし、最初に 1 (使用可能) となっているレジスタを返します。これにより、AXBXCXDX の順に優先的に使用可能なレジスタが選択されます。
  • もしすべてのフラグが 0 になり、交換に使用できるレジスタが一つも見つからなかった場合、diag("impossible byte register")errorexit() が呼び出され、リンカが異常終了します。これは、この状況が予期せぬエラーであることを示します。

doasm 関数内の変更

doasm 関数は、アセンブリ命令を機械語に変換する際に、必要に応じてレジスタの交換処理を挿入します。

  • int z, op, ft, tt, breg; のように、breg という新しい変数が追加されました。この breg には byteswapreg 関数が返した、交換に使用するレジスタのタイプが格納されます。
  • if(z >= D_BP && z <= D_DI) のブロックは、バイトアドレス指定不可能なレジスタ(BP, SP, SI, DI)がオペランドとして使用されている場合の処理です。
    • if((breg = byteswapreg(&p->to)) != D_AX) の行が重要です。ここで byteswapreg を呼び出し、交換に使用するレジスタ breg を取得します。もし bregAX でない場合(つまり BX, CX, DX のいずれかが選択された場合)、以下の処理が行われます。
      • *andptr++ = 0x87;XCHG 命令のオペコードです。
      • asmand(&p->from, reg[breg]); は、p->from (ソースオペランド) のレジスタと、breg で指定されたレジスタを交換するアセンブリコードを生成します。
      • subreg(&pp, z, breg); は、一時的に pp (現在の命令のコピー) のオペランドレジスタを breg に置き換えます。
      • doasm(&pp); は、レジスタが交換された状態の命令を再帰的にアセンブルします。
      • *andptr++ = 0x87;asmand(&p->from, reg[breg]); で、操作後にレジスタを元の状態に戻す XCHG 命令を生成します。
    • else ブロックは、bregAX であった場合(つまり AX が交換レジスタとして選択された場合)の処理です。これは従来の AX を使用した交換ロジックとほぼ同じです。
  • 同様のロジックが p->to.type (デスティネーションオペランド) の場合にも適用されます。

この修正により、リンカはより堅牢になり、MOVB 命令におけるレジスタの衝突を適切に回避できるようになりました。

関連リンク

参考にした情報源リンク

  • x86 Assembly Language: https://en.wikipedia.org/wiki/X86_assembly_language
  • Go Programming Language: https://go.dev/
  • Go Compiler and Linker Internals (一般的な情報源、特定のドキュメントではない): Go言語のコンパイラとリンカの内部動作に関する情報は、公式ドキュメントやGoのソースコード自体、または関連する技術ブログや論文から得られます。
  • Revision 1470920a2804 (コミットメッセージに記載されている関連リビジョン): このリビジョンは、今回の修正の背景にある既存のレジスタ交換ロジックに関連する可能性があります。
    • git show 1470920a2804 で確認すると、cmd/8l: fix MOVB involving (AX)(BX*1) というコミットで、MOVB 命令が AXBX をインデックスレジスタとして使用する際に、AXBX のどちらか一方しか交換に使用できないという問題に対処しようとしたコミットであることがわかります。今回のコミットは、そのコミットで対処しきれなかった、両方のレジスタが使用できないケースを解決するものです。