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

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

このコミットは、Goコンパイラのバックエンドの一部である src/cmd/6g/gen.c ファイルに対する変更です。6g は、Go言語の初期のコンパイラツールチェーンの一部であり、Plan 9アーキテクチャをベースにしたGoコンパイラのx86-64 (AMD64) アーキテクチャ向けコードジェネレータです。gen.c は、抽象構文木 (AST) や中間表現 (IR) からターゲットマシンコードを生成する役割を担っています。具体的には、アセンブリ命令の生成やレジスタ割り当てに関連する処理が含まれています。

コミット

このコミットは、Ken Thompsonによって2008年12月20日に行われました。コミットメッセージは「LEAQ bug」と簡潔に記されており、LEAQ 命令に関連するバグの修正が目的であることが示唆されています。

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

https://github.com/golang/go/commit/7cfe782a693456346d92376e230dbb3b72cd6184

元コミット内容

commit 7cfe782a693456346d92376e230dbb3b72cd6184
Author: Ken Thompson <ken@golang.org>
Date:   Sat Dec 20 16:05:12 2008 -0800

    LEAQ bug
    
    R=r
    OCL=21684
    CL=21684
---
 src/cmd/6g/gen.c | 10 ++++++++--
 1 file changed, 8 insertions(+), 2 deletions(-)

diff --git a/src/cmd/6g/gen.c b/src/cmd/6g/gen.c
index 0f625c649b..640243f7f5 100644
--- a/src/cmd/6g/gen.c
+++ b/src/cmd/6g/gen.c
@@ -880,7 +880,7 @@ cgen_callret(Node *n, Node *res)\n void\n cgen_aret(Node *n, Node *res)\n {\n-\tNode nod1;\n+\tNode nod1, nod2;\n \tType *fp, *t;\n \tIter flist;\n \n@@ -900,7 +900,13 @@ cgen_aret(Node *n, Node *res)\n \tnod1.xoffset = fp->width;\n \tnod1.type = fp->type;\n \n-\tgins(ALEAQ, &nod1, res);\n+\tif(res->op != OREGISTER) {\n+print(\"its 1\\n\");\n+\t\tregalloc(&nod2, types[tptr], res);\n+\t\tgins(ALEAQ, &nod1, &nod2);\n+\t\tgins(AMOVQ, &nod2, res);\n+\t} else\n+\t\tgins(ALEAQ, &nod1, res);\n }\n \n void

変更の背景

このコミットは「LEAQ bug」という簡潔なメッセージで示されている通り、LEAQ (Load Effective Address) 命令の生成に関するバグを修正するものです。LEAQ 命令は、メモリのアドレスを計算し、その結果をレジスタにロードするために使用されます。これはポインタ演算や構造体のアドレス計算など、様々な場面で利用されます。

コンパイラがコードを生成する際、中間表現 (IR) の結果を最終的なマシンコードに変換します。この過程で、変数のアドレスをレジスタにロードする必要がある場合、LEAQ 命令が使われます。しかし、この命令のターゲット(結果を格納する場所)がレジスタではない場合、直接 LEAQ を発行すると問題が発生する可能性があります。

元のコードでは、LEAQ 命令のターゲットが常に res という Node オブジェクトに直接指定されていました。res がレジスタではない場合(例えば、メモリ上の場所を指す場合)、LEAQ 命令はレジスタをオペランドとして期待するため、不正なコードが生成されるか、コンパイラがクラッシュするなどの問題を引き起こす可能性がありました。このバグは、LEAQ 命令のセマンティクスと、コンパイラのレジスタ割り当ておよびコード生成ロジックの間の不整合に起因していたと考えられます。

前提知識の解説

このコミットを理解するためには、以下の概念が重要です。

  • Goコンパイラ (6g): Go言語の初期のコンパイラの一つで、x86-64アーキテクチャ向けのコードを生成します。コンパイラは、ソースコードを解析し、中間表現に変換し、最終的にターゲットマシンが実行できるアセンブリコードを生成します。
  • LEAQ (Load Effective Address) 命令: x86-64アーキテクチャのアセンブリ命令の一つです。LEA は "Load Effective Address" の略で、メモリのアドレスを計算し、その結果を汎用レジスタに格納します。例えば、LEAQ (%rax, %rbx, 4), %rcx は、%rax + %rbx * 4 のアドレスを計算し、その結果を %rcx レジスタに格納します。これは、ポインタの計算や、定数による乗算を伴うアドレス計算を効率的に行うために使われます。MOV 命令がメモリの内容をロードするのに対し、LEA はアドレス自体をロードします。
  • Node 構造体: コンパイラ内部で、抽象構文木 (AST) や中間表現 (IR) のノードを表すために使われるデータ構造です。各 Node は、変数、定数、演算子、関数呼び出しなど、プログラムの要素に対応します。Node には、その種類 (op)、型 (type)、値、レジスタ割り当て情報などが含まれます。
  • OREGISTER: Nodeop フィールドが取りうる値の一つで、その Node がレジスタを表していることを示します。
  • Type 構造体: コンパイラ内部で、Go言語の型システムを表すために使われるデータ構造です。types[tptr] は、ポインタ型を表すグローバルな Type オブジェクトへの参照と考えられます。
  • regalloc(Node *n, Type *t, Node *res): レジスタ割り当てを行う関数です。n にレジスタを割り当て、そのレジスタの情報を n に格納します。t は割り当てるレジスタの型、res は結果を格納する最終的な場所(ヒント)です。
  • gins(Op op, Node *src, Node *dst): アセンブリ命令を生成する関数です。op は命令の種類(例: ALEAQ, AMOVQ)、src はソースオペランド、dst はデスティネーションオペランドです。
  • ALEAQ: LEAQ 命令に対応する内部的なオペレーションコードです。
  • AMOVQ: MOVQ (Move Quadword) 命令に対応する内部的なオペレーションコードです。MOVQ は、64ビットの値を移動させる命令です。

技術的詳細

このコミットの技術的な核心は、cgen_aret 関数における LEAQ 命令の生成ロジックの改善にあります。cgen_aret 関数は、おそらく関数の戻り値のアドレスを計算し、それを特定の場所に格納する役割を担っています。

元のコードでは、gins(ALEAQ, &nod1, res); という行で、nod1 のアドレスを計算し、その結果を res に直接格納しようとしていました。ここで問題となるのは、res が必ずしもレジスタであるとは限らない点です。LEAQ 命令は、その結果をレジスタに格納することを前提としています。もし res がメモリ上の場所を指す Node であった場合、LEAQ 命令は不正なオペランドを受け取ることになり、コンパイラの誤動作やクラッシュにつながります。

修正後のコードでは、この問題を解決するために条件分岐が導入されました。

  1. if(res->op != OREGISTER): まず、res がレジスタではないかどうかをチェックします。
  2. 一時レジスタの導入: もし res がレジスタではない場合、一時的なレジスタ nod2 を導入します。
    • regalloc(&nod2, types[tptr], res);: nod2 にポインタ型 (types[tptr]) のレジスタを割り当てます。res はレジスタ割り当てのヒントとして使われます。
    • gins(ALEAQ, &nod1, &nod2);: nod1 のアドレスを計算し、その結果を一時レジスタ nod2 に格納する LEAQ 命令を生成します。これで LEAQ 命令のデスティネーションが常にレジスタになります。
    • gins(AMOVQ, &nod2, res);: 一時レジスタ nod2 に格納されたアドレスを、本来のデスティネーションである res に移動させる MOVQ 命令を生成します。これにより、res がメモリ上の場所であっても、正しくアドレスが格納されます。
  3. 既存のロジックの維持: もし res が既にレジスタである場合は、元の gins(ALEAQ, &nod1, res); のロジックをそのまま使用します。この場合、LEAQ 命令のデスティネーションは既にレジスタであるため、問題ありません。

この修正により、LEAQ 命令が常に適切なデスティネーション(レジスタ)を持つことが保証され、コンパイラの堅牢性が向上しました。これは、コンパイラのコード生成におけるレジスタ割り当てと命令セマンティクスの厳密な遵守の重要性を示す典型的な例です。

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

--- a/src/cmd/6g/gen.c
+++ b/src/cmd/6g/gen.c
@@ -880,7 +880,7 @@ cgen_callret(Node *n, Node *res)\n void\n cgen_aret(Node *n, Node *res)\n {\n-\tNode nod1;\n+\tNode nod1, nod2;\n \tType *fp, *t;\n \tIter flist;\n \n@@ -900,7 +900,13 @@ cgen_aret(Node *n, Node *res)\n \tnod1.xoffset = fp->width;\n \tnod1.type = fp->type;\n \n-\tgins(ALEAQ, &nod1, res);\n+\tif(res->op != OREGISTER) {\n+print(\"its 1\\n\");\n+\t\tregalloc(&nod2, types[tptr], res);\n+\t\tgins(ALEAQ, &nod1, &nod2);\n+\t\tgins(AMOVQ, &nod2, res);\n+\t} else\n+\t\tgins(ALEAQ, &nod1, res);\n }\n \n void

コアとなるコードの解説

cgen_aret 関数内の変更点に焦点を当てて解説します。

  1. Node nod1; から Node nod1, nod2; への変更:

    • nod2 という新しい Node 型の変数が追加されました。これは、LEAQ 命令の結果を一時的に保持するためのレジスタを表すために使用されます。
  2. gins(ALEAQ, &nod1, res); の変更:

    • この行が、より複雑な条件分岐に置き換えられました。

    • if(res->op != OREGISTER) { ... }:

      • この条件は、res ノードがレジスタではない場合に真となります。つまり、LEAQ 命令の直接のデスティネーションとして res を使用できない状況です。
      • print("its 1\\n");: デバッグ用の出力と思われます。このパスが実行されたことを示すために一時的に追加された可能性があります。
      • regalloc(&nod2, types[tptr], res);: nod2 にポインタ型 (types[tptr]) のレジスタを割り当てます。res は、レジスタ割り当てのヒントとして渡されます。これにより、nod2 は一時的なレジスタとして機能します。
      • gins(ALEAQ, &nod1, &nod2);: nod1 のアドレスを計算し、その結果を新しく割り当てられた一時レジスタ nod2 に格納する LEAQ 命令を生成します。これで LEAQ 命令のデスティネーションは常にレジスタになります。
      • gins(AMOVQ, &nod2, res);: 一時レジスタ nod2 に格納されたアドレス(nod1 のアドレス)を、最終的なデスティネーションである res に移動させる MOVQ 命令を生成します。これにより、res がメモリ上の場所であっても、正しくアドレスが書き込まれます。
    • else gins(ALEAQ, &nod1, res);:

      • res が既にレジスタである場合(res->op == OREGISTER)、元のロジックがそのまま適用されます。この場合、LEAQ 命令のデスティネーションは既にレジスタであるため、追加の MOVQ は不要です。

この修正は、コンパイラが生成するアセンブリコードの正確性を保証するために不可欠であり、特定の命令(LEAQ)のセマンティクスと、コンパイラのレジスタ割り当て戦略との整合性を保つための典型的なパターンを示しています。

関連リンク

  • Go言語の初期のコンパイラに関する情報:
    • Goのコンパイラツールチェーンの歴史や設計思想について、公式ドキュメントや初期の設計ドキュメントを参照すると理解が深まります。
    • Go Compiler Design (これは一般的なGoコンパイラの設計に関するものですが、初期の設計思想も含まれる可能性があります)
  • x86-64 (AMD64) アセンブリ命令セット:
  • コンパイラのレジスタ割り当て:
    • コンパイラにおけるレジスタ割り当てのアルゴリズムや課題について学ぶことで、このコミットの背景にある技術的な複雑さをより深く理解できます。
    • Wikipedia: Register allocation

参考にした情報源リンク

このコミットは、Goコンパイラのバックエンドの一部である src/cmd/6g/gen.c ファイルに対する変更です。6g は、Go言語の初期のコンパイラツールチェーンの一部であり、Plan 9アーキテクチャをベースにしたGoコンパイラのx86-64 (AMD64) アーキテクチャ向けコードジェネレータです。gen.c は、抽象構文木 (AST) や中間表現 (IR) からターゲットマシンコードを生成する役割を担っています。具体的には、アセンブリ命令の生成やレジスタ割り当てに関連する処理が含まれています。

コミット

このコミットは、Ken Thompsonによって2008年12月20日に行われました。コミットメッセージは「LEAQ bug」と簡潔に記されており、LEAQ 命令に関連するバグの修正が目的であることが示唆されています。

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

https://github.com/golang/go/commit/7cfe782a693456346d92376e230dbb3b72cd6184

元コミット内容

commit 7cfe782a693456346d92376e230dbb3b72cd6184
Author: Ken Thompson <ken@golang.org>
Date:   Sat Dec 20 16:05:12 2008 -0800

    LEAQ bug
    
    R=r
    OCL=21684
    CL=21684
---
 src/cmd/6g/gen.c | 10 ++++++++--
 1 file changed, 8 insertions(+), 2 deletions(-)\n
diff --git a/src/cmd/6g/gen.c b/src/cmd/6g/gen.c
index 0f625c649b..640243f7f5 100644
--- a/src/cmd/6g/gen.c
+++ b/src/cmd/6g/gen.c
@@ -880,7 +880,7 @@ cgen_callret(Node *n, Node *res)\n void\n cgen_aret(Node *n, Node *res)\n {\n-\tNode nod1;\n+\tNode nod1, nod2;\n \tType *fp, *t;\n \tIter flist;\n \n@@ -900,7 +900,13 @@ cgen_aret(Node *n, Node *res)\n \tnod1.xoffset = fp->width;\n \tnod1.type = fp->type;\n \n-\tgins(ALEAQ, &nod1, res);\n+\tif(res->op != OREGISTER) {\n+print(\"its 1\\n\");\n+\t\tregalloc(&nod2, types[tptr], res);\n+\t\tgins(ALEAQ, &nod1, &nod2);\n+\t\tgins(AMOVQ, &nod2, res);\n+\t} else\n+\t\tgins(ALEAQ, &nod1, res);\n }\n \n void

変更の背景

このコミットは「LEAQ bug」という簡潔なメッセージで示されている通り、LEAQ (Load Effective Address) 命令の生成に関するバグを修正するものです。LEAQ 命令は、メモリのアドレスを計算し、その結果をレジスタにロードするために使用されます。これはポインタ演算や構造体のアドレス計算など、様々な場面で利用されます。

コンパイラがコードを生成する際、中間表現 (IR) の結果を最終的なマシンコードに変換します。この過程で、変数のアドレスをレジスタにロードする必要がある場合、LEAQ 命令が使われます。しかし、この命令のターゲット(結果を格納する場所)がレジスタではない場合、直接 LEAQ を発行すると問題が発生する可能性があります。

元のコードでは、LEAQ 命令のターゲットが常に res という Node オブジェクトに直接指定されていました。res がレジスタではない場合(例えば、メモリ上の場所を指す場合)、LEAQ 命令はレジスタをオペランドとして期待するため、不正なコードが生成されるか、コンパイラがクラッシュするなどの問題を引き起こす可能性がありました。このバグは、LEAQ 命令のセマンティクスと、コンパイラのレジスタ割り当ておよびコード生成ロジックの間の不整合に起因していたと考えられます。

前提知識の解説

このコミットを理解するためには、以下の概念が重要です。

  • Goコンパイラ (6g): Go言語の初期のコンパイラの一つで、x86-64アーキテクチャ向けのコードを生成します。コンパイラは、ソースコードを解析し、中間表現に変換し、最終的にターゲットマシンが実行できるアセンブリコードを生成します。
  • LEAQ (Load Effective Address) 命令: x86-64アーキテクチャのアセンブリ命令の一つです。LEA は "Load Effective Address" の略で、メモリのアドレスを計算し、その結果を汎用レジスタに格納します。例えば、LEAQ (%rax, %rbx, 4), %rcx は、%rax + %rbx * 4 のアドレスを計算し、その結果を %rcx レジスタに格納します。これは、ポインタの計算や、定数による乗算を伴うアドレス計算を効率的に行うために使われます。MOV 命令がメモリの内容をロードするのに対し、LEA はアドレス自体をロードします。
  • Node 構造体: コンパイラ内部で、抽象構文木 (AST) や中間表現 (IR) のノードを表すために使われるデータ構造です。各 Node は、変数、定数、演算子、関数呼び出しなど、プログラムの要素に対応します。Node には、その種類 (op)、型 (type)、値、レジスタ割り当て情報などが含まれます。
  • OREGISTER: Nodeop フィールドが取りうる値の一つで、その Node がレジスタを表していることを示します。
  • Type 構造体: コンパイラ内部で、Go言語の型システムを表すために使われるデータ構造です。types[tptr] は、ポインタ型を表すグローバルな Type オブジェクトへの参照と考えられます。
  • regalloc(Node *n, Type *t, Node *res): レジスタ割り当てを行う関数です。n にレジスタを割り当て、そのレジスタの情報を n に格納します。t は割り当てるレジスタの型、res は結果を格納する最終的な場所(ヒント)です。
  • gins(Op op, Node *src, Node *dst): アセンブリ命令を生成する関数です。op は命令の種類(例: ALEAQ, AMOVQ)、src はソースオペランド、dst はデスティネーションオペランドです。
  • ALEAQ: LEAQ 命令に対応する内部的なオペレーションコードです。
  • AMOVQ: MOVQ (Move Quadword) 命令に対応する内部的なオペレーションコードです。MOVQ は、64ビットの値を移動させる命令です。

技術的詳細

このコミットの技術的な核心は、cgen_aret 関数における LEAQ 命令の生成ロジックの改善にあります。cgen_aret 関数は、おそらく関数の戻り値のアドレスを計算し、それを特定の場所に格納する役割を担っています。

元のコードでは、gins(ALEAQ, &nod1, res); という行で、nod1 のアドレスを計算し、その結果を res に直接格納しようとしていました。ここで問題となるのは、res が必ずしもレジスタであるとは限らない点です。LEAQ 命令は、その結果をレジスタに格納することを前提としています。もし res がメモリ上の場所を指す Node であった場合、LEAQ 命令は不正なオペランドを受け取ることになり、コンパイラの誤動作やクラッシュにつながります。

修正後のコードでは、この問題を解決するために条件分岐が導入されました。

  1. if(res->op != OREGISTER): まず、res がレジスタではないかどうかをチェックします。
  2. 一時レジスタの導入: もし res がレジスタではない場合、一時的なレジスタ nod2 を導入します。
    • regalloc(&nod2, types[tptr], res);: nod2 にポインタ型 (types[tptr]) のレジスタを割り当てます。res はレジスタ割り当てのヒントとして使われます。
    • gins(ALEAQ, &nod1, &nod2);: nod1 のアドレスを計算し、その結果を一時レジスタ nod2 に格納する LEAQ 命令を生成します。これで LEAQ 命令のデスティネーションが常にレジスタになります。
    • gins(AMOVQ, &nod2, res);: 一時レジスタ nod2 に格納されたアドレスを、本来のデスティネーションである res に移動させる MOVQ 命令を生成します。これにより、res がメモリ上の場所であっても、正しくアドレスが格納されます。
  3. 既存のロジックの維持: もし res が既にレジスタである場合は、元の gins(ALEAQ, &nod1, res); のロジックをそのまま使用します。この場合、LEAQ 命令のデスティネーションは既にレジスタであるため、問題ありません。

この修正により、LEAQ 命令が常に適切なデスティネーション(レジスタ)を持つことが保証され、コンパイラの堅牢性が向上しました。これは、コンパイラのコード生成におけるレジスタ割り当てと命令セマンティクスの厳密な遵守の重要性を示す典型的な例です。

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

--- a/src/cmd/6g/gen.c
+++ b/src/cmd/6g/gen.c
@@ -880,7 +880,7 @@ cgen_callret(Node *n, Node *res)\n void\n cgen_aret(Node *n, Node *res)\n {\n-\tNode nod1;\n+\tNode nod1, nod2;\n \tType *fp, *t;\n \tIter flist;\n \n@@ -900,7 +900,13 @@ cgen_aret(Node *n, Node *res)\n \tnod1.xoffset = fp->width;\n \tnod1.type = fp->type;\n \n-\tgins(ALEAQ, &nod1, res);\n+\tif(res->op != OREGISTER) {\n+print(\"its 1\\n\");\n+\t\tregalloc(&nod2, types[tptr], res);\n+\t\tgins(ALEAQ, &nod1, &nod2);\n+\t\tgins(AMOVQ, &nod2, res);\n+\t} else\n+\t\tgins(ALEAQ, &nod1, res);\n }\n \n void

コアとなるコードの解説

cgen_aret 関数内の変更点に焦点を当てて解説します。

  1. Node nod1; から Node nod1, nod2; への変更:

    • nod2 という新しい Node 型の変数が追加されました。これは、LEAQ 命令の結果を一時的に保持するためのレジスタを表すために使用されます。
  2. gins(ALEAQ, &nod1, res); の変更:

    • この行が、より複雑な条件分岐に置き換えられました。

    • if(res->op != OREGISTER) { ... }:

      • この条件は、res ノードがレジスタではない場合に真となります。つまり、LEAQ 命令の直接のデスティネーションとして res を使用できない状況です。
      • print("its 1\\n");: デバッグ用の出力と思われます。このパスが実行されたことを示すために一時的に追加された可能性があります。
      • regalloc(&nod2, types[tptr], res);: nod2 にポインタ型 (types[tptr]) のレジスタを割り当てます。res は、レジスタ割り当てのヒントとして渡されます。これにより、nod2 は一時的なレジスタとして機能します。
      • gins(ALEAQ, &nod1, &nod2);: nod1 のアドレスを計算し、その結果を新しく割り当てられた一時レジスタ nod2 に格納する LEAQ 命令を生成します。これで LEAQ 命令のデスティネーションは常にレジスタになります。
      • gins(AMOVQ, &nod2, res);: 一時レジスタ nod2 に格納されたアドレス(nod1 のアドレス)を、最終的なデスティネーションである res に移動させる MOVQ 命令を生成します。これにより、res がメモリ上の場所であっても、正しくアドレスが書き込まれます。
    • else gins(ALEAQ, &nod1, res);:

      • res が既にレジスタである場合(res->op == OREGISTER)、元のロジックがそのまま適用されます。この場合、LEAQ 命令のデスティネーションは既にレジスタであるため、追加の MOVQ は不要です。

この修正は、コンパイラが生成するアセンブリコードの正確性を保証するために不可欠であり、特定の命令(LEAQ)のセマンティクスと、コンパイラのレジスタ割り当て戦略との整合性を保つための典型的なパターンを示しています。

関連リンク

  • Go言語の初期のコンパイラに関する情報:
    • Goのコンパイラツールチェーンの歴史や設計思想について、公式ドキュメントや初期の設計ドキュメントを参照すると理解が深まります。
    • Go Compiler Design (これは一般的なGoコンパイラの設計に関するものですが、初期の設計思想も含まれる可能性があります)
  • x86-64 (AMD64) アセンブリ命令セット:
  • コンパイラのレジスタ割り当て:
    • コンパイラにおけるレジスタ割り当てのアルゴリズムや課題について学ぶことで、このコミットの背景にある技術的な複雑さをより深く理解できます。
    • Wikipedia: Register allocation

参考にした情報源リンク