[インデックス 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
レジスタに一時的に交換し、操作後に元に戻すという処理を行います。しかし、このバグは、AX
と BX
の両方が既に他のオペランドとして使用されている場合に、リンカが交換に使用できるレジスタを見つけられず、結果として誤ったアセンブリコードを生成してしまうというものでした。
コミットメッセージには、MOVB BP, (BX)(AX*1)
というアセンブリコードの例が示されており、修正前と修正後の生成される機械語の違いが明確に示されています。修正前は BP
を BX
と交換しようとしていますが、BX
は既にアドレス指定に使用されているため、問題が発生していました。修正後は BP
を CX
と交換することで、この問題を回避しています。
変更の背景
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
命令のオペランドとして、既に AX
や BX
が使用されている場合、リンカは交換用の一時レジスタとして AX
や BX
を選択することができません。この状況下で、リンカが交換に使用できる適切なレジスタ(CX
や DX
など)を正しく選択できないために、誤ったアセンブリコードが生成され、結果としてプログラムの誤動作やクラッシュを引き起こす可能性がありました。
このバグは、特に複雑なメモリアドレッシングモード(例: (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)
のようなベースレジスタとインデックスレジスタ、スケールファクタを組み合わせた複雑なメモリアドレッシングの仕組み。
- レジスタ: x86アーキテクチャにおける汎用レジスタ(
- 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
関数: この関数は、与えられたアドレスa
がAX
レジスタ(またはその一部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
の場合(オペランドが空の場合)、AX
とDX
を使用不可とします。これはMULB
のような命令がDX
とAX
を使用する可能性があるため、安全策としています。- 残った使用可能なレジスタの中から、優先順位(
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
を取得します。- もし
breg
がD_AX
でない場合(つまり、BX
,CX
,DX
のいずれかが選択された場合)、選択されたbreg
を用いてXCHG
命令を生成し、オペランドのレジスタをbreg
に置き換えてdoasm
を再帰的に呼び出し、操作後に再度XCHG
で元に戻します。 - もし
breg
がD_AX
の場合(つまり、AX
が最適な交換レジスタとして選択された場合)、従来のAX
を使用したXCHG
ロジックが適用されます。
- もし
この変更により、リンカは MOVB
命令でバイトアドレス指定不可能なレジスタを扱う際に、オペランドとして使用されているレジスタを考慮し、衝突しない適切なバイトアドレス指定可能なレジスタ(AX
, BX
, CX
, DX
のいずれか)を動的に選択できるようになりました。これにより、AX
や BX
が既に占有されている場合でも、CX
や DX
を利用して正しくアセンブルできるようになります。
コアとなるコードの変更箇所
変更は src/cmd/8l/span.c
ファイルに集中しています。
isax
関数の削除とbyteswapreg
関数の追加:isax
関数 (794行目付近) が削除され、代わりにbyteswapreg
関数が新しく定義されました。
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
(アドレス) が参照しているレジスタをチェックします。もしa
がAX
を参照していればcana
を0
に、BX
を参照していればcanb
を0
に、といった具合に、既に使われているレジスタに対応するフラグを0
に設定します。D_NONE
のケースではcana = cand = 0;
となっています。これは、オペランドが空の場合(例:MULB
命令のように暗黙的にAX
やDX
を使用する命令)に、これらのレジスタを交換用として避けるための安全策です。
- フラグが設定された後、
if(cana)
、if(canb)
、if(canc)
、if(cand)
の順にチェックし、最初に1
(使用可能) となっているレジスタを返します。これにより、AX
、BX
、CX
、DX
の順に優先的に使用可能なレジスタが選択されます。 - もしすべてのフラグが
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
を取得します。もしbreg
がAX
でない場合(つまり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
ブロックは、breg
がAX
であった場合(つまりAX
が交換レジスタとして選択された場合)の処理です。これは従来のAX
を使用した交換ロジックとほぼ同じです。
- 同様のロジックが
p->to.type
(デスティネーションオペランド) の場合にも適用されます。
この修正により、リンカはより堅牢になり、MOVB
命令におけるレジスタの衝突を適切に回避できるようになりました。
関連リンク
- Go言語のIssueトラッカー: https://golang.org/cl/7226066 (コミットメッセージに記載されているCLリンク)
- Go言語のソースコードリポジトリ: https://github.com/golang/go
参考にした情報源リンク
- 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
命令がAX
とBX
をインデックスレジスタとして使用する際に、AX
とBX
のどちらか一方しか交換に使用できないという問題に対処しようとしたコミットであることがわかります。今回のコミットは、そのコミットで対処しきれなかった、両方のレジスタが使用できないケースを解決するものです。