[インデックス 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命令 (Branch with Link)
ARMアーキテクチャにおけるBL
命令は、「分岐とリンク」を意味します。これは、指定されたアドレスにプログラムの実行フローを移す(分岐する)と同時に、現在のプログラムカウンタ(PC)の次の命令のアドレスをリンクレジスタ(LR)に保存する命令です。これにより、呼び出されたサブルーチンが実行を終えた後、LR
に保存されたアドレスに戻ることで、呼び出し元に制御を戻すことができます。
このコミットでは、BL
命令をアドレス0に対して使用しています。アドレス0は通常、有効な実行可能コードが存在しない領域であり、ここに分岐することは実質的にプログラムのクラッシュを引き起こします。
panicindex
とGoのパニック
Go言語には、プログラムの異常終了を扱うためのpanic
とrecover
というメカニズムがあります。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.c
のbuildop
関数は、命令のサイズや配置を決定するリンカの重要な部分です。AUNDEF
がbreak
文のケースに追加されることで、リンカがAUNDEF
命令を正しく処理し、そのサイズを決定できるようになります。
コアとなるコードの変更箇所
このコミットは、Go言語の各アーキテクチャ向けリンカ(5l
, 6l
, 8l
)とアセンブラ(5a
, 6a
, 8a
)にわたる広範な変更を含んでいます。主要な変更箇所は以下の通りです。
-
アセンブラの字句解析器 (
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
命令の追加。
-
リンカの命令定義ヘッダ (
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
列挙子の追加。
-
リンカのオペレーションテーブル (
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
を指定)。
-
リンカのアセンブルロジック (
src/cmd/5l/asm.c
):src/cmd/5l/asm.c
:AUNDEF
命令をARMアーキテクチャでアドレス0へのBL
命令としてアセンブルするロジックを追加。
-
リンカのパス解析/フロー制御 (
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
を追加。
-
リンカの命令スパン処理 (
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;
: 計算されたオフセットv
をBL
命令のオペランドフィールドに埋め込みます。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命令 (Branch with Link)
ARMアーキテクチャにおけるBL
命令は、「分岐とリンク」を意味します。これは、指定されたアドレスにプログラムの実行フローを移す(分岐する)と同時に、現在のプログラムカウンタ(PC)の次の命令のアドレスをリンクレジスタ(LR)に保存する命令です。これにより、呼び出されたサブルーチンが実行を終えた後、LR
に保存されたアドレスに戻ることで、呼び出し元に制御を戻すことができます。
このコミットでは、BL
命令をアドレス0に対して使用しています。アドレス0は通常、有効な実行可能コードが存在しない領域であり、ここに分岐することは実質的にプログラムのクラッシュを引き起こします。
panicindex
とGoのパニック
Go言語には、プログラムの異常終了を扱うためのpanic
とrecover
というメカニズムがあります。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.c
のbuildop
関数は、命令のサイズや配置を決定するリンカの重要な部分です。AUNDEF
がbreak
文のケースに追加されることで、リンカがAUNDEF
命令を正しく処理し、そのサイズを決定できるようになります。
コアとなるコードの変更箇所
このコミットは、Go言語の各アーキテクチャ向けリンカ(5l
, 6l
, 8l
)とアセンブラ(5a
, 6a
, 8a
)にわたる広範な変更を含んでいます。主要な変更箇所は以下の通りです。
-
アセンブラの字句解析器 (
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
命令の追加。
-
リンカの命令定義ヘッダ (
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
列挙子の追加。
-
リンカのオペレーションテーブル (
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
を指定)。
-
リンカのアセンブルロジック (
src/cmd/5l/asm.c
):src/cmd/5l/asm.c
:AUNDEF
命令をARMアーキテクチャでアドレス0へのBL
命令としてアセンブルするロジックを追加。
-
リンカのパス解析/フロー制御 (
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
を追加。
-
リンカの命令スパン処理 (
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;
: 計算されたオフセットv
をBL
命令のオペランドフィールドに埋め込みます。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)