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

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

このコミットは、Go言語のツールチェインにおいて、リンカ(5l, 6l, 8l)にAUNDEFという新しい命令を追加するものです。この命令は、到達不能なコードパスをリンカに明示的に伝えるためのメカニズムを提供し、特にpanicindexのような関数が呼び出し元に戻らないことをリンカが理解するのを助けることを目的としています。

コミット

commit f2bd3a977d105f8a4ee3f4c86fe8daf52f629495
Author: Russ Cox <rsc@golang.org>
Date:   Wed May 30 16:47:56 2012 -0400

    cmd/6l, cmd/8l, cmd/5l: add AUNDEF instruction

    On 6l and 8l, this is a real instruction, guaranteed to
    cause an 'undefined instruction' exception.

    On 5l, we simulate it as BL to address 0.

    The plan is to use it as a signal to the linker that this
    point in the instruction stream cannot be reached
    (hence the changes to nofollow).  This will help the
    compiler explain that panicindex and friends do not
    return without having to put a list of these functions
    in the linker.

    R=ken2
    CC=golang-dev
    https://golang.org/cl/6255064

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

https://github.com/golang/go/commit/f2bd3a977d105f8a4ee3f4c86fe8daf52f629495

元コミット内容

Go言語のリンカであるcmd/6l (AMD64), cmd/8l (386), cmd/5l (ARM) にAUNDEF命令を追加します。

  • 6l (AMD64) および 8l (386) では、AUNDEFは実際に「未定義命令」例外を引き起こすことが保証された命令として実装されます。
  • 5l (ARM) では、AUNDEFはアドレス0へのBL(Branch with Link)命令としてシミュレートされます。これは、通常到達不能なアドレスへの分岐であり、実質的にプログラムの実行を停止させる効果を持ちます。

この命令の目的は、命令ストリーム内の特定のポイントが到達不能であることをリンカに伝えるシグナルとして使用することです。これにより、リンカのnofollowメカニズムが強化され、panicindexなどの関数が呼び出し元に戻らない(リターンしない)ことをコンパイラがリンカに説明する際に、リンカ側でこれらの関数のリストを保持する必要がなくなります。

変更の背景

Go言語のコンパイラとリンカは密接に連携して動作します。特に、パニック(panic)を引き起こすような関数(例: panicindex)は、通常の関数のように呼び出し元に制御を戻しません。しかし、リンカはコードのフローを解析する際に、このような非ローカルな制御フローの変更を常に正確に把握できるわけではありませんでした。

従来、リンカが特定の関数がリターンしないことを知るためには、リンカ自身がその関数のリストを持つか、あるいはコンパイラが特別なメタデータを提供する必要がありました。これは、リンカとコンパイラの間の結合度を高め、メンテナンスを複雑にする可能性がありました。

このコミットの背景にあるのは、より汎用的でアーキテクチャに依存しない方法で、到達不能なコードパスをリンカに通知するメカニズムを導入することです。これにより、リンカはコードの到達可能性をより正確に判断できるようになり、デッドコードの削除や最適化の精度が向上することが期待されます。また、panicindexのような関数の特殊な振る舞いをリンカに伝えるための、よりクリーンな方法を提供します。

前提知識の解説

Go言語のツールチェインとリンカ

Go言語のビルドプロセスは、ソースコードを機械語に変換し、実行可能ファイルを生成する一連のツールで構成されています。主要なツールには以下があります。

  • コンパイラ (go tool compile): Goのソースコードをアセンブリコード(または中間表現)に変換します。
  • アセンブラ (go tool asm): アセンブリコードをオブジェクトファイルに変換します。
  • リンカ (go tool link): 複数のオブジェクトファイルやライブラリを結合し、最終的な実行可能ファイルを生成します。Goのリンカは、5l (ARM), 6l (AMD64), 8l (386) のように、ターゲットアーキテクチャごとに異なる名前が付けられています。

リンカの重要な役割の一つは、プログラム内のすべてのコードパスを解析し、どのコードが到達可能で、どのコードが到達不能であるかを判断することです。到達不能なコード(デッドコード)は、最終的なバイナリから削除されることがあります。

未定義命令例外 (Undefined Instruction Exception)

CPUには、それぞれが実行できる命令セットが定義されています。プログラムがCPUの命令セットに含まれていない、または不正な形式の命令を実行しようとすると、「未定義命令例外」が発生します。これは通常、プログラムのバグ、メモリ破損、または意図的に不正な命令を挿入してプログラムの実行を停止させる場合などに発生します。オペレーティングシステムは、この例外を捕捉し、通常はプログラムを異常終了させます。

ARMアーキテクチャにおけるBL命令は、「分岐とリンク」を意味します。これは、指定されたアドレスにプログラムの実行フローを移す(分岐する)と同時に、現在のプログラムカウンタ(PC)の次の命令のアドレスをリンクレジスタ(LR)に保存する命令です。これにより、呼び出されたサブルーチンが実行を終えた後、LRに保存されたアドレスに戻ることで、呼び出し元に制御を戻すことができます。 このコミットでは、BL命令をアドレス0に対して使用しています。アドレス0は通常、有効な実行可能コードが存在しない領域であり、ここに分岐することは実質的にプログラムのクラッシュを引き起こします。

panicindexとGoのパニック

Go言語には、プログラムの異常終了を扱うためのpanicrecoverというメカニズムがあります。panicは、回復不能なエラーが発生した場合に、通常の実行フローを中断し、スタックを巻き戻しながら遅延関数(defer)を実行します。最終的にrecoverされない場合、プログラムは終了します。 panicindexのような関数は、配列のインデックスが範囲外であるなど、特定の実行時エラーが発生した際にpanicを引き起こすGoランタイム内の関数です。これらの関数は、呼び出し元に正常にリターンすることはありません。

リンカのnofollowメカニズム

リンカは、コードの到達可能性を判断するために、命令のシーケンスを追跡します。しかし、panicindexのようにリターンしない関数や、無限ループ、あるいは意図的に到達不能にされたコードパスが存在する場合、リンカはそれらを正しく認識する必要があります。nofollowは、リンカが特定のコードパスを追跡しないようにするための内部的なフラグやメカニズムを指します。これにより、リンカはデッドコードをより正確に識別し、最終的なバイナリから除外することができます。

技術的詳細

このコミットの核心は、各アーキテクチャのリンカにAUNDEFという新しい擬似命令を導入し、そのセマンティクスを定義することにあります。

AUNDEF命令の導入

  • src/cmd/5a/lex.c, src/cmd/6a/lex.c, src/cmd/8a/lex.c: 各アセンブラの字句解析器にUNDEFという新しいキーワードと、それに対応する内部命令コードAUNDEFが追加されます。これにより、アセンブリコード内でUNDEF命令を記述できるようになります。
  • src/cmd/5l/5.out.h, src/cmd/6l/6.out.h, src/cmd/8l/8.out.h: 各リンカの命令定義ヘッダファイルにAUNDEFが列挙型に追加されます。これは、リンカがこの新しい命令を認識するための内部的な識別子です。
  • src/cmd/5l/optab.c, src/cmd/6l/optab.c, src/cmd/8l/optab.c: 各リンカのオペレーションテーブルにAUNDEF命令のエントリが追加されます。これにより、リンカはこの命令のタイプ、オペランドの有無、およびアセンブル時のバイトコード(またはその生成方法)を認識します。

アーキテクチャごとのAUNDEFの実装

  • src/cmd/6l/optab.c (AMD64) および src/cmd/8l/optab.c (386): AUNDEFは、0x0f, 0x0bというバイトシーケンスとして定義されています。これは、Intel/AMDの命令セットにおけるUD2(Undefined Instruction)命令のオペコードです。UD2命令は、意図的に未定義命令例外を発生させるために使用されます。これにより、プログラムの実行がこの点に到達した場合、確実にクラッシュまたはデバッガへの制御移行が発生します。

  • src/cmd/5l/asm.c (ARM): ARMアーキテクチャでは、AUNDEFは直接的な未定義命令としてではなく、アドレス0へのBL命令としてシミュレートされます。

    case 96:    /* UNDEF */
        // This is supposed to be something that stops execution.
        // It's not supposed to be reached, ever, but if it is, we'd
        // like to be able to tell how we got there.  Assemble as
        //  BL $0
        v = (0 - pc) - 8; // Calculate relative offset to address 0
        o1 = opbra(ABL, C_SCOND_NONE); // Generate BL instruction opcode
        o1 |= (v >> 2) & 0xffffff; // Embed the calculated offset
        break;
    

    BL $0は、プログラムカウンタ(pc)からアドレス0への相対オフセットを計算し、そのオフセットをBL命令のオペランドとして埋め込みます。アドレス0は通常、実行可能なコードが存在しない領域であるため、この命令が実行されると、プログラムは不正なメモリ領域に分岐し、クラッシュします。これは、UD2命令と同様に、到達不能なコードパスが誤って実行された場合にプログラムを停止させる効果を持ちます。

リンカのnofollowメカニズムとの連携

  • src/cmd/5l/pass.c, src/cmd/6l/pass.c, src/cmd/8l/pass.c: 各リンカのpass.cファイルには、nofollowという関数(または同様のロジック)が存在し、リンカがコードパスを追跡すべきでない命令を識別します。このコミットでは、AUNDEF命令がこのnofollowリストに追加されます。
    // src/cmd/6l/pass.c の例
    nofollow(int a)
    {
        switch(a) {
        // ... 既存のケース ...
        case ARETFL:
        case ARETFQ:
        case ARETFW:
        case AUNDEF: // AUNDEFが追加された
            return 1;
        }
        return 0;
    }
    
    これにより、リンカはAUNDEF命令に遭遇した場合、その後の命令が到達不能であると判断し、コードパスの追跡を停止します。これは、panicindexのようなリターンしない関数が、その最後にAUNDEFを配置することで、リンカにその関数が呼び出し元に戻らないことを明示的に伝えることを可能にします。リンカは、AUNDEF以降のコードをデッドコードとして扱い、最適化の対象とすることができます。

src/cmd/5l/span.cの変更

src/cmd/5l/span.cbuildop関数は、命令のサイズや配置を決定するリンカの重要な部分です。AUNDEFbreak文のケースに追加されることで、リンカがAUNDEF命令を正しく処理し、そのサイズを決定できるようになります。

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

このコミットは、Go言語の各アーキテクチャ向けリンカ(5l, 6l, 8l)とアセンブラ(5a, 6a, 8a)にわたる広範な変更を含んでいます。主要な変更箇所は以下の通りです。

  1. アセンブラの字句解析器 (src/cmd/*/lex.c):

    • src/cmd/5a/lex.c: UNDEFキーワードとAUNDEF命令の追加。
    • src/cmd/6a/lex.c: UNDEFキーワードとAUNDEF命令の追加。
    • src/cmd/8a/lex.c: UNDEFキーワードとAUNDEF命令の追加。
  2. リンカの命令定義ヘッダ (src/cmd/*/?.out.h):

    • src/cmd/5l/5.out.h: AUNDEF列挙子の追加。
    • src/cmd/6l/6.out.h: AUNDEF列挙子の追加。
    • src/cmd/8l/8.out.h: AUNDEF列挙子の追加。
  3. リンカのオペレーションテーブル (src/cmd/*/optab.c):

    • src/cmd/5l/optab.c: AUNDEF命令のエントリ追加(タイプ、オペランド、アセンブル時のバイトコード情報)。
    • src/cmd/6l/optab.c: AUNDEF命令のエントリ追加(UD2命令のバイトコード 0x0f, 0x0b を指定)。
    • src/cmd/8l/optab.c: AUNDEF命令のエントリ追加(UD2命令のバイトコード 0x0f, 0x0b を指定)。
  4. リンカのアセンブルロジック (src/cmd/5l/asm.c):

    • src/cmd/5l/asm.c: AUNDEF命令をARMアーキテクチャでアドレス0へのBL命令としてアセンブルするロジックを追加。
  5. リンカのパス解析/フロー制御 (src/cmd/*/pass.c):

    • src/cmd/5l/pass.c: nofollowロジックにAUNDEFを追加し、この命令以降のコードが到達不能であることをリンカに伝える。
    • src/cmd/6l/pass.c: nofollowロジックにAUNDEFを追加。
    • src/cmd/8l/pass.c: nofollowロジックにAUNDEFを追加。
  6. リンカの命令スパン処理 (src/cmd/5l/span.c):

    • src/cmd/5l/span.c: buildop関数にAUNDEFケースを追加し、命令のサイズ計算に含める。

コアとなるコードの解説

src/cmd/5l/asm.c における AUNDEF の実装 (ARM)

diff --git a/src/cmd/5l/asm.c b/src/cmd/5l/asm.c
index c8e50305c6..22695b0716 100644
--- a/src/cmd/5l/asm.c
+++ b/src/cmd/5l/asm.c
@@ -1791,6 +1791,15 @@ if(debug['G']) print("%ux: %s: arm %d\n", (uint32)(p->pc), p->from.sym->name, p-
 		to1 |= (-p->from.offset) & 0xfff;
 		} else
 		to1 |= p->from.offset & 0xfff;
+	case 96:	/* UNDEF */
+		// This is supposed to be something that stops execution.
+		// It's not supposed to be reached, ever, but if it is, we'd
+		// like to be able to tell how we got there.  Assemble as
+		//	BL $0
+		v = (0 - pc) - 8;
+		o1 = opbra(ABL, C_SCOND_NONE);
+		o1 |= (v >> 2) & 0xffffff;
+		break;
 	}

 out[0] = o1;

このコードスニペットは、ARMアーキテクチャのリンカ(5l)がAUNDEF命令をどのように機械語に変換するかを示しています。

  • case 96: /* UNDEF */: AUNDEF命令の内部コードが96であることを示しています。
  • v = (0 - pc) - 8;: BL命令は相対分岐であるため、現在のプログラムカウンタ(pc)からターゲットアドレス(0)までのオフセットを計算します。ARMのBL命令は、PC+8からの相対オフセットを使用するため、-8が引かれています。
  • o1 = opbra(ABL, C_SCOND_NONE);: ABL(Branch with Link)命令の基本オペコードを生成します。C_SCOND_NONEは条件なしの分岐を意味します。
  • o1 |= (v >> 2) & 0xffffff;: 計算されたオフセットvBL命令のオペランドフィールドに埋め込みます。ARMのBL命令のオフセットは2ビット右シフトされ、24ビットのフィールドに収まります。

src/cmd/*/pass.c における nofollow の変更

diff --git a/src/cmd/5l/pass.c b/src/cmd/5l/pass.c
index 34932fd4a0..50593ced97 100644
--- a/src/cmd/5l/pass.c
+++ b/src/cmd/5l/pass.c
@@ -119,7 +119,7 @@ loop:
 			i--;
 			continue;
 		}
-		if(a == AB || (a == ARET && q->scond == 14) || a == ARFE)
+		if(a == AB || (a == ARET && q->scond == 14) || a == ARFE || a == AUNDEF)
 			goto copy;
 		if(q->cond == P || (q->cond->mark&FOLL))
 			continue;
@@ -140,7 +140,7 @@ loop:
 				}
 				(*last)->link = r;
 				*last = r;
-				if(a == AB || (a == ARET && q->scond == 14) || a == ARFE)
+				if(a == AB || (a == ARET && q->scond == 14) || a == ARFE || a == AUNDEF)
 					return;
 				r->as = ABNE;
 				if(a == ABNE)
@@ -166,7 +166,7 @@ loop:
 	p->mark |= FOLL;
 	(*last)->link = p;
 	*last = p;
-	if(a == AB || (a == ARET && p->scond == 14) || a == ARFE){
+	if(a == AB || (a == ARET && p->scond == 14) || a == ARFE || a == AUNDEF){
 		return;
 	}
 	if(p->cond != P)

このスニペットは、リンカのパス解析ロジック(pass.c)がAUNDEF命令をどのように扱うかを示しています。AB(無条件分岐)、ARET(リターン)、ARFE(例外からのリターン)といった命令と同様に、AUNDEFもコードフローがそこで終了するか、あるいは別の場所へ分岐して戻らないことを示す命令として扱われます。 if(a == AB || ... || a == AUNDEF) の条件が追加されたことで、リンカはAUNDEF命令に遭遇した場合、その後のコードが到達不能であると判断し、現在のコードパスの追跡を停止します。これにより、panicindexのような関数がAUNDEFを生成することで、リンカにその関数がリターンしないことを明示的に伝え、リンカが不要なコードを削除したり、より正確なコード解析を行ったりできるようになります。

関連リンク

  • Go言語の公式ドキュメント: https://golang.org/
  • Go言語のツールチェインに関するドキュメント(cmd/goなど): https://golang.org/cmd/go/
  • Go言語のリンカに関する詳細な情報(Goのソースコード内のドキュメントやコメント)

参考にした情報源リンク

  • Go言語のソースコード (特に src/cmd/ ディレクトリ)
  • Intel 64 and IA-32 Architectures Software Developer's Manuals (UD2命令に関する情報)
  • ARM Architecture Reference Manual (BL命令に関する情報)
  • Go言語のIssueトラッカーやメーリングリストの議論 (CL 6255064に関連する議論があれば)
  • Go言語のコンパイラとリンカに関する技術ブログや解説記事 (一般的な情報)
  • https://golang.org/cl/6255064 (元のGo CL)
  • https://go.dev/doc/asm (Go Assembly Language)# [インデックス 13228] ファイルの概要

このコミットは、Go言語のツールチェインにおいて、リンカ(5l, 6l, 8l)にAUNDEFという新しい命令を追加するものです。この命令は、到達不能なコードパスをリンカに明示的に伝えるためのメカニズムを提供し、特にpanicindexのような関数が呼び出し元に戻らないことをリンカが理解するのを助けることを目的としています。

コミット

commit f2bd3a977d105f8a4ee3f4c86fe8daf52f629495
Author: Russ Cox <rsc@golang.org>
Date:   Wed May 30 16:47:56 2012 -0400

    cmd/6l, cmd/8l, cmd/5l: add AUNDEF instruction

    On 6l and 8l, this is a real instruction, guaranteed to
    cause an 'undefined instruction' exception.

    On 5l, we simulate it as BL to address 0.

    The plan is to use it as a signal to the linker that this
    point in the instruction stream cannot be reached
    (hence the changes to nofollow).  This will help the
    compiler explain that panicindex and friends do not
    return without having to put a list of these functions
    in the linker.

    R=ken2
    CC=golang-dev
    https://golang.org/cl/6255064

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

https://github.com/golang/go/commit/f2bd3a977d105f8a4ee3f4c86fe8daf52f629495

元コミット内容

Go言語のリンカであるcmd/6l (AMD64), cmd/8l (386), cmd/5l (ARM) にAUNDEF命令を追加します。

  • 6l (AMD64) および 8l (386) では、AUNDEFは実際に「未定義命令」例外を引き起こすことが保証された命令として実装されます。
  • 5l (ARM) では、AUNDEFはアドレス0へのBL(Branch with Link)命令としてシミュレートされます。これは、通常到達不能なアドレスへの分岐であり、実質的にプログラムの実行を停止させる効果を持ちます。

この命令の目的は、命令ストリーム内の特定のポイントが到達不能であることをリンカに伝えるシグナルとして使用することです。これにより、リンカのnofollowメカニズムが強化され、panicindexなどの関数が呼び出し元に戻らない(リターンしない)ことをコンパイラがリンカに説明する際に、リンカ側でこれらの関数のリストを保持する必要がなくなります。

変更の背景

Go言語のコンパイラとリンカは密接に連携して動作します。特に、パニック(panic)を引き起こすような関数(例: panicindex)は、通常の関数のように呼び出し元に制御を戻しません。しかし、リンカはコードのフローを解析する際に、このような非ローカルな制御フローの変更を常に正確に把握できるわけではありませんでした。

従来、リンカが特定の関数がリターンしないことを知るためには、リンカ自身がその関数のリストを持つか、あるいはコンパイラが特別なメタデータを提供する必要がありました。これは、リンカとコンパイラの間の結合度を高め、メンテナンスを複雑にする可能性がありました。

このコミットの背景にあるのは、より汎用的でアーキテクチャに依存しない方法で、到達不能なコードパスをリンカに通知するメカニズムを導入することです。これにより、リンカはコードの到達可能性をより正確に判断できるようになり、デッドコードの削除や最適化の精度が向上することが期待されます。また、panicindexのような関数の特殊な振る舞いをリンカに伝えるための、よりクリーンな方法を提供します。

前提知識の解説

Go言語のツールチェインとリンカ

Go言語のビルドプロセスは、ソースコードを機械語に変換し、実行可能ファイルを生成する一連のツールで構成されています。主要なツールには以下があります。

  • コンパイラ (go tool compile): Goのソースコードをアセンブリコード(または中間表現)に変換します。
  • アセンブラ (go tool asm): アセンブリコードをオブジェクトファイルに変換します。
  • リンカ (go tool link): 複数のオブジェクトファイルやライブラリを結合し、最終的な実行可能ファイルを生成します。Goのリンカは、5l (ARM), 6l (AMD64), 8l (386) のように、ターゲットアーキテクチャごとに異なる名前が付けられています。

リンカの重要な役割の一つは、プログラム内のすべてのコードパスを解析し、どのコードが到達可能で、どのコードが到達不能であるかを判断することです。到達不能なコード(デッドコード)は、最終的なバイナリから削除されることがあります。

未定義命令例外 (Undefined Instruction Exception)

CPUには、それぞれが実行できる命令セットが定義されています。プログラムがCPUの命令セットに含まれていない、または不正な形式の命令を実行しようとすると、「未定義命令例外」が発生します。これは通常、プログラムのバグ、メモリ破損、または意図的に不正な命令を挿入してプログラムの実行を停止させる場合などに発生します。オペレーティングシステムは、この例外を捕捉し、通常はプログラムを異常終了させます。

ARMアーキテクチャにおけるBL命令は、「分岐とリンク」を意味します。これは、指定されたアドレスにプログラムの実行フローを移す(分岐する)と同時に、現在のプログラムカウンタ(PC)の次の命令のアドレスをリンクレジスタ(LR)に保存する命令です。これにより、呼び出されたサブルーチンが実行を終えた後、LRに保存されたアドレスに戻ることで、呼び出し元に制御を戻すことができます。 このコミットでは、BL命令をアドレス0に対して使用しています。アドレス0は通常、有効な実行可能コードが存在しない領域であり、ここに分岐することは実質的にプログラムのクラッシュを引き起こします。

panicindexとGoのパニック

Go言語には、プログラムの異常終了を扱うためのpanicrecoverというメカニズムがあります。panicは、回復不能なエラーが発生した場合に、通常の実行フローを中断し、スタックを巻き戻しながら遅延関数(defer)を実行します。最終的にrecoverされない場合、プログラムは終了します。 panicindexのような関数は、配列のインデックスが範囲外であるなど、特定の実行時エラーが発生した際にpanicを引き起こすGoランタイム内の関数です。これらの関数は、呼び出し元に正常にリターンすることはありません。

リンカのnofollowメカニズム

リンカは、コードの到達可能性を判断するために、命令のシーケンスを追跡します。しかし、panicindexのようにリターンしない関数や、無限ループ、あるいは意図的に到達不能にされたコードパスが存在する場合、リンカはそれらを正しく認識する必要があります。nofollowは、リンカが特定のコードパスを追跡しないようにするための内部的なフラグやメカニズムを指します。これにより、リンカはデッドコードをより正確に識別し、最終的なバイナリから除外することができます。

技術的詳細

このコミットの核心は、各アーキテクチャのリンカにAUNDEFという新しい擬似命令を導入し、そのセマンティクスを定義することにあります。

AUNDEF命令の導入

  • src/cmd/5a/lex.c, src/cmd/6a/lex.c, src/cmd/8a/lex.c: 各アセンブラの字句解析器にUNDEFという新しいキーワードと、それに対応する内部命令コードAUNDEFが追加されます。これにより、アセンブリコード内でUNDEF命令を記述できるようになります。
  • src/cmd/5l/5.out.h, src/cmd/6l/6.out.h, src/cmd/8l/8.out.h: 各リンカの命令定義ヘッダファイルにAUNDEFが列挙型に追加されます。これは、リンカがこの新しい命令を認識するための内部的な識別子です。
  • src/cmd/5l/optab.c, src/cmd/6l/optab.c, src/cmd/8l/optab.c: 各リンカのオペレーションテーブルにAUNDEF命令のエントリが追加されます。これにより、リンカはこの命令のタイプ、オペランドの有無、およびアセンブル時のバイトコード(またはその生成方法)を認識します。

アーキテクチャごとのAUNDEFの実装

  • src/cmd/6l/optab.c (AMD64) および src/cmd/8l/optab.c (386): AUNDEFは、0x0f, 0x0bというバイトシーケンスとして定義されています。これは、Intel/AMDの命令セットにおけるUD2(Undefined Instruction)命令のオペコードです。UD2命令は、意図的に未定義命令例外を発生させるために使用されます。これにより、プログラムの実行がこの点に到達した場合、確実にクラッシュまたはデバッガへの制御移行が発生します。

  • src/cmd/5l/asm.c (ARM): ARMアーキテクチャでは、AUNDEFは直接的な未定義命令としてではなく、アドレス0へのBL命令としてシミュレートされます。

    case 96:    /* UNDEF */
        // This is supposed to be something that stops execution.
        // It's not supposed to be reached, ever, but if it is, we'd
        // like to be able to tell how we got there.  Assemble as
        //  BL $0
        v = (0 - pc) - 8; // Calculate relative offset to address 0
        o1 = opbra(ABL, C_SCOND_NONE); // Generate BL instruction opcode
        o1 |= (v >> 2) & 0xffffff; // Embed the calculated offset
        break;
    

    BL $0は、プログラムカウンタ(pc)からアドレス0への相対オフセットを計算し、そのオフセットをBL命令のオペランドとして埋め込みます。ARMのBL命令のオフセットは2ビット右シフトされ、24ビットのフィールドに収まります。アドレス0は通常、実行可能なコードが存在しない領域であるため、この命令が実行されると、プログラムは不正なメモリ領域に分岐し、クラッシュします。これは、UD2命令と同様に、到達不能なコードパスが誤って実行された場合にプログラムを停止させる効果を持ちます。

リンカのnofollowメカニズムとの連携

  • src/cmd/5l/pass.c, src/cmd/6l/pass.c, src/cmd/8l/pass.c: 各リンカのpass.cファイルには、nofollowという関数(または同様のロジック)が存在し、リンカがコードパスを追跡すべきでない命令を識別します。このコミットでは、AUNDEF命令がこのnofollowリストに追加されます。
    // src/cmd/6l/pass.c の例
    nofollow(int a)
    {
        switch(a) {
        // ... 既存のケース ...
        case ARETFL:
        case ARETFQ:
        case ARETFW:
        case AUNDEF: // AUNDEFが追加された
            return 1;
        }
        return 0;
    }
    
    これにより、リンカはAUNDEF命令に遭遇した場合、その後の命令が到達不能であると判断し、コードパスの追跡を停止します。これは、panicindexのようなリターンしない関数が、その最後にAUNDEFを配置することで、リンカにその関数が呼び出し元に戻らないことを明示的に伝えることを可能にします。リンカは、AUNDEF以降のコードをデッドコードとして扱い、最適化の対象とすることができます。

src/cmd/5l/span.cの変更

src/cmd/5l/span.cbuildop関数は、命令のサイズや配置を決定するリンカの重要な部分です。AUNDEFbreak文のケースに追加されることで、リンカがAUNDEF命令を正しく処理し、そのサイズを決定できるようになります。

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

このコミットは、Go言語の各アーキテクチャ向けリンカ(5l, 6l, 8l)とアセンブラ(5a, 6a, 8a)にわたる広範な変更を含んでいます。主要な変更箇所は以下の通りです。

  1. アセンブラの字句解析器 (src/cmd/*/lex.c):

    • src/cmd/5a/lex.c: UNDEFキーワードとAUNDEF命令の追加。
    • src/cmd/6a/lex.c: UNDEFキーワードとAUNDEF命令の追加。
    • src/cmd/8a/lex.c: UNDEFキーワードとAUNDEF命令の追加。
  2. リンカの命令定義ヘッダ (src/cmd/*/?.out.h):

    • src/cmd/5l/5.out.h: AUNDEF列挙子の追加。
    • src/cmd/6l/6.out.h: AUNDEF列挙子の追加。
    • src/cmd/8l/8.out.h: AUNDEF列挙子の追加。
  3. リンカのオペレーションテーブル (src/cmd/*/optab.c):

    • src/cmd/5l/optab.c: AUNDEF命令のエントリ追加(タイプ、オペランド、アセンブル時のバイトコード情報)。
    • src/cmd/6l/optab.c: AUNDEF命令のエントリ追加(UD2命令のバイトコード 0x0f, 0x0b を指定)。
    • src/cmd/8l/optab.c: AUNDEF命令のエントリ追加(UD2命令のバイトコード 0x0f, 0x0b を指定)。
  4. リンカのアセンブルロジック (src/cmd/5l/asm.c):

    • src/cmd/5l/asm.c: AUNDEF命令をARMアーキテクチャでアドレス0へのBL命令としてアセンブルするロジックを追加。
  5. リンカのパス解析/フロー制御 (src/cmd/*/pass.c):

    • src/cmd/5l/pass.c: nofollowロジックにAUNDEFを追加し、この命令以降のコードが到達不能であることをリンカに伝える。
    • src/cmd/6l/pass.c: nofollowロジックにAUNDEFを追加。
    • src/cmd/8l/pass.c: nofollowロジックにAUNDEFを追加。
  6. リンカの命令スパン処理 (src/cmd/5l/span.c):

    • src/cmd/5l/span.c: buildop関数にAUNDEFケースを追加し、命令のサイズ計算に含める。

コアとなるコードの解説

src/cmd/5l/asm.c における AUNDEF の実装 (ARM)

diff --git a/src/cmd/5l/asm.c b/src/cmd/5l/asm.c
index c8e50305c6..22695b0716 100644
--- a/src/cmd/5l/asm.c
+++ b/src/cmd/5l/asm.c
@@ -1791,6 +1791,15 @@ if(debug['G']) print("%ux: %s: arm %d\n", (uint32)(p->pc), p->from.sym->name, p-
 		to1 |= (-p->from.offset) & 0xfff;
 		} else
 		to1 |= p->from.offset & 0xfff;
+	case 96:	/* UNDEF */
+		// This is supposed to be something that stops execution.
+		// It's not supposed to be reached, ever, but if it is, we'd
+		// like to be able to tell how we got there.  Assemble as
+		//	BL $0
+		v = (0 - pc) - 8;
+		o1 = opbra(ABL, C_SCOND_NONE);
+		o1 |= (v >> 2) & 0xffffff;
+		break;
 	}

 out[0] = o1;

このコードスニペットは、ARMアーキテクチャのリンカ(5l)がAUNDEF命令をどのように機械語に変換するかを示しています。

  • case 96: /* UNDEF */: AUNDEF命令の内部コードが96であることを示しています。
  • v = (0 - pc) - 8;: BL命令は相対分岐であるため、現在のプログラムカウンタ(pc)からターゲットアドレス(0)までのオフセットを計算します。ARMのBL命令は、PC+8からの相対オフセットを使用するため、-8が引かれています。
  • o1 = opbra(ABL, C_SCOND_NONE);: ABL(Branch with Link)命令の基本オペコードを生成します。C_SCOND_NONEは条件なしの分岐を意味します。
  • o1 |= (v >> 2) & 0xffffff;: 計算されたオフセットvBL命令のオペランドフィールドに埋め込みます。ARMのBL命令のオフセットは2ビット右シフトされ、24ビットのフィールドに収まります。

src/cmd/*/pass.c における nofollow の変更

diff --git a/src/cmd/5l/pass.c b/src/cmd/5l/pass.c
index 34932fd4a0..50593ced97 100644
--- a/src/cmd/5l/pass.c
+++ b/src/cmd/5l/pass.c
@@ -119,7 +119,7 @@ loop:
 			i--;
 			continue;
 		}
-		if(a == AB || (a == ARET && q->scond == 14) || a == ARFE)
+		if(a == AB || (a == ARET && q->scond == 14) || a == ARFE || a == AUNDEF)
 			goto copy;
 		if(q->cond == P || (q->cond->mark&FOLL))
 			continue;
@@ -140,7 +140,7 @@ loop:
 				}
 				(*last)->link = r;
 				*last = r;
-				if(a == AB || (a == ARET && q->scond == 14) || a == ARFE)
+				if(a == AB || (a == ARET && q->scond == 14) || a == ARFE || a == AUNDEF)
 					return;
 				r->as = ABNE;
 				if(a == ABNE)
@@ -166,7 +166,7 @@ loop:
 	p->mark |= FOLL;
 	(*last)->link = p;
 	*last = p;
-	if(a == AB || (a == ARET && p->scond == 14) || a == ARFE){
+	if(a == AB || (a == ARET && p->scond == 14) || a == ARFE || a == AUNDEF){
 		return;
 	}
 	if(p->cond != P)

このスニペットは、リンカのパス解析ロジック(pass.c)がAUNDEF命令をどのように扱うかを示しています。AB(無条件分岐)、ARET(リターン)、ARFE(例外からのリターン)といった命令と同様に、AUNDEFもコードフローがそこで終了するか、あるいは別の場所へ分岐して戻らないことを示す命令として扱われます。 if(a == AB || ... || a == AUNDEF) の条件が追加されたことで、リンカはAUNDEF命令に遭遇した場合、その後のコードが到達不能であると判断し、現在のコードパスの追跡を停止します。これにより、panicindexのような関数がAUNDEFを生成することで、リンカにその関数がリターンしないことを明示的に伝え、リンカが不要なコードを削除したり、より正確なコード解析を行ったりできるようになります。

関連リンク

  • Go言語の公式ドキュメント: https://golang.org/
  • Go言語のツールチェインに関するドキュメント(cmd/goなど): https://golang.org/cmd/go/
  • Go言語のリンカに関する詳細な情報(Goのソースコード内のドキュメントやコメント)

参考にした情報源リンク

  • Go言語のソースコード (特に src/cmd/ ディレクトリ)
  • Intel 64 and IA-32 Architectures Software Developer's Manuals (UD2命令に関する情報)
  • ARM Architecture Reference Manual (BL命令に関する情報)
  • Go言語のIssueトラッカーやメーリングリストの議論 (CL 6255064に関連する議論があれば)
  • Go言語のコンパイラとリンカに関する技術ブログや解説記事 (一般的な情報)
  • https://golang.org/cl/6255064 (元のGo CL)
  • https://go.dev/doc/asm (Go Assembly Language)